DOLE is a proof-of-concept peer-to-peer payment system based on the GOC-Ledger model. Every account belongs to a physical Java Card that protects its private key, maintains its balance and monotonic counters, and signs every state transition. Verified transactions are stored as commits in a local Git ledger and synchronised directly between devices.
The wallet runs on Android, iOS, Windows, and macOS. It uses Iroh for local and global peer-to-peer synchronisation and Bluetooth Low Energy as an offline fallback on Android and Windows. No central service owns the ledger or decides which transactions are valid.
This version was developed as a bachelor thesis at the University of Basel. It replaces the closed-source Ditto storage and synchronisation layer from the original seminar prototype with a transparent Rust implementation. DOLE is a research prototype, not a production payment system.
The shared Kotlin Multiplatform wallet drives the user flow and communicates with the card through NFC on Android and iOS or PC/SC on desktop systems. It passes signed operations to a native Rust core through UniFFI.
The Rust core performs P-256 verification, validates the card certificate against the root CA, and recovers the signing public key from non-genesis transaction signatures. This allows the compact wire format to omit a separate author identifier. Accepted transactions become Git commits on an author-specific branch; balances and history are derived from the verified ledger rather than trusted from the network.
Synchronisation exchanges compact transaction batches and Frontier messages. A peer compares the received Frontier with its own branches and transfers only the missing suffix. Devices can therefore catch up from a partial history without downloading transactions they already hold.
| Path | Role |
|---|---|
| Java Card | Protects the private key, balance, role, counters, and per-peer receive state |
| Kotlin Multiplatform | Provides the shared wallet, account management, pending actions, and platform adapters |
| Rust core | Verifies transactions, maintains the Git ledger, encodes sync frames, and runs networking |
| Iroh | Provides local mDNS discovery, QUIC connectivity, gossip, and global hub connectivity |
| BLE | Provides connectionless offline exchange through extended advertisements on Android and Windows |
| Hub | Acts as an optional always-online Iroh bootstrap and gossip peer without storing the ledger |
| Platform | Card access | Local Iroh | Global Iroh | BLE |
|---|---|---|---|---|
| Android | NFC | Yes | Yes | Yes |
| iOS | CoreNFC | Yes | Yes | No |
| Windows | PC/SC | Yes | Yes | Yes |
| macOS | PC/SC | Yes | Yes | No |
Apple's public Bluetooth APIs do not expose the extended advertising functionality required by DOLE's connectionless BLE transport. Apple devices therefore use Iroh for synchronisation.
| Path | Purpose |
|---|---|
app/shared | Shared Kotlin logic, Compose UI, view model, and platform interfaces |
app/androidApp | Android application and NFC integration |
app/desktopApp | Windows and macOS application with PC/SC support |
app/iosApp | iOS application, CoreNFC integration, and Swift bridge |
card | Java Card wallet and NDEF applets plus provisioning tools |
core | Rust cryptography, Git ledger, compact wire format, networking, and benchmarks |
hub | Standalone Iroh gossip bootstrap peer |
constants.conf | Protocol and platform constants generated for Rust, Kotlin, and Java |
| Component | Requirement |
|---|---|
| Rust core | Rustup with Cargo, Rust 1.85 or newer, and a native C/C++ toolchain |
| Client runtime | JDK 21 |
| Android | Android Studio, Android SDK 37, Android NDK, and Android 8.0 or newer |
| iOS | macOS, Xcode with the iOS 26 SDK or newer, and an Apple developer team |
| Smart card | Java Card with EC curve support, such as the NXP J3R180 Classic 3.0.5 |
| Desktop card access | USB PC/SC reader |
All remaining project dependencies and Java Card build tools are downloaded automatically.
All commands are executed from the repository root. Clone the repository and build it with the Gradle wrapper:
git clone https://github.com/dominikkipfer/DOLE.git
cd DOLE
./gradlew buildThe platform-specific Rust targets described below must be installed before building the corresponding application.
Connect a PC/SC reader, insert a blank card, and provision it as either a user card or a minter card:
./gradlew :card:setupUser
./gradlew :card:setupMinterRun only one setup command for each card. Both tasks install the required applets before starting the interactive provisioner.
With a reader connected and a provisioned card inserted, start the desktop wallet on Windows or macOS:
./gradlew :app:desktopApp:runInstall Cargo NDK and the ARM64 Android Rust target:
cargo install cargo-ndk
rustup target add aarch64-linux-androidRun the application from Android Studio or install it on a connected device:
./gradlew :app:androidApp:installDebugGrant the requested NFC, Bluetooth, and nearby-device permissions.
Install the Rust targets for physical ARM64 devices and Apple-Silicon simulators:
rustup target add aarch64-apple-ios
rustup target add aarch64-apple-ios-simOpen app/iosApp/iosApp.xcodeproj in Xcode. Select an Apple developer team and a physical iOS device, then build and run the application. The Xcode build phase builds the shared Kotlin XCFramework and Rust library automatically. A physical device is required for NFC access.
The Developer screen allows BLE, local mDNS, and online Iroh connectivity to be switched independently. It also shows discovered peers, the number of accounts and commits, and controls for workload generation, local-storage measurement, and transport latency. Benchmark results are written to the application log under dole::bench.
The compact transaction sizes used by synchronisation can be printed with:
cargo test --release --manifest-path core/Cargo.toml \
--features bench-workload wire_size_stats_print_for_manual_inspection -- --nocaptureGlobal Iroh synchronisation requires at least one continuously running hub. The repository already points to the public hub directory configured by IROH_HUB_DIRECTORY_URL in constants.conf. To use another hub directory, change that value and rebuild the project.
On the hub server, select a persistent storage directory, print the hub identifier, and start the hub:
export HUB_STORAGE="$HOME/.dole-hub"
cargo run --release --manifest-path hub/Cargo.toml -- --print-id
cargo run --release --manifest-path hub/Cargo.tomlAdd the printed identifier to the publicly hosted hubs.txt, with one identifier per line. Empty lines and lines beginning with # are ignored. Keep the generated hub.key; it gives the hub its ID.
The hub joins the shared gossip topic and helps devices meet globally. It does not hold account keys, store the Git ledger, verify transactions, or act as a central authority.
BLE is the slowest transport because advertisements remain unchanged long enough for less reliable scanners to receive them. In practice this limits the offline path to roughly one transaction per second. BLE is unavailable on Apple platforms because the required extended advertising API is not public.
The global Iroh network uses a shared gossip topic, which keeps peer discovery simple but is not designed for an unrestricted production-scale payment network. The wallet also targets recent platforms: Android 8.0 or newer and iOS 26 or newer.
The Git-based implementation preserves the wallet behaviour of the earlier Ditto prototype while making storage, verification, and synchronisation inspectable. Its compact transaction representation and local store are substantially smaller than Ditto's. In the evaluation, Iroh achieved lower online transaction latency, while Ditto generally caught up larger histories faster. BLE remained the principal latency limitation.
A card is provisioned once as either a normal user card or a minter card. The wallet reads its certificate, creates the account, and writes the genesis transaction to the ledger. The same physical card can then be used with another DOLE device.
A normal user can send and burn funds. Only a certified minter card exposes minting in the unmodified wallet.
User card Minter card
The PIN unlocks operations on the card but does not expose its private key. Incorrect attempts are counted by the card, and three failed attempts block further access.
Once another account's genesis branch has been discovered, the account appears in the recipient selector. The sender chooses the peer and amount and presents the card to authorise the transaction.
An incoming send remains pending until the recipient's card is present. After verification and processing by the card, the transaction moves to Recent and the card balance is updated.
Mint and burn operations also require the card. The applet updates its protected state and signs the transaction before the Rust core accepts it into the ledger.
Mint Burn
Even if a modified application displays the Mint button for a normal user, the card refuses the operation because the minter privilege is checked by the applet.
BLE, local mDNS/Iroh, and global Iroh can be enabled independently. If the same transaction arrives through multiple transports, it is stored only once.










