Cross-compiling vantad for darwin: Apple Silicon, sign + notarise
Shipping vantad as a notarised Mac binary inside a Tauri app meant fixing libconsensus link order, building Rust release with the right target triple, signing every sidecar, and stapling the DMG separately. The notes from the trenches.
- FROM
- Dax the Dev <[email protected]>
- SOURCE
- https://blog.skill-issue.dev/blog/vanta_darwin_apple_silicon_build/
- FILED
- 2026-04-13 18:25 UTC
- REVISED
- 2026-04-23 19:31 UTC
- TIME
- 8 min read
- SERIES
- Vanta In Practice
- TAGS
The 2026-04-13 commit eff33f7a chain+build: mined genesis nonce + libconsensus links FFI and the 2026-04-23 commit 0edddc82 build: darwin frameworks, wallet-ui node types, v2 test renames are the bookends of the macOS build story. In between is a week of “why does my dylib not load” and “why does Gatekeeper not trust this DMG even though everything inside it is signed.”
This post is the field notes from cross-compiling vantad for darwin-aarch64, signing everything that needs signing, notarising what needs notarising, and stapling the DMG so users don’t see “this came from the internet” prompts. Everything I describe is in vanta-desktop/build-release.sh and the doc/build-osx.md it leans on. The goal is so an engineer running their first ARM Mac doesn’t have to repeat the mistakes.
What you actually have to ship
A Vanta desktop install on macOS contains three sidecar binaries inside one signed .app:
vantad-aarch64-apple-darwin— the C++ Bitcoin Core forkvanta-cli-aarch64-apple-darwin— the matching CLIvanta-node-aarch64-apple-darwin— the Rust L2 sidecar
Plus the Tauri host binary (Vanta Wallet) and the WebView assets. All of this rides inside a .dmg that has to itself be notarised separately.
There’s a sentence I’m going to repeat because it tripped me twice: on macOS Tauri 2.x notarises the .app, not the .dmg that wraps it. Gatekeeper checks the file the user downloaded, which is the DMG. So you have to submit the DMG to notarytool separately and staple the resulting ticket. This is documented in approximately zero places. I figured it out by attempting to install my own DMG on a clean VM and watching Gatekeeper refuse it with a generic error.
The build sequence
The release script does seven steps. I’ll narrate them.
Step 1: build vantad. This is the C++ Bitcoin Core fork’s autotools build:
./autogen.sh
./configure --without-gui --disable-tests --disable-bench \
--without-bdb --without-miniupnpc --without-natpmp
make -j$(sysctl -n hw.ncpu)
The --without-bdb --without-miniupnpc --without-natpmp flags are the canonical “don’t pull in dependencies the wallet doesn’t need” set. BerkeleyDB only matters for legacy wallets, miniupnpc is for UPnP NAT traversal, natpmp is the same on Apple’s stack. Skipping them shaves 30+ MB and a bunch of failure modes off the binary.
--without-gui is because we’re not shipping vanta-qt. The Qt UI is also possible to ship — the upstream Bitcoin Core team supports it — but on Vanta the desktop wallet is the Tauri app, and the C++ binary is just a sidecar. No need for two UIs.
Step 2: build vanta-node.
cd vanta && cargo build --release -p vanta-node
Cargo handles the cross-compile to whatever the host target is. On an Apple Silicon Mac that produces the aarch64-apple-darwin binary you want. On Intel macs you’d get x86_64-apple-darwin; the Tauri sidecar resolution in node.rs handles both.
The target_triple() helper in node.rs makes the runtime resolution honest:
pub fn target_triple() -> &'static str {
if cfg!(target_os = "macos") {
if cfg!(target_arch = "aarch64") {
"aarch64-apple-darwin"
} else {
"x86_64-apple-darwin"
}
} else if cfg!(target_os = "linux") {
"x86_64-unknown-linux-gnu"
} else {
"x86_64-pc-windows-msvc"
}
}
The host binary discovers its sidecars by appending the target triple suffix. This is also how Tauri itself decides which file to bundle — tauri.conf.json declares "binaries/vantad" and the bundler looks for vantad-aarch64-apple-darwin next to the conf.
Step 3: copy the sidecars.
cp "$REPO_ROOT/src/bitcoind" "$BINDIR/vantad-$TRIPLE"
cp "$REPO_ROOT/src/bitcoin-cli" "$BINDIR/vanta-cli-$TRIPLE"
cp "$REPO_ROOT/vanta/target/release/vanta-node" "$BINDIR/vanta-node-$TRIPLE"
The C++ binary is still called bitcoind after the upstream fork (we haven’t renamed the actual file in src/ because that breaks too much of the upstream build); we rename it during the copy.
Step 4: install frontend deps. pnpm install. Vite/Tauri build needs the React app’s deps for the bundler.
Step 5: build the Tauri app. pnpm tauri build. Tauri auto-signs the .app (and every binary inside it) with the Developer ID identity declared in tauri.conf.json:
"macOS": {
"signingIdentity": "474F624D8F3783B4D607CFF2331AD4C6CC26A1B5",
"providerShortName": "9HD4Q82U58",
"entitlements": "entitlements.plist",
"minimumSystemVersion": "10.15"
}
Tauri also auto-notarises the .app if APPLE_ID, APPLE_PASSWORD, and APPLE_TEAM_ID are set in the environment. The release script reads them from .env.local. If they’re missing the script prints a warning and proceeds without notarisation — useful for local dev builds.
Step 6: notarise the DMG separately.
xcrun notarytool submit "$DMG_PATH" \
--apple-id "$APPLE_ID" \
--password "$APPLE_PASSWORD" \
--team-id "$APPLE_TEAM_ID" \
--wait
xcrun stapler staple "$DMG_PATH"
This is the step I had to discover. notarytool submit uploads the DMG, Apple’s notary service runs its scan, and stapler staple attaches the resulting ticket to the file so Gatekeeper can verify offline.
Step 7: verify everything.
codesign --verify --deep --strict --verbose=2 "$APP_PATH"
spctl -a -t open --context context:primary-signature -v "$DMG_PATH"
xcrun stapler validate "$APP_PATH"
xcrun stapler validate "$DMG_PATH"
If any of these fail I want to know before the DMG ships, not after a user tries to install it. The verification is fast — under a second on a recent Mac — so it’s free to run as a final step.
The libconsensus link order issue
The 2026-04-13 commit eff33f7a chain+build: mined genesis nonce + libconsensus links FFI was the fix for a problem that took an embarrassing amount of time. The C++ build’s link order didn’t include the FFI verifier static library (libvanta_verifier.a) before the Bitcoin libconsensus shared library, with the result that consensus-time calls to vanta_verify_and_decode came back as undefined symbols at runtime.
The fix was a Makefile.am patch making the linker order explicit:
src_bitcoind_LDADD = libvanta_verifier.a $(LIBBITCOIN_CONSENSUS) ...
The lesson: when you’re FFI-binding a Rust static lib into a C++ autotools project, LDADD order is load-bearing. The static lib has to come before the shared lib that depends on it, or the linker won’t resolve symbols. This is one of those things autotools makes more painful than it should be; in cmake you’d never trip on it.
The share/pixmaps straggler
A bunch of the macOS-build pain wasn’t actual build pain; it was rebrand pain. The Bitcoin Core stock build copies icons from share/pixmaps/bitcoin*.{png,xpm,ico} into the bundle. Those files still showed Bitcoin’s logo even after the chain rebrand, because the icon files weren’t git mv’d during the zera→vanta rebrand.
From CLAUDE.md:
Bitcoin Core stock icons in
share/pixmaps/bitcoin*.{png,xpm,ico}still show Bitcoin logo;src/qt/res/Qt resources also unrebranded. Qt wallet rebrand is secondary.
We don’t ship vanta-qt so this is a cosmetic-only issue, but it’s the kind of thing an external auditor will flag and rightly so. TODO: Dax confirm we ship the icon rename in the next pass.
Why ship a Mac binary at all
A reasonable challenge: if Vanta is meant to be operator-driven and most operators run Linux servers, why spend this much effort on Mac packaging?
The answer is that desktop runs on Mac. Servers run Linux; that’s the vantad people deploy with systemd. But the wallet — the thing a person actually opens to send a transaction — needs to feel native on the platform the user has. In 2026 that’s Mac for half my user base, Linux for the other half (with a long tail of Windows, which we ship via the bd7d6299 MSI build).
A privacy-chain wallet that only works on Linux is a wallet that’s only used by people who already agree with you. The Mac story is the bridge to normal users.
What I would do differently
- Codesign-by-default in the Rust build. I have my Apple Developer creds in
.env.localand the release script reads them. If I’m doing a quick dev build I sometimes forget to enable signing, and then the resulting binary won’t load on a fresh macOS sandbox. Default-on signing for any release build, opt-out for dev builds, would be safer. - Universal binary instead of two builds. Right now I build aarch64 and x86_64 separately and ship two DMGs.
lipocan produce a universal binary that runs on both. Tauri 2.x supports it. On the list. - Reproducible builds. Bitcoin Core has a Guix-based reproducible build setup that produces byte-identical binaries on any host with the right toolchain. I haven’t ported that to the Vanta build because it’d require pulling vanta-node into the Guix manifest. Important for downstream trust; not blocking for a first release.
Further reading
vanta-desktop/build-release.sh— the script this post narratesvanta-desktop/src-tauri/tauri.conf.json— the bundle configdoc/build-osx.md— the upstream Bitcoin Core macOS build docdoc/guix.md— reproducible-build path I haven’t taken yet- Vanta Desktop: a Tauri wallet that ships its own full node — the app this build produces
- Apple’s notarytool docs — the notarisation contract