> For the complete documentation index, see [llms.txt](https://docs.sigilvault.xyz/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.sigilvault.xyz/rust/cliffjumper.md).

# cliffjumper (Ethereum)

Ethereum signer that implements `alloy_signer::Signer`, delegating signing to a paired Sigil device through octane. Drop it into any `alloy` (or ethers, via adapters) pipeline as a regular signer — no custom transaction-construction code needed.

## Install

```toml
[dependencies]
cliffjumper = "0.1"
alloy       = { version = "1.7", features = ["full"] }
tokio       = { version = "1", features = ["full"] }
```

## Configuration

You need:

|                    |                                                                                     |
| ------------------ | ----------------------------------------------------------------------------------- |
| `verifying_key`    | secp256k1 public key — the SP's Ethereum identity. SP's address is derived from it. |
| `octane_url`       | `https://api.sigilvault.xyz/octane` (prod)                                          |
| `api_key`          | SP API key from ironhide                                                            |
| `private_key_seed` | 32-byte Ed25519 seed, base64url, no padding — co-signs each request                 |
| `target_address`   | The **device** EVM address you got back as a binding from `await_pair_result`       |

`target_address` is the appliance's Ethereum address — distinct from the SP's address. Octane routes signing requests to the device that owns that address.

## Usage

```rust
use alloy_signer::Signer;
use cliffjumper::CliffjumperSigner;
use k256::ecdsa::VerifyingKey;

let signer = CliffjumperSigner::new(
    sp_verifying_key,
    "https://api.sigilvault.xyz/octane",
    &api_key,
    &private_key_seed_b64url,
    "0xAbC123…",                     // device address from a pair binding
)?;

// Sign a 32-byte hash directly (used by alloy under the hood for typed
// data, EIP-1559 tx hashes, EIP-191 personal messages, etc.):
let sig = signer.sign_hash(&hash).await?;

// Sign an arbitrary message (alloy will keccak256 it):
let sig = signer.sign_message(b"hello world").await?;
```

`CliffjumperSigner` implements `alloy_signer::Signer` fully — wire it into any alloy transaction builder:

```rust
use alloy::providers::ProviderBuilder;
use alloy::rpc::types::TransactionRequest;

let provider = ProviderBuilder::new()
    .signer(signer.clone())
    .connect_http("https://eth.example.com".parse()?);

let tx = TransactionRequest::default()
    .to(recipient)
    .value(amount);

let pending = provider.send_transaction(tx).await?;
```

## Signature parity handling

EVM secp256k1 signatures need a recovery byte that disambiguates two possible public keys for a given `(hash, signature)` pair. Octane returns the device signature with a recovery byte; cliffjumper **validates** that recovery byte by recovering the address from the signature and checking it matches `target_address`. A mismatch returns `CliffjumperError::InvalidDeviceSignature` rather than a misleading "signed by the wrong key" downstream.

This means cliffjumper-produced signatures are immediately EIP-2-compliant and can be submitted to any Ethereum chain without post-processing.

## Errors

```rust
pub enum CliffjumperError {
    InvalidDeviceSignature(String),
    InvalidTargetAddress(String),
    Seed(astrotrain::SignError),
    SyncNotSupported,
}
```

`alloy_signer`'s API treats both async and sync signing as separate trait methods. Cliffjumper only implements the async one — `sign_hash`, `sign_message`, etc. The sync variants return `CliffjumperError::SyncNotSupported`. Most alloy code paths use the async variants; if you hit a sync requirement, wrap a \[`tokio::runtime`] handle.

## Pair handshake

Cliffjumper re-exports `astrotrain::pairing` so you don't need a separate dep:

```rust
use cliffjumper::pairing;

let session = pairing::create_pair_session(
    octane_url,
    &api_key,
    pairing::CreatePairSessionRequest {
        sp_user_ref: "user-42".into(),
        requested_scopes: vec![pairing::RequestedScope {
            chain: "ethereum".into(),
            access: pairing::ScopeAccess::Sign,
        }],
        ..Default::default()
    },
).await?;

let result = pairing::await_pair_result(octane_url, &api_key, &session.session_id).await?;
let device_address = result.bindings
    .iter()
    .find(|b| b.chain == "ethereum")
    .map(|b| b.address.clone())
    .expect("user granted ethereum");

let signer = CliffjumperSigner::new(
    sp_verifying_key,
    octane_url,
    &api_key,
    &private_key_seed_b64url,
    &device_address,
)?;
```

See [astrotrain](/rust/astrotrain.md) for the full pairing surface (events feed, revocation).

## Identity vs. delegation

`CliffjumperSigner::address()` returns the **SP's own address** — derived from the `VerifyingKey` you passed at construction. This is the address recorded in `signing_keys` on ironhide; it is **not** the on-chain address being signed for.

`CliffjumperSigner::target_address()` returns the **device's address** — where the actual on-chain key lives, the address that ends up as `from` on transactions. Use this when displaying the "signing as" address in your UI.

The two are usually different: the SP key is per-SP-deployment, the target address is per-paired-user-wallet.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.sigilvault.xyz/rust/cliffjumper.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
