mirror of
https://github.com/luxfi/zap.git
synced 2026-10-30 15:04:16 +00:00
Orchestration ONLY — ZAP is NEVER settlement authority. A ZAP response is never sufficient to move money; money moves only when C consumes a real D->C atomic export object or D consumes a real C->D object. The layer holds no StateDB/ AtomicState/shared-memory and has no compile edge to precompile/dex or the EVM: it cannot be imported into the C Verify path, and Verify does not import it. - DexSession capabilities (Cap'n-Proto): bootstrap -> restricted session; unforgeable, grant-scoped QuoteCap/IntentCap/WatchCap/SettlementCap/AdminCap. No cap exposes a value-moving method (proof by exhaustion). - prepareSwapIntent returns 0x9999 CALLDATA the user signs (no funds reserved, no enforceable amountOut). notifyIntent triggers a D scan (cannot validate a match without a D block). importSettlement points at a DExportRef pointer; the chain binds recipient/asset/amount to the real object. - Wire format + DeriveIntentID/DeriveUTXOID + V4 swap selector + hookData phase tags reproduced as pure values (no EVM/cgo dep) and pinned byte-identical to precompile/dex + chains/dexvm by parity tests (three-homes pattern). - Promise pipelining (thenAsync + SwapFlow) overlaps network latency end-to-end. - 26 tests incl. TestRED_ZAP_ResponseAloneCannotMoveMoney. CGO_ENABLED=0, pure Go.
108 lines
3.6 KiB
Go
108 lines
3.6 KiB
Go
// Copyright (C) 2026, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package dexsession
|
|
|
|
import (
|
|
"context"
|
|
"sync"
|
|
)
|
|
|
|
// promise.go is Cap'n-Proto-style promise pipelining over the ZAP Node.Call
|
|
// primitive. Two complementary mechanisms:
|
|
//
|
|
// 1. Promise[T] — a CLIENT future. A call returns immediately with a Promise;
|
|
// dependent calls take the Promise and dispatch the instant it resolves, so
|
|
// network latencies OVERLAP. The user writes the chain declaratively:
|
|
//
|
|
// quote := dex.Quote(ctx, req)
|
|
// intent := dex.PrepareSwapIntent(ctx, req, quote) // waits on quote internally
|
|
// watch := dex.NotifyIntent(ctx, intent) // waits on intent internally
|
|
// settle := dex.ImportSettlement(ctx, watch.OnCommitted())
|
|
// res, err := settle.Await(ctx)
|
|
//
|
|
// 2. Pipeline batch (see pipeline.go) — a SERVER-side mechanism: a single frame
|
|
// carries an ordered list of dependent calls with placeholder refs the server
|
|
// substitutes from prior results, collapsing a whole chain into ONE round trip.
|
|
//
|
|
// Neither mechanism touches value: a Promise resolves to a quote estimate, a
|
|
// PreparedIntent (calldata), an IntentWatch, or a SettlementSubmitResult (calldata
|
|
// / tx hash). The consensus path stays native atomic; pipelining only hides
|
|
// latency.
|
|
|
|
// Promise is a future for an async DexSession call result of type T. It is safe
|
|
// for one producer (the dispatching goroutine) and many consumers (Await is
|
|
// idempotent and concurrent-safe).
|
|
type Promise[T any] struct {
|
|
mu sync.Mutex
|
|
done chan struct{}
|
|
val T
|
|
err error
|
|
ready bool
|
|
}
|
|
|
|
// newPromise creates an unresolved Promise.
|
|
func newPromise[T any]() *Promise[T] {
|
|
return &Promise[T]{done: make(chan struct{})}
|
|
}
|
|
|
|
// resolved returns an already-resolved Promise (used to lift a concrete value
|
|
// into the pipeline, e.g. a request the caller already holds).
|
|
func resolved[T any](v T, err error) *Promise[T] {
|
|
p := newPromise[T]()
|
|
p.fulfil(v, err)
|
|
return p
|
|
}
|
|
|
|
// fulfil resolves the promise exactly once. Subsequent calls are no-ops (the first
|
|
// result wins), so a racing double-resolve cannot corrupt the value.
|
|
func (p *Promise[T]) fulfil(v T, err error) {
|
|
p.mu.Lock()
|
|
if p.ready {
|
|
p.mu.Unlock()
|
|
return
|
|
}
|
|
p.val, p.err, p.ready = v, err, true
|
|
close(p.done)
|
|
p.mu.Unlock()
|
|
}
|
|
|
|
// Await blocks until the promise resolves or ctx is cancelled. On cancellation it
|
|
// returns the context error WITHOUT resolving the promise (a later resolution
|
|
// still delivers to other awaiters).
|
|
func (p *Promise[T]) Await(ctx context.Context) (T, error) {
|
|
select {
|
|
case <-p.done:
|
|
p.mu.Lock()
|
|
v, err := p.val, p.err
|
|
p.mu.Unlock()
|
|
return v, err
|
|
case <-ctx.Done():
|
|
var zero T
|
|
return zero, ctx.Err()
|
|
}
|
|
}
|
|
|
|
// thenAsync chains a dependent call: it spawns a goroutine that awaits this
|
|
// promise, then runs fn (which issues the next Call) and resolves the returned
|
|
// promise. This is the client-side pipelining primitive — the dependent call's
|
|
// goroutine is queued immediately and fires the moment the dependency resolves, so
|
|
// the caller never blocks between stages and the network round trips overlap.
|
|
//
|
|
// fn receives the resolved value of THIS promise and the context. An error in this
|
|
// promise short-circuits: fn is not called and the error propagates.
|
|
func thenAsync[T, U any](ctx context.Context, p *Promise[T], fn func(context.Context, T) (U, error)) *Promise[U] {
|
|
out := newPromise[U]()
|
|
go func() {
|
|
v, err := p.Await(ctx)
|
|
if err != nil {
|
|
var zero U
|
|
out.fulfil(zero, err)
|
|
return
|
|
}
|
|
u, ferr := fn(ctx, v)
|
|
out.fulfil(u, ferr)
|
|
}()
|
|
return out
|
|
}
|