Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -748,11 +748,13 @@ class AndroidLayerProofTest {

@Test
fun t64_error_wrongPayloadSize_forAcceptBilateral() {
// acceptBilateralByCommitment expects exactly 32 bytes
val wrongSize = ByteArray(16)
val resp = callBridgeMethod("acceptBilateralByCommitment", wrongSize)
// Should return success with empty data (validation rejects < 32 bytes)
assertTrue("Must not crash", resp.first)
// acceptBilateralByCommitment takes exactly 32 bytes. Anything else is
// the dispatcher's error carrying the arm's reason, never a success.
val resp = callBridgeMethod("acceptBilateralByCommitment", ByteArray(16))
assertFalse("A 16-byte commitment is not accepted", resp.first)
val error = BridgeEnvelopeCodec.decodeBridgeRpcError(resp.second)
assertNotNull("The error decodes", error)
assertTrue(error!!.message, error.message.contains("expected 32 bytes, got 16"))
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,6 @@ object Unified {
// ---------- Protobuf-only externals ----------
@Keep @JvmStatic fun initSdk(baseDir: String): Boolean =
UnifiedNativeApi.initSdk(baseDir)
@Keep @JvmStatic fun initSdkV3(baseDir: String): ByteArray =
UnifiedNativeApi.initSdkV3(baseDir)
@Keep @JvmStatic fun initStorageBaseDir(path: ByteArray) {
UnifiedNativeApi.initStorageBaseDir(path)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@ internal object UnifiedNativeApi {
}

@Keep @JvmStatic external fun initSdk(baseDir: String): Boolean
@Keep @JvmStatic external fun initSdkV3(baseDir: String): ByteArray
@Keep @JvmStatic external fun initStorageBaseDir(path: ByteArray)
@Keep @JvmStatic external fun initDsmSdk(configPath: String)
@Keep @JvmStatic external fun dispatchStartup(requestBytes: ByteArray): ByteArray
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1613,8 +1613,13 @@ class MainActivity : AppCompatActivity(), NfcAdapter.ReaderCallback {
}

Log.i(tag, "initDsmAndSignalReady: Calling initSdk...")
Unified.initSdk(baseDir)
Log.i(tag, "initDsmAndSignalReady: SDK initialized; switching to UI thread...")
// A failed startup is Rust's to report: it records the reason as
// the session's fatal error, and the page shows it.
if (Unified.initSdk(baseDir)) {
Log.i(tag, "initDsmAndSignalReady: SDK initialized")
} else {
Log.e(tag, "initDsmAndSignalReady: SDK initialization failed; the session carries Rust's reason")
}

Log.i(tag, "initDsmAndSignalReady: event-driven bridge mode enabled; will rely on dsm-bridge-ready signal")
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.dsm.wallet.bridge

import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
Expand Down Expand Up @@ -82,21 +83,30 @@ class BridgeLoggerTest {
assertTrue("Log should contain BRIDGE prefix", content.contains("BRIDGE:"))
}

// A bridge payload can be the mnemonic, and the log is a file on disk:
// it names the payload's size and never a byte of it.
@Test
fun logBridgeCall_shortPayload_notTruncated() {
val payload = byteArrayOf(0x01, 0x02, 0x03)
val b32 = BridgeEncoding.base32CrockfordEncode(payload)
fun logBridgeCall_namesThePayloadSize_neverItsBytes() {
val payload = byteArrayOf(0x01, 0x02, 0x03, 0x04, 0x05)
BridgeLogger.logBridgeCall("m", payload, null, null)
val content = String(BridgeLogger.readLogBytes(), Charsets.UTF_8)
assertTrue("Short payload b32 should appear in full", content.contains(b32))
assertTrue("The size is logged", content.contains("payload=5b"))
assertFalse(
"No byte of the payload is logged",
content.contains(BridgeEncoding.base32CrockfordEncode(payload))
)
}

@Test
fun logBridgeCall_longPayload_truncated() {
fun logBridgeCall_longPayload_notEvenAPrefix() {
val payload = ByteArray(100) { it.toByte() }
BridgeLogger.logBridgeCall("m", payload, null, null)
val content = String(BridgeLogger.readLogBytes(), Charsets.UTF_8)
assertTrue("Long payload should be truncated with ...", content.contains("..."))
assertTrue("The size is logged", content.contains("payload=100b"))
assertFalse(
"Not even its first five bytes",
content.contains(BridgeEncoding.base32CrockfordEncode(payload.copyOfRange(0, 5)))
)
}

@Test
Expand Down
10 changes: 0 additions & 10 deletions dsm_client/deterministic_state_machine/dsm/src/core/bridge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -648,16 +648,6 @@ pub fn handle_envelope_universal(env_bytes: &[u8]) -> Vec<u8> {
})
}

// Init/status messages are produced by the SDK/JNI surfaces and should not be routed
// through the core universal handler as "requests".
Some(gp::envelope::Payload::InitFailed(_)) => gp::envelope::Payload::Error(gp::Error {
code: 409,
message: "InitFailed should not be sent as a request".to_string(),
context: vec![],
source_tag: 10,
is_recoverable: false,
debug_b32: "".to_string(),
}),

// NEW: Explicit guard for genesis-created responses (SDK-only)
Some(gp::envelope::Payload::GenesisCreatedResponse(_)) => {
Expand Down
11 changes: 6 additions & 5 deletions dsm_client/deterministic_state_machine/dsm/src/sofi/smt/fold.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,6 @@ impl SmtHashes for EconomicHashes {
}
}

/// Fold a core's entries into its pre- and post-roots.
pub fn batch_fold(entries: &[FoldEntry]) -> Result<Folded, FoldError> {
batch_fold::batch_fold::<EconomicHashes>(entries)
}

/// Fold against a claimed pre-root, returning the post-root.
pub fn verify_batch(pre_root: &[u8; 32], entries: &[FoldEntry]) -> Result<[u8; 32], FoldError> {
batch_fold::verify_batch::<EconomicHashes>(pre_root, entries)
Expand All @@ -49,6 +44,12 @@ mod tests {
use crate::economic::tree::{empty_economic_root, root_from_path, EconomicSmt};
use proptest::prelude::*;

/// The one fold over the economic tree's hashes: what `verify_batch`
/// runs, with its pre-root returned instead of checked.
fn batch_fold(entries: &[FoldEntry]) -> Result<Folded, FoldError> {
batch_fold::batch_fold::<EconomicHashes>(entries)
}

fn key(i: u64) -> [u8; 32] {
let mut k = [0u8; 32];
let mut x = i.wrapping_add(1).wrapping_mul(0x9E37_79B9_7F4A_7C15);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,4 @@
//! decides no canonicality, because `advance_resolved` already did.
pub mod fold;

pub use fold::{batch_fold, verify_batch, FoldEntry, FoldError, Folded};
pub use fold::{verify_batch, FoldEntry, FoldError, Folded};
14 changes: 12 additions & 2 deletions dsm_client/deterministic_state_machine/dsm/src/sofi/validation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1896,7 +1896,12 @@ pub(crate) mod fixtures {
//! conformance tests (R7): a real `P(E)`, its `E`, a precommit whose legs
//! derive from it, and the acquired evidence.
use super::*;
use crate::sofi::smt::batch_fold;
/// The one fold over the economic tree's hashes, as `verify_batch` runs it.
fn batch_fold(
entries: &[crate::sofi::smt::FoldEntry],
) -> Result<crate::sofi::smt::Folded, crate::sofi::smt::FoldError> {
crate::merkle::batch_fold::batch_fold::<crate::sofi::smt::fold::EconomicHashes>(entries)
}
use crate::economic::tree::EconomicSmt;
use crate::sofi::wire::{PreEClosureIndex, PrecommitLeg, SofiSetupBody, ValidationRef};

Expand Down Expand Up @@ -2472,7 +2477,12 @@ mod tests {
use super::fixtures::*;
use super::*;
use crate::economic::tree::{EconomicSmt, ECONOMIC_SMT_HEIGHT};
use crate::sofi::smt::batch_fold;
/// The one fold over the economic tree's hashes, as `verify_batch` runs it.
fn batch_fold(
entries: &[crate::sofi::smt::FoldEntry],
) -> Result<crate::sofi::smt::Folded, crate::sofi::smt::FoldError> {
crate::merkle::batch_fold::batch_fold::<crate::sofi::smt::fold::EconomicHashes>(entries)
}
use crate::sofi::wire::{PreEClosureIndex, PrecommitLeg};

#[test]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -733,14 +733,14 @@ async fn bearer_pair() -> (
Pair,
OfflineDevice,
OfflineDevice,
crate::test_support::appliance::HostAppliance,
crate::test_support::appliance::InstalledAppliance,
Operation,
) {
let pair = Pair::boot(100, 0).await;
let a = OfflineDevice::new(&pair.a);
let b = OfflineDevice::new(&pair.b);
let appliance = crate::test_support::appliance::HostAppliance::birth(&pair.a, [0xC4; 32], 16);
appliance.install();
let appliance =
crate::test_support::appliance::HostAppliance::birth(&pair.a, [0xC4; 32], 16).install();
a.device.enter();
let loaded = a
.device
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,14 @@ pub fn install_anchor_appliance_factory(factory: AnchorApplianceFactory) {
}
}

/// Remove the installed factory: the process has no appliance again.
#[cfg(test)]
pub(crate) fn uninstall_anchor_appliance_factory() {
if let Ok(mut g) = ANCHOR_APPLIANCE_FACTORY.write() {
*g = None;
}
}

#[must_use]
pub fn anchor_appliance_factory() -> Option<AnchorApplianceFactory> {
ANCHOR_APPLIANCE_FACTORY.read().ok()?.clone()
Expand Down
62 changes: 58 additions & 4 deletions dsm_client/deterministic_state_machine/dsm_sdk/src/ingress.rs
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,7 @@ fn initialize_sdk_core() -> Result<Vec<u8>, pb::Error> {
match crate::runtime::get_runtime().block_on(crate::init_dsm_sdk()) {
Ok(()) => {
crate::sdk::session_manager::set_sdk_ready(true);
crate::sdk::session_manager::clear_startup_failure();

// Resume any identity whose publication never reached quorum.
// Publication is a precondition of "identity created", so a device
Expand All @@ -273,10 +274,9 @@ fn initialize_sdk_core() -> Result<Vec<u8>, pb::Error> {
}
Err(e) => {
crate::sdk::session_manager::set_sdk_ready(false);
Err(ingress_error(
ERROR_CODE_NOT_READY,
format!("startup: init_dsm_sdk failed: {e}"),
))
let message = format!("startup: init_dsm_sdk failed: {e}");
crate::sdk::session_manager::record_startup_failure(&message);
Err(ingress_error(ERROR_CODE_NOT_READY, message))
}
}
}
Expand Down Expand Up @@ -977,6 +977,60 @@ mod tests {
drop(fleet);
}

/// A phone an older build provisioned keeps its store at that build's
/// schema, and this build refuses it: beta does not migrate. Startup fails
/// in the store's own words and that is the session's error, so the page
/// leaves "starting runtime" and says what to do. The nodes run, the device
/// is created as wallet creation creates it, and its store is left as the
/// older build left it: stamped 24, without the table 25 added.
#[test]
#[serial]
fn a_store_at_an_older_schema_fails_startup_as_the_sessions_error() {
let fleet = fleet();
let _identity = economic_fixtures::local_device(0x12).0;
{
let store = crate::storage::client_db::get_connection().expect("the store");
let conn = store.lock().expect("the store lock");
conn.execute_batch("DROP TABLE history_repair_queue; PRAGMA user_version = 24;")
.expect("leave the store as schema 24 left it");
}
// The process that provisioned it is gone.
crate::storage::client_db::close_database_for_tests();
crate::sdk::app_state::AppState::reset_memory_for_testing();
fresh_process();
crate::sdk::session_manager::clear_fatal_error_and_snapshot().expect("clear");

let start = || {
dispatch_startup(StartupRequest {
operation: Some(startup_request::Operation::InitializeSdk(
pb::InitializeSdkOp {},
)),
})
};
let snapshot = || {
crate::sdk::session_manager::SESSION_MANAGER
.lock()
.unwrap_or_else(|p| p.into_inner())
.compute_snapshot()
};
let error = expect_startup_error(start());
assert!(
error.message.contains("SCHEMA RESET REQUIRED"),
"{}",
error.message
);
assert_eq!(snapshot().phase, "error");
assert_eq!(snapshot().fatal_error, error.message);

// The remedy the refusal names, a wiped store: startup then succeeds
// and takes its own failure back.
crate::storage::client_db::reset_database_for_tests();
expect_startup_ok(start());
assert_eq!(snapshot().fatal_error, "");
assert_ne!(snapshot().phase, "error");
drop(fleet);
}

#[test]
#[serial]
fn initialize_identity_context_sets_identity_and_router() {
Expand Down
9 changes: 5 additions & 4 deletions dsm_client/deterministic_state_machine/dsm_sdk/src/init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -554,16 +554,17 @@ pub fn init_dsm_sdk(cfg: &SdkConfig) -> Result<(), String> {
// create_genesis_v2 registered. No DBRW / device secret. The wallet seed is the
// unlocked-session secret; it is cached at unlock (RecoverySDK::derive_and_cache_key)
// and sealed at rest, so on a cold start we first try the hardware seed vault before
// demanding the mnemonic again. A missing/auth-gated bundle fails closed → locked UI.
// demanding the mnemonic again. Either way init fails without it, and the failure is
// the session's fatal error. A vault that cannot be read is that failure in its own
// words: reporting it as "not unlocked" sent a device whose store refused its schema
// looking for its mnemonic.
if crate::sdk::recovery_sdk::RecoverySDK::get_cached_wallet_seed().is_none() {
match crate::sdk::recovery_sdk::RecoverySDK::load_and_cache_wallet_seed() {
Ok(true) => log::info!("[SDK Init] Wallet seed unsealed from vault (cold start)"),
Ok(false) => {
log::info!("[SDK Init] No sealed wallet seed — mnemonic unlock required")
}
Err(e) => {
log::warn!("[SDK Init] Seed vault unlock failed ({e}) — mnemonic required")
}
Err(e) => return Err(format!("the sealed wallet seed could not be read: {e}")),
}
}
let wallet_seed = crate::sdk::recovery_sdk::RecoverySDK::get_cached_wallet_seed()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -385,75 +385,6 @@ pub extern "system" fn Java_com_dsm_wallet_bridge_UnifiedNativeApi_getAllBalance
)
}

/// Protobuf-first init entrypoint.
#[no_mangle]
pub extern "system" fn Java_com_dsm_wallet_bridge_UnifiedNativeApi_initSdkV3(
env: jni::sys::JNIEnv,
_class: jni::sys::jclass,
jbase: jni::sys::jstring,
) -> jni::sys::jbyteArray {
crate::jni::bridge_utils::jni_catch_unwind_jbytearray(
"initSdkV3",
std::panic::AssertUnwindSafe(|| {
let mut env = match unsafe { env_from(env) } {
Some(e) => e,
None => return std::ptr::null_mut(),
};
let jbase = unsafe { jstr_from(jbase) };
let base: String = match env.get_string(&jbase) {
Ok(s) => s.into(),
Err(e) => {
log::error!("initSdkV3: failed to read baseDir: {}", e);
String::new()
}
};

let respond =
|payload: pb::envelope::Payload, env: &mut JNIEnv| -> jni::sys::jbyteArray {
framed_payload_byte_array(env, payload).into_raw()
};

if base.is_empty() {
SDK_READY.store(false, Ordering::SeqCst);
return respond(
pb::envelope::Payload::InitFailed(pb::InitFailed {
reason: pb::init_failed::Reason::InvalidInput as i32,
message: "baseDir is empty".to_string(),
}),
&mut env,
);
}

if let Err(error) = route_startup_via_ingress(
pb::startup_request::Operation::SetStorageBaseDir(pb::SetStorageBaseDirOp {
path_utf8: base.clone(),
}),
) {
SDK_READY.store(false, Ordering::SeqCst);
return respond(
pb::envelope::Payload::InitFailed(pb::InitFailed {
reason: pb::init_failed::Reason::PlatformContextMissing as i32,
message: format!("failed to set storage base dir: {}", error.message),
}),
&mut env,
);
}

// Genesis v2: no C-DBRW binding key to gate init on. The device signing key is
// re-derived from the unlocked wallet seed on demand; signing operations fail
// closed individually when the wallet is locked.
SDK_READY.store(true, Ordering::SeqCst);
respond(
pb::envelope::Payload::AppStateResponse(pb::AppStateResponse {
key: "sdk.init".to_string(),
value: Some("ok".to_string()),
}),
&mut env,
)
}),
)
}

/// Remove a contact by contact_id.
/// Returns 1 on success, 0 on failure.
#[no_mangle]
Expand Down
Loading
Loading