diff --git a/api-reference/overview.mdx b/api-reference/overview.mdx index e033980..39477ef 100644 --- a/api-reference/overview.mdx +++ b/api-reference/overview.mdx @@ -1,6 +1,6 @@ --- title: "API Overview" -description: "REST APIs for geospatial operations and location records" +description: "REST APIs for location verification, verifiable geospatial operations, and location records" --- @@ -9,19 +9,23 @@ description: "REST APIs for geospatial operations and location records" # API Reference -Astral provides two REST APIs: +Astral provides three REST APIs: | API | Purpose | Base URL | |-----|---------|----------| +| **Verify API** | Location proof verification | `/verify/v0` | | **Compute API** | Verifiable geospatial operations | `/compute/v0` | -| **Records API** | Query location attestations | `/api/v0` | +| **Records API** | Query location attestations | `/records/v0` | - + + + Verify location stamps and evaluate proof credibility + Distance, containment, proximity checks with signed attestations - Query existing location attestations across chains + Query location records across systems @@ -163,3 +167,149 @@ The following table shows addresses for supported chains. The **Attester Address Length of a line + +--- + +# Verify API + +The Verify API verifies location stamps and evaluates location proofs, returning credibility assessments with optional TEE-signed attestations. + +## Base URL + +``` +https://api.astral.global/verify/v0 +``` + +## Endpoints + +### POST /verify/v0/stamp + +Verify a stamp's internal validity (signatures, structure, signal consistency). Does **not** evaluate the stamp against a claim. + +```bash +curl https://api.astral.global/verify/v0/stamp \ + -H "Content-Type: application/json" \ + -d '{ + "stamp": { + "lpVersion": "0.2", + "locationType": "geojson-point", + "location": {"type": "Point", "coordinates": [2.2945, 48.8584]}, + "srs": "http://www.opengis.net/def/crs/OGC/1.3/CRS84", + "temporalFootprint": {"start": 1738200000, "end": 1738200300}, + "plugin": "mock", + "pluginVersion": "0.1.0", + "signals": {}, + "signatures": [{"signer": {"scheme": "eth-address", "value": "0x..."}, "algorithm": "secp256k1", "value": "0x...", "timestamp": 1738200000}] + } + }' +``` + +**Response (200):** + +```json +{ + "valid": true, + "signaturesValid": true, + "structureValid": true, + "signalsConsistent": true, + "details": { + "signatureRecovered": true, + "recoveredAddress": "0x..." + } +} +``` + +### POST /verify/v0/proof + +Verify a full location proof (claim + stamps) and return a credibility assessment with a signed EAS attestation. + +```bash +curl https://api.astral.global/verify/v0/proof \ + -H "Content-Type: application/json" \ + -d '{ + "proof": { + "claim": { + "lpVersion": "0.2", + "locationType": "geojson-point", + "location": {"type": "Point", "coordinates": [2.2945, 48.8584]}, + "srs": "http://www.opengis.net/def/crs/OGC/1.3/CRS84", + "subject": {"scheme": "eth-address", "value": "0x..."}, + "radius": 100, + "time": {"start": 1738200000, "end": 1738200300} + }, + "stamps": [...] + }, + "options": { + "chainId": 84532, + "schema": "0x...", + "recipient": "0x..." + } + }' +``` + +**Response (200):** + +```json +{ + "uid": "0x...", + "credibility": { + "confidence": 0.72, + "stampResults": [ + { + "stampIndex": 0, + "plugin": "mock", + "signaturesValid": true, + "structureValid": true, + "signalsConsistent": true, + "supportsClaim": true, + "claimSupportScore": 0.72, + "pluginResult": {} + } + ] + }, + "proof": { "..." : "..." }, + "attestation": { "..." : "..." }, + "delegatedAttestation": { "..." : "..." }, + "attester": "0x...", + "timestamp": 1738200000 +} +``` + +### GET /verify/v0/plugins + +List available verification plugins. + +**Response (200):** + +```json +{ + "plugins": [ + { + "name": "proofmode", + "version": "0.1.0", + "environments": ["react-native"], + "description": "ProofMode device-level location proofs" + }, + { + "name": "witnesschain", + "version": "0.1.0", + "environments": ["react-native", "node", "browser"], + "description": "WitnessChain network infrastructure proofs" + } + ] +} +``` + +## Error Format + +Errors follow the same [RFC 7807](https://tools.ietf.org/html/rfc7807) format as the Compute API: + +```json +{ + "type": "https://api.astral.global/errors/invalid-input", + "title": "Invalid Input", + "status": 400, + "detail": "stamp.signatures: Required", + "instance": "/verify/v0/stamp" +} +``` diff --git a/concepts/location-proofs.mdx b/concepts/location-proofs.mdx new file mode 100644 index 0000000..ce272b9 --- /dev/null +++ b/concepts/location-proofs.mdx @@ -0,0 +1,152 @@ +--- +title: "Location proofs" +description: "Evidence-based location verification through proof-of-location plugins" +--- + + + **Research Preview** — The location proof system is under active development. + + +# Location proofs + +GPS is spoofable. A user can claim to be anywhere in the world, and a smart contract has no way to challenge that claim. Astral's location proof system addresses this by combining evidence from multiple independent proof-of-location sources — device sensors, network measurements, hardware attestation — into a single credibility assessment. + +## The evidence problem + +Astral Location Services solve the *computation* problem — given location data, we can verifiably check spatial relationships. But computation alone doesn't answer: **was the user actually there?** + +```mermaid +graph LR + A["1. Location Verification
← Location Proofs"] --> B["2. Geospatial Computation
Astral Location Services"] + B --> C["3. Onchain Verification
EAS Resolvers"] +``` + +Location proofs fill in step 1. They provide *evidence* that a subject was at a claimed location, scored by credibility rather than binary yes/no. + +## Terminology + +| Term | Definition | +|------|-----------| +| **Claim** | An assertion that a subject was at a location during a time window | +| **Stamp** | Evidence from a single proof-of-location system (one observation) | +| **Proof** | A claim bundled with one or more stamps | +| **Verify** | Check a stamp's internal validity — signatures, structure, signal consistency | +| **Evaluate** | Assess how well stamps support a claim — SDK-side analysis producing a credibility vector | + +## How it works + +A location proof bundles a **claim** with supporting **stamps**: + +```typescript +interface LocationProof { + claim: LocationClaim; // "I was at [2.2945, 48.8584] at 14:30" + stamps: LocationStamp[]; // Evidence from ProofMode, WitnessChain, etc. +} +``` + +Each stamp carries evidence from a different proof-of-location system — device GPS with sensor data, network latency measurements, hardware attestation results. The verification system evaluates each stamp independently and then analyzes cross-correlation. + +### The verification pipeline + + + + Each stamp is checked for internal validity: are the cryptographic signatures correct? Is the data structure well-formed? Are the signals self-consistent? + + + Each stamp is evaluated against the claim. How close is the stamp's location to the claimed location? Does the temporal footprint overlap the claimed time window? The result is a **credibility vector** with spatial and temporal scores. + + + For multi-stamp proofs, the system analyzes independence (are stamps from different physical phenomena?) and agreement (do they corroborate each other?). Two stamps from different systems that agree are stronger than two from the same system. + + + A multidimensional credibility vector is produced with spatial, temporal, validity, and independence measurements. Applications define their own trust models to weight these dimensions according to their requirements. + + + +## Credibility, not certainty + +The credibility assessment is **not** a calibrated probability. It's a multidimensional measurement across four dimensions: + +- **Spatial**: How close are stamps to the claimed location? (mean/max distance, within-radius fraction) +- **Temporal**: How well do stamps overlap the claimed time window? (mean/min overlap, fully-overlapping fraction) +- **Validity**: Are signatures correct, structures well-formed, and signals consistent? (per-check fractions) +- **Independence**: Are stamps from different plugins? How well do they agree spatially? (unique plugin ratio, spatial agreement) + +Applications weight these dimensions according to their own trust models. A ProofMode stamp with self-reported GPS provides device-level evidence. A WitnessChain stamp using network latency triangulation provides independent infrastructure-level verification. Together, they score higher on the independence dimension than either alone. + +## The plugin architecture + +Plugins are the abstraction layer that makes Astral a framework. Each proof-of-location system — ProofMode, WitnessChain, and others in the future — is a plugin that implements a standard interface. + +```mermaid +graph TB + subgraph SDK["Astral SDK"] + Registry[Plugin Registry] + Stamps[stamps module] + Proofs[proofs module] + end + + subgraph Plugins + PM[ProofMode Plugin] + WC[WitnessChain Plugin] + Mock[Mock Plugin] + end + + Registry --> PM + Registry --> WC + Registry --> Mock + Stamps --> Registry + Proofs --> Registry +``` + +All methods on the plugin interface are optional. A mobile-only plugin might implement `collect`/`create`/`sign` but skip `verify` (letting the hosted service handle validation). A verification-only plugin might implement just `verify` for stamp validation. + + + How the plugin system works — interface, registry, runtime model + + +## Available plugins + + + + Device-level evidence: GPS, cell tower, WiFi, hardware attestation. Runs on Android via React Native. + + + Infrastructure-level evidence: network latency triangulation, multi-source IP geolocation. Runs everywhere. + + + Configurable test plugin with real ECDSA crypto. For development and testing without hardware. + + + +## Local vs hosted verification + +Plugins implement `verify` as regular code — you can run it anywhere. The distinction between local and hosted verification is whether you want a TEE attestation on top: + +| Mode | What happens | Attestation | +|------|-------------|-------------| +| **Local** | SDK calls plugin's `verify` directly and evaluates in the ProofsModule | None — you trust the plugin code | +| **Hosted** | SDK calls the Astral service, which runs the same plugin code in a TEE and evaluates server-side | Signed EAS attestation from TEE | + +```typescript +// Local verification — runs plugin code in your environment +const result = await astral.stamps.verify(stamp); +const assessment = await astral.proofs.verify(proof); + +// Hosted verification — same logic, but with TEE attestation +const assessment = await astral.proofs.verify(proof, { + mode: 'hosted', + endpoint: 'https://api.astral.global/verify/v0' +}); +``` + +Both paths produce the same credibility assessment. The hosted path adds a signed attestation you can submit onchain. + + + + End-to-end tutorial with the mock plugin + + + Build a custom proof-of-location plugin + + diff --git a/concepts/plugins.mdx b/concepts/plugins.mdx new file mode 100644 index 0000000..a41e645 --- /dev/null +++ b/concepts/plugins.mdx @@ -0,0 +1,208 @@ +--- +title: "Plugin architecture" +description: "How proof-of-location plugins work — interface, registry, runtime model" +--- + + + **Research Preview** — The plugin system is under active development. + + +# Plugin architecture + +Every proof-of-location system is different. ProofMode is a React Native native module that reads device sensors. WitnessChain is an HTTP client that queries a decentralized network. The plugin interface embraces this heterogeneity — one contract, many implementations. + +## The plugin interface + +```typescript +interface LocationProofPlugin { + readonly name: string; + readonly version: string; + readonly runtimes: Runtime[]; + readonly requiredCapabilities: string[]; + readonly description: string; + + collect?(options?: CollectOptions): Promise; + create?(signals: RawSignals): Promise; + sign?(stamp: UnsignedLocationStamp, signer: StampSigner): Promise; + verify?(stamp: LocationStamp): Promise; +} +``` + +All four methods are optional. Plugins implement what makes sense for their environment and capabilities. + +### Method lifecycle + +```mermaid +graph LR + A[collect] --> B[create] --> C[sign] --> D["LocationStamp"] + D --> E[verify] +``` + +| Method | Purpose | When to implement | +|--------|---------|-------------------| +| `collect` | Gather raw signals from the environment | Plugin has access to sensors, APIs, or hardware | +| `create` | Transform raw signals into an unsigned stamp | Plugin produces stamps (not just verifying existing ones) | +| `sign` | Cryptographically bind the stamp to a signer | Plugin stamps need signatures | +| `verify` | Check a stamp's internal validity | Plugin can validate its own stamp format | + +### Runtimes and capabilities + +Plugins declare where they can run and what they need: + +```typescript +type Runtime = 'react-native' | 'node' | 'browser'; +``` + +| Plugin | Runtimes | Required capabilities | +|--------|----------|----------------------| +| ProofMode | `react-native` | `gps`, `hardware-keystore`, `cell-radio` | +| WitnessChain | `react-native`, `node`, `browser` | `network` | +| Mock | `react-native`, `node`, `browser` | *(none)* | + +`runtimes` controls registration — the SDK rejects a plugin that doesn't support the current environment. `requiredCapabilities` is informational, helping developers understand why a plugin is restricted. + +## Plugin registry + +The SDK manages plugins through a `PluginRegistry` that validates runtime compatibility at registration time: + +```typescript +import { AstralSDK } from '@decentralized-geo/astral-sdk'; +import { MockPlugin } from '@decentralized-geo/plugin-mock'; +import { WitnessChainPlugin } from '@decentralized-geo/plugin-witnesschain'; + +const astral = new AstralSDK({ chainId: 84532 }); + +// Register plugins at startup +astral.plugins.register(new MockPlugin()); +astral.plugins.register(new WitnessChainPlugin({ proverId: '0x...' })); + +// Now use stamps/proofs/verify modules +const signals = await astral.stamps.collect({ plugins: ['mock'] }); +``` + +The registry automatically detects the current runtime (`react-native`, `node`, or `browser`) and throws if you register a plugin that doesn't support it. + +```typescript +// This throws in Node.js — ProofMode only runs on React Native +import { ProofModePlugin } from '@decentralized-geo/plugin-proofmode'; +astral.plugins.register(new ProofModePlugin()); // Error! +``` + +### Registry operations + +| Method | What it does | +|--------|-------------| +| `register(plugin)` | Add a plugin (validates runtime) | +| `get(name)` | Get a plugin by name (throws if not found) | +| `has(name)` | Check if a plugin is registered | +| `list()` | Get metadata for all plugins | +| `withMethod(method)` | Find plugins that implement a specific method | + +## The stamp lifecycle + +### 1. Collect raw signals + +```typescript +const signals = await astral.stamps.collect({ + plugins: ['proofmode', 'witnesschain'], + timeout: 10000, + accuracy: 'high', +}); +// Returns RawSignals[] — one per plugin +``` + +Each plugin gathers observations from its own sources. ProofMode reads device GPS, cell towers, WiFi APs, and hardware attestation. WitnessChain queries its decentralized network for latency-based location challenges. + +### 2. Create unsigned stamps + +```typescript +const unsigned = await astral.stamps.create( + { plugin: 'proofmode' }, + signals[0] +); +``` + +The plugin transforms its raw signals into an `UnsignedLocationStamp` — the standard format with location, temporal footprint, and plugin-specific signal data. + +### 3. Sign stamps + +```typescript +const stamp = await astral.stamps.sign( + { plugin: 'proofmode' }, + unsigned, + mySigner +); +``` + +The `StampSigner` abstraction lets plugins sign with whatever mechanism is appropriate — a hardware keystore, a software wallet, a PGP key. + +### 4. Bundle into a proof + +```typescript +const proof = astral.proofs.create(claim, [stamp1, stamp2]); +``` + +### 5. Verify + +```typescript +const assessment = await astral.proofs.verify(proof); +console.log(assessment.dimensions); // Multidimensional credibility vector +``` + +## Credibility vector + +When the SDK evaluates a proof, it returns a `CredibilityVector`: + +```typescript +interface CredibilityVector { + dimensions: { + spatial: { + meanDistanceMeters: number; + maxDistanceMeters: number; + withinRadiusFraction: number; + }; + temporal: { + meanOverlap: number; + minOverlap: number; + fullyOverlappingFraction: number; + }; + validity: { + signaturesValidFraction: number; + structureValidFraction: number; + signalsConsistentFraction: number; + }; + independence: { + uniquePluginRatio: number; + spatialAgreement: number; + pluginNames: string[]; + }; + }; + stampResults: StampVerificationResult[]; + meta: { + stampCount: number; + evaluatedAt: number; + evaluationMode: 'local' | 'hosted'; + }; +} +``` + +This multidimensional model lets applications define their own trust models. A stamp might have high temporal accuracy (captured at the right time) but lower spatial accuracy (location is approximate). Applications weight these dimensions according to their requirements. + +## How the hosted service uses plugins + +The Astral hosted service imports plugin packages and runs verify inside a TEE, then evaluates stamps against claims server-side. The service adds three things that local verification doesn't provide: + +1. **TEE execution** — plugin code runs in a trusted execution environment +2. **Signed attestation** — results are signed by a TEE-held key +3. **EAS integration** — attestation is formatted for onchain submission + +The verification and evaluation logic is the same. The difference is the trust model: local verification trusts your environment, hosted verification trusts the TEE. + + + + Build your first proof with the mock plugin + + + Implement the LocationProofPlugin interface + + diff --git a/concepts/verifiable-computation.mdx b/concepts/verifiable-computation.mdx index dd20eaa..057db6b 100644 --- a/concepts/verifiable-computation.mdx +++ b/concepts/verifiable-computation.mdx @@ -24,7 +24,7 @@ graph LR **Astral Location Services solve the middle step** — verifiable geospatial computation. Given location inputs, we can provably check spatial relationships and produce signed attestations for onchain use. - **What about step 1?** Verifying *where* a user actually is remains an open problem. GPS is spoofable. We're developing the [Location Proof framework](https://collective.flashbots.net/t/towards-stronger-location-proofs/5323) to address this. Location proofs will plug into Astral Location Services to complete the pipeline. + **What about step 1?** GPS is spoofable. Astral's [location proof system](/concepts/location-proofs) addresses this through a plugin architecture — evidence from device sensors ([ProofMode](/guides/proofmode-integration)), network infrastructure ([WitnessChain](/guides/witnesschain-integration)), and other sources is combined into credibility assessments. ## Why Verification Matters @@ -144,17 +144,17 @@ Location A + Location B → TEE (PostGIS) → Signed attestation: "distance = 45 Even with verifiable computation, users can lie about their location. GPS is spoofable. The attestation proves the *computation* was correct, not that the *inputs* were honest. -This is why we're developing the [Location Proof framework](https://collective.flashbots.net/t/towards-stronger-location-proofs/5323) — to provide evidence-based location claims that can feed into Astral Location Services for a fully verifiable pipeline. +Astral's [location proof plugins](/concepts/location-proofs) address this by combining evidence from multiple independent proof-of-location systems into credibility assessments. See the [creating location proofs](/guides/creating-location-proofs) guide to get started. - **Be upfront with your users**: Until location proof plugins are integrated, location verification relies on trusting the user's GPS. Astral Location Services provides verifiable *computation*, not verifiable *location*. + **Be upfront with your users**: Location proof confidence scores are heuristic assessments, not calibrated probabilities. A single GPS-based stamp is capped at ~0.7 confidence because the device controls the sensor. Multi-source proofs from independent systems provide stronger assurance. ## Why Build the Geospatial Layer First? Location proofs become useful when they can trigger onchain actions through geospatial policies. Astral Location Services apply those policies — checking if a location is inside a boundary, within range of a target, or satisfies other spatial constraints. -By building the geospatial policy infrastructure now, we can experiment with use cases and iterate while developing the [Location Proof plugins](https://collective.flashbots.net/t/towards-stronger-location-proofs/5323). As proof mechanisms mature, they plug directly into the existing pipeline. +By building the geospatial policy infrastructure now, we can experiment with use cases and iterate as [location proof plugins](/concepts/plugins) mature. New proof-of-location systems plug directly into the existing pipeline through the `LocationProofPlugin` interface. ## Determinism Guarantees diff --git a/guides/creating-location-proofs.mdx b/guides/creating-location-proofs.mdx new file mode 100644 index 0000000..2371f68 --- /dev/null +++ b/guides/creating-location-proofs.mdx @@ -0,0 +1,309 @@ +--- +title: "Creating location proofs" +description: "End-to-end tutorial: collect evidence, build a proof, verify credibility" +--- + + + **Research Preview** — Code snippets need testing against actual implementation. + + +# Creating location proofs + +This guide walks through the full location proof lifecycle using the mock plugin. By the end, you'll collect evidence, build a proof, verify it, and get a credibility assessment — the same flow that works with real plugins like ProofMode and WitnessChain. + +## Prerequisites + +```bash +npm install @decentralized-geo/astral-sdk @decentralized-geo/plugin-mock ethers +``` + +## Setup + +```typescript +import { AstralSDK } from '@decentralized-geo/astral-sdk'; +import { MockPlugin } from '@decentralized-geo/plugin-mock'; +import { ethers } from 'ethers'; + +// Create SDK instance +const astral = new AstralSDK({ chainId: 84532 }); + +// Register the mock plugin +astral.plugins.register(new MockPlugin({ + defaultLocation: { lat: 48.8584, lon: 2.2945 }, // Eiffel Tower + jitterMeters: 50, // Random offset up to 50m +})); +``` + +The mock plugin generates realistic stamps with real ECDSA signatures — everything except the actual sensor data is genuine. + +## Step 1: Collect evidence + +```typescript +const signals = await astral.stamps.collect({ + plugins: ['mock'], +}); + +console.log(signals[0]); +// { +// plugin: 'mock', +// timestamp: 1738200000, +// data: { +// latitude: 48.8587, +// longitude: 2.2941, +// accuracy: 12.5, +// provider: 'mock-gps', +// ... +// } +// } +``` + +With real plugins, `collect()` reads device sensors (ProofMode) or queries APIs (WitnessChain). The mock plugin generates plausible signals based on its configuration. + +## Step 2: Create an unsigned stamp + +```typescript +const unsigned = await astral.stamps.create( + { plugin: 'mock' }, + signals[0] +); + +console.log(unsigned.location); +// { type: 'Point', coordinates: [2.2941, 48.8587] } + +console.log(unsigned.plugin); +// 'mock' +``` + +The plugin transforms raw signals into the standard `UnsignedLocationStamp` format — a GeoJSON location, temporal footprint, and the original signals as structured data. + +## Step 3: Sign the stamp + +```typescript +// Create a signer (in production, use a hardware wallet or keystore) +const wallet = new ethers.Wallet(process.env.PRIVATE_KEY); +const signer = { + algorithm: 'secp256k1', + signer: { scheme: 'eth-address', value: wallet.address }, + sign: async (data: string) => wallet.signMessage(data), +}; + +const stamp = await astral.stamps.sign( + { plugin: 'mock' }, + unsigned, + signer +); + +console.log(stamp.signatures.length); // 1 +console.log(stamp.signatures[0].algorithm); // 'secp256k1' +``` + +## Step 4: Define a claim + +The claim is your assertion about where and when something happened: + +```typescript +const claim = { + lpVersion: '0.2', + locationType: 'geojson-point', + location: { type: 'Point', coordinates: [2.2945, 48.8584] }, + srs: 'http://www.opengis.net/def/crs/OGC/1.3/CRS84', + subject: { scheme: 'eth-address', value: wallet.address }, + radius: 100, // Claiming to be within 100m + time: { + start: Math.floor(Date.now() / 1000) - 300, // 5 minutes ago + end: Math.floor(Date.now() / 1000), + }, +}; +``` + +## Step 5: Build a proof + +```typescript +const proof = astral.proofs.create(claim, [stamp]); + +console.log(proof.stamps.length); // 1 +``` + +A proof bundles your claim with supporting evidence. You can include stamps from multiple plugins for stronger credibility. + +## Step 6: Verify + +```typescript +const assessment = await astral.proofs.verify(proof); + +console.log(assessment.dimensions); +// { +// spatial: { +// meanDistanceMeters: 45.2, +// maxDistanceMeters: 45.2, +// withinRadiusFraction: 1.0 +// }, +// temporal: { +// meanOverlap: 1.0, +// minOverlap: 1.0, +// fullyOverlappingFraction: 1.0 +// }, +// validity: { +// signaturesValidFraction: 1.0, +// structureValidFraction: 1.0, +// signalsConsistentFraction: 1.0 +// }, +// independence: { +// uniquePluginRatio: 1.0, +// spatialAgreement: 1.0, +// pluginNames: ['mock'] +// } +// } + +console.log(assessment.stampResults[0]); +// { +// stampIndex: 0, +// plugin: 'mock', +// signaturesValid: true, +// structureValid: true, +// signalsConsistent: true, +// supportsClaim: true, +// distanceMeters: 45.2, +// temporalOverlap: 1.0 +// } +``` + +--- + +## Multi-stamp proofs + +Stronger proofs use evidence from independent sources. Register multiple plugins and collect from all of them: + +```typescript +import { WitnessChainPlugin } from '@decentralized-geo/plugin-witnesschain'; + +astral.plugins.register(new WitnessChainPlugin({ proverId: '0x...' })); + +// Collect from both +const signals = await astral.stamps.collect({ + plugins: ['mock', 'witnesschain'], +}); + +// Create and sign stamps from each +const stamps = []; +for (const s of signals) { + const unsigned = await astral.stamps.create({ plugin: s.plugin }, s); + const signed = await astral.stamps.sign({ plugin: s.plugin }, unsigned, signer); + stamps.push(signed); +} + +// Build multi-stamp proof +const proof = astral.proofs.create(claim, stamps); +const assessment = await astral.proofs.verify(proof); + +console.log(assessment.dimensions.independence); +// { +// uniquePluginRatio: 1.0, // All stamps from different plugins +// spatialAgreement: 0.95, // Stamps agree on location +// pluginNames: ['mock', 'witnesschain'] +// } + +console.log(assessment.dimensions.spatial); +// { +// meanDistanceMeters: 35.8, // Average distance from claim +// maxDistanceMeters: 52.1, // Worst case distance +// withinRadiusFraction: 1.0 // All stamps within radius +// } +``` + +Multi-stamp proofs from independent systems provide stronger evidence through cross-validation: +- **Independence**: `uniquePluginRatio` measures how many different plugins were used +- **Agreement**: `spatialAgreement` measures how closely stamps corroborate each other +- Applications define their own trust models to weight these dimensions + +## Verifying a single stamp + +If you just want to check whether a stamp is valid without evaluating it against a claim: + +```typescript +const result = await astral.stamps.verify(stamp); + +console.log(result.valid); // true/false +console.log(result.signaturesValid); // Crypto signatures check out +console.log(result.structureValid); // Data format is correct +console.log(result.signalsConsistent); // Signals don't contradict each other +``` + +## Hosted verification + +For onchain use, verify through the Astral hosted service to get a TEE-signed attestation: + +```typescript +// Local verification — no attestation +const local = await astral.proofs.verify(proof); + +// Hosted verification — adds TEE attestation +const hosted = await astral.proofs.verify(proof, { + mode: 'hosted', + endpoint: 'https://api.astral.global/verify/v0' +}); +// hosted includes attestation and delegatedAttestation for onchain submission +``` + + + Hosted verification is not yet available. Use local verification for now. + + +## Putting it together + +Here's the full flow in one script: + +```typescript +import { AstralSDK } from '@decentralized-geo/astral-sdk'; +import { MockPlugin } from '@decentralized-geo/plugin-mock'; +import { ethers } from 'ethers'; + +// Setup +const astral = new AstralSDK({ chainId: 84532 }); +astral.plugins.register(new MockPlugin({ + defaultLocation: { lat: 48.8584, lon: 2.2945 }, +})); + +const wallet = new ethers.Wallet(process.env.PRIVATE_KEY); +const signer = { + algorithm: 'secp256k1', + signer: { scheme: 'eth-address', value: wallet.address }, + sign: async (data: string) => wallet.signMessage(data), +}; + +// Collect → Create → Sign +const signals = await astral.stamps.collect({ plugins: ['mock'] }); +const unsigned = await astral.stamps.create({ plugin: 'mock' }, signals[0]); +const stamp = await astral.stamps.sign({ plugin: 'mock' }, unsigned, signer); + +// Claim + Proof +const claim = { + lpVersion: '0.2', + locationType: 'geojson-point', + location: { type: 'Point', coordinates: [2.2945, 48.8584] }, + srs: 'http://www.opengis.net/def/crs/OGC/1.3/CRS84', + subject: { scheme: 'eth-address', value: wallet.address }, + radius: 100, + time: { + start: Math.floor(Date.now() / 1000) - 300, + end: Math.floor(Date.now() / 1000), + }, +}; + +const proof = astral.proofs.create(claim, [stamp]); + +// Verify +const assessment = await astral.proofs.verify(proof); +console.log('Spatial accuracy:', assessment.dimensions.spatial.meanDistanceMeters, 'meters'); +console.log('Signatures valid:', assessment.stampResults[0].signaturesValid); +console.log('Supports claim:', assessment.stampResults[0].supportsClaim); +``` + + + + Use real device sensors on Android + + + Use network infrastructure verification + + diff --git a/guides/location-gated-nft.mdx b/guides/location-gated-nft.mdx index 3725e2f..34788b8 100644 --- a/guides/location-gated-nft.mdx +++ b/guides/location-gated-nft.mdx @@ -12,7 +12,7 @@ description: "Build an NFT that requires physical presence to mint" Create an NFT collection where minting requires passing a geospatial policy check — verifying the user is within range of a target location. - **About location verification**: This guide uses GPS coordinates as input. GPS is spoofable. We're working on [Location Proof plugins](https://collective.flashbots.net/t/towards-stronger-location-proofs/5323) that will replace `navigator.geolocation` for stronger verification — these are still in development. + **About location verification**: This guide uses GPS coordinates as input. GPS is spoofable. For stronger verification, see [location proofs](/concepts/location-proofs) — Astral's plugin system combines evidence from device sensors, network infrastructure, and other sources into credibility assessments. See the [creating location proofs](/guides/creating-location-proofs) guide to integrate location proofs into your app. ## Overview @@ -207,7 +207,7 @@ console.log('Schema UID:', SCHEMA_UID); ## Step 4: Frontend Integration - **Location source**: The `navigator.geolocation` API provides GPS coordinates which are spoofable. In production, replace with [Location Proof plugins](https://collective.flashbots.net/t/towards-stronger-location-proofs/5323) as they become available. + **Location source**: The `navigator.geolocation` API provides GPS coordinates which are spoofable. For stronger verification, use [location proof plugins](/concepts/location-proofs) to collect evidence from multiple independent sources. ```typescript diff --git a/guides/proofmode-integration.mdx b/guides/proofmode-integration.mdx new file mode 100644 index 0000000..0f8e0a4 --- /dev/null +++ b/guides/proofmode-integration.mdx @@ -0,0 +1,203 @@ +--- +title: "ProofMode integration" +description: "Device-level location proofs with ProofMode on Android" +--- + + + **Research Preview** — The React Native bridge is under development. The parser and verification are available now. + + +# ProofMode integration + +[ProofMode](https://proofmode.org/) by the Guardian Project captures rich device-level evidence — GPS coordinates, cell tower data, WiFi networks, hardware attestation (SafetyNet/Play Integrity), and PGP signatures. The Astral ProofMode plugin parses these proof bundles, verifies their cryptographic signatures, and evaluates them against location claims. + +## What ProofMode provides + +Each ProofMode proof bundle is a ZIP file containing: + +| File | Content | +|------|---------| +| Media file | Photo or video that triggered the proof | +| `.csv` / `.json` | 33+ signal fields (GPS, cell, WiFi, device info) | +| `.asc` | PGP detached signature | +| `pubkey.asc` | PGP public key | +| `.gst` | SafetyNet/Play Integrity JWT | +| `.ots` | OpenTimestamps proof (optional) | +| SHA-256 hash | Integrity check for the media file | + +## Installation + +```bash +npm install @decentralized-geo/plugin-proofmode +``` + + + `@decentralized-geo/plugin-proofmode` requires `@decentralized-geo/astral-sdk` as a peer dependency. + + +## Parsing proof bundles + +The most immediate use case is accepting ProofMode bundles however they arrive — exported from the app, shared via a file, uploaded through an API — and verifying them. + +```typescript +import { ProofModePlugin } from '@decentralized-geo/plugin-proofmode'; +import { parseProofBundle } from '@decentralized-geo/plugin-proofmode/parse'; + +const plugin = new ProofModePlugin(); + +// Parse a proof bundle from a ZIP file +const bundle = await parseProofBundle(zipBuffer); + +console.log(bundle.signals); +// { +// 'Location.Latitude': 48.8584, +// 'Location.Longitude': 2.2945, +// 'Location.Accuracy': 12.5, +// 'Location.Provider': 'gps', +// 'CellInfo.CellId': 12345, +// 'WiFi.SSID': 'CafeWifi', +// 'Device.Hardware': 'Pixel 7', +// ...33+ fields +// } +``` + +### Signal fields + +ProofMode captures a rich set of signals from the device: + +| Category | Key fields | +|----------|-----------| +| **GPS** | `Location.Latitude`, `Location.Longitude`, `Location.Accuracy`, `Location.Provider`, `Location.Speed`, `Location.Bearing` | +| **Cell** | `CellInfo.CellId`, `CellInfo.LAC`, `CellInfo.MCC`, `CellInfo.MNC`, `CellInfo.SignalStrength` | +| **WiFi** | `WiFi.SSID`, `WiFi.BSSID`, `WiFi.SignalStrength` | +| **Device** | `Device.Hardware`, `Device.Manufacturer`, `Device.Model`, `Device.Fingerprint` | +| **Timestamps** | `File.DateTimeOriginal`, `Proof.Timestamp` | + +## Verification + +The plugin verifies three layers of integrity: + +```typescript +const stamp = await plugin.create(await plugin.collect()); +const result = await plugin.verify(stamp); + +console.log(result.signaturesValid); // PGP signature checks +console.log(result.structureValid); // Bundle format and completeness +console.log(result.signalsConsistent); // Signal cross-checks +console.log(result.details); +// { +// pgpSignatureValid: true, +// sha256HashValid: true, +// safetyNetPassed: true, +// basicIntegrity: true, +// ctsProfileMatch: true, +// } +``` + +### What gets verified + +1. **PGP signatures** — detached `.asc` signature verified against included `pubkey.asc` +2. **SHA-256 hash** — media file integrity check +3. **SafetyNet/Play Integrity** — JWT structure and `basicIntegrity`/`ctsProfileMatch` flags +4. **OpenTimestamps** — proof timestamp verification (if present) +5. **Signal consistency** — location provider matches accuracy range, timestamps are coherent + + + **v0 limitation**: SafetyNet/Play Integrity JWT certificate chain validation is not performed. The plugin checks the JWT structure and flag values but does not validate the signing certificate against Google's root. This is a known limitation documented in the threat model. + + +## Evaluating proofs + +When you verify a proof containing ProofMode stamps, the SDK evaluates them as part of the multidimensional credibility assessment: + +```typescript +const claim = { + lpVersion: '0.2', + locationType: 'geojson-point', + location: { type: 'Point', coordinates: [2.2945, 48.8584] }, + srs: 'http://www.opengis.net/def/crs/OGC/1.3/CRS84', + subject: { scheme: 'eth-address', value: '0x...' }, + radius: 100, + time: { start: 1738200000, end: 1738200300 }, +}; + +const proof = astral.proofs.create(claim, [stamp]); +const assessment = await astral.proofs.verify(proof); + +console.log(assessment.dimensions.spatial); +// { +// meanDistanceMeters: 45, +// maxDistanceMeters: 45, +// withinRadiusFraction: 1.0 +// } + +console.log(assessment.dimensions.validity); +// { +// signaturesValidFraction: 1.0, +// structureValidFraction: 1.0, +// signalsConsistentFraction: 1.0 +// } +``` + +### What the SDK analyzes + +The SDK's proof evaluation considers ProofMode-specific signals: +- **Spatial accuracy**: Distance from claim location, weighted by `Location.Accuracy` +- **Temporal accuracy**: Overlap between stamp footprint and claim window +- **SafetyNet integrity**: Whether `basicIntegrity` and `ctsProfileMatch` flags are present +- **Signal completeness**: Whether key signals (GPS, cell, WiFi) are present + +Applications define their own trust models to weight these dimensions. + + + **Device-level evidence**: ProofMode GPS is self-reported by the device. A compromised device can report any coordinates. Pair ProofMode with an independent source (like WitnessChain) to validate device claims with infrastructure-level evidence. This is captured in the `independence` dimension. + + +## React Native bridge + + + **Coming soon** — The React Native native module that wraps `libproofmode` for live evidence collection is under development. The parser and verification above work now. + + +When the RN bridge is available, the full lifecycle will work on mobile: + +```typescript +import { ProofModePlugin } from '@decentralized-geo/plugin-proofmode'; + +const plugin = new ProofModePlugin(); + +// Collect triggers camera capture and gathers all device signals +const signals = await plugin.collect({ timeout: 15000 }); + +// Create stamp from device evidence +const unsigned = await plugin.create(signals); + +// Sign with device keystore +const stamp = await plugin.sign(unsigned, deviceSigner); +``` + +### Android setup + +The Android bridge wraps the `android-libproofmode` Maven artifact (`org.witness:android-libproofmode`). Required permissions: + +- `ACCESS_FINE_LOCATION` — GPS coordinates +- `READ_PHONE_STATE` — Cell tower information +- `ACCESS_WIFI_STATE` — WiFi network scanning +- `CAMERA` — Media capture (ProofMode triggers on photo/video) +- `READ_EXTERNAL_STORAGE` / `WRITE_EXTERNAL_STORAGE` — Proof bundle storage + +### The media trigger + +ProofMode's evidence collection is triggered by photo or video capture — there is no `generateLocationProof()` API without media. The plugin supports two modes: + +1. **Camera trigger** — `collect()` opens the camera; evidence is captured alongside the photo +2. **Bundle adapter** — accept pre-existing ProofMode bundles from the app's export feature + + + + Add infrastructure-level verification + + + Full tutorial with the mock plugin + + diff --git a/guides/witnesschain-integration.mdx b/guides/witnesschain-integration.mdx new file mode 100644 index 0000000..c912b7b --- /dev/null +++ b/guides/witnesschain-integration.mdx @@ -0,0 +1,220 @@ +--- +title: "WitnessChain integration" +description: "Infrastructure-level location proofs via network latency triangulation" +--- + + + **Research Preview** — Code snippets need testing against actual implementation. + + +# WitnessChain integration + +[WitnessChain](https://www.witnesschain.com/) verifies location through network infrastructure — latency measurements, IP geolocation from multiple sources, and decentralized challenge protocols. Unlike device-based systems, the evidence comes from infrastructure the user doesn't control, providing an independent signal that's harder to spoof. + +## How WitnessChain works + +WitnessChain uses a challenge-response protocol: + +1. A **challenger** node sends network probes to a **prover** (the user's machine) +2. Round-trip latency measurements triangulate the prover's physical location +3. Multiple IP geolocation services provide independent estimates +4. Results are signed with ECDSA by the challenger +5. A `consolidated_result` combines all signals into a location determination + +## Installation + +```bash +npm install @decentralized-geo/plugin-witnesschain +``` + + + `@decentralized-geo/plugin-witnesschain` requires `@decentralized-geo/astral-sdk` and `ethers` as peer dependencies. + + +## Quick start + +```typescript +import { AstralSDK } from '@decentralized-geo/astral-sdk'; +import { WitnessChainPlugin } from '@decentralized-geo/plugin-witnesschain'; + +const astral = new AstralSDK({ chainId: 84532 }); + +astral.plugins.register(new WitnessChainPlugin({ + proverId: '0x1234...', // The prover address to fetch challenges for + apiUrl: 'https://api.witnesschain.com', +})); +``` + +## Fetching historical proofs + +The primary mode for v0 is fetching existing challenge results: + +```typescript +const signals = await astral.stamps.collect({ + plugins: ['witnesschain'], +}); + +console.log(signals[0].data); +// { +// challengeId: 'abc123', +// challenger: '0xchallenger...', +// prover: '0xprover...', +// challengeStatus: 'success', +// claims: { latitude: 48.858, longitude: 2.295 }, +// consolidated_result: { +// verified: true, +// latitude: 48.857, +// longitude: 2.294, +// KnowLocUncertaintyKm: 5.2, +// ... +// }, +// pingDelay: { avg: 12.5, min: 10, max: 15 }, +// ... +// } +``` + +This queries the WitnessChain API for completed challenges associated with the prover address. + + + **On-demand challenges** (triggering a new challenge via the API) require POINTS tokens and are not available in v0. The plugin throws a descriptive error if you attempt this. Use historical proofs for now. + + +## Creating stamps + +```typescript +const unsigned = await astral.stamps.create( + { plugin: 'witnesschain' }, + signals[0] +); + +console.log(unsigned.location); +// { type: 'Point', coordinates: [2.295, 48.858] } + +console.log(unsigned.signals); +// { +// challengeId: 'abc123', +// challenger: '0xchallenger...', +// pingDelay: { avg: 12.5, min: 10, max: 15 }, +// consolidatedResult: { verified: true, ... }, +// knowLocUncertaintyKm: 5.2, +// } +``` + +The plugin extracts location from the challenge result's `claims` field and maps all relevant signals into the stamp. + +## Verification + +WitnessChain stamps are verified with real ECDSA cryptography: + +```typescript +const result = await astral.stamps.verify(stamp); + +console.log(result.signaturesValid); // ECDSA signature recovery +console.log(result.structureValid); // lpVersion, plugin name, coordinates +console.log(result.signalsConsistent); // consolidated_result structure +console.log(result.details); +// { +// challengeSignatureValid: true, +// recoveredSigner: '0xchallenger...', +// coordinatesInRange: true, +// consolidatedResultPresent: true, +// } +``` + +### What gets verified + +1. **Challenge signature** — ECDSA signature on the challenge message is recovered and compared against the `challenger` address +2. **Stamp-level signatures** — any Astral-level signatures on the stamp are verified +3. **Structure** — `lpVersion` is `0.2`, `plugin` is `witnesschain`, coordinates are valid +4. **Consolidated result** — the `consolidated_result` object has the expected structure + +## Evaluating proofs + +When you verify a proof containing WitnessChain stamps, the SDK evaluates them as part of the multidimensional credibility assessment: + +```typescript +const proof = astral.proofs.create(claim, [stamp]); +const assessment = await astral.proofs.verify(proof); + +console.log(assessment.dimensions.spatial); +// { +// meanDistanceMeters: 450, +// maxDistanceMeters: 450, +// withinRadiusFraction: 1.0 +// } + +console.log(assessment.dimensions.temporal); +// { +// meanOverlap: 1.0, +// minOverlap: 1.0, +// fullyOverlappingFraction: 1.0 +// } + +console.log(assessment.stampResults[0].distanceMeters); +// 450 +``` + +### What the SDK analyzes + +The SDK's proof evaluation considers WitnessChain-specific signals: +- **Spatial accuracy**: Distance from claim location, weighted by `KnowLocUncertaintyKm` +- **Temporal accuracy**: Challenge timestamp vs claim time window +- **Consolidated result**: Whether `consolidated_result.verified === true` +- **IP geolocation agreement**: Agreement between multiple IP geolocation sources + +Applications define their own trust models to weight these dimensions. + +## Multi-stamp proofs + +WitnessChain is most powerful when combined with device-level evidence. A proof with both ProofMode (device sensors) and WitnessChain (network infrastructure) stamps gets an independence bonus because the evidence comes from fundamentally different physical phenomena: + +```typescript +import { MockPlugin } from '@decentralized-geo/plugin-mock'; + +// Register both plugins +astral.plugins.register(new MockPlugin()); // Simulating ProofMode +astral.plugins.register(new WitnessChainPlugin({ proverId: '0x...' })); + +// Collect from both +const signals = await astral.stamps.collect(); + +// Build and sign stamps +const stamps = []; +for (const s of signals) { + const unsigned = await astral.stamps.create({ plugin: s.plugin }, s); + const signed = await astral.stamps.sign({ plugin: s.plugin }, unsigned, signer); + stamps.push(signed); +} + +// Multi-stamp proof +const proof = astral.proofs.create(claim, stamps); +const assessment = await astral.proofs.verify(proof); + +console.log(assessment.dimensions.independence); +// { +// uniquePluginRatio: 1.0, // Fully independent +// spatialAgreement: 0.95, // Stamps agree on location +// pluginNames: ['mock', 'witnesschain'] +// } +``` + +## Authentication + +WitnessChain's API uses wallet-based authentication — you sign a challenge message to prove you're the prover: + +```typescript +const plugin = new WitnessChainPlugin({ + proverId: '0x1234...', + apiUrl: 'https://api.witnesschain.com', + // Authentication is handled internally when collect() is called +}); +``` + + + + Add device-level evidence + + + Full tutorial with the mock plugin + + diff --git a/guides/writing-plugins.mdx b/guides/writing-plugins.mdx new file mode 100644 index 0000000..d9bc7c6 --- /dev/null +++ b/guides/writing-plugins.mdx @@ -0,0 +1,315 @@ +--- +title: "Writing a plugin" +description: "Build a custom proof-of-location plugin for the Astral framework" +--- + + + **Research Preview** — The plugin interface is under active development and may change. + + +# Writing a plugin + +Any proof-of-location system can integrate with Astral by implementing the `LocationProofPlugin` interface. This guide walks through building a plugin from scratch. + +## The interface + +```typescript +import type { + LocationProofPlugin, + Runtime, + CollectOptions, + RawSignals, + UnsignedLocationStamp, + LocationStamp, + StampSigner, + StampVerificationResult, + CredibilityVector, + LocationClaim, +} from '@decentralized-geo/astral-sdk/plugins'; +``` + +All four methods are optional. Implement what makes sense for your system: + +| If your system... | Implement | +|-------------------|-----------| +| Collects evidence from sensors/APIs | `collect`, `create`, `sign` | +| Can verify its own stamps | `verify` | +| Does all of the above | All four | + +## Scaffold + +```typescript +import type { + LocationProofPlugin, + Runtime, + CollectOptions, + RawSignals, + UnsignedLocationStamp, + LocationStamp, + StampSigner, + StampVerificationResult, +} from '@decentralized-geo/astral-sdk/plugins'; + +export class MyPlugin implements LocationProofPlugin { + readonly name = 'my-plugin'; + readonly version = '0.1.0'; + readonly runtimes: Runtime[] = ['node', 'browser']; + readonly requiredCapabilities = ['network']; + readonly description = 'My custom proof-of-location plugin'; + + async collect(options?: CollectOptions): Promise { + // Gather evidence from your system + } + + async create(signals: RawSignals): Promise { + // Transform signals into a stamp + } + + async sign( + stamp: UnsignedLocationStamp, + signer: StampSigner + ): Promise { + // Add cryptographic signature + } + + async verify(stamp: LocationStamp): Promise { + // Check internal validity + } +} +``` + +## Implementing collect + +`collect` gathers raw observations from your proof-of-location system. Return whatever data you have — coordinates, measurements, attestations, timestamps. + +```typescript +async collect(options?: CollectOptions): Promise { + const response = await fetch('https://my-location-api.com/proof', { + signal: options?.timeout + ? AbortSignal.timeout(options.timeout) + : undefined, + }); + const data = await response.json(); + + return { + plugin: this.name, + timestamp: Math.floor(Date.now() / 1000), + data: { + latitude: data.lat, + longitude: data.lon, + accuracy: data.accuracy_meters, + source: data.source, + rawResponse: data, + }, + }; +} +``` + +## Implementing create + +`create` transforms raw signals into an `UnsignedLocationStamp` — the standard format the verification system understands. + +```typescript +async create(signals: RawSignals): Promise { + const { latitude, longitude, accuracy } = signals.data as { + latitude: number; + longitude: number; + accuracy: number; + }; + + return { + lpVersion: '0.2', + locationType: 'geojson-point', + location: { + type: 'Point', + coordinates: [longitude, latitude], + }, + srs: 'http://www.opengis.net/def/crs/OGC/1.3/CRS84', + temporalFootprint: { + start: signals.timestamp, + end: signals.timestamp, + }, + plugin: this.name, + pluginVersion: this.version, + signals: signals.data as Record, + }; +} +``` + +### Key requirements + +- `lpVersion` must be `'0.2'` +- `location` should be a GeoJSON geometry object (coordinates in `[lon, lat]` order) +- `srs` should be the OGC CRS84 URI for WGS84 +- `temporalFootprint` uses Unix seconds +- `signals` preserves the raw evidence for verification + +## Implementing sign + +`sign` adds cryptographic binding using the `StampSigner` abstraction: + +```typescript +async sign( + stamp: UnsignedLocationStamp, + signer: StampSigner +): Promise { + const message = JSON.stringify(stamp); + const signatureValue = await signer.sign(message); + + return { + ...stamp, + signatures: [ + { + signer: signer.signer, + algorithm: signer.algorithm, + value: signatureValue, + timestamp: Math.floor(Date.now() / 1000), + }, + ], + }; +} +``` + +The `StampSigner` is provided by the application — it might be an ethers wallet, a hardware keystore, or a PGP key. Your plugin doesn't need to know. + +## Implementing verify + +`verify` checks a stamp's internal validity. Return `StampVerificationResult` with four boolean flags and a details object. + +```typescript +async verify(stamp: LocationStamp): Promise { + const details: Record = {}; + let signaturesValid = true; + let structureValid = true; + let signalsConsistent = true; + + // Check structure + if (stamp.lpVersion !== '0.2' || stamp.plugin !== this.name) { + structureValid = false; + details.structureError = 'Wrong lpVersion or plugin name'; + } + + // Check signatures (example with ethers) + for (const sig of stamp.signatures) { + try { + const message = JSON.stringify({ + lpVersion: stamp.lpVersion, + locationType: stamp.locationType, + location: stamp.location, + srs: stamp.srs, + temporalFootprint: stamp.temporalFootprint, + plugin: stamp.plugin, + pluginVersion: stamp.pluginVersion, + signals: stamp.signals, + }); + const recovered = ethers.verifyMessage(message, sig.value); + if (recovered.toLowerCase() !== sig.signer.value.toLowerCase()) { + signaturesValid = false; + details.signatureError = 'Recovered address mismatch'; + } + } catch { + signaturesValid = false; + details.signatureError = 'Signature verification failed'; + } + } + + // Check signal consistency (plugin-specific) + const signals = stamp.signals; + if (signals.accuracy && (signals.accuracy as number) < 0) { + signalsConsistent = false; + details.signalError = 'Negative accuracy'; + } + + return { + valid: signaturesValid && structureValid && signalsConsistent, + signaturesValid, + structureValid, + signalsConsistent, + details, + }; +} +``` + + + **Evaluation is SDK-side**: Plugins no longer implement `evaluate()`. The SDK's `ProofsModule` handles evaluation by analyzing all stamps against the claim to produce a multidimensional credibility vector. + + +## Package structure + +``` +my-plugin/ + src/ + index.ts # Plugin class + exports + collect.ts # Evidence collection + create.ts # Signal → stamp transformation + verify.ts # Stamp verification + types.ts # Plugin-specific types + test/ + fixtures/ # Test data + plugin.test.ts # Tests + package.json + tsconfig.json +``` + +### package.json + +```json +{ + "name": "@your-org/plugin-my-system", + "version": "0.1.0", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "peerDependencies": { + "@decentralized-geo/astral-sdk": ">=0.3.0" + } +} +``` + +## Testing your plugin + +Follow the mock plugin's test patterns — test the full lifecycle: + +```typescript +import { describe, it, expect } from 'vitest'; +import { MyPlugin } from '../src/index'; + +describe('MyPlugin', () => { + const plugin = new MyPlugin(); + + it('collects signals', async () => { + const signals = await plugin.collect(); + expect(signals.plugin).toBe('my-plugin'); + expect(signals.data.latitude).toBeDefined(); + }); + + it('full lifecycle: collect → create → sign → verify', async () => { + const signals = await plugin.collect(); + const unsigned = await plugin.create(signals); + const stamp = await plugin.sign(unsigned, testSigner); + + const verification = await plugin.verify(stamp); + expect(verification.valid).toBe(true); + expect(verification.signaturesValid).toBe(true); + expect(verification.structureValid).toBe(true); + }); +}); +``` + +## Registration + +Users of your plugin register it with the SDK at startup: + +```typescript +import { AstralSDK } from '@decentralized-geo/astral-sdk'; +import { MyPlugin } from '@your-org/plugin-my-system'; + +const astral = new AstralSDK({ chainId: 84532 }); +astral.plugins.register(new MyPlugin()); + +// Now available through stamps/proofs/verify modules +const signals = await astral.stamps.collect({ plugins: ['my-plugin'] }); +``` + + + Deep dive into the plugin system design + diff --git a/how-it-works.mdx b/how-it-works.mdx index cb5c12c..c50fc62 100644 --- a/how-it-works.mdx +++ b/how-it-works.mdx @@ -155,9 +155,9 @@ There are two distinct trust questions in location-based systems: ### 1. Are the inputs truthful? -This is the **location verification problem** — how do you know a user was actually at the location they claim? GPS is spoofable, and proving physical presence remains an open research problem. +This is the **location verification problem** — how do you know a user was actually at the location they claim? GPS is spoofable, and proving physical presence requires evidence from multiple independent sources. -We're actively working on this through our [Location Proof framework](https://collective.flashbots.net/t/towards-stronger-location-proofs/5323). For now, Astral Location Services trust the location data provided as input. +Astral's [location proof system](/concepts/location-proofs) addresses this through a plugin architecture. Evidence from device sensors ([ProofMode](/guides/proofmode-integration)), network infrastructure ([WitnessChain](/guides/witnesschain-integration)), and other sources is combined into credibility assessments scored from 0 to 1. ### 2. Was the computation performed correctly? diff --git a/introduction.mdx b/introduction.mdx index 645102c..3e20724 100644 --- a/introduction.mdx +++ b/introduction.mdx @@ -3,6 +3,10 @@ title: "Introduction" description: "Geospatial policy engine for Ethereum" --- +
+ +
+ **Research Preview** — Astral Location Services are under active development and not yet production-ready. APIs may change. We're building in public and welcome your feedback! @@ -95,7 +99,7 @@ Astral solves this by providing: This introduces **location as a new onchain primitive** — enabling smart contracts to reason about geography for the first time. - **What about location verification?** GPS is spoofable, and verifying *where* a user actually is remains an open problem. We're developing the [Location Proof framework](https://collective.flashbots.net/t/towards-stronger-location-proofs/5323) to address this. Astral Location Services provide the geospatial policy layer that location proofs will plug into. + **What about location verification?** GPS is spoofable. Astral's [location proof system](/concepts/location-proofs) addresses this by combining evidence from multiple independent proof-of-location plugins — device sensors ([ProofMode](/guides/proofmode-integration)), network infrastructure ([WitnessChain](/guides/witnesschain-integration)), and more — into credibility assessments. The plugin architecture is extensible, so new proof-of-location systems can integrate as they emerge. ## What You Can Build @@ -117,7 +121,7 @@ This introduces **location as a new onchain primitive** — enabling smart contr ## The SDK -The Astral SDK provides a namespaced API for location attestations and geospatial computations: +The Astral SDK provides a namespaced API for location attestations, geospatial computations, and location verification: ```typescript // Location attestations @@ -128,7 +132,12 @@ astral.location.onchain.create(input); astral.compute.distance(from, to, options); astral.compute.contains(container, containee, options); astral.compute.within(geometry, target, radius, options); -astral.compute.submit(delegatedAttestation); + +// Location verification (new in v0.3.0) +astral.stamps.collect({ plugins: ['proofmode', 'witnesschain'] }); +astral.stamps.verify(stamp); +astral.proofs.create(claim, stamps); +astral.proofs.verify(proof); ``` diff --git a/mint.json b/mint.json index 575ceaf..267d6a1 100644 --- a/mint.json +++ b/mint.json @@ -1,6 +1,7 @@ { "$schema": "https://mintlify.com/schema.json", "name": "Astral Location Services", + "js": "/scripts/ascii-globe.js", "redirects": [ { "source": "/", @@ -73,7 +74,9 @@ "concepts/geospatial-operations", "concepts/policy-attestations", "concepts/eas-resolvers", - "concepts/verifiable-computation" + "concepts/verifiable-computation", + "concepts/location-proofs", + "concepts/plugins" ] }, { @@ -81,7 +84,11 @@ "pages": [ "guides/location-gated-nft", "guides/geofenced-token", - "guides/delivery-verification" + "guides/delivery-verification", + "guides/creating-location-proofs", + "guides/proofmode-integration", + "guides/witnesschain-integration", + "guides/writing-plugins" ] }, { diff --git a/scripts/ascii-globe.js b/scripts/ascii-globe.js new file mode 100644 index 0000000..97be82c --- /dev/null +++ b/scripts/ascii-globe.js @@ -0,0 +1,264 @@ +/** + * Interactive ASCII Globe with Mouse Repulsion + * A rotating 3D globe made of ASCII characters that react to mouse movement + */ + +(function() { + 'use strict'; + + // Wait for DOM to be ready + function initGlobe() { + const canvas = document.getElementById('ascii-globe-canvas'); + if (!canvas) { + // Retry if canvas not found yet + setTimeout(initGlobe, 100); + return; + } + + const ctx = canvas.getContext('2d'); + const container = document.getElementById('ascii-globe-container'); + if (!container) return; + + // Configuration + const GLOBE_CHARS = ['@', '#', '*', '+', '=', '-', ':', '.', 'o', 'O', '0', '%', '&']; + const POINT_COUNT = 600; + const GLOBE_RADIUS = 110; + const ROTATION_SPEED = 0.002; + const MOUSE_RADIUS = 100; + const REPEL_STRENGTH = 25; + const RETURN_SPEED = 0.06; + const FRICTION = 0.88; + + let width, height; + let mouseX = -1000; + let mouseY = -1000; + let isMouseInCanvas = false; + let rotation = 0; + let animationId; + + // Particle class for each ASCII character + class Particle { + constructor(theta, phi) { + this.theta = theta; + this.phi = phi; + this.char = GLOBE_CHARS[Math.floor(Math.random() * GLOBE_CHARS.length)]; + this.x = 0; + this.y = 0; + this.targetX = 0; + this.targetY = 0; + this.vx = 0; + this.vy = 0; + this.baseSize = 8 + Math.random() * 6; + this.depth = 0; + this.originalTheta = theta; + this.originalPhi = phi; + } + + update(rotationY) { + // Calculate 3D position on sphere + const x3d = GLOBE_RADIUS * Math.sin(this.phi) * Math.cos(this.theta + rotationY); + const y3d = GLOBE_RADIUS * Math.cos(this.phi); + const z3d = GLOBE_RADIUS * Math.sin(this.phi) * Math.sin(this.theta + rotationY); + + // Project to 2D with perspective + const perspective = 350; + const scale = perspective / (perspective + z3d + GLOBE_RADIUS); + + this.targetX = width / 2 + x3d * scale; + this.targetY = height / 2 + y3d * scale; + this.depth = z3d; + + // Mouse repulsion with explosive effect + if (isMouseInCanvas) { + const dx = this.x - mouseX; + const dy = this.y - mouseY; + const dist = Math.sqrt(dx * dx + dy * dy); + + if (dist < MOUSE_RADIUS && dist > 0) { + const force = Math.pow((MOUSE_RADIUS - dist) / MOUSE_RADIUS, 2) * REPEL_STRENGTH; + const angle = Math.atan2(dy, dx); + + // Add some randomness for more organic shattering + const randomAngle = angle + (Math.random() - 0.5) * 0.5; + this.vx += Math.cos(randomAngle) * force; + this.vy += Math.sin(randomAngle) * force; + } + } + + // Spring back to target position + const springX = (this.targetX - this.x) * RETURN_SPEED; + const springY = (this.targetY - this.y) * RETURN_SPEED; + + this.vx += springX; + this.vy += springY; + + // Apply friction + this.vx *= FRICTION; + this.vy *= FRICTION; + + // Update position + this.x += this.vx; + this.y += this.vy; + } + + draw(ctx) { + // Only draw particles on the front half of the globe (with some buffer) + if (this.depth < 30) { + const normalizedDepth = (this.depth + GLOBE_RADIUS) / (GLOBE_RADIUS * 2); + const alpha = Math.max(0.1, 0.15 + normalizedDepth * 0.85); + const size = this.baseSize * (0.4 + normalizedDepth * 0.6); + + // Calculate displacement for visual feedback + const displacement = Math.sqrt( + Math.pow(this.x - this.targetX, 2) + + Math.pow(this.y - this.targetY, 2) + ); + + // Gold theme colors matching the site (#D4A63A) + // Shift toward brighter/whiter when displaced + const hue = 43; + const saturation = Math.max(30, 70 - displacement * 0.5); + const lightness = Math.min(80, 45 + normalizedDepth * 25 + displacement * 0.3); + + ctx.fillStyle = `hsla(${hue}, ${saturation}%, ${lightness}%, ${alpha})`; + ctx.font = `${size}px "SF Mono", "Monaco", "Inconsolata", "Roboto Mono", monospace`; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + ctx.fillText(this.char, this.x, this.y); + } + } + } + + let particles = []; + + function init() { + // Set canvas size + const rect = container.getBoundingClientRect(); + width = rect.width || 600; + height = Math.min(400, window.innerHeight * 0.5); + + const dpr = window.devicePixelRatio || 1; + canvas.width = width * dpr; + canvas.height = height * dpr; + canvas.style.width = width + 'px'; + canvas.style.height = height + 'px'; + ctx.scale(dpr, dpr); + + // Create particles distributed on sphere using golden spiral (Fibonacci sphere) + particles = []; + const goldenRatio = (1 + Math.sqrt(5)) / 2; + + for (let i = 0; i < POINT_COUNT; i++) { + const theta = 2 * Math.PI * i / goldenRatio; + const phi = Math.acos(1 - 2 * (i + 0.5) / POINT_COUNT); + + const particle = new Particle(theta, phi); + // Initialize at center, will animate to position + particle.x = width / 2; + particle.y = height / 2; + particles.push(particle); + } + } + + function animate() { + // Clear canvas + ctx.clearRect(0, 0, width, height); + + // Slow rotation + rotation += ROTATION_SPEED; + + // Update all particles + particles.forEach(p => p.update(rotation)); + + // Sort by depth (back to front) for proper layering + particles.sort((a, b) => a.depth - b.depth); + + // Draw all visible particles + particles.forEach(p => p.draw(ctx)); + + animationId = requestAnimationFrame(animate); + } + + // Mouse event handlers + function handleMouseMove(e) { + const rect = canvas.getBoundingClientRect(); + mouseX = e.clientX - rect.left; + mouseY = e.clientY - rect.top; + isMouseInCanvas = true; + } + + function handleMouseLeave() { + isMouseInCanvas = false; + mouseX = -1000; + mouseY = -1000; + } + + function handleTouchMove(e) { + const rect = canvas.getBoundingClientRect(); + const touch = e.touches[0]; + mouseX = touch.clientX - rect.left; + mouseY = touch.clientY - rect.top; + isMouseInCanvas = true; + } + + function handleTouchEnd() { + isMouseInCanvas = false; + mouseX = -1000; + mouseY = -1000; + } + + // Handle window resize + let resizeTimeout; + function handleResize() { + clearTimeout(resizeTimeout); + resizeTimeout = setTimeout(() => { + const rect = container.getBoundingClientRect(); + if (Math.abs(rect.width - width) > 10) { + init(); + } + }, 150); + } + + // Set up event listeners + canvas.addEventListener('mousemove', handleMouseMove, { passive: true }); + canvas.addEventListener('mouseleave', handleMouseLeave); + canvas.addEventListener('touchmove', handleTouchMove, { passive: true }); + canvas.addEventListener('touchend', handleTouchEnd); + window.addEventListener('resize', handleResize, { passive: true }); + + // Initialize and start animation + init(); + animate(); + + // Store cleanup function + window._asciiGlobeCleanup = function() { + if (animationId) { + cancelAnimationFrame(animationId); + } + canvas.removeEventListener('mousemove', handleMouseMove); + canvas.removeEventListener('mouseleave', handleMouseLeave); + canvas.removeEventListener('touchmove', handleTouchMove); + canvas.removeEventListener('touchend', handleTouchEnd); + window.removeEventListener('resize', handleResize); + }; + } + + // Initialize when DOM is ready + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', initGlobe); + } else { + initGlobe(); + } + + // Also try on page navigation (for SPA behavior) + if (typeof window !== 'undefined') { + let lastUrl = location.href; + new MutationObserver(() => { + const url = location.href; + if (url !== lastUrl) { + lastUrl = url; + setTimeout(initGlobe, 200); + } + }).observe(document, { subtree: true, childList: true }); + } +})(); diff --git a/sdk/overview.mdx b/sdk/overview.mdx index 8d81de5..6bea7d3 100644 --- a/sdk/overview.mdx +++ b/sdk/overview.mdx @@ -1,6 +1,6 @@ --- title: "SDK Overview" -description: "Unified TypeScript SDK for Astral Location Services v0.2.0" +description: "Unified TypeScript SDK for Astral Location Services v0.3.0" --- @@ -8,17 +8,17 @@ description: "Unified TypeScript SDK for Astral Location Services v0.2.0" - **v0.2.0 Unified SDK** — The SDK now provides a unified API with namespaced modules. - Location attestations and geospatial computations are now in a single package: `@decentralized-geo/astral-sdk`. + **v0.3.0** — The SDK now includes stamps, proofs, and verify modules for location verification alongside location attestations and geospatial computations. # SDK Overview -The Astral SDK is the official TypeScript client for Astral Location Services, providing a unified interface for location attestations and verifiable geospatial computations. +The Astral SDK is the official TypeScript client for Astral Location Services, providing a unified interface for location attestations, verifiable geospatial computations, and location verification. ## Design Philosophy -- **Namespaced API**: Clear separation between location and compute operations +- **Namespaced API**: Clear separation between location, compute, and verification operations +- **Plugin architecture**: Extensible proof-of-location system through `LocationProofPlugin` interface - **Workflow-oriented**: Distinct offchain and onchain workflows for location attestations - **Type-safe**: Full TypeScript support with comprehensive types - **Batteries included**: Handles signing, encoding, and EAS integration @@ -64,6 +64,21 @@ astral.compute.intersects(a, b, options); astral.compute.submit(delegatedAttestation); astral.compute.estimate(delegatedAttestation); astral.compute.health(); + +// Plugin registry - register proof-of-location plugins +astral.plugins.register(plugin); +astral.plugins.get('mock'); +astral.plugins.list(); + +// Stamps module - evidence collection and verification +astral.stamps.collect({ plugins: ['mock'] }); +astral.stamps.create({ plugin: 'mock' }, signals); +astral.stamps.sign({ plugin: 'mock' }, unsigned, signer); +astral.stamps.verify(stamp); + +// Proofs module - bundle claim + stamps and evaluate credibility +astral.proofs.create(claim, stamps); +astral.proofs.verify(proof, options); ``` ## Quick Start @@ -171,16 +186,30 @@ AstralSDK │ ├── build() - Build unsigned attestation │ ├── encode() - Encode for EAS │ └── decode() - Decode from EAS -└── compute: ComputeModule - ├── distance() - Distance between geometries - ├── area() - Area of polygon - ├── length() - Length of line - ├── contains() - Containment check - ├── within() - Proximity check - ├── intersects() - Intersection check - ├── submit() - Submit delegated attestation - ├── estimate() - Estimate gas - └── health() - Service health check +├── compute: ComputeModule +│ ├── distance() - Distance between geometries +│ ├── area() - Area of polygon +│ ├── length() - Length of line +│ ├── contains() - Containment check +│ ├── within() - Proximity check +│ ├── intersects() - Intersection check +│ ├── submit() - Submit delegated attestation +│ ├── estimate() - Estimate gas +│ └── health() - Service health check +├── plugins: PluginRegistry +│ ├── register() - Register a proof-of-location plugin +│ ├── get() - Get plugin by name +│ ├── has() - Check if plugin is registered +│ ├── list() - List all plugin metadata +│ └── withMethod() - Find plugins implementing a method +├── stamps: StampsModule +│ ├── collect() - Gather raw signals from plugins +│ ├── create() - Transform signals into unsigned stamp +│ ├── sign() - Cryptographically sign a stamp +│ └── verify() - Verify a stamp's internal validity +├── proofs: ProofsModule +│ ├── create() - Bundle claim + stamps into proof +│ └── verify() - Full proof verification + credibility assessment ``` ## Pages @@ -195,6 +224,12 @@ AstralSDK Geospatial computation methods + + Evidence-based location verification + + + Proof-of-location plugin system + Migrate from v0.1.x to v0.2.0