macula_direct_dial (macula v10.1.1)
View SourceDirect-dial resolve-and-call: shared internals for macula_request/macula_response, macula_streamer/ macula_stream_sink, and macula_feeder/macula_download.
Not a public API on its own — macula_request:start_link_direct/6,7,8, macula_response:advertise_direct/6,7, macula_stream_sink:start_link_direct/5,6, macula_streamer:advertise_direct/6,7, macula_download:start_link_direct/4,5, and macula_feeder:start_link_direct/5,6 are the entry points. Factored out because RPC, streaming, and content-download all need the same shape of resolve sequence (find_records -> verify -> read the record -> build a quic:// dial URL, retrying past DHT propagation lag throughout). Streaming and RPC share the IDENTICAL discovery mechanism — a procedure_advertisement does not distinguish RPC from streaming, only the eventual dial (call_station/7 vs call_stream_station/6) does — so publish_advertisement/4,5 is reused as-is by both providers, and call/6/call_stream/6 share the same resolve_dial_url/4 and "Trust model" below. Content has no publish step here at all — see "Content" further down.
The resolve side retries: a record just published on the provider's station has not necessarily replicated to the caller's station yet, and treating the first miss as failure would make every direct-dial call racy against DHT propagation lag. macula_station_cert_chain_SUITE (macula-station) proved this exact resolve+dial sequence works cross-station against the live fleet; this module is that sequence, lifted out of the test and made reusable.
Trust model
Two independent checks, both mandatory, cover what the QUIC/TLS layer cannot: (1) every candidate procedure_advertisement must carry a valid Ed25519 signature before its serving_station is trusted at all — otherwise any identity able to sign SOME record could name a real, legitimate station as the server for a procedure it has no authority over, and the station_endpoint check below would still pass (it only proves we reached the station we were told to reach, not that whoever told us so was authorized to); (2) the resolved station_endpoint must be signed by the station itself (verify_and_build/2, unchanged). The actual QUIC dial trusts NEITHER the TLS certificate (pin_tls_cert => false — a production station's TLS is terminated by an unrelated PKI, e.g. Let's Encrypt, so pinning the cert key can never succeed) NOR nothing (verify => none alone would trust whoever answers): trust is enforced at the application layer instead, via the cryptographically signed CONNECT/HELLO handshake (macula_peering_conn:bind_peer_identity/2) checked against the exact pubkey the signed DHT chain above resolved.
A third check is available but OPT-IN, via Opts (managed realms only — see macula_record:verify_advertisement_cert_chain/3): verify_cert_chain => {RealmCaPem, Org} additionally requires the advertisement's embedded X.509 service-cert chain to verify to RealmCaPem under Org (Slice 7c Direction B), proving the ADVERTISER — not just the station it names — is an org/realm- authorized identity. Without it, (1) above still rejects an unsigned or badly-signed advertisement, but not one signed by an unauthorized (if self-consistent) identity; unmanaged realms have no realm CA to check against, so this stays opt-in rather than mandatory.
Content
put_content/4 has no resolve step at all — unlike a GET, a PUT names its OWN target: the caller already knows (or is choosing) which station to seed, so it takes Station directly and resolves only that station's own station_endpoint (resolve_station_endpoint/2, the same machinery call/6 uses internally for serving_station).
get_content/3 resolves and fetches deliberately WITHOUT the cert-chain machinery above — content's threat model genuinely differs from RPC's. An RPC reply is opaque and unverifiable except by trusting whoever answered, so proving the ADVERTISER is authorized matters. Content is content-addressed: the fetched bytes are independently re-hashed against the MCID client-side (macula:verify_block_hash/2 for single-block, macula_manifest:verify/2 for chunked) regardless of which peer served them, so a rogue or unauthorized announcer can at most refuse to serve or waste a dial — it cannot make a caller accept content that does not hash to the MCID it asked for. What still matters, and is still mandatory, is (1)'s analogue for content_announcement: the signer must equal the announcer_node it claims (macula:decode_provider/1), so an attacker cannot at least misattribute who is claiming to serve what.
Summary
Functions
As call/6 with no cert-chain verification.
Resolve Procedure's provider and call it there directly. Same return shape as macula:call/5; resolve failures surface as {error, {unresolved, Reason}} so a caller can tell "nobody has advertised this via direct-dial yet" apart from a real call failure. Opts may include verify_cert_chain => {RealmCaPem, Org} — see the module doc's "Trust model" section.
As call_stream/6 with no cert-chain verification.
As call/6, but opens a stream (macula:call_stream_station/6's shape) instead of making a single-reply call, built on the exact same resolve+trust machinery — see the module doc. StreamOpts is forwarded to call_stream_station/6 alongside the resolved trust override (mode, owner, dial_timeout_ms, etc); Opts is the resolve-side verify_cert_chain opt, same as call/6.
Resolve MCID's provider via its signed content_announcement and fetch it there directly, retrying past DHT propagation lag the same way call/6 does for procedures. Same return shape as macula:get_content/2; resolve failures surface as {error, {unresolved, Reason}}. TimeoutMs bounds only the QUIC handshake if a fresh link must be dialed (macula:get_content_station/5) — the underlying block/manifest transfer has its own internal timeouts. See the module doc's "Content" section for why this has no verify_cert_chain-equivalent opt, unlike call/6. Only chunked content is discoverable this way — see macula:find_content_providers/2.
As publish_advertisement/5 with no cert chain embedded.
Publish a signed procedure_advertisement for Procedure, naming Pool's currently-connected station as the serving station. Identity signs it — the pool itself has no identity to sign with, so the caller supplies one (reuse the same one across re-advertises; a fresh identity per call is valid but needless DHT churn). Opts may include cert_chain => ChainPem (leaf ++ org CA, PEM) so a verifying consumer's verify_cert_chain opt (see call/6) can check this advertiser's org/realm authorization — Slice 7c Direction B, managed realms only.
Resolve Station's dialable quic:// URL from its own signed station_endpoint record and put Bytes there directly. Same return shape as macula:put_content/2; resolve failures surface as {error, {unresolved, Reason}}. TimeoutMs bounds only the QUIC handshake if a fresh link must be dialed (macula:put_content_station/5) — the underlying block/manifest transfer has its own internal timeouts.
Resolve MCID's provider via a signed content_announcement, retrying past a not-yet-replicated announcement. Returns the first candidate macula:find_content_providers/2 finds — that function already discards unsigned or signer-mismatched announcements before this ever sees them.
Resolve Station's dialable quic:// URL from its own signed station_endpoint record, verifying the record's signer is exactly Station and retrying past a stale/expired replica — the same discipline call/6 applies internally once it has resolved a procedure's serving_station.
Functions
-spec call(macula:pool(), macula:realm(), macula:procedure(), term(), pos_integer()) -> {ok, term()} | {error, term()}.
As call/6 with no cert-chain verification.
-spec call(macula:pool(), macula:realm(), macula:procedure(), term(), pos_integer(), map()) -> {ok, term()} | {error, term()}.
Resolve Procedure's provider and call it there directly. Same return shape as macula:call/5; resolve failures surface as {error, {unresolved, Reason}} so a caller can tell "nobody has advertised this via direct-dial yet" apart from a real call failure. Opts may include verify_cert_chain => {RealmCaPem, Org} — see the module doc's "Trust model" section.
-spec call_stream(macula:pool(), macula:realm(), macula:procedure(), term(), map()) -> {ok, macula:stream()} | {error, term()}.
As call_stream/6 with no cert-chain verification.
-spec call_stream(macula:pool(), macula:realm(), macula:procedure(), term(), map(), map()) -> {ok, macula:stream()} | {error, term()}.
As call/6, but opens a stream (macula:call_stream_station/6's shape) instead of making a single-reply call, built on the exact same resolve+trust machinery — see the module doc. StreamOpts is forwarded to call_stream_station/6 alongside the resolved trust override (mode, owner, dial_timeout_ms, etc); Opts is the resolve-side verify_cert_chain opt, same as call/6.
-spec get_content(macula:pool(), macula:mcid(), pos_integer()) -> {ok, binary()} | {error, term()}.
Resolve MCID's provider via its signed content_announcement and fetch it there directly, retrying past DHT propagation lag the same way call/6 does for procedures. Same return shape as macula:get_content/2; resolve failures surface as {error, {unresolved, Reason}}. TimeoutMs bounds only the QUIC handshake if a fresh link must be dialed (macula:get_content_station/5) — the underlying block/manifest transfer has its own internal timeouts. See the module doc's "Content" section for why this has no verify_cert_chain-equivalent opt, unlike call/6. Only chunked content is discoverable this way — see macula:find_content_providers/2.
-spec publish_advertisement(macula:pool(), macula:realm(), macula:procedure(), macula_identity:key_pair()) -> ok | {error, term()}.
As publish_advertisement/5 with no cert chain embedded.
-spec publish_advertisement(macula:pool(), macula:realm(), macula:procedure(), macula_identity:key_pair(), map()) -> ok | {error, term()}.
Publish a signed procedure_advertisement for Procedure, naming Pool's currently-connected station as the serving station. Identity signs it — the pool itself has no identity to sign with, so the caller supplies one (reuse the same one across re-advertises; a fresh identity per call is valid but needless DHT churn). Opts may include cert_chain => ChainPem (leaf ++ org CA, PEM) so a verifying consumer's verify_cert_chain opt (see call/6) can check this advertiser's org/realm authorization — Slice 7c Direction B, managed realms only.
-spec put_content(macula:pool(), macula_identity:pubkey(), binary(), pos_integer()) -> {ok, macula:mcid()} | {error, term()}.
Resolve Station's dialable quic:// URL from its own signed station_endpoint record and put Bytes there directly. Same return shape as macula:put_content/2; resolve failures surface as {error, {unresolved, Reason}}. TimeoutMs bounds only the QUIC handshake if a fresh link must be dialed (macula:put_content_station/5) — the underlying block/manifest transfer has its own internal timeouts.
-spec resolve_content_provider(macula:pool(), macula:mcid()) -> {ok, map()} | {error, term()}.
Resolve MCID's provider via a signed content_announcement, retrying past a not-yet-replicated announcement. Returns the first candidate macula:find_content_providers/2 finds — that function already discards unsigned or signer-mismatched announcements before this ever sees them.
-spec resolve_station_endpoint(macula:pool(), macula_identity:pubkey()) -> {ok, binary()} | {error, term()}.
Resolve Station's dialable quic:// URL from its own signed station_endpoint record, verifying the record's signer is exactly Station and retrying past a stale/expired replica — the same discipline call/6 applies internally once it has resolved a procedure's serving_station.