For the complete documentation index, see llms.txt. This page is also available as Markdown.

.Net

This section covers installing, understanding, and integrating the DeRec protocol from .NET using the DeRec.Library NuGet package.


.NET SDK Overview

The DeRec .NET SDK provides a managed surface over the same DeRecProtocol orchestrator the Rust SDK exposes.

The SDK allows applications to:

  • Distribute Secret shares across trusted Helpers using a threshold secret-sharing scheme

  • Verify Helpers still hold shares

  • Recover the Secret from a threshold of Helpers

  • Optionally extend the Owner identity across multiple devices via the replica feature, with automatic Secret synchronization between them

The SDK is published as a single NuGet package:

DeRec.Library

Internally the package is built on top of the C ABI exposed by the Rust derec-library crate. The FFI bridge, the pinned managed delegates, and the native handle lifecycle are all internal — the .NET consumer sees a managed DeRec.Library.Orchestrator.DeRecProtocol class, async methods, typed exceptions, and IDisposable lifetime management.


Installation

Add the SDK to your project:

dotnet add package DeRec.Library --version 0.0.8

These chapters document lib-derec 0.0.8.

The package targets net10.0 and ships pre-built native binaries (libderec_library.dylib on macOS, libderec_library.so on Linux) for the supported runtime identifiers (osx-arm64, osx-x64, linux-x64, linux-arm64). NuGet's RID-resolution picks the correct one at build time.

The package also ships the DeRec protobuf schema under schema/ — the flat .proto files in schema/proto/ and the precompiled schema/derec_descriptor.bin — for generating code in other languages. See Schema distribution.


Quick Start

Implement the five store interfaces, a transport, and drive the orchestrator. The example below is the minimum scaffold; the flow chapters show how each protocol round-trip flows through ProcessAsync(...).

DeRecProtocol is IDisposable — the instance owns the native handle plus a set of pinned delegates, and Dispose() releases both. Wrap it in a using block (or hold it on a long-lived service and dispose at shutdown). The native handle serializes overlapping calls on one instance behind a mutex, but Dispose() must never race an in-flight call, so await each call before starting the next. Distinct instances on the same secret_id and stores are not serialized against each other — see Concurrency and scheduling.

