Skip to content
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
19876b6
chore: add reproducible build verification script
basantagoswami Dec 19, 2025
cb3bee7
fix (reproducibility): add --network=host to verify script to fix pod…
basantagoswami Jan 29, 2026
e79b339
fix (reproducibility): issues in dockerfile
basantagoswami Jan 30, 2026
a84c643
fix (reproducibility): increase ulimit and configure gradle memory
basantagoswami Jan 31, 2026
3db65e6
fix (reproducibility): add verification tools container
basantagoswami Feb 5, 2026
217e16e
fix (reproducibility): fix apktool download url
basantagoswami Feb 5, 2026
591a973
fix (reproducibility): fix apkDir path resolution and commit extraction
basantagoswami Feb 5, 2026
442b340
refactor (reproducibility): clean up Dockerfile and makefile
basantagoswami Mar 5, 2026
947f33a
fix (reproducibility): use CARGO_ENCODED_RUSTFLAGS with correct separ…
basantagoswami Mar 9, 2026
9d36c99
fix (reproducibility): set SOURCE_DATE_EPOCH to eliminate OpenSSL bui…
basantagoswami Mar 10, 2026
46b3553
refactor (reproducibility): overhaul verify_build.sh and verification…
basantagoswami Mar 10, 2026
a79cedc
fix (reproducibility): skip tag check when --apk is provided
basantagoswami Mar 11, 2026
968dfec
docs/fix (reproducibility): update README, make --version optional wh…
basantagoswami Mar 11, 2026
aaca481
fix (reproducibility): always enforce tag check regardless of --apk
basantagoswami Mar 11, 2026
3aa3424
chore (reproducibility): update makefile docker-build command with up…
basantagoswami Mar 11, 2026
d722588
feat: dockerignore build to accelerate COPY command
ethicnology Apr 9, 2026
6116988
Merge branch 'develop' into reproducibility
ethicnology Apr 11, 2026
48c4dbc
refactor: ignore more
ethicnology Apr 13, 2026
29c7172
test: script two builds
ethicnology Apr 13, 2026
dbc6d2a
fix: use bdk-dart fork with deterministic uniffi trait ordering
ethicnology Apr 14, 2026
badd84f
refactor: remove fake keystore from Docker build to produce unsigned …
ethicnology Apr 14, 2026
3014af1
feat: extract APK to host after Docker build
ethicnology Apr 14, 2026
de44c1b
refactor: remove dotenv, hardcode config, produce unsigned release APK
ethicnology Apr 14, 2026
fe6b686
fix: use signed commit
ethicnology Apr 14, 2026
0eba593
Merge branch 'develop' into repro-from-devcontainers
i5hi Apr 14, 2026
2351209
fix: add missing constant
ethicnology Apr 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 29 additions & 29 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
@@ -1,32 +1,32 @@
{
"name": "${localWorkspaceFolderBasename}",
"build": {
"dockerfile": "../Dockerfile",
"args": {
"USERNAME": "${localEnv:USER}",
"name": "${localWorkspaceFolderBasename}",
"build": {
"dockerfile": "../Dockerfile",
"args": {
"USERNAME": "${localEnv:USER}"
}
},
},
"remoteUser": "${localEnv:USER}",
"updateRemoteUserUID": true,
"shutdownAction": "stopContainer",
"initializeCommand": ".devcontainer/init-ssh-agent.sh",
"runArgs": [
"--name",
"${localWorkspaceFolderBasename}",
"--hostname",
"bull",
"--memory=8g",
"--security-opt",
"label=disable",
],
"mounts": [
"source=${localEnv:HOME}/.ssh,target=/home/${localEnv:USER}/.ssh,type=bind,consistency=cached",
"source=/tmp/ssh-agent.sock,target=/ssh-agent,type=bind,consistency=cached",
"source=${localEnv:HOME}/.gitconfig,target=/home/${localEnv:USER}/.gitconfig,type=bind,consistency=cached",
],
"containerEnv": {
"SSH_AUTH_SOCK": "/ssh-agent",
},
"postCreateCommand": "flutter config --no-analytics && dart --disable-analytics && cp -n .env.template .env",
"postStartCommand": "sudo chown -R --no-dereference ${localEnv:USER}:${localEnv:USER} /home/${localEnv:USER}/.ssh || true; chmod 700 /home/${localEnv:USER}/.ssh; find /home/${localEnv:USER}/.ssh -maxdepth 1 -type f -exec chmod 600 {} + 2>/dev/null || true",
"remoteUser": "${localEnv:USER}",
"updateRemoteUserUID": true,
"shutdownAction": "stopContainer",
"initializeCommand": ".devcontainer/init-ssh-agent.sh",
"runArgs": [
"--name",
"${localWorkspaceFolderBasename}",
"--hostname",
"bull",
"--memory=8g",
"--security-opt",
"label=disable"
],
"mounts": [
"source=${localEnv:HOME}/.ssh,target=/home/${localEnv:USER}/.ssh,type=bind,consistency=cached",
"source=/tmp/ssh-agent.sock,target=/ssh-agent,type=bind,consistency=cached",
"source=${localEnv:HOME}/.gitconfig,target=/home/${localEnv:USER}/.gitconfig,type=bind,consistency=cached"
],
"containerEnv": {
"SSH_AUTH_SOCK": "/ssh-agent"
},
"postCreateCommand": "flutter config --no-analytics && dart --disable-analytics",
"postStartCommand": "sudo chown -R --no-dereference ${localEnv:USER}:${localEnv:USER} /home/${localEnv:USER}/.ssh || true; chmod 700 /home/${localEnv:USER}/.ssh; find /home/${localEnv:USER}/.ssh -maxdepth 1 -type f -exec chmod 600 {} + 2>/dev/null || true"
}
7 changes: 7 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
build/
.dart_tool/
.gradle/
android/.gradle/
ios/
.idea/
*.iml
11 changes: 0 additions & 11 deletions .env.template

This file was deleted.

42 changes: 42 additions & 0 deletions Dockerfile.apk
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
FROM bull-mobile

# Build configuration arguments
# MODE: flutter build mode: debug or release
ARG MODE=debug
# FORMAT: output format: apk or aab
ARG FORMAT=apk
# GRADLE_HEAP: JVM heap size for Gradle (e.g. 2g, 4g, 6g)
ARG GRADLE_HEAP=4g
COPY --chown=$USER:$USER . /app/

WORKDIR /app

# Install Flutter version specified in .fvmrc (no-op if it matches base image)
RUN fvm install

# Setup the project
RUN fvm flutter pub get
RUN fvm dart run build_runner build --delete-conflicting-outputs
RUN fvm flutter gen-l10n


# Configure Gradle for containerized builds
RUN mkdir -p $HOME/.gradle && \
echo "org.gradle.daemon=false" > $HOME/.gradle/gradle.properties && \
echo "org.gradle.jvmargs=-Xmx${GRADLE_HEAP} -XX:+HeapDumpOnOutOfMemoryError" >> $HOME/.gradle/gradle.properties

# Build the app
# SOURCE_DATE_EPOCH: makes OpenSSL use a deterministic build timestamp instead of wall-clock time
# CARGO_ENCODED_RUSTFLAGS: remaps absolute paths so they don't differ between machines
RUN SOURCE_DATE_EPOCH=$(git -C /app log -1 --format=%ct) && \
CARGO_ENCODED_RUSTFLAGS=$(printf '%s\037%s\037%s' \
"--remap-path-prefix=$HOME/.cargo=/cargo" \
"--remap-path-prefix=$HOME/.rustup=/rustup" \
"--remap-path-prefix=/app=/build") && \
CARGO_PROFILE_RELEASE_CODEGEN_UNITS=1 && \
export SOURCE_DATE_EPOCH CARGO_ENCODED_RUSTFLAGS CARGO_PROFILE_RELEASE_CODEGEN_UNITS && \
if [ "$FORMAT" = "aab" ]; then \
fvm flutter build appbundle --${MODE}; \
else \
fvm flutter build apk --${MODE}; \
fi
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ Both these wallets are able to send and receive Lightning Network payments via t

### [Default service providers](https://github.com/SatoshiPortal/bullbitcoin-mobile/blob/develop/lib/core/utils/constants.dart#L60)

### [Reproducible builds](reproducibility/README.md)

The app can be built reproducibly from source and verified against official releases. See the [`reproducibility/`](reproducibility/) directory for instructions.

### General features

- Non-custodial: private keys are generated on the device, and never leave the device.
Expand Down
2 changes: 1 addition & 1 deletion android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ android {
release {
minifyEnabled false
shrinkResources false
signingConfig signingConfigs.release
signingConfig keystorePropertiesFile.exists() ? signingConfigs.release : null
}
}

Expand Down
2 changes: 1 addition & 1 deletion cargokit_options.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@ verbose_logging: false
# and deployed precompiled binaries, these will be by default used whenever Rustup
# is not installed. With `use_precompiled_binaries` set to false, the build will
# instead be aborted prompting user to install Rustup.
use_precompiled_binaries: true
use_precompiled_binaries: false
7 changes: 4 additions & 3 deletions integration_test/payjoin_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@ import 'package:bb_mobile/features/send/domain/usecases/prepare_bitcoin_send_use
import 'package:bb_mobile/features/settings/domain/usecases/set_environment_usecase.dart';
import 'package:bb_mobile/locator.dart';
import 'package:bb_mobile/main.dart';
import 'dart:io' show Platform;

import 'package:flutter/material.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:flutter_test/flutter_test.dart' show TestWidgetsFlutterBinding;
import 'package:test/test.dart';

Expand All @@ -36,8 +37,8 @@ Future<void> main({bool isInitialized = false}) async {
final sendWithPayjoinUsecase = locator<SendWithPayjoinUsecase>();
final prepareBitcoinSendUsecase = locator<PrepareBitcoinSendUsecase>();

final receiverMnemonic = dotenv.env['TEST_ALICE_MNEMONIC'];
final senderMnemonic = dotenv.env['TEST_BOB_MNEMONIC'];
final receiverMnemonic = Platform.environment['TEST_ALICE_MNEMONIC'];
final senderMnemonic = Platform.environment['TEST_BOB_MNEMONIC'];

if (receiverMnemonic == null || receiverMnemonic.isEmpty) {
throw Exception('TEST_ALICE_MNEMONIC environment variable is not set');
Expand Down
2 changes: 0 additions & 2 deletions lib/core/background_tasks/handler.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import 'package:bb_mobile/core/wallet/domain/usecases/get_wallets_usecase.dart';
import 'package:bb_mobile/core/wallet/domain/usecases/sync_wallet_usecase.dart';
import 'package:bb_mobile/locator.dart';
import 'package:bb_mobile/main.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:get_it/get_it.dart';
import 'package:lwk/lwk.dart';
import 'package:workmanager/workmanager.dart';
Expand All @@ -21,7 +20,6 @@ void backgroundTasksHandler() {
Future<bool> tasksHandler(String task) async {
final startTime = DateTime.now();

await dotenv.load(isOptional: true);
await Bull.initLogs();
await LibLwk.init();

Expand Down
27 changes: 14 additions & 13 deletions lib/core/utils/constants.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';

class Device {
static late Size screen;
Expand Down Expand Up @@ -98,20 +97,22 @@ class ApiServiceConstants {
static const boltzReferralId = 'BULL';

// BullBitcoin API
static String bbApiUrl =
dotenv.env['BB_API_URL'] ?? 'https://api.bullbitcoin.com';
static String bbApiTestUrl =
dotenv.env['BB_API_TEST_URL'] ?? 'https://api05.bullbitcoin.dev';
static String bbAuthUrl = 'https://${dotenv.env['BB_AUTH_URL']}';
static String bbAuthTestUrl = 'https://${dotenv.env['BB_AUTH_TEST_URL']}';
static String bbKycUrl = 'https://app.bullbitcoin.com/kyc';
static String bbKycTestUrl = 'https://bbx05.bullbitcoin.dev/kyc';
static String bbAppUrl = 'https://app.bullbitcoin.com';
static String googleDriveClientId =
dotenv.env['GOOGLE_DRIVE_CLIENT_ID'] ?? '';
static const String bbApiUrl = 'https://api.bullbitcoin.com';
static const String bbApiTestUrl = 'https://api05.bullbitcoin.dev';
static const String bbAuthUrl = 'https://accounts.bullbitcoin.com';
static const String bbAuthTestUrl = 'https://accounts05.bullbitcoin.dev';
static const String bbKycUrl = 'https://app.bullbitcoin.com/kyc';
static const String bbKycTestUrl = 'https://bbx05.bullbitcoin.dev/kyc';
static const String googleDriveClientId =
'97584343569-0mc4e5q9q1qino4vvo97mqomdi89sae5.apps.googleusercontent.com';

// Error reports
static String sentryDsn = dotenv.env['SENTRY_DSN'] ?? '';
static const String sentryDsn =
'https://b6a8d5134da043eda72f231891c6e51a@cc.bullbitcoin.com/1';

// Exchange basic auth
static const String basicAuthUsername = 'bbadmin';
static const String basicAuthPassword = 'We are staging 05!';
}

class LocatorInstanceNameConstants {
Expand Down
5 changes: 2 additions & 3 deletions lib/features/exchange/ui/screens/exchange_auth_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import 'package:bb_mobile/features/settings/presentation/bloc/settings_cubit.dar
import 'package:bb_mobile/features/wallet/ui/wallet_router.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:go_router/go_router.dart';
import 'package:webview_cookie_manager/webview_cookie_manager.dart';
import 'package:webview_flutter/webview_flutter.dart';
Expand Down Expand Up @@ -171,8 +170,8 @@ class _ExchangeAuthScreenState extends State<ExchangeAuthScreen> {
onHttpAuthRequest: (HttpAuthRequest request) {
request.onProceed(
WebViewCredential(
user: dotenv.env['BASIC_AUTH_USERNAME'] ?? '',
password: dotenv.env['BASIC_AUTH_PASSWORD'] ?? '',
user: ApiServiceConstants.basicAuthUsername,
password: ApiServiceConstants.basicAuthPassword,
),
);
},
Expand Down
5 changes: 2 additions & 3 deletions lib/features/exchange/ui/screens/exchange_kyc_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import 'package:bb_mobile/features/exchange/presentation/exchange_cubit.dart';
import 'package:bb_mobile/features/settings/presentation/bloc/settings_cubit.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:go_router/go_router.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:webview_flutter/webview_flutter.dart';
Expand Down Expand Up @@ -95,8 +94,8 @@ class _ExchangeKycScreenState extends State<ExchangeKycScreen> {
onHttpAuthRequest: (HttpAuthRequest request) {
request.onProceed(
WebViewCredential(
user: dotenv.env['BASIC_AUTH_USERNAME'] ?? '',
password: dotenv.env['BASIC_AUTH_PASSWORD'] ?? '',
user: ApiServiceConstants.basicAuthUsername,
password: ApiServiceConstants.basicAuthPassword,
),
);
},
Expand Down
2 changes: 0 additions & 2 deletions lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ import 'package:dart_bbqr/bbqr.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart' show dotenv;
import 'package:lwk/lwk.dart';
import 'package:path_provider/path_provider.dart';
import 'package:payjoin_flutter/common.dart';
Expand All @@ -49,7 +48,6 @@ class Bull {

static Future<void> initFlutterRustBridgeDependencies() async {
final initTasks = [
dotenv.load(isOptional: true),
LibLwk.init(),
BoltzCore.init(),
PConfig.initializeApp(),
Expand Down
33 changes: 32 additions & 1 deletion makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.PHONY: all setup clean deps build-runner translations hooks ios-pod-update drift-migrations devcontainer docker-build test unit-test integration-test fvm-check
.PHONY: all setup clean deps build-runner translations hooks ios-pod-update drift-migrations devcontainer docker-build apk verify test unit-test integration-test fvm-check

fvm-check:
@echo "🔍 Checking FVM"
Expand Down Expand Up @@ -70,6 +70,37 @@ docker-build:
--build-arg ANDROID_NDK=$$(grep 'android.ndkVersion' android/gradle.properties | cut -d= -f2) \
.

MODE ?= debug
FORMAT ?= apk

# Allow "make apk release" or "make apk debug" syntax
ifneq (,$(filter release,$(MAKECMDGOALS)))
MODE := release
endif
ifneq (,$(filter debug,$(MAKECMDGOALS)))
MODE := debug
endif
release debug:
@:

apk: docker-build
@echo "🔨 Building $(FORMAT) ($(MODE)) via Docker"
@docker build -f Dockerfile.apk \
--build-arg MODE=$(MODE) \
--build-arg FORMAT=$(FORMAT) \
--build-arg GRADLE_HEAP=$(or $(GRADLE_HEAP),4g) \
-t bull-mobile-apk .
@docker rm -f bull-apk-extract > /dev/null 2>&1 || true
@docker create --name bull-apk-extract bull-mobile-apk > /dev/null
@docker cp bull-apk-extract:/app/build/app/outputs/flutter-apk/app-$(MODE).apk ./app-$(MODE).apk
@docker rm bull-apk-extract > /dev/null
@echo "✅ APK extracted: ./app-$(MODE).apk"
@sha256sum ./app-$(MODE).apk

verify:
@echo "🔍 Verifying reproducible build"
@./reproducibility/verify_build.sh $(if $(VERSION),--version $(VERSION)) $(if $(APK),--apk $(APK))

devcontainer:
@echo "🏗️ Building Dev Container"
@devcontainer up --workspace-folder . --config ./.devcontainer/devcontainer.json
Expand Down
14 changes: 3 additions & 11 deletions pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -94,9 +94,9 @@ packages:
dependency: "direct main"
description:
path: "."
ref: "6dfdddbfa6da8fbfa82d796c2564fbeec899232e"
resolved-ref: "6dfdddbfa6da8fbfa82d796c2564fbeec899232e"
url: "https://github.com/bitcoindevkit/bdk-dart"
ref: ebf4a6675ede8f7337ab18121157717dc9dc9751
resolved-ref: ebf4a6675ede8f7337ab18121157717dc9dc9751
url: "https://github.com/ethicnology/bdk-dart"
source: git
version: "2.3.0-alpha.0"
bdk_flutter:
Expand Down Expand Up @@ -685,14 +685,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "9.1.1"
flutter_dotenv:
dependency: "direct main"
description:
name: flutter_dotenv
sha256: d4130c4a43e0b13fefc593bc3961f2cb46e30cb79e253d4a526b1b5d24ae1ce4
url: "https://pub.dev"
source: hosted
version: "6.0.0"
flutter_driver:
dependency: transitive
description: flutter
Expand Down
6 changes: 2 additions & 4 deletions pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ dependencies:
sdk: flutter
bdk_dart:
git:
url: https://github.com/bitcoindevkit/bdk-dart
ref: 6dfdddbfa6da8fbfa82d796c2564fbeec899232e
url: https://github.com/ethicnology/bdk-dart
ref: ebf4a6675ede8f7337ab18121157717dc9dc9751
flutter_bloc: ^9.1.1
freezed: ^3.2.3
get_it: ^9.1.1
Expand Down Expand Up @@ -40,7 +40,6 @@ dependencies:
git:
url: https://github.com/SatoshiPortal/bitbox-dart
ref: d44d96983a39896fdc02a2d320036a5ef340e5eb
flutter_dotenv: ^6.0.0
hex: ^0.2.0
auto_size_text: ^3.0.0
no_screenshot: ^0.3.1
Expand Down Expand Up @@ -156,7 +155,6 @@ flutter:
uses-material-design: true

assets:
- .env
- assets/
- assets/icons/
- assets/logos/
Expand Down
Loading
Loading