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
180 changes: 180 additions & 0 deletions docs/dev-guide/mainnet_state_reset_rollout_plan_20260317.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
# Mainnet State Reset Rollout Plan (2026-03-17)

## 1. Purpose

This document records the agreed merge order and mainnet execution order for the Phase 1 slim public-mainnet rollout.

The goal is to avoid losing the operational sequence across multiple branches and documents.

## 2. Branch Strategy

Three branches now have distinct responsibilities:

- `feat/system-state-reset`
- Move/native/runtime support for state reset
- `object::clear_fields_by_system`
- admin-gated reset entry functions
- `feat/statedb-rebase-phase1`
- `statedb rebase-export`
- `statedb rebase-build`
- canonical-state rebase behavior
- `feat/slim-mainnet-integration`
- end-to-end validation only
- not intended for direct merge to `main`

Working rule:

- merge capability branches,
- keep the integration branch for rehearsal.

## 3. Merge Order

The recommended merge order is:

1. merge `feat/system-state-reset`
2. merge `feat/statedb-rebase-phase1`
3. keep `feat/slim-mainnet-integration` unmerged unless a later targeted fix must be backported

Reasoning:

- `feat/system-state-reset` is the chain-level capability change
- `feat/statedb-rebase-phase1` is the DB/tooling path that consumes the reset result
- separating the PRs keeps review, rollback, and bisect simple

## 4. Scope Of The First PR

The first PR should be `feat/system-state-reset`.

It should contain:

- `moveos_std::object::clear_fields_by_system`
- native/runtime support for field-tree clearing
- `reset_rooch_to_bitcoin_mapping`
- `reset_utxo_store`
- `reset_inscription_store`
- Move tests
- regenerated Move docs

It should not contain:

- `statedb rebase` Rust changes
- rollout-only integration fixes unrelated to reset capability

## 5. Scope Of The Second PR

The second PR should be `feat/statedb-rebase-phase1`.

It should contain:

- `rebase-export`
- `rebase-build`
- canonical-state rebuild logic
- the fix that preserves reset empty shells instead of dropping the three target objects during export

Important Phase 1 rule:

- the rebase pipeline must rebuild from canonical chain state after reset
- it must not reapply a second object-level drop policy for
- `BitcoinUTXOStore`
- `InscriptionStore`
- `RoochToBitcoinAddressMapping`

## 6. Mainnet Rollout Preconditions

Before executing any mainnet reset transaction:

1. `feat/system-state-reset` must be merged
2. the framework upgrade that includes reset hooks must be deployed
3. `feat/statedb-rebase-phase1` should also be merged, or at minimum the tested rebase binary must be ready
4. the current long-running snapshot remains alive as fallback
5. at least one disk-level recovery point remains available
6. the node role remains explicitly:
- Bitcoin header-only
- no ord / bitseed
- no Bitcoin transaction ingestion

## 7. Mainnet Execution Order

The recommended execution order is:

1. enter a controlled maintenance window
2. reduce or block new public writes as much as practical
3. execute reset transactions on mainnet in this exact order:
- `0x4::utxo::reset_utxo_store`
- `0x4::ord::reset_inscription_store`
- `0x3::address_mapping::reset_rooch_to_bitcoin_mapping`
4. immediately verify post-reset object state
5. immediately take a checkpoint from the reset chain state
6. run `rebase-export`
7. run `rebase-build`
8. boot the slim node on a separate data dir / port
9. run cutover validation
10. switch public traffic only after validation passes

## 8. Why AddressMapping Must Be Last

`RoochToBitcoinAddressMapping` is special.

Normal Rooch transaction execution may write it again during `transaction_validator::pre_execute`.

That means:

- if the mapping is cleared too early,
- and other transactions continue to flow,
- the mapping can regrow before the checkpoint/export step

Therefore:

- `reset_rooch_to_bitcoin_mapping` should be executed last
- the checkpoint/export step should follow as soon as possible

## 9. Post-Reset Validation Checklist

Immediately after the three reset transactions:

- `BitcoinUTXOStore` exists and `size == 0`
- `InscriptionStore` exists and `size == 0`
- `InscriptionStore` counters are zero
- `RoochToBitcoinAddressMapping` is empty or near-empty
- record:
- post-reset `state_root`
- `last_order`
- reset transaction hashes

## 10. Slim Node Validation Checklist

After `rebase-build` and slim node startup:

- startup succeeds without genesis re-init
- latest root equals the rebuilt root
- sequencer metadata loads correctly
- account reads work
- balance reads work
- module/object reads work
- the three reset domains have the expected canonical shape
- a normal Rooch write transaction succeeds

If any of the above fail:

- do not cut traffic
- keep the existing public/archive path serving

