Files
salvium-rs/docs/ffi-scanner.md
T

185 lines
8.0 KiB
Markdown

# FFI CARROT Scanner Integration Reference
## 1. Overview
The Rust CARROT scanner performs the entire 7-step output scan in a single FFI call, eliminating the per-operation JS-to-Rust round-trips that made the pure-JS path slow.
**Two entry points:**
| Function | Purpose | Step 1 |
|----------|---------|--------|
| `salvium_carrot_scan_output` | Standard scan (incoming payments) | X25519 ECDH: `s_sr_unctx = k_vi * D_e` |
| `salvium_carrot_scan_internal` | Self-send scan (change/burns) | Uses `view_balance_secret` directly as `s_sr_unctx` |
**Return codes:** `1` = owned, `0` = not owned, `-1` = error.
Both functions write their result as a Rust-allocated JSON buffer via the `out_ptr`/`out_len` output parameters. The caller **must** free this buffer with `salvium_storage_free_buf`.
## 2. FFI Function Signatures
### `salvium_carrot_scan_output`
```c
int32_t salvium_carrot_scan_output(
const uint8_t *ko, // 1
const uint8_t *view_tag, // 2
const uint8_t *d_e, // 3
const uint8_t *enc_amount, // 4
const uint8_t *commitment, // 5 (nullable)
const uint8_t *k_vi, // 6
const uint8_t *account_spend_pubkey, // 7
const uint8_t *input_context, // 8
uintptr_t input_context_len, // 9
uint64_t clear_text_amount, // 10
const uint8_t *subaddr_data, // 11
uint32_t n_sub, // 12
uint8_t **out_ptr, // 13
uintptr_t *out_len // 14
);
```
| # | Name | Type | Size | Notes |
|---|------|------|------|-------|
| 1 | `ko` | `*const u8` | 32 | Onetime output pubkey (compressed Ed25519) |
| 2 | `view_tag` | `*const u8` | 3 | View tag bytes |
| 3 | `d_e` | `*const u8` | 32 | Ephemeral pubkey |
| 4 | `enc_amount` | `*const u8` | 8 | Encrypted amount (little-endian) |
| 5 | `commitment` | `*const u8` | 32 | Pedersen commitment. **Nullable** -- pass null for coinbase outputs |
| 6 | `k_vi` | `*const u8` | 32 | View incoming key (secret scalar) |
| 7 | `account_spend_pubkey` | `*const u8` | 32 | Main account spend pubkey K_s |
| 8 | `input_context` | `*const u8` | var | TX input context (key image hashes) |
| 9 | `input_context_len` | `usize` | -- | Byte length of `input_context` |
| 10 | `clear_text_amount` | `u64` | 8 | Known amount (coinbase), or `u64::MAX` sentinel for "not provided" |
| 11 | `subaddr_data` | `*const u8` | n*40 | Binary subaddress map (see section 3) |
| 12 | `n_sub` | `u32` | 4 | Number of entries in `subaddr_data` |
| 13 | `out_ptr` | `*mut *mut u8` | 8 | Output: pointer to Rust-allocated JSON buffer |
| 14 | `out_len` | `*mut usize` | 8 | Output: byte length of JSON buffer |
### `salvium_carrot_scan_internal`
Identical signature. Parameter 6 is `view_balance_secret` instead of `k_vi`:
| # | Name | Type | Size | Notes |
|---|------|------|------|-------|
| 6 | `view_balance_secret` | `*const u8` | 32 | View balance secret (used directly as `s_sr_unctx`, no ECDH) |
### `salvium_storage_free_buf`
```c
void salvium_storage_free_buf(uint8_t *buf_ptr, uintptr_t len);
```
Frees the Rust-allocated JSON result buffer. Must be called exactly once per successful scan (`rc == 1`).
## 3. Binary Formats
### Subaddress map
Each entry is 40 bytes, tightly packed:
```
[32 bytes: spend pubkey] [4 bytes: major index LE] [4 bytes: minor index LE]
```
The buffer passed as `subaddr_data` must be exactly `n_sub * 40` bytes. Pass `n_sub = 0` with an empty/null buffer if there are no subaddresses.
### Clear text amount sentinel
Pass `0xFFFFFFFFFFFFFFFF` (u64::MAX) to indicate the amount is not known in clear text. The scanner will decrypt it from `enc_amount` using the derived mask. Pass the actual amount for coinbase outputs.
## 4. JSON Result Format
On success (`rc == 1`), the buffer at `*out_ptr` contains UTF-8 JSON:
```json
{
"amount": 1000000000,
"mask": "hex64",
"enote_type": 0,
"shared_secret": "hex64",
"address_spend_pubkey": "hex64",
"subaddress_major": 0,
"subaddress_minor": 0,
"is_main_address": true
}
```
| Field | Type | Description |
|-------|------|-------------|
| `amount` | u64 | Decrypted amount in atomic units |
| `mask` | hex string (64 chars) | Commitment mask (32 bytes) |
| `enote_type` | 0 or 1 | 0 = PAYMENT, 1 = CHANGE |
| `shared_secret` | hex string (64 chars) | Contextualized sender-receiver secret `s_sr_ctx` |
| `address_spend_pubkey` | hex string (64 chars) | Recovered address spend pubkey |
| `subaddress_major` | u32 | Major subaddress index (0 for main) |
| `subaddress_minor` | u32 | Minor subaddress index (0 for main) |
| `is_main_address` | bool | `true` if matched main account K_s |
## 5. Memory Management
1. Allocate `out_ptr` (8 bytes) and `out_len` (8 bytes) on the caller side.
2. Call the scan function.
3. If `rc == 1`: read `*out_ptr` and `*out_len`, copy/parse the JSON.
4. Call `salvium_storage_free_buf(*out_ptr, *out_len)` to release the buffer.
5. If `rc == 0` or `rc == -1`: no buffer was allocated, do not call free.
**Dart/Flutter example (dart:ffi):**
```dart
final outPtr = calloc<Pointer<Uint8>>();
final outLen = calloc<IntPtr>();
final rc = scanOutput(ko, viewTag, dE, encAmount, commitment,
kVi, accountSpendPubkey, inputContext, inputContextLen,
clearTextAmount, subaddrData, nSub, outPtr, outLen);
if (rc == 1) {
final json = outPtr.value.cast<Utf8>().toDartString(length: outLen.value);
freeBuf(outPtr.value, outLen.value);
final result = jsonDecode(json);
// use result...
}
calloc.free(outPtr);
calloc.free(outLen);
```
## 6. Scanning Algorithm Reference
All hash operations use keyed Blake2b. Transcript format: `[domain_len_byte][domain][data...]`.
| Step | Operation | Domain separator | Inputs | Output |
|------|-----------|-----------------|--------|--------|
| 1 | ECDH (standard only) | -- | `k_vi`, `D_e` | `s_sr_unctx` (32 bytes) |
| 2 | View tag test | `"Carrot view tag"` | `s_sr_unctx`, `input_context`, `Ko` | 3-byte tag; reject if mismatch |
| 3 | Contextualize secret | `"Carrot sender-receiver secret"` | `s_sr_unctx`, `D_e`, `input_context` | `s_sr_ctx` (32 bytes) |
| 4 | Recover spend pubkey | `"Carrot key extension G"`, `"Carrot key extension T"` | `s_sr_ctx`, commitment | `K^j_s = Ko - (k^o_g * G + k^o_t * T)` |
| 5 | Address matching | -- | recovered pubkey, subaddress map | Match against K_s or subaddress entries; reject if no match |
| 6 | Decrypt amount | `"Carrot encryption mask a"` | `s_sr_ctx`, `Ko` | XOR 8-byte mask with `enc_amount` |
| 7 | Verify commitment | `"Carrot commitment mask"` | `s_sr_ctx`, amount, address, enote_type | Derive mask, compute Pedersen `C = mask*G + amount*H`; try PAYMENT(0) then CHANGE(1) |
For the internal (self-send) path, step 1 is skipped -- `view_balance_secret` is used directly as `s_sr_unctx`.
## 7. Integration Checklist
- [ ] **Build the Rust crate** for each target:
- Android: `cargo ndk -t arm64-v8a -t armeabi-v7a -o jniLibs build --release`
- iOS: `cargo lipo --release` (or `cargo build --target aarch64-apple-ios`)
- [ ] **Load the shared library** via `dart:ffi` (`DynamicLibrary.open` on Android, `DynamicLibrary.process()` on iOS with static linking)
- [ ] **Define FFI bindings** matching the C signatures in section 2
- [ ] **Marshal inputs**: hex-decode keys to `Uint8List`, pack subaddress map as 40-byte entries, set `u64::MAX` for unknown clear text amounts
- [ ] **Two-pass scan** for each output:
1. Call `salvium_carrot_scan_output` (standard path) with `k_vi`
2. If `rc == 0`, call `salvium_carrot_scan_internal` with `view_balance_secret` to detect self-sends
- [ ] **Parse JSON result** and map fields to your wallet model
- [ ] **Free the buffer** with `salvium_storage_free_buf` after reading JSON
- [ ] **Handle errors** (`rc == -1`): log and continue scanning remaining outputs
## Source Files
| File | What |
|------|------|
| `crates/salvium-crypto/src/ffi.rs:873-1016` | FFI entry points |
| `crates/salvium-crypto/src/carrot_scan.rs` | Scanner algorithm and `CarrotScanResult` |
| `src/crypto/backend-ffi.js:628-722` | JS reference implementation of marshalling |