add protocol flow design spec for descro + solisting

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
thesn10
2026-05-19 17:04:45 +02:00
parent 8bd5da968e
commit 02616b8b68

View File

@@ -0,0 +1,414 @@
# Descro — Protocol Flow Design
_2026-05-19_
## Vision
Ein dezentrales Escrow-Protokoll für physische Waren auf Solana. `descro` ist ein universeller, agnostischer Escrow-Primitive. Darüber liegt `solisting` (Listing + Order Flow). Beide Programme sind vollständig unabhängig voneinander — keine gegenseitigen Abhängigkeiten, keine hardcoded Program-IDs mit Sonderrechten.
---
## Architektur
```
APPLICATION LAYER
Zentrale Platform (custodial Wallets, normale UX)
Dezentrale Clients (direkter Protokoll-Zugriff)
PROTOCOL LAYER
┌─────────────────────┐
│ solisting │ ← Listings + Order/Consent Flow
│ (descro_market) │
└──────────┬──────────┘
│ CPI (permissionless, kein Sonderrecht)
┌──────────▼──────────┐ ┌──────────────────────────┐
│ descro │───▶│ descro_ext_resolvers │
│ (Escrow + Vault) │CPI │ (Resolver Registry) │
└─────────────────────┘ └──────────────────────────┘
```
**Kernprinzip:** `descro` kennt `solisting` nicht. Jeder kann `descro` direkt nutzen. `solisting` ist ein optionaler Komfort-Layer.
---
## Programm 1: `descro` (Escrow Program)
### State
```rust
#[account]
pub struct EscrowAccount {
pub seller: Pubkey,
pub buyer: Pubkey,
pub amount: u64,
pub dispute_resolver: Option<Pubkey>,
pub state: EscrowState,
pub bump: u8,
pub vault_bump: u8,
pub escrow_id: u64,
pub dispute_raised_at: Option<i64>, // gesetzt wenn dispute() aufgerufen wird
}
pub enum EscrowState {
AwaitingDeposit, // Seller hat erstellt, Buyer noch nicht gezahlt
Active, // SOL im Vault, Ware unterwegs
Disputed, // Streit offen
Complete,
Cancelled,
}
```
**PDAs:**
```
EscrowAccount: ["escrow", seller, escrow_id_le]
Vault: ["vault", escrow_account_pubkey]
```
### Instructions
#### `create_escrow(amount, dispute_resolver, escrow_id)` — bestehend
```
Signer: Seller
State: AwaitingDeposit
Zweck: Direktnutzung ohne solisting — Seller initiiert manuell
```
#### `create_escrow_prefunded(seller, buyer, amount, dispute_resolver, escrow_id)` — neu
```
Signer: Seller (zahlt Rent, propagiert via CPI von solisting)
Precond: Vault-PDA hat bereits ≥ amount Lamports
State: Active (kein AwaitingDeposit)
Zweck: Wird von solisting via CPI aufgerufen NACHDEM OrderVault → EscrowVault transferiert wurde
Vollständig permissionless — descro prüft nur ob Vault funded ist
```
Keine Kopplung an solisting. Jeder kann `create_escrow_prefunded` aufrufen solange der Vault-PDA die Funds hat.
#### `deposit()` — bestehend
```
Signer: Buyer
State: AwaitingDeposit → Active
```
#### `complete()` — bestehend
```
Signer: Buyer
State: Active → Complete
Aktion: Vault → Seller
```
#### `dispute()` — modifiziert
```
Signer: Buyer oder Seller
State: Active → Disputed
Neu: escrow.dispute_raised_at = Clock::get()?.unix_timestamp
```
#### `resolve(winner)` — bestehend
```
Signer: dispute_resolver (Wallet ODER Program via CPI)
State: Disputed → Complete
Side: CPI → Registry.update_stats()
```
#### `cancel()` — bestehend
```
Signer: Seller
State: AwaitingDeposit → Cancelled
```
#### `emergency_resolve(winner)` — neu
```
Signer: Buyer UND Seller (beide müssen signieren)
State: Disputed → Complete
Guards: dispute_raised_at + DISPUTE_TIMEOUT (14 Tage) < now
Zweck: Wenn Resolver verschwindet nach Commitment — beide Parteien können sich
einigen ohne Resolver. Requires echten bilateralen Consent.
```
---
## Programm 2: `descro_ext_resolvers` (Resolver Registry)
Weitgehend unverändert. Einzige Ergänzung: `acceptance_policy` Feld.
```rust
#[account]
pub struct ResolverEntry {
pub authority: Pubkey,
pub resolver_type: ResolverType,
pub acceptance_policy: AcceptancePolicy, // neu
pub name: String,
pub description: String,
pub fee_bps: u16,
pub fee_recipient: Pubkey,
pub metadata_uri: String,
pub total_resolved: u64,
pub ruled_for_buyer: u64,
pub ruled_for_seller: u64,
pub registered_at: i64,
}
pub enum AcceptancePolicy {
Open, // Jeder kann diesen Resolver nutzen, keine Signatur bei Creation
SignatureGated, // Resolver muss bei accept_order() co-signieren
ProgramGated, // Resolver ist ein Program mit eigener validate()-Logik via CPI
}
```
### Resolver Signature Mechanics
Solana-Transaktionen haben binäre Signaturen — es gibt keine konditionalen `Signer<>` Constraints. Die Lösung: Der Resolver-Account wird als `AccountInfo<'info>` (nicht `Signer<'info>`) deklariert, und die Policy bestimmt ob `is_signer` geprüft wird:
```rust
pub struct AcceptOrder<'info> {
/// CHECK: Signiert nur wenn Policy == SignatureGated
pub resolver: AccountInfo<'info>,
pub resolver_entry: Option<Account<'info, ResolverEntry>>,
}
// Im Handler:
let policy = ctx.accounts.resolver_entry
.as_ref()
.map(|e| e.acceptance_policy)
.unwrap_or(AcceptancePolicy::SignatureGated); // kein Registry-Eintrag → muss signieren
match policy {
Open => {}
SignatureGated => require!(resolver.is_signer, SolistingError::ResolverSignatureRequired),
ProgramGated => { /* CPI zu resolver_program.accept_escrow() */ }
}
```
**Policy-Bedeutung:**
| Policy | Mechanismus | Typischer Use Case |
|---|---|---|
| `Open` | Kein Signature-Check | JuryDAO, MAD — permissionless Programs |
| `SignatureGated` | `resolver.is_signer == true` | Zentrale Plattform, Treuhandservice, Privatperson |
| `ProgramGated` | CPI zu `resolver.validate_escrow()` | Algorithmic, Multisig |
| _(kein Eintrag)_ | Default: `SignatureGated` | Privatperson ohne Registry-Eintrag |
**Das Darknet-Problem ist gelöst:** Ein `SignatureGated` Resolver (z.B. eBay-Äquivalent) co-signiert `accept_order()` nur für Kunden die sie kennen. Trägt jemand fremdes eBay als Resolver ein ohne ihr Consent → der `accept_order()` Call schlägt fehl weil eBay's Backend nicht signiert. Der Escrow wird gar nicht erst erstellt.
---
## Programm 3: `solisting` (Listing + Order Program)
### State
```rust
#[account]
pub struct ListingAccount {
pub seller: Pubkey,
pub price: u64, // Lamports — fix vom Seller gesetzt
pub quantity: u32,
pub resolver: Pubkey, // bevorzugter Resolver
pub metadata_uri: String, // IPFS/Arweave → Titel, Bilder, Versandbedingungen, Beschreibung
pub listing_id: u64,
pub state: ListingState,
pub bump: u8,
}
pub enum ListingState { Active, Closed }
#[account]
pub struct OrderAccount {
pub listing: Pubkey,
pub buyer: Pubkey,
pub seller: Pubkey,
pub resolver: Pubkey,
pub amount: u64,
pub escrow_id: u64, // vorher determiniert, für PDA-Berechnung
pub state: OrderState,
pub order_id: u64,
pub created_at: i64, // für cancel_order Timeout
pub bump: u8,
pub vault_bump: u8,
}
pub enum OrderState { AwaitingSellerAccept, Accepted, Rejected }
```
**PDAs:**
```
Listing: ["listing", seller, listing_id_le]
Order: ["order", listing, buyer, order_id_le]
OrderVault: ["order_vault", order]
```
### Instructions
#### `create_listing(price, quantity, resolver, metadata_uri, listing_id)`
```
Signer: Seller
Erstellt: ListingAccount
```
#### `update_listing(price, quantity, metadata_uri)`
```
Signer: Seller
Guards: state == Active
keine offenen Orders (quantity guard reicht als Proxy)
```
#### `close_listing()`
```
Signer: Seller
Guards: Keine offenen OrderAccounts gegen dieses Listing
```
#### `create_order(listing_pda, order_id, escrow_id)`
```
Signer: Buyer
Aktion: SOL (listing.price) → OrderVault
State: AwaitingSellerAccept
Guards: listing.state == Active
listing.quantity > 0
Amount: Aus listing.price gelesen — kein user-supplied amount (verhindert Manipulation)
```
`escrow_id` wird vom Buyer-Client beim Aufruf festgelegt (z.B. random u64). Der spätere EscrowVault-PDA ist damit von Anfang an berechenbar — `accept_order` kann Vault-PDA aus `(seller, escrow_id)` ableiten ohne dass der EscrowAccount schon existiert. Bei Kollision (extrem unwahrscheinlich bei random u64) schlägt `create_escrow_prefunded` mit PDA-Already-In-Use fehl — Buyer wählt einfach eine neue ID.
#### `accept_order()`
```
Signer: Seller [+ Resolver falls SignatureGated]
Aktion:
1. Resolver Policy prüfen (is_signer oder CPI)
2. EscrowVault-PDA Adresse berechnen:
escrow_pda = find_pda(["escrow", seller, escrow_id], DESCRO_PROGRAM_ID)
vault_pda = find_pda(["vault", escrow_pda], DESCRO_PROGRAM_ID)
3. System transfer: OrderVault → vault_pda (Lamports landen auf nicht-existentem PDA)
4. CPI → descro.create_escrow_prefunded(seller, buyer, amount, resolver, escrow_id)
descro erstellt EscrowAccount, verifiziert Vault-Balance ≥ amount, State = Active
5. listing.quantity -= 1
6. OrderAccount schließen (rent → seller)
```
**Kopplung:** `solisting` kennt die Program-ID von `descro` (als Konfigurationsparameter, nicht hardcoded in `descro`). `descro` kennt `solisting` nicht.
#### `reject_order()`
```
Signer: Seller
Aktion: OrderVault → Buyer, OrderAccount schließen
```
#### `cancel_order()`
```
Signer: Buyer
Guards: state == AwaitingSellerAccept
Clock::get()?.unix_timestamp > order.created_at + ORDER_TIMEOUT (z.B. 3 Tage)
Aktion: OrderVault → Buyer, OrderAccount schließen
Zweck: Verhindert SOL-Lockup wenn Seller nie reagiert
```
---
## Vollständiger Flow: End-to-End
### Normalfall (kein Dispute)
```
SELLER BUYER RESOLVER
│ │ │
│ create_listing() │ │
│ price=1 SOL, qty=5 │ │
│ resolver=eBay-Pubkey │ │
│────────────────────────▶ Chain │
│ │ │
│ │ [sieht Listing] │
│ │ create_order() │
│ │ [1 SOL → OrderVault] │
│ │────────────────────────▶ Chain
│ │ │
│ [Event: neue Order] │ │
│ [prüft: Ware verfügbar?] │ │
│ │ │
├─ accept_order() ─────────────────────────────────── ┤
│ Seller signiert │
│ Resolver co-signiert (falls SignatureGated) │
│ → OrderVault → EscrowVault │
│ → CPI: descro.create_escrow_prefunded() │
│────────────────────────▶ Chain │
│ │ │
│ [versendet Ware] │ │
│ │ │
│ │ [Paket erhalten ✓] │
│ │ complete() │
│ │────────────────────────▶ Chain
│ │ │
│ [1 SOL im Wallet] │ │
```
### Dispute-Fall
```
│ │ dispute() │
│ │────────────────────────▶ Chain
│ │ dispute_raised_at = now │
│ │ │
│ │ [Resolver prüft: Tracking, Fotos, Beweise]
│ │ │
│ │ resolve(Buyer) │
│ │◀────────────────────────┤
│ │ CPI → Registry.update_stats()
│ │────────────────────────▶ Chain
```
### Resolver-Ausfall nach Commitment (Notfall)
```
[14 Tage vergangen, kein resolve()]
Seller UND Buyer │
signieren gemeinsam: │
emergency_resolve(winner) │
─────────────────────────▶ Chain
[SOL → winner, fertig]
```
### Direktnutzung ohne solisting (dezentraler Client)
```
Buyer kennt Seller direkt, kein Listing nötig:
Seller: create_escrow(buyer, resolver, amount, escrow_id)
Buyer: deposit()
... normaler Flow ...
```
---
## Signing-Matrix
| Instruction | Seller | Buyer | Resolver | Notes |
|---|---|---|---|---|
| `create_listing` | ✓ | | | |
| `create_order` | | ✓ | | SOL in OrderVault |
| `accept_order` | ✓ | | cond. | Resolver nur wenn SignatureGated |
| `reject_order` | ✓ | | | SOL zurück an Buyer |
| `cancel_order` | | ✓ | | Nur nach Timeout |
| `create_escrow` | ✓ | | | Direkte Nutzung |
| `create_escrow_prefunded` | ✓ | | | Via CPI von solisting |
| `deposit` | | ✓ | | Direkte Nutzung |
| `complete` | | ✓ | | |
| `dispute` | ✓/✓ | ✓/✓ | | Einer von beiden |
| `resolve` | | | ✓ | Wallet oder Program via CPI |
| `emergency_resolve` | ✓ | ✓ | | Beide müssen signieren |
---
## Was diese Programme bewusst NICHT definieren
- **`descro`:** Listing-Format, Matching-Logik, wer solisting ist, interne Resolver-Logik
- **`solisting`:** Wie der Resolver den Dispute löst, Identity/KYC, Shipping-Verifizierung
- **Beide:** Privacy, USDC-Support (spätere Phase), EU-Compliance (Application Layer)
---
## Offene Fragen (spätere Phasen)
- **Mehrere simultane Orders auf letztes Item:** Seller kann nur eine accepten. Die anderen warten auf `cancel_order` nach Timeout. Verbesserung möglich: Seller kann `reject_order` auf alle anderen aktiv aufrufen sobald Lager leer.
- **Listing-Preis in USDC:** Erfordert SPL Token Integration in beiden Programmen.
- **Shipping-Adresse:** Muss off-chain encrypted übermittelt werden (on-chain wäre Privacy-Desaster). Nur für Seller sichtbar, z.B. via Buyer's Public Key verschlüsselt.
- **Events / Indexing:** `emit!()` in allen State-ändernden Instructions damit Indexer kein vollständiges Chain-Scanning brauchen.
- **Listing-Referenz im EscrowAccount:** Aktuell hat `descro` kein `listing_pda` Feld. Für Nachvollziehbarkeit könnte es Optional aufgenommen werden — `descro` validiert es aber nicht.