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 @@ -150,21 +150,27 @@ impl StateMachine {
// Mirror the verbatim State for the validation-tooling compat shims.
// NOT read by current_state(); see the field doc.
self.compat_shim_state = Some(state.clone());
if self.device_state.is_none() {
let mut ds = crate::types::device_state::DeviceState::new(
[0u8; 32],
state.device_info.device_id,
state.device_info.public_key.clone(),
1024,
);
// Seed SMT root with the State's hash for legacy compat.
// Re-seed an EXISTING head only. This function does not know the
// genesis authority root `G`, so it must not manufacture a head that
// claims one.
//
// It used to build `DeviceState::new([0u8; 32], ...)` whenever no head
// existed. That zero is not "unset" to any reader — `genesis_digest()`
// returns it as a genesis root like any other. Because genesis install
// calls this BEFORE `write_genesis_device_head`, the fabricated
// zero-root head is the one that got persisted, and the ERA faucet's
// authority evidence — which re-derives the real seed-rooted `v3.g` —
// fail-closed against zeros on every freshly created wallet. The check
// was right; the state was fabricated.
//
// Every production caller either holds `G` and writes the head itself
// immediately after (`install_v2_genesis`,
// `initialize_with_genesis_state`, `create_genesis_with_passive_contributors`
// all call `write_genesis_device_head`), or already has a head
// (`migrate_token_balance_keys`). None needs a pre-genesis head, so
// nothing legitimate is lost by refusing to invent one.
if let Some(ds) = self.device_state.as_mut() {
ds.bootstrap_legacy_root(state_hash);
self.device_state = Some(ds);
} else {
// Re-seed with new state hash for tests that swap state.
if let Some(ds) = self.device_state.as_mut() {
ds.bootstrap_legacy_root(state_hash);
}
}
}

Expand Down Expand Up @@ -562,6 +568,17 @@ mod state_machine_tests {
};

let mut state_machine = StateMachine::new();
// Install the head EXPLICITLY, carrying the real genesis root. This used
// to come free from `set_state`, which fabricated a head with a
// `[0u8; 32]` genesis whenever none existed — the production defect that
// fail-closed the ERA faucet's authority check. A test that needs a head
// now says so, and says which root it has.
state_machine.set_device_head(crate::types::device_state::DeviceState::new(
genesis_state.hash,
device_id,
genesis_state.device_info.public_key.clone(),
1024,
));
state_machine.set_state(genesis_state);

let dev_id = device_id;
Expand All @@ -582,6 +599,14 @@ mod state_machine_tests {
let mut machine = StateMachine::new();
let (initial_state, _pk, _sk) = create_test_genesis_state_with_keypair();
let dev_id = initial_state.device_info.device_id;
// Explicit head with the real genesis root — see the note in
// `test_first_post_genesis_transition_is_allowed`.
machine.set_device_head(crate::types::device_state::DeviceState::new(
initial_state.hash,
dev_id,
initial_state.device_info.public_key.clone(),
1024,
));
machine.set_state(initial_state);

// SMT-advance mechanics test: non-balance op, no deltas (see conservation guard).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1210,6 +1210,27 @@ impl DeviceState {
self.genesis
}

/// Install the canonical genesis authority root `G` on an ALREADY
/// CONSTRUCTED head.
///
/// Only the constructor used to write `genesis`, which made
/// `CoreSDK::write_genesis_device_head` silently unable to honour its own
/// contract: it takes the existing head when one is present, so on that
/// branch `genesis` kept whatever the head was built with. Genesis install
/// always hits that branch — `StateMachine::set_state` materialises a head
/// first — so every freshly created wallet ended up with a head whose
/// `genesis` was the `[0u8; 32]` that `set_state` invented. Every consumer
/// reading `genesis_digest()` as the authority root then compared against
/// zeros: the ERA faucet's authority evidence re-derived the real seed-rooted
/// `v3.g` and fail-closed on every device, correctly.
///
/// This is the narrow repair for that: a head that HAS the canonical root
/// can be told it. It does not make the root optional and does not add a
/// second notion of `G` — there is one, the seed-derived `v3.g`.
pub fn set_genesis_digest(&mut self, genesis: [u8; 32]) {
self.genesis = genesis;
}

/// Device identifier.
pub fn devid(&self) -> [u8; 32] {
self.devid
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +424,71 @@ pub(crate) fn handle_create_genesis_v2_query(q: AppQuery) -> AppResult {
"createGenesisV2",
);

// 5d. IDENTITY PUBLICATION — drive it in THIS session.
//
// This route used to do neither half of what `system.genesis` does: it never
// wrote a publication row and never called `publish_identity_now`. On the v2
// path publication was therefore driven only by `retry_pending_publications`
// at SDK init, so a wallet created in a session stayed in
// `publication_pending` until the app was RESTARTED — the user watching
// "PUBLISHING IDENTITY…" had no way forward, and a restart is not something
// a real user can be asked to do. (5c publishes the device TREE; that is a
// different object and does not mark the identity published.)
//
// Two halves, in order. The row FIRST, so a crash between here and the
// publish still leaves something for the startup retry to find — today the
// retry only works because `backfill_publication_rows_for_local_identities`
// reconstructs the row this route never wrote. Then the publish itself,
// SPAWNED: genesis never blocks on the network (same contract as 5c), and a
// slow or unreachable fleet must not hold the response. Failure is
// non-fatal — local genesis stays durable, the row stays unpublished, and
// the startup retry remains the backstop rather than the only driver.
{
let dev_b32 = device_id_b32.clone();
let g_b32 = crate::util::text_id::encode_base32_crockford(&g);
let pk_b32 = crate::util::text_id::encode_base32_crockford(&ak_pk);
if let Err(e) = crate::storage::client_db::publication::upsert_publication_state(
&dev_b32,
&g_b32,
crate::storage::client_db::publication::PublicationState::LocalGenesisCommitted,
0,
"",
) {
log::warn!(
"system.createGenesisV2: failed to record local-genesis publication state: {e}"
);
}
crate::runtime::get_runtime().spawn(async move {
match crate::sdk::identity_publication::publish_identity_now(
&dev_b32, &pk_b32, &g_b32,
)
.await
{
Ok(report) if report.is_published() => log::info!(
"system.createGenesisV2: identity PUBLISHED for device={} ({}/{} verified, quorum {})",
&dev_b32[..8.min(dev_b32.len())],
report.verified,
report.total_nodes,
report.required
),
Ok(report) => log::warn!(
"system.createGenesisV2: identity NOT published for device={} ({}/{} verified, \
quorum {}) — local genesis is durable and startup will retry. failures: {:?}",
&dev_b32[..8.min(dev_b32.len())],
report.verified,
report.total_nodes,
report.required,
report.failures
),
Err(e) => log::warn!(
"system.createGenesisV2: identity publication failed for device={}: {e} \
— local genesis is durable and startup will retry",
&dev_b32[..8.min(dev_b32.len())]
),
}
});
}

// Success rail (mirrors finalize_bootstrap_core): complete → ok. The wallet_ready screen
// transition itself rides the fresh session snapshot Kotlin publishes after this response.
emit(LifecycleKind::GenesisKindSecuringComplete, 0);
Expand Down
Loading
Loading