WithOwnTransports(IEnumerable<TransportProtocol>) is required. It lists every endpoint this node serves, in its own preference order, with at most one entry per protocol (Protocol.Https or Protocol.Grpc; URIs https://, http://, grpcs://, grpc://). An empty list is refused at Build() with DeRecException (InvalidInput); a duplicate protocol is refused as a transport-validation error. Every listed endpoint must actually be served — delivery is push-only, so an endpoint you do not serve makes pairing succeed and replies vanish.

Optional setters:

Setter
Default

WithThreshold(int)

3; a value below 2 is refused at Build() with InvalidInput

WithTimeouts(Timeouts)

library defaults — see Timeouts

WithCommunicationInfo(Dictionary<string, string>)

empty

WithAutoRespondOnFailure(bool)

false

WithUnpairAck(UnpairAck)

UnpairAck.Required

WithAutoReplyTo(bool)

false

WithAutoAccept(AutoAcceptPolicy)

every flow off

WithUnsafeConnection(bool)

false

WithReplicaId(ulong)

unset

WithParameterRange(ParameterRange)

unset — no constraints advertised, every peer range accepted

WithUnsafeConnection(true) accepts plaintext http:// and grpc:// endpoints for any host, including endpoints a peer supplies. Default false; a loopback endpoint of your own works without it. See Transport endpoints and plaintext connections.

WithReplicaId takes the device's replica identity. Mint it once with DeRecProtocolBuilder.GenerateReplicaId() (never 0), persist it, and pass the same value on every start.

WithAutoAccept(AutoAcceptPolicy) lets ProcessAsync accept chosen request kinds itself; it emits AutoAcceptedEvent in place of ActionRequiredEvent. AutoAcceptPolicy.All() turns every flow on. Read the per-flow caveats in Channel management before enabling it.

Runtime setters and accessors

Member
Purpose

protocol.SecretId

The ulong secret id the instance was built with

protocol.SetOwnTransports(IReadOnlyList<TransportProtocol>)

Replace every advertised endpoint. Same validation as the builder; a rejected list leaves the previous set intact

protocol.SetCommunicationInfo(Dictionary<string, string>)

Replace the local communication_info map

protocol.RejectAsync(byte[] action, StatusEnum status, string memo)

Refuse a pending request instead of accepting it

protocol.RemoveExpiredChannelsAsync(ulong olderThanSecs)

Remove Pending channels older than the threshold; returns the removed ids

ProtocolVersion.Current()

The DeRec protocol version this build speaks (Major, Minor); see Library version and protocol version

Neither setter contacts peers. Follow them with StartAsync(FlowKind.UpdateChannelInfo, ...) — see Channel management.

Timeouts

WithTimeouts configures four independent budgets. Every field is optional — omit one to keep the library default for it.

The values shown are the library defaults. A TimeSpan must be a whole, non-negative number of seconds: a negative or fractional value is refused at Build() with DeRecException code FfiBadProto, not clamped.

InboundMessage is a security boundary — it bounds how stale a message may be and still be accepted, so it has to tolerate transport latency and clock skew. The other three are liveness budgets. See Concurrency and scheduling.

TickAsync()

Timeouts advance only inside ProcessAsync and TickAsync. A publish whose peers all go quiet has nothing left to close it: the round stays open and no SharingComplete is ever emitted.

TickAsync() runs expired-channel cleanup (when enabled), sharing-round and unpair-ack timeouts, and the round tally that emits SharingComplete. It is idempotent and safe on an idle protocol — with nothing in flight it touches no state and returns an empty list. It mutates the same round state an inbound response does, so serialise it against ProcessAsync for the same secret. See Timeouts and tick().


Protocol Flows

The SDK exposes every DeRec flow through a single protocol.StartAsync(FlowKind, params) entry point plus the inbound ProcessAsync / AcceptAsync / RejectAsync loop. Each FlowKind takes a typed params record:

FlowKind

Params

Chapter

Pairing

PairingParams

ProtectSecret

ProtectSecretParams

VerifyShares

VerifySharesParams

Discovery

DiscoveryParams

RecoverSecret

RecoverSecretParams

Unpair

UnpairParams

UpdateChannelInfo

UpdateChannelInfoParams

ReplicaDiscovery

ReplicaDiscoveryParams

UnpairReplica

UnpairReplicaParams

ReplicaDiscovery and UnpairReplica are replica-only and require WithReplicaId; without it they throw DeRecException with Code = DeRecCode.ReplicaIdNotConfigured. The protocol-level description of every flow lives in Protocol Specification / Flows.

Chapter
Purpose

Establish secure communication between two participants — Owner ↔ Helper, or Replica pairings

Split the Secret and distribute shares to Helpers; sync the full Secret to every replica group member

Verify Helpers still hold shares

Recover shares, reconstruct the Secret, and restore operational state

Unpair, update channel info, replica discovery and removal, reject, auto-accept, timeouts

Each flow follows the same shape: the initiator calls StartAsync(FlowKind.X, params); the library produces outbound envelopes through your ITransport; the responder feeds inbound envelopes to ProcessAsync and answers any ActionRequiredEvent with AcceptAsync or RejectAsync.

Every *StartedEvent (PairingStartedEvent, DiscoveryStartedEvent, ProtectSecretStartedEvent, VerifySharesStartedEvent, RecoverSecretStartedEvent, UnpairStartedEvent, UpdateChannelInfoStartedEvent) carries a ulong TraceId. One token is drawn per StartAsync call, shared by every request it sends, and echoed by the peer — ActionRequiredEvent.TraceId and MessageIgnoredEvent.TraceId on the other side carry the same value. Envelope.ReadTraceId(byte[]) reads it from raw envelope bytes. Role mismatches and invalid input throw from StartAsync before any request goes out; they are not *FailedEvents.


Message Model

The SDK operates on serialized protocol envelopes rather than exposing protobuf types directly.

Secret format. The recoverable Secret — the bytes Helpers store and recovery reconstructs — is [ version byte ] · payload; the current version byte is 3, and the payload is gzip-compressed JSON (RFC 1952), with byte fields as standard-padded base64 (RFC 4648 §4) and u64 fields as decimal strings. Version 3 records each peer's endpoints as a transports list of {uri, protocol} and the replica role as the string "Source" / "Destination". The decoder accepts versions 3 and 2, and rejects 1. This is internal to the SDK — you always pass and receive a typed Secret — and only matters for cross-app interop or primitive-layer use. See Concepts → Secret for the full schema. The replica-sync message and every other wire message stay protobuf.

  • Inbound and outbound envelopes are plain byte[] arrays on both sides of the orchestrator

  • These bytes represent:

    • A serialized DeRecMessage envelope, or

    • A plain serialized ContactMessage (the out-of-band bootstrap, see Pairing)

  • For encrypted protocol exchanges, the DeRecMessage envelope contains an encrypted inner protobuf message — the library performs the encryption, decryption, and framing for you

Every id on events, flow params, and Secret is a typed ulong (versions are uint). Status fields on events (ShareRejectedEvent.Status, UnpairRejectedEvent.Status, …) are the protobuf Org.Derecalliance.Derec.Protobuf.StatusEnum.

Transport

The library opens no sockets. It calls ITransport.Send(IReadOnlyList<TransportProtocol> endpoints, byte[] message) for every outbound envelope:

  • endpoints is never empty, and every entry addresses the same peer, in the order the peer advertised them. The library does not rank them.

  • Deliver once: stop at the first endpoint that succeeds. Throw only when the message reached none of them.

Rather than implementing that loop yourself, implement ISendOne.SendOne(TransportProtocol endpoint, byte[] message) — one dial to one endpoint — and wrap it:

  • new SequentialFailover(dialer) — tries each endpoint in order, first success wins. The right default.

  • new SingleEndpointTransport(dialer) — uses only the first endpoint. Choose it explicitly when failover is genuinely unwanted, rather than indexing endpoints[0] in your own Send.

The transport is a mailbox, not request/response: a reply is posted to the peer's address, not returned from ProcessAsync. See Concurrency and scheduling.

Applications are responsible for:

  • Delivering outbound byte[] envelopes handed to ITransport.Send to the counterparty

  • Receiving inbound byte[] envelopes and passing them back through DeRecProtocol.ProcessAsync(...)


Error Handling

All library-side errors surface as the typed DeRec.Library.DeRecException:

DeRecException.Category and DeRecException.Code are int values; the DeRecCategory and DeRecCode static classes hold the constants (DeRecCategory.Pairing, DeRecCode.ReplicaIdNotConfigured, …). CategoryName and CodeName return the stable lower_snake names every binding reports, so (CategoryName, CodeName) is the same string pair in Rust, TypeScript, React Native, Go and .NET. Binding-side failures (bad JSON across the C ABI, a null pointer) use category ffi. See the cross-binding error catalog.

When process fails

ProcessAsync first settles expired deadlines (sharing-round and unpair timeouts) and saves the result, then handles the message. If the message then fails, those settled deadlines are never reported again, so the exception carries them:

  • DeRecException.Events — every event the call produced before failing. Handle them exactly as a successful call's events.

  • DeRecException.ChannelId (ulong?) — the channel the message arrived on; null when the bytes were not a decodable envelope.

Both fields are empty for every call other than ProcessAsync. See When process fails.

Events you may not recognise

UnmappedEvent { Variant } stands in for an event variant this version of the managed layer does not marshal. Log it rather than failing on it.


API Reference

The package surfaces XML doc comments on every public type. Full API documentation lives in the GitHub repository under packages/dotnet/DeRec.Library/src/.


SDK Architecture

DeRec.Library provides the managed DeRecProtocol orchestrator surface and the bridging delegates that satisfy the Rust-side store and transport traits. The native derec-library crate is the source of truth for protocol logic — every binding is a thin translation over the same core.

Last updated