## 11. Rollback Posture

Rollback remains operationally simple only if the old path is preserved during the cutover window.

Therefore:

- do not stop the long-running fallback snapshot before slim validation passes
- do not immediately destroy old archive storage at first successful slim boot
- keep the old serving path alive through a stability window

## 12. Current Phase 1 Decision

The current agreed implementation model is:

- clear the heavy state domains on-chain first
- preserve the resulting empty-shell objects in canonical state
- rebuild a slim DB from that canonical state

This replaces the earlier Phase 1 idea of dropping those objects inside the exporter itself.
14 changes: 14 additions & 0 deletions frameworks/bitcoin-move/doc/ord.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
- [Function `genesis_init`](#0x4_ord_genesis_init)
- [Function `borrow_mut_inscription_store`](#0x4_ord_borrow_mut_inscription_store)
- [Function `borrow_inscription_store`](#0x4_ord_borrow_inscription_store)
- [Function `reset_inscription_store`](#0x4_ord_reset_inscription_store)
- [Function `blessed_inscription_count`](#0x4_ord_blessed_inscription_count)
- [Function `cursed_inscription_count`](#0x4_ord_cursed_inscription_count)
- [Function `unbound_inscription_count`](#0x4_ord_unbound_inscription_count)
Expand Down Expand Up @@ -138,9 +139,11 @@
<b>use</b> <a href="">0x2::event_queue</a>;
<b>use</b> <a href="">0x2::json</a>;
<b>use</b> <a href="">0x2::object</a>;
<b>use</b> <a href="">0x2::signer</a>;
<b>use</b> <a href="">0x2::simple_map</a>;
<b>use</b> <a href="">0x2::string_utils</a>;
<b>use</b> <a href="">0x2::type_info</a>;
<b>use</b> <a href="">0x3::onchain_config</a>;
<b>use</b> <a href="bitcoin_hash.md#0x4_bitcoin_hash">0x4::bitcoin_hash</a>;
<b>use</b> <a href="temp_state.md#0x4_temp_state">0x4::temp_state</a>;
<b>use</b> <a href="types.md#0x4_types">0x4::types</a>;
Expand Down Expand Up @@ -523,6 +526,17 @@ A struct to represent the Inscription Charm



<a name="0x4_ord_reset_inscription_store"></a>

## Function `reset_inscription_store`



<pre><code><b>public</b> entry <b>fun</b> <a href="ord.md#0x4_ord_reset_inscription_store">reset_inscription_store</a>(<a href="">account</a>: &<a href="">signer</a>)
</code></pre>



<a name="0x4_ord_blessed_inscription_count"></a>

## Function `blessed_inscription_count`
Expand Down
14 changes: 14 additions & 0 deletions frameworks/bitcoin-move/doc/utxo.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
- [Function `genesis_init`](#0x4_utxo_genesis_init)
- [Function `borrow_utxo_store`](#0x4_utxo_borrow_utxo_store)
- [Function `borrow_mut_utxo_store`](#0x4_utxo_borrow_mut_utxo_store)
- [Function `reset_utxo_store`](#0x4_utxo_reset_utxo_store)
- [Function `new`](#0x4_utxo_new)
- [Function `mock_utxo`](#0x4_utxo_mock_utxo)
- [Function `derive_utxo_id`](#0x4_utxo_derive_utxo_id)
Expand Down Expand Up @@ -54,9 +55,11 @@
<b>use</b> <a href="">0x2::address</a>;
<b>use</b> <a href="">0x2::event_queue</a>;
<b>use</b> <a href="">0x2::object</a>;
<b>use</b> <a href="">0x2::signer</a>;
<b>use</b> <a href="">0x2::simple_multimap</a>;
<b>use</b> <a href="">0x2::type_info</a>;
<b>use</b> <a href="">0x3::chain_id</a>;
<b>use</b> <a href="">0x3::onchain_config</a>;
<b>use</b> <a href="temp_state.md#0x4_temp_state">0x4::temp_state</a>;
<b>use</b> <a href="types.md#0x4_types">0x4::types</a>;
</code></pre>
Expand Down Expand Up @@ -210,6 +213,17 @@ The event is onchain event, and the event_queue name is type_name of the tempora



<a name="0x4_utxo_reset_utxo_store"></a>

## Function `reset_utxo_store`



<pre><code><b>public</b> entry <b>fun</b> <a href="utxo.md#0x4_utxo_reset_utxo_store">reset_utxo_store</a>(<a href="">account</a>: &<a href="">signer</a>)
</code></pre>



<a name="0x4_utxo_new"></a>

## Function `new`
Expand Down
33 changes: 32 additions & 1 deletion frameworks/bitcoin-move/sources/ord.move
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ module bitcoin_move::ord {
use bitcoin_move::bitcoin_hash;
use bitcoin_move::types::{Self, Transaction, Witness, OutPoint};
use bitcoin_move::temp_state;
use rooch_framework::onchain_config;

friend bitcoin_move::genesis;
friend bitcoin_move::bitcoin;
Expand Down Expand Up @@ -172,6 +173,26 @@ module bitcoin_move::ord {
object::borrow(inscription_store_obj)
}

public entry fun reset_inscription_store(account: &signer) {
onchain_config::ensure_admin(account);
reset_inscription_store_internal();
}

fun reset_inscription_store_internal() {
let store_id = object::named_object_id<InscriptionStore>();
if (object::exists_object_with_type<InscriptionStore>(store_id)) {
let system = moveos_std::signer::module_signer<InscriptionStore>();
let store_obj = object::borrow_mut_object_shared<InscriptionStore>(store_id);
object::clear_fields_by_system(&system, store_obj);
let store = object::borrow_mut(store_obj);
store.cursed_inscription_count = 0;
store.blessed_inscription_count = 0;
store.unbound_inscription_count = 0;
store.lost_sats = 0;
store.next_sequence_number = 0;
}
}

public(friend) fun blessed_inscription_count(inscription_store: &InscriptionStore): u32 {
inscription_store.blessed_inscription_count
}
Expand Down Expand Up @@ -1227,6 +1248,11 @@ module bitcoin_move::ord {
object::transfer_extend(inscription_obj, to);
}

#[test_only]
public fun transfer_inscription_for_test(inscription_obj: Object<Inscription>, to: address) {
object::transfer_extend(inscription_obj, to);
}

// If the inscription is transferred, the permanent area will be kept and the temporary area will be dropped.
#[test]
fun test_transfer() {
Expand Down Expand Up @@ -1270,10 +1296,15 @@ module bitcoin_move::ord {
event_queue::unsubscribe(subscriber);
}


#[test_only]
struct TestProtocol has key {}

#[test_only]
public fun inscription_store_field_size_for_test(): u64 {
let store_obj = object::borrow_object<InscriptionStore>(object::named_object_id<InscriptionStore>());
object::field_size(store_obj)
}

#[test(genesis_account= @0x4)]
fun test_metaprotocol_validity(genesis_account: &signer): InscriptionID {
// prepare test inscription
Expand Down
57 changes: 57 additions & 0 deletions frameworks/bitcoin-move/sources/tests/state_reset_tests.move
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// Copyright (c) RoochNetwork
// SPDX-License-Identifier: Apache-2.0

#[test_only]
module bitcoin_move::state_reset_tests {
use std::option;
use moveos_std::account;
use moveos_std::object;
use bitcoin_move::genesis;
use bitcoin_move::ord;
use bitcoin_move::types;
use bitcoin_move::utxo;
use rooch_framework::onchain_config;

#[test]
fun test_reset_utxo_store() {
genesis::init_for_test();
let txid = @0x77dfc2fe598419b00641c296181a96cf16943697f573480b023b77cce82ada21;
let vout = 0;
let outpoint = types::new_outpoint(txid, vout);
let utxo_obj = utxo::new_for_testing(txid, vout, 100);
utxo::transfer_for_testing(utxo_obj, @0x42);
assert!(utxo::exists_utxo(outpoint), 1);
assert!(object::field_size(utxo::borrow_utxo_store()) == 1, 2);
let admin = account::create_account_for_testing(onchain_config::admin());
utxo::reset_utxo_store(&admin);
assert!(!utxo::exists_utxo(outpoint), 3);
assert!(object::field_size(utxo::borrow_utxo_store()) == 0, 4);
}

#[test]
fun test_reset_inscription_store() {
genesis::init_for_test();
let txid = @0x77dfc2fe598419b00641c296181a96cf16943697f573480b023b77cce82ada21;
let inscription_id = ord::new_inscription_id(txid, 0);
let inscription_obj = ord::new_inscription_object_for_test(
inscription_id,
0,
0,
vector[],
option::none(),
option::none(),
vector[],
option::none(),
vector[],
option::none(),
);
ord::transfer_inscription_for_test(inscription_obj, @0x42);
assert!(ord::inscription_store_field_size_for_test() == 2, 1);
assert!(ord::exists_inscription(inscription_id), 2);
let admin = account::create_account_for_testing(onchain_config::admin());
ord::reset_inscription_store(&admin);
assert!(ord::inscription_store_field_size_for_test() == 0, 3);
assert!(!ord::exists_inscription(inscription_id), 4);
assert!(ord::get_inscription_next_sequence_number() == 0, 5);
}
}
Loading
Loading