Files
salvium-rs/docs/explorer-spec.md
T
Matt Hess 65d596a6ea Add sync cancellation via AtomicBool flag for FFI consumers
Allows Android/FFI callers to interrupt an in-progress wallet sync
  cleanly between batches. Adds salvium_wallet_stop_sync FFI export,
  WalletError::Cancelled, SyncEvent::Cancelled, and WalletHandle
  wrapper that pairs each wallet with its cancellation flag.
2026-02-26 02:23:12 +00:00

478 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Explorer WASM Integration Spec
How to use the `salvium-explorer` WASM module in a Cloudflare Worker or browser to parse blocks, analyze transactions, and scan outputs.
## 1. Build
```bash
# Build the wasm-bindgen package (JS glue + .wasm binary)
wasm-pack build crates/salvium-explorer --release --target bundler --out-dir pkg
# Output:
# crates/salvium-explorer/pkg/salvium_explorer.js (JS glue)
# crates/salvium-explorer/pkg/salvium_explorer_bg.wasm (WASM binary)
# crates/salvium-explorer/pkg/salvium_explorer.d.ts (TypeScript types)
# crates/salvium-explorer/pkg/package.json
```
Or use the build script which also produces the C ABI static library:
```bash
scripts/build-wasm.sh
# Output in prebuilt/wasm/
```
## 2. Initialize the Module
### Cloudflare Worker (bundler target)
```typescript
import init, { initSync } from 'salvium-explorer';
import wasmModule from 'salvium-explorer/salvium_explorer_bg.wasm';
// Synchronous init (preferred for Workers)
initSync({ module: wasmModule });
// Or async init
await init({ module_or_path: wasmModule });
```
### Browser
```typescript
import init from 'salvium-explorer';
await init(); // Fetches .wasm from same directory
```
After initialization, all exported functions are available as direct imports.
## 3. Explorer-Specific APIs
These are the 3 high-level functions unique to the explorer crate. They combine multiple salvium-crypto primitives into single calls.
### 3a. `parse_and_analyze_tx`
Parse a raw transaction binary and return enriched JSON.
```typescript
import { parse_and_analyze_tx } from 'salvium-explorer';
const result: string = parse_and_analyze_tx(txBytes: Uint8Array);
const tx = JSON.parse(result);
```
**Returns** all fields from `parse_transaction_bytes()` plus these analysis fields:
| Field | Type | Description |
|-------|------|-------------|
| `tx_type_name` | string | Human-readable TX type (e.g. `"Transfer"`, `"Miner"`, `"Protocol"`) |
| `rct_type_name` | string | Human-readable RCT type (e.g. `"RctBulletproofPlus"`) |
| `input_count` | number | Number of inputs |
| `output_count` | number | Number of outputs |
| `is_coinbase` | boolean | Whether this is a miner/coinbase TX |
| `is_carrot` | boolean | Whether any output uses CARROT format (type `0x04`) |
| `key_images` | string[] | Key image hex strings from inputs |
| `output_keys` | string[] | Output public key hex strings |
| `fee` | string | Transaction fee in atomic units (decimal string) |
**Error:** Returns `{"error": "..."}` on parse failure.
**Example response (trimmed):**
```json
{
"prefix": {
"version": 3,
"txType": 3,
"unlockTime": "0",
"vin": [...],
"vout": [...],
"extra": { "pubkey": "ab12...", ... }
},
"rct": {
"type": 6,
"txnFee": "24960000",
...
},
"tx_type_name": "Transfer",
"rct_type_name": "RctBulletproofPlus",
"input_count": 2,
"output_count": 2,
"is_coinbase": false,
"is_carrot": true,
"key_images": ["aabb...", "ccdd..."],
"output_keys": ["eeff...", "1122..."],
"fee": "24960000"
}
```
### 3b. `parse_and_analyze_block`
Parse a raw block binary and return enriched JSON.
```typescript
import { parse_and_analyze_block } from 'salvium-explorer';
const result: string = parse_and_analyze_block(blockBytes: Uint8Array);
const block = JSON.parse(result);
```
**Returns** all fields from `parse_block_bytes()` plus:
| Field | Type | Description |
|-------|------|-------------|
| `tx_count` | number | Number of transaction hashes (excluding miner tx) |
**Base block fields:**
| Field | Type | Description |
|-------|------|-------------|
| `majorVersion` | number | Block major version |
| `minorVersion` | number | Block minor version |
| `timestamp` | number | Block timestamp (Unix) |
| `prevId` | string | Previous block hash (hex) |
| `nonce` | number | Mining nonce |
| `minerTx` | object | Miner transaction (full parsed TX) |
| `txHashes` | string[] | Transaction hashes in this block (hex) |
### 3c. `decode_outputs_for_view_key`
Scan a transaction for owned outputs using a view key pair.
```typescript
import { decode_outputs_for_view_key } from 'salvium-explorer';
const result: string = decode_outputs_for_view_key(
txBytes: Uint8Array, // raw transaction binary
viewSecret: Uint8Array, // 32-byte view secret key
spendPub: Uint8Array, // 32-byte spend public key
);
const outputs = JSON.parse(result);
```
**Parameters:**
- `txBytes`: Raw transaction blob (binary, NOT hex)
- `viewSecret`: 32-byte view secret key (the private view key)
- `spendPub`: 32-byte spend public key (the public spend key)
**Returns** a JSON array of owned outputs:
```json
[
{
"output_index": 0,
"amount": "1000000000",
"output_key": "aabbccdd...",
"subaddress_major": 0,
"subaddress_minor": 0
}
]
```
| Field | Type | Description |
|-------|------|-------------|
| `output_index` | number | Index within the transaction's outputs |
| `amount` | string | Amount in atomic units (1 SAL = 100,000,000) |
| `output_key` | string | Output public key (hex) |
| `subaddress_major` | number | Subaddress major index (0 = main) |
| `subaddress_minor` | number | Subaddress minor index (0 = main) |
Returns `[]` if no outputs match. Returns `{"error": "..."}` on failure.
**How it works internally:**
1. Parses the transaction binary to extract `vout` and `extra.pubkey`
2. Computes key derivation: `D = 8 * view_secret * tx_pub_key`
3. For each output at index `i`: derives expected key `P' = H(D, i)*G + spend_pub`
4. Compares `P'` against the actual output key — match means the output is owned
**Note:** This is a CryptoNote (legacy) scan only. CARROT output scanning requires the full CARROT key set — use the low-level CARROT helpers for that (see Section 5).
## 4. Core Parsing Functions
These do raw binary parsing without the analysis enrichment.
### `parse_transaction_bytes`
```typescript
import { parse_transaction_bytes } from 'salvium-explorer';
const json: string = parse_transaction_bytes(data: Uint8Array);
```
Returns the full parsed transaction as JSON. All binary fields are hex-encoded, amounts are decimal strings.
### `parse_block_bytes`
```typescript
import { parse_block_bytes } from 'salvium-explorer';
const json: string = parse_block_bytes(data: Uint8Array);
```
### `parse_extra`
```typescript
import { parse_extra } from 'salvium-explorer';
const json: string = parse_extra(extraBytes: Uint8Array);
```
Parses just the TX extra field. Returns JSON with:
- `pubkey`: TX public key (hex)
- `nonces`: payment IDs, extra nonces
- `additionalPubkeys`: additional TX public keys (for subaddresses)
### `compute_tx_prefix_hash`
```typescript
import { compute_tx_prefix_hash } from 'salvium-explorer';
const hash: Uint8Array = compute_tx_prefix_hash(data: Uint8Array);
// Returns 32-byte keccak256 of the TX prefix
```
## 5. Crypto Primitives Available
The explorer re-exports the full salvium-crypto function set. These are the ones most relevant for explorer use:
### Hashing
```typescript
keccak256(data: Uint8Array): Uint8Array // 32-byte CryptoNote hash
blake2b_hash(data: Uint8Array, outLen: number): Uint8Array
sha256(data: Uint8Array): Uint8Array
```
### Key Operations
```typescript
// CryptoNote key derivation (used by decode_outputs_for_view_key internally)
generate_key_derivation(pubKey: Uint8Array, secKey: Uint8Array): Uint8Array
derive_public_key(derivation: Uint8Array, outputIndex: number, basePub: Uint8Array): Uint8Array
// Key image
generate_key_image(pubKey: Uint8Array, secKey: Uint8Array): Uint8Array
is_valid_key_image(keyImage: Uint8Array): boolean
// Point operations
scalar_mult_base(s: Uint8Array): Uint8Array // s*G
hash_to_point(data: Uint8Array): Uint8Array // H_p(data)
```
### Address Utilities
```typescript
wasm_parse_address(address: string): string // Address → JSON
wasm_is_valid_address(address: string): boolean // Validate
wasm_describe_address(address: string): string // Human-readable description
wasm_create_address( // Create from components
network: number, // 0=mainnet, 1=testnet, 2=stagenet
format: number, // 0=CryptoNote, 1=CARROT
addrType: number, // 0=standard, 1=subaddress, 2=integrated
spendKey: Uint8Array,
viewKey: Uint8Array
): string
wasm_to_integrated_address(address: string, paymentId: Uint8Array): string
```
**`wasm_parse_address` returns:**
```json
{
"network": "mainnet",
"format": "CryptoNote",
"address_type": "Standard",
"spend_public_key": "hex64",
"view_public_key": "hex64"
}
```
### TX Type / RCT Type Names
```typescript
wasm_tx_type_name(txType: number): string // e.g. "Transfer", "Miner", "Protocol"
wasm_rct_type_name(rctType: number): string // e.g. "RctBulletproofPlus"
```
### CARROT Output Scanning (Low-Level)
For full CARROT scanning (beyond what `decode_outputs_for_view_key` provides):
```typescript
// Build input context for a TX
make_input_context_rct(firstKeyImage: Uint8Array): Uint8Array // Regular TX
make_input_context_coinbase(blockHeight: bigint): Uint8Array // Coinbase TX
// View tag check (fast rejection)
compute_carrot_view_tag(sSrUnctx: Uint8Array, inputContext: Uint8Array, ko: Uint8Array): Uint8Array
// After view tag matches — full decryption
decrypt_carrot_amount(encAmount: Uint8Array, sSrCtx: Uint8Array, ko: Uint8Array): bigint
recover_carrot_address_spend_pubkey(ko: Uint8Array, sSrCtx: Uint8Array, commitment: Uint8Array): Uint8Array
derive_carrot_commitment_mask(sSrCtx: Uint8Array, amount: bigint, addressSpendPubkey: Uint8Array, enoteType: number): Uint8Array
// CARROT key derivation (9 keys from master secret)
derive_carrot_keys_batch(masterSecret: Uint8Array): Uint8Array // Returns 288 bytes (9 × 32)
derive_carrot_view_only_keys_batch(viewBalanceSecret: Uint8Array, accountSpendPubkey: Uint8Array): Uint8Array
// Subaddress map generation
carrot_subaddress_map_batch(
accountSpendPubkey: Uint8Array,
accountViewPubkey: Uint8Array,
generateAddressSecret: Uint8Array,
majorCount: number,
minorCount: number
): Uint8Array // Returns majorCount * minorCount * 40 bytes (32-byte key + 4-byte major + 4-byte minor each)
```
### Verification
```typescript
// Full RCT signature verification (all ring sigs + bulletproofs in one call)
verify_rct_signatures_wasm(
rctType: number,
inputCount: number,
ringSize: number,
txPrefixHash: Uint8Array,
rctBaseBytes: Uint8Array,
bpComponents: Uint8Array,
keyImagesFlat: Uint8Array,
pseudoOutsFlat: Uint8Array,
sigsFlat: Uint8Array,
ringPubkeysFlat: Uint8Array,
ringCommitmentsFlat: Uint8Array
): Uint8Array // Returns JSON with verification result
```
## 6. Typical Explorer Workflow
### Block Page
```typescript
import { parse_and_analyze_block, parse_and_analyze_tx } from 'salvium-explorer';
// 1. Fetch block from daemon RPC (get_block endpoint returns binary blob)
const blockBlob: Uint8Array = await fetchBlockBlob(height);
// 2. Parse + analyze the block
const block = JSON.parse(parse_and_analyze_block(blockBlob));
// block.majorVersion, block.timestamp, block.prevId, block.tx_count, ...
// 3. The miner TX is embedded in the block
const minerTx = block.minerTx;
// minerTx.prefix.vout → miner reward outputs
// 4. Fetch and parse each transaction
for (const txHash of block.txHashes) {
const txBlob: Uint8Array = await fetchTxBlob(txHash);
const tx = JSON.parse(parse_and_analyze_tx(txBlob));
// tx.tx_type_name, tx.input_count, tx.output_count, tx.fee, ...
}
```
### Transaction Page
```typescript
import { parse_and_analyze_tx, wasm_tx_type_name } from 'salvium-explorer';
const tx = JSON.parse(parse_and_analyze_tx(txBlob));
// Display summary
console.log(`Type: ${tx.tx_type_name}`); // "Transfer"
console.log(`Fee: ${tx.fee} atomic`); // "24960000"
console.log(`Inputs: ${tx.input_count}`);
console.log(`Outputs: ${tx.output_count}`);
console.log(`CARROT: ${tx.is_carrot}`);
console.log(`Coinbase: ${tx.is_coinbase}`);
// Key images (for double-spend checking)
for (const ki of tx.key_images) {
console.log(`Key image: ${ki}`);
}
// Output keys (for output lookup)
for (const ok of tx.output_keys) {
console.log(`Output key: ${ok}`);
}
```
### Output Decoding (View Key Search)
```typescript
import { decode_outputs_for_view_key } from 'salvium-explorer';
// User provides their view key + spend public key
const viewSecret = hexToBytes(viewSecretHex); // 32 bytes
const spendPub = hexToBytes(spendPubHex); // 32 bytes
const owned = JSON.parse(decode_outputs_for_view_key(txBlob, viewSecret, spendPub));
for (const out of owned) {
console.log(`Output #${out.output_index}: ${out.amount} atomic SAL`);
console.log(` Key: ${out.output_key}`);
}
```
### Address Validation
```typescript
import { wasm_is_valid_address, wasm_parse_address } from 'salvium-explorer';
if (wasm_is_valid_address(userAddress)) {
const info = JSON.parse(wasm_parse_address(userAddress));
console.log(`Network: ${info.network}, Type: ${info.address_type}`);
}
```
## 7. Data Types — All Inputs are Raw Bytes
Every function that takes transaction or block data expects **raw binary** (`Uint8Array`), not hex strings. If your daemon returns hex, decode first:
```typescript
function hexToBytes(hex: string): Uint8Array {
const bytes = new Uint8Array(hex.length / 2);
for (let i = 0; i < hex.length; i += 2) {
bytes[i / 2] = parseInt(hex.substring(i, i + 2), 16);
}
return bytes;
}
// Daemon RPC returns hex → convert before passing to WASM
const txHex: string = await daemon.getTransaction(txHash);
const txBytes: Uint8Array = hexToBytes(txHex);
const result = parse_and_analyze_tx(txBytes);
```
## 8. Error Handling
All string-returning functions follow the same pattern:
```typescript
const result = JSON.parse(parse_and_analyze_tx(txBytes));
if (result.error) {
// Parse failed
console.error(result.error);
} else {
// Success — use result fields
}
```
For functions returning `Uint8Array`: an empty array (`length === 0`) indicates an error (invalid input, failed point decompression, etc).
For functions returning `boolean`: they return `false` on invalid input.
## 9. Memory / Performance Notes
- The WASM module is ~4MB (uncompressed). Cloudflare Workers supports this.
- All functions are synchronous — no async/await needed after `initSync()`.
- The module manages its own WASM linear memory. `Uint8Array` inputs are copied into WASM memory and results are copied out — no manual memory management needed from JS.
- `parse_and_analyze_tx` and `parse_and_analyze_block` do JSON serialization internally (serde_json). For hot paths parsing thousands of transactions, prefer `parse_transaction_bytes` and extract only the fields you need.
## Source Files
| File | What |
|------|------|
| `crates/salvium-explorer/src/lib.rs` | Explorer WASM APIs (3 custom + 61 re-exports) |
| `crates/salvium-explorer/Cargo.toml` | Crate config |
| `crates/salvium-explorer/pkg/` | Built wasm-pack output (JS glue, .wasm, .d.ts) |
| `crates/salvium-crypto/src/lib.rs` | Underlying crypto implementations |
| `crates/salvium-crypto/src/wasm_ffi.rs` | C ABI static lib (alternative to wasm-bindgen) |
| `crates/salvium-crypto/src/tx_parse.rs` | Transaction/block binary parser |
| `scripts/build-wasm.sh` | Build script (wasm-pack + staticlib) |