diff --git a/Cargo.lock b/Cargo.lock index b773e5e..c80908d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -227,6 +227,7 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "chat-proto" version = "0.1.0" +source = "git+https://github.com/logos-messaging/chat_proto?rev=e05c21229a18bab655c9aec06409189447d24a6b#e05c21229a18bab655c9aec06409189447d24a6b" dependencies = [ "prost", ] @@ -1013,6 +1014,7 @@ checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a" [[package]] name = "logos-delivery" version = "0.1.0" +source = "git+https://github.com/logos-messaging/libchat?rev=5c55c2ee76bebd1dbd5eb4bfa95d9e71acd0fa14#5c55c2ee76bebd1dbd5eb4bfa95d9e71acd0fa14" dependencies = [ "base64", "crossbeam-channel", diff --git a/Cargo.toml b/Cargo.toml index 1e4be94..d84e850 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,18 +15,16 @@ path = "src/main.rs" anyhow = "1.0" axum = "0.7" base64 = "0.22" -# Wire schema for the delivery submissions, shared with libchat. -# TODO: switch to `{ git = "https://github.com/logos-messaging/chat_proto", rev = "..." }` -# once the store submission schemas land on chat_proto's main. Path dep only so -# this builds ahead of that merge. -chat-proto = { path = "../chat_proto/gen/rust" } +# Wire schema for the delivery submissions, shared with libchat. Pinned to a rev +# rather than a branch so the wire format can only change deliberately. +chat-proto = { git = "https://github.com/logos-messaging/chat_proto", rev = "e05c21229a18bab655c9aec06409189447d24a6b" } clap = { version = "4", features = ["derive"] } ed25519-dalek = "2.2.0" hex = "0.4" -# Embedded logos-delivery node for subscription-based ingestion. Path dep on -# the sibling libchat checkout; links the native liblogosdelivery (enter the -# libchat dev shell or set LOGOS_DELIVERY_LIB_DIR to build a runnable binary). -logos-delivery = { path = "../libchat/extensions/logos-delivery-rust" } +# Embedded logos-delivery node for subscription-based ingestion. Links the +# native liblogosdelivery, which this repo's flake builds: `nix develop` exports +# LOGOS_DELIVERY_LIB_DIR, without which the binary fails at link. +logos-delivery = { git = "https://github.com/logos-messaging/libchat", rev = "5c55c2ee76bebd1dbd5eb4bfa95d9e71acd0fa14" } prost = "0.14" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" diff --git a/Dockerfile b/Dockerfile index b0c6088..0785206 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,24 +3,55 @@ ######################################## # Build stage ######################################## -FROM rust:1-bookworm AS builder +# Nix, not the `rust` image: the binary links the native liblogosdelivery, which +# only nix builds. That library comes from nixpkgs' glibc (2.40), newer than +# bookworm's, so cargo has to run against the same nixpkgs — mixing the two +# libcs in one process does not work. Everything below therefore builds inside +# the flake's dev shell. +FROM nixos/nix:2.32.0 AS builder -WORKDIR /app +# `filter-syscalls` off because the seccomp filter fails inside an emulated +# (qemu) container, which is how this image gets built on arm64 hosts. +RUN printf 'experimental-features = nix-command flakes\nfilter-syscalls = false\n' \ + >> /etc/nix/nix.conf -# Build dependencies first against a stub binary so the (slow) dependency -# compilation — bundled libsqlite3 and the async stack — is cached and only -# re-runs when Cargo.toml/Cargo.lock change. +WORKDIR /src + +# The native library first and on its own layer: it is by far the slowest part +# of the build and only changes when the flake pins do. +COPY flake.nix flake.lock ./ +RUN nix --accept-flake-config build .#logos-delivery --no-link + +# Then the Rust dependency graph, against a stub binary, so the (also slow) +# dependency compile is cached and only re-runs when Cargo.toml/Cargo.lock change. COPY Cargo.toml Cargo.lock ./ RUN mkdir src \ && echo "fn main() {}" > src/main.rs \ - && cargo build --release --locked \ + && nix --accept-flake-config develop --command cargo build --release --locked \ && rm -rf src target/release/deps/chat_store* target/release/chat-store -# Now build the real binary; dependency artifacts above are reused. `migrations/` -# is needed at build time too — `sqlx::migrate!()` embeds the .sql files. +# Now the real binary; dependency artifacts above are reused. `migrations/` is +# needed at build time too — `sqlx::migrate!()` embeds the .sql files. COPY src ./src COPY migrations ./migrations -RUN cargo build --release --locked --bin chat-store +RUN nix --accept-flake-config develop --command \ + cargo build --release --locked --bin chat-store + +# Collect what the binary loads at runtime. logos-delivery's build.rs stamps +# liblogosdelivery with an absolute store path as its soname, so the binary +# names it — along with its ELF interpreter and rpath — by absolute /nix/store +# path. Seeding the closure from those three is all the runtime image needs. +RUN nix --accept-flake-config develop --command sh -c '\ + bin=target/release/chat-store; \ + { patchelf --print-needed "$bin"; \ + patchelf --print-interpreter "$bin"; \ + patchelf --print-rpath "$bin" | tr ":" "\n"; } \ + | grep "^/nix/store/" \ + | sed "s|\(/nix/store/[^/]*\).*|\1|" | sort -u \ + | xargs nix-store --query --requisites | sort -u > /closure.txt' \ + && mkdir -p /output/nix/store \ + && cp target/release/chat-store /output/chat-store \ + && cp -a $(cat /closure.txt) /output/nix/store/ ######################################## # Runtime stage @@ -31,7 +62,9 @@ RUN apt-get update \ && apt-get install -y --no-install-recommends ca-certificates \ && rm -rf /var/lib/apt/lists/* -COPY --from=builder /app/target/release/chat-store /usr/local/bin/chat-store +# The nix closure the binary resolves by absolute path, then the binary itself. +COPY --from=builder /output/nix/store /nix/store +COPY --from=builder /output/chat-store /usr/local/bin/chat-store # Matches the default --bind 0.0.0.0:8080. EXPOSE 8080 diff --git a/README.md b/README.md index cf9dd05..ab322b1 100644 --- a/README.md +++ b/README.md @@ -8,14 +8,14 @@ its own. Standalone service that caches MLS KeyPackages keyed by **`device_id`**, so a client can fetch a contact's keypackage without an out-of-band exchange. Throwaway by design: scheduled to be replaced by a λLEZ-based service in v0.3, -with no libchat-core dependency (the embedded logos-delivery node comes from the -sibling libchat checkout's transport crate). +with no libchat-core dependency (the embedded logos-delivery node comes from +libchat's transport crate, pulled in as a pinned git dependency). Submissions arrive on either of two write paths feeding the same verification + storage pipeline: - **HTTP POST** (`/v0/keypackage`, `/v0/account`) — synchronous and acknowledged; -- **logos-delivery subscription** — clients publish the same JSON bodies on the +- **logos-delivery subscription** — clients publish protobuf submissions on the store's content topics and the server picks them up from the network (see [Delivery ingestion](#delivery-ingestion)). @@ -56,11 +56,17 @@ exactly one way — no delimiter, even though `key_package` is arbitrary bytes. ## Building & running +Building a runnable binary links the native `liblogosdelivery`, which this +repo's flake builds. The dev shell exports `LOGOS_DELIVERY_LIB_DIR` for you: + ```bash +nix develop # or set LOGOS_DELIVERY_LIB_DIR yourself cargo build --release ./target/release/chat-store # binds 0.0.0.0:8080, db ./chat-store.db ``` +Without the library, `cargo check` and `clippy` still pass — only linking fails. + | Flag | Default | Description | |------|---------|-------------| | `--bind ` | `0.0.0.0:8080` | HTTP bind address | @@ -74,10 +80,6 @@ cargo build --release Logs via `RUST_LOG` (default `info`). -Building a runnable binary links the native `liblogosdelivery`; enter the -libchat dev shell (`nix develop` in the sibling checkout) or set -`LOGOS_DELIVERY_LIB_DIR` to the directory containing the library. - ## Delivery ingestion Unless `--no-delivery` is given, the server runs an embedded logos-delivery @@ -88,12 +90,17 @@ node and subscribes to two content topics: /logos-chat/1/store-account-v0/proto account device-list submissions ``` -Each received message is the same JSON body as the corresponding POST endpoint -(below) and goes through identical signature verification and storage rules. +Each received message is a protobuf `KeyPackageSubmissionV1` or +`AccountSubmissionV1` — matching the `/proto` topic suffix — carrying the same +fields as the corresponding POST body and going through identical signature +verification and storage rules. The schemas live in +[chat_proto](https://github.com/logos-messaging/chat_proto) (`protos/store.proto`), +pinned by rev in `Cargo.toml` so the wire format only changes deliberately. + Publishing is fire-and-forget on the client side: rejected submissions are only logged by the server, which the trust model can afford because consumers verify -every bundle on retrieval anyway. libchat's `DeliveryRegistry` publishes on -these topics when its publish mode is set to delivery. +every bundle on retrieval anyway. libchat's `ContactRegistry` publishes on these +topics when constructed with `RegistryPublishMode::Delivery`. ## Docker @@ -105,6 +112,13 @@ docker build -t chat-store . docker run --rm -p 8080:8080 -v chat-store-data:/data chat-store ``` +The build runs inside nix, because the binary links `liblogosdelivery` and the +nixpkgs glibc it is built against is newer than bookworm's — so cargo has to run +against that same nixpkgs. The runtime image is still `debian:bookworm-slim`, +carrying only the nix store closure the binary resolves by absolute path. The +first build compiles the native library from source and takes a long time; +afterwards it is a cached layer that only changes with `flake.lock`. + The image runs the binary with `--bind 0.0.0.0:8080 --db /data/chat-store.db` by default; override the `CMD` to change flags, e.g.: @@ -262,6 +276,11 @@ A non-zero exit from `chat-cli` means the server rejected the submission — e.g the signature failed verification. `GET /v0/keypackage/{device_id}` returns `200` for a registered device and `404` otherwise. +Add `--registry-publish delivery` to those `chat-cli` invocations to exercise +the delivery write path instead of the HTTP POST API. Publishing is then +fire-and-forget, so `chat-cli` exits `0` regardless and the bundles appear in +the database only once the server has received and accepted them. + ## Benchmark The bundled [`benchmark`](examples/benchmark.rs) performs an end-to-end load diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..553450c --- /dev/null +++ b/flake.lock @@ -0,0 +1,115 @@ +{ + "nodes": { + "logos-delivery": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ], + "rust-overlay": "rust-overlay", + "zerokit": "zerokit" + }, + "locked": { + "lastModified": 1779915920, + "narHash": "sha256-rcIgP6MVyUoNEH6xpdLrZtfd4OcvIcMUloX4IhRq5AA=", + "owner": "logos-messaging", + "repo": "logos-delivery", + "rev": "74057c66224f43b4aa27b42033d4ed52eed5c7a7", + "type": "github" + }, + "original": { + "owner": "logos-messaging", + "repo": "logos-delivery", + "rev": "74057c66224f43b4aa27b42033d4ed52eed5c7a7", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1782231800, + "narHash": "sha256-AyPvq+cx3JFicSd687dhhIfUMryk9PUFmHnofBjalMg=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "6f5e2d2ea55618c5197ce40e346f205c35d2213e", + "type": "github" + }, + "original": { + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "6f5e2d2ea55618c5197ce40e346f205c35d2213e", + "type": "github" + } + }, + "root": { + "inputs": { + "logos-delivery": "logos-delivery", + "nixpkgs": "nixpkgs" + } + }, + "rust-overlay": { + "inputs": { + "nixpkgs": [ + "logos-delivery", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1775099554, + "narHash": "sha256-3xBsGnGDLOFtnPZ1D3j2LU19wpAlYefRKTlkv648rU0=", + "owner": "oxalica", + "repo": "rust-overlay", + "rev": "8d6387ed6d8e6e6672fd3ed4b61b59d44b124d99", + "type": "github" + }, + "original": { + "owner": "oxalica", + "repo": "rust-overlay", + "type": "github" + } + }, + "rust-overlay_2": { + "inputs": { + "nixpkgs": [ + "logos-delivery", + "zerokit", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1771211437, + "narHash": "sha256-lcNK438i4DGtyA+bPXXyVLHVmJjYpVKmpux9WASa3ro=", + "owner": "oxalica", + "repo": "rust-overlay", + "rev": "c62195b3d6e1bb11e0c2fb2a494117d3b55d410f", + "type": "github" + }, + "original": { + "owner": "oxalica", + "repo": "rust-overlay", + "type": "github" + } + }, + "zerokit": { + "inputs": { + "nixpkgs": [ + "logos-delivery", + "nixpkgs" + ], + "rust-overlay": "rust-overlay_2" + }, + "locked": { + "owner": "vacp2p", + "repo": "zerokit", + "rev": "5e64cb8822bee65eed6cf459f95ae72b80c6ba63", + "type": "github" + }, + "original": { + "owner": "vacp2p", + "repo": "zerokit", + "rev": "5e64cb8822bee65eed6cf459f95ae72b80c6ba63", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..73348cb --- /dev/null +++ b/flake.nix @@ -0,0 +1,62 @@ +{ + description = "chat-store - keypackage and account registry for Logos Chat"; + + # Repeated from the logos-delivery flake: only the top-level flake's nixConfig + # is honoured, so an input's substituters do nothing for us. + nixConfig = { + extra-substituters = [ "https://nix-cache.status.im/" ]; + extra-trusted-public-keys = [ + "nix-cache.status.im-1:x/93lOfLU+duPplwMSBR+OlY4+mo+dCN7n0mr4oPwgY=" + ]; + }; + + inputs = { + # Both revs are exactly what libchat's flake.lock pins, and they only work + # as a pair: logos-delivery's derivation wants `nim-2_2`, which newer + # nixos-unstable-small no longer has. Bump the two together. + nixpkgs.url = "github:NixOS/nixpkgs/6f5e2d2ea55618c5197ce40e346f205c35d2213e"; + # `follows` keeps the library and the toolchain that links it on a single + # glibc — the Docker build depends on that. + logos-delivery = { + url = "github:logos-messaging/logos-delivery/74057c66224f43b4aa27b42033d4ed52eed5c7a7"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + }; + + outputs = { self, nixpkgs, logos-delivery }: + let + systems = [ "aarch64-darwin" "x86_64-darwin" "aarch64-linux" "x86_64-linux" ]; + forAllSystems = f: nixpkgs.lib.genAttrs systems (system: f { + inherit system; + pkgs = import nixpkgs { inherit system; }; + }); + in + { + # The one thing this flake exists for: the native library the + # `logos-delivery` crate links. Same override as libchat's flake — no + # postgres archive driver, no Nim dlopen debug hooks, quiet logs. + packages = forAllSystems ({ system, ... }: { + logos-delivery = logos-delivery.packages.${system}.liblogosdelivery.override { + enablePostgres = false; + enableNimDebugDlOpen = false; + chroniclesLogLevel = "FATAL"; + }; + default = self.packages.${system}.logos-delivery; + }); + + devShells = forAllSystems ({ pkgs, system, ... }: + let logosDeliveryLib = self.packages.${system}.logos-delivery; + in { + default = pkgs.mkShell { + nativeBuildInputs = [ pkgs.cargo pkgs.rustc pkgs.pkg-config ] + # logos-delivery's build.rs rewrites the shipped library's soname + # with patchelf in its default (absolute-path) linking mode. + ++ pkgs.lib.optionals pkgs.stdenv.isLinux [ pkgs.patchelf ]; + buildInputs = [ logosDeliveryLib ]; + shellHook = '' + export LOGOS_DELIVERY_LIB_DIR="${logosDeliveryLib}/lib" + ''; + }; + }); + }; +}