Files
arkhe-origin-chain/docs/SLICE_4_SC_SERVICE_IMPLEMENTATION_PLAN_V1.md
T
davidherranandClaude Opus 4.8 28408cdf73 docs(slice-4): sc-service implementation plan V1 — reference verbatim stable2509-3
Captura el plan completo de Slice 4 (full sc-service) con el service.rs VERBATIM
del solochain-template polkadot-stable2509-3 (obtenido vía WebFetch sesión 4+),
para ejecución mecánica próxima sesión.

Razón: sesión 4+ tuvo degradación intermitente de tooling de lectura
(truncation/duplicación) que hace el ciclo compile-iterate de sc-service inseguro.
Disciplina "nunca shipear roto" → NO commiteo código sc-service sin poder verificar
que compila. En su lugar capturo el reference exacto + prerequisitos descubiertos.

Hallazgo clave (prerequisito nuevo): Slice 4 NO es un solo ship — son 4 pasos
encadenados:
- 4a: runtime exporta WASM_BINARY const (HOY NO lo hace — grep -c WASM_BINARY = 0).
  Requiere build.rs con substrate_wasm_builder + include!(OUT_DIR/wasm_binary.rs).
- 4b: chain_spec → sc_service::GenericChainSpec con WASM_BINARY embebido.
- 4c: service.rs + rpc.rs (núcleo sc-service wire) — verbatim capturado §3-4.
- 4d: CLI inline → sc_cli::SubstrateCli + comando run.

Contenido del plan:
- §2 Paso 4a WASM_BINARY export + build.rs
- §3 service.rs VERBATIM stable2509-3 (correcciones vs reconstrucción: standalone::
  slot_duration_at + from_timestamp_and_slot_duration + warp_sync_config +
  import_queue 6 type params + spawn_tasks Arc::new(network) + grandpa sync Arc)
- §4 rpc.rs pattern estándar (System + TransactionPayment)
- §5 Paso 4b chain_spec GenericChainSpec
- §6 Paso 4d CLI sc_cli migration
- §7 Orden 3 ships (v0.0.47-49) + node Cargo deps exactos
- §8 Risk register 7 risks + mitigaciones
- §9 Reference checkout command

Ejecución: 3 ships próxima sesión con tooling sano. El service.rs verbatim
coincide exacto con workspace pins stable2509-3 → bajo riesgo de API drift.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 10:26:55 -05:00

26 KiB

Slice 4 — Full sc-service Implementation Plan V1

Documento: docs/SLICE_4_SC_SERVICE_IMPLEMENTATION_PLAN_V1.md Filed: 2026-05-27 (sesión 4+ Charon) Estado: PLAN — reference verbatim stable2509-3 capturado, ejecución próxima sesión Autor: Charon ⚓ (Pilar 4)

Razón del plan: sesión 4+ tuvo degradación intermitente de tooling de lectura (truncation/duplicación) que hace el ciclo compile-iterate de sc-service inseguro. Disciplina "nunca shipear roto" → capturo el reference exacto + prerequisitos para ejecución mecánica próxima sesión con tooling sano. El service.rs aquí es verbatim del solochain-template polkadot-stable2509-3 (obtenido vía WebFetch), no reconstruido.


§0 — Objetivo + hallazgo de prerequisitos

Reemplazar el node stub actual con full sc-service: Aura + Grandpa consensus + JSON-RPC + libp2p + telemetry. origin-chain-node --dev produce bloques reales.

Slice 4 NO es un solo ship — son 4 pasos prerequisito encadenados descubiertos sesión 4+:

Paso Prerequisito Por qué
4a Runtime exporta WASM_BINARY const chain_spec GenericChainSpec necesita los bytes del runtime para genesis. HOY el runtime NO lo exporta (verificado: grep -c WASM_BINARY lib.rs = 0).
4b chain_spec → sc_service::GenericChainSpec Reemplaza el builder serde_json + MOCK_WASM por el real con WASM_BINARY embebido.
4c service.rs + rpc.rs (sc-service wire) El núcleo: new_partial + new_full + RPC.
4d CLI → sc_cli::SubstrateCli + comando run Migra el CLI inline actual a sc_cli (que construye Configuration).

Precondiciones YA cumplidas:

  • ✅ WASM real 2.8 MB buildeable (--features wasm-build + brew LLVM env vars)
  • ✅ Substrate workspace stable2509-3 puro (v0.0.38)
  • ✅ Runtime impl AuraApi + GrandpaApi + SessionKeys + impl_runtime_apis! → genera RuntimeApi
  • ✅ Runtime exporta Block (root) + pub mod opaque + SLOT_DURATION

§1 — Estado actual del node crate

crates/origin-chain-node/src/
├── main.rs        (231 LOC) — CLI inline clap (Version/ListPresets/PrintPreset/Info/BuildSpec). mod chain_spec. NO run.
└── chain_spec.rs  (287 LOC) — builder serde_json puro + MOCK_WASM_HEX="0x". NO GenericChainSpec.

NO hay cli.rs ni command.rs separados (todo en main.rs). NO hay service.rs ni rpc.rs.

Cargo.toml node deps actuales: origin-chain-runtime, sp-genesis-builder, clap, serde_json, log.


§2 — Paso 4a: Runtime exporta WASM_BINARY

En crates/origin-chain-runtime/src/lib.rs, añadir cerca del top (post extern crate alloc):

// WASM binary canónico — generado por substrate-wasm-builder en build.rs cuando
// feature "wasm-build" activo. Consumido por node chain_spec GenericChainSpec.
#[cfg(feature = "std")]
include!(concat!(env!("OUT_DIR"), "/wasm_binary.rs"));

/// Helper canónico: WASM_BINARY o panic con mensaje claro.
#[cfg(feature = "std")]
pub fn wasm_binary_unwrap() -> &'static [u8] {
    WASM_BINARY.expect(
        "Runtime WASM binary no disponible. Build con `--features wasm-build` + \
         CC=llvm env vars (ver SLICE_2E). Para node, build el node con feature que \
         propague runtime/wasm-build.",
    )
}

Requiere: el build.rs del runtime debe llamar substrate_wasm_builder::WasmBuilder::build() cuando wasm-build activo. Verificar si existe crates/origin-chain-runtime/build.rs — si no, crearlo:

// crates/origin-chain-runtime/build.rs
fn main() {
    #[cfg(feature = "wasm-build")]
    substrate_wasm_builder::WasmBuilder::build_using_defaults();
}

OJO: el wasm_binary.rs generado define pub const WASM_BINARY: Option<&[u8]>. El node debe buildearse con el runtime feature wasm-build activo (propagar vía node Cargo feature default = ["runtime-wasm"] → origin-chain-runtime/wasm-build). Alternativamente, mantener WASM_BINARY = None fallback y el node falla claro si falta.

Build env canónico (per SLICE_2E):

export CC=/opt/homebrew/opt/llvm/bin/clang
export CC_wasm32_unknown_unknown=/opt/homebrew/opt/llvm/bin/clang
export AR_wasm32_unknown_unknown=/opt/homebrew/opt/llvm/bin/llvm-ar
WASM_BUILD_STD=0 cargo build -p origin-chain-node --release

§3 — Paso 4c: service.rs (VERBATIM stable2509-3, adaptado a origin-chain-runtime)

Fuente: paritytech/polkadot-sdk tag polkadot-stable2509-3, templates/solochain/node/src/service.rs — verbatim WebFetch sesión 4+. Único cambio: el import del runtime.

Template usa use solochain_template_runtime::{self, apis::RuntimeApi, opaque::Block}; Adaptación Origin: use origin_chain_runtime::{RuntimeApi, opaque::Block}; — verificar que origin_chain_runtime::opaque::Block existe (hay pub mod opaque). Si RuntimeApi no está en root, usar el path que genere impl_runtime_apis! (normalmente root crate). El template lo tiene en apis:: porque su runtime re-exporta así; el mío lo genera en root → usar origin_chain_runtime::RuntimeApi.

//! Service and ServiceFactory implementation. Origin Chain Slice 4.
//! VERBATIM solochain-template polkadot-stable2509-3, runtime import adaptado.

use futures::FutureExt;
use sc_client_api::{Backend, BlockBackend};
use sc_consensus_aura::{ImportQueueParams, SlotProportion, StartAuraParams};
use sc_consensus_grandpa::SharedVoterState;
use sc_service::{error::Error as ServiceError, Configuration, TaskManager, WarpSyncConfig};
use sc_telemetry::{Telemetry, TelemetryWorker};
use sc_transaction_pool_api::OffchainTransactionPoolFactory;
use origin_chain_runtime::{RuntimeApi, opaque::Block};
use sp_consensus_aura::sr25519::AuthorityPair as AuraPair;
use std::{sync::Arc, time::Duration};

pub(crate) type FullClient = sc_service::TFullClient<
    Block,
    RuntimeApi,
    sc_executor::WasmExecutor<sp_io::SubstrateHostFunctions>,
>;
type FullBackend = sc_service::TFullBackend<Block>;
type FullSelectChain = sc_consensus::LongestChain<FullBackend, Block>;

const GRANDPA_JUSTIFICATION_PERIOD: u32 = 512;

pub type Service = sc_service::PartialComponents<
    FullClient,
    FullBackend,
    FullSelectChain,
    sc_consensus::DefaultImportQueue<Block>,
    sc_transaction_pool::TransactionPoolHandle<Block, FullClient>,
    (
        sc_consensus_grandpa::GrandpaBlockImport<FullBackend, Block, FullClient, FullSelectChain>,
        sc_consensus_grandpa::LinkHalf<Block, FullClient, FullSelectChain>,
        Option<Telemetry>,
    ),
>;

pub fn new_partial(config: &Configuration) -> Result<Service, ServiceError> {
    let telemetry = config
        .telemetry_endpoints
        .clone()
        .filter(|x| !x.is_empty())
        .map(|endpoints| -> Result<_, sc_telemetry::Error> {
            let worker = TelemetryWorker::new(16)?;
            let telemetry = worker.handle().new_telemetry(endpoints);
            Ok((worker, telemetry))
        })
        .transpose()?;

    let executor = sc_service::new_wasm_executor::<sp_io::SubstrateHostFunctions>(&config.executor);
    let (client, backend, keystore_container, task_manager) =
        sc_service::new_full_parts::<Block, RuntimeApi, _>(
            config,
            telemetry.as_ref().map(|(_, telemetry)| telemetry.handle()),
            executor,
        )?;
    let client = Arc::new(client);

    let telemetry = telemetry.map(|(worker, telemetry)| {
        task_manager.spawn_handle().spawn("telemetry", None, worker.run());
        telemetry
    });

    let select_chain = sc_consensus::LongestChain::new(backend.clone());

    let transaction_pool = Arc::from(
        sc_transaction_pool::Builder::new(
            task_manager.spawn_essential_handle(),
            client.clone(),
            config.role.is_authority().into(),
        )
        .with_options(config.transaction_pool.clone())
        .with_prometheus(config.prometheus_registry())
        .build(),
    );

    let (grandpa_block_import, grandpa_link) = sc_consensus_grandpa::block_import(
        client.clone(),
        GRANDPA_JUSTIFICATION_PERIOD,
        &client,
        select_chain.clone(),
        telemetry.as_ref().map(|x| x.handle()),
    )?;

    let cidp_client = client.clone();
    let import_queue =
        sc_consensus_aura::import_queue::<AuraPair, _, _, _, _, _>(ImportQueueParams {
            block_import: grandpa_block_import.clone(),
            justification_import: Some(Box::new(grandpa_block_import.clone())),
            client: client.clone(),
            create_inherent_data_providers: move |parent_hash, _| {
                let cidp_client = cidp_client.clone();
                async move {
                    let slot_duration = sc_consensus_aura::standalone::slot_duration_at(
                        &*cidp_client,
                        parent_hash,
                    )?;
                    let timestamp = sp_timestamp::InherentDataProvider::from_system_time();
                    let slot =
                        sp_consensus_aura::inherents::InherentDataProvider::from_timestamp_and_slot_duration(
                            *timestamp,
                            slot_duration,
                        );
                    Ok((slot, timestamp))
                }
            },
            spawner: &task_manager.spawn_essential_handle(),
            registry: config.prometheus_registry(),
            check_for_equivocation: Default::default(),
            telemetry: telemetry.as_ref().map(|x| x.handle()),
            compatibility_mode: Default::default(),
        })?;

    Ok(sc_service::PartialComponents {
        client,
        backend,
        task_manager,
        import_queue,
        keystore_container,
        select_chain,
        transaction_pool,
        other: (grandpa_block_import, grandpa_link, telemetry),
    })
}

pub fn new_full<
    N: sc_network::NetworkBackend<Block, <Block as sp_runtime::traits::Block>::Hash>,
>(
    config: Configuration,
) -> Result<TaskManager, ServiceError> {
    let sc_service::PartialComponents {
        client,
        backend,
        mut task_manager,
        import_queue,
        keystore_container,
        select_chain,
        transaction_pool,
        other: (block_import, grandpa_link, mut telemetry),
    } = new_partial(&config)?;

    let mut net_config = sc_network::config::FullNetworkConfiguration::<
        Block,
        <Block as sp_runtime::traits::Block>::Hash,
        N,
    >::new(&config.network, config.prometheus_registry().cloned());
    let metrics = N::register_notification_metrics(config.prometheus_registry());

    let peer_store_handle = net_config.peer_store_handle();
    let grandpa_protocol_name = sc_consensus_grandpa::protocol_standard_name(
        &client.block_hash(0).ok().flatten().expect("Genesis block exists; qed"),
        &config.chain_spec,
    );
    let (grandpa_protocol_config, grandpa_notification_service) =
        sc_consensus_grandpa::grandpa_peers_set_config::<_, N>(
            grandpa_protocol_name.clone(),
            metrics.clone(),
            peer_store_handle,
        );
    net_config.add_notification_protocol(grandpa_protocol_config);

    let warp_sync = Arc::new(sc_consensus_grandpa::warp_proof::NetworkProvider::new(
        backend.clone(),
        grandpa_link.shared_authority_set().clone(),
        Vec::default(),
    ));

    let (network, system_rpc_tx, tx_handler_controller, sync_service) =
        sc_service::build_network(sc_service::BuildNetworkParams {
            config: &config,
            net_config,
            client: client.clone(),
            transaction_pool: transaction_pool.clone(),
            spawn_handle: task_manager.spawn_handle(),
            import_queue,
            block_announce_validator_builder: None,
            warp_sync_config: Some(WarpSyncConfig::WithProvider(warp_sync)),
            block_relay: None,
            metrics,
        })?;

    if config.offchain_worker.enabled {
        let offchain_workers =
            sc_offchain::OffchainWorkers::new(sc_offchain::OffchainWorkerOptions {
                runtime_api_provider: client.clone(),
                is_validator: config.role.is_authority(),
                keystore: Some(keystore_container.keystore()),
                offchain_db: backend.offchain_storage(),
                transaction_pool: Some(OffchainTransactionPoolFactory::new(
                    transaction_pool.clone(),
                )),
                network_provider: Arc::new(network.clone()),
                enable_http_requests: true,
                custom_extensions: |_| vec![],
            })?;
        task_manager.spawn_handle().spawn(
            "offchain-workers-runner",
            "offchain-worker",
            offchain_workers.run(client.clone(), task_manager.spawn_handle()).boxed(),
        );
    }

    let role = config.role;
    let force_authoring = config.force_authoring;
    let backoff_authoring_blocks: Option<()> = None;
    let name = config.network.node_name.clone();
    let enable_grandpa = !config.disable_grandpa;
    let prometheus_registry = config.prometheus_registry().cloned();

    let rpc_extensions_builder = {
        let client = client.clone();
        let pool = transaction_pool.clone();
        Box::new(move |_| {
            let deps = crate::rpc::FullDeps { client: client.clone(), pool: pool.clone() };
            crate::rpc::create_full(deps).map_err(Into::into)
        })
    };

    let _rpc_handlers = sc_service::spawn_tasks(sc_service::SpawnTasksParams {
        network: Arc::new(network.clone()),
        client: client.clone(),
        keystore: keystore_container.keystore(),
        task_manager: &mut task_manager,
        transaction_pool: transaction_pool.clone(),
        rpc_builder: rpc_extensions_builder,
        backend,
        system_rpc_tx,
        tx_handler_controller,
        sync_service: sync_service.clone(),
        config,
        telemetry: telemetry.as_mut(),
    })?;

    if role.is_authority() {
        let proposer_factory = sc_basic_authorship::ProposerFactory::new(
            task_manager.spawn_handle(),
            client.clone(),
            transaction_pool.clone(),
            prometheus_registry.as_ref(),
            telemetry.as_ref().map(|x| x.handle()),
        );

        let slot_duration = sc_consensus_aura::slot_duration(&*client)?;

        let aura = sc_consensus_aura::start_aura::<AuraPair, _, _, _, _, _, _, _, _, _, _>(
            StartAuraParams {
                slot_duration,
                client,
                select_chain,
                block_import,
                proposer_factory,
                create_inherent_data_providers: move |_, ()| async move {
                    let timestamp = sp_timestamp::InherentDataProvider::from_system_time();
                    let slot =
                        sp_consensus_aura::inherents::InherentDataProvider::from_timestamp_and_slot_duration(
                            *timestamp,
                            slot_duration,
                        );
                    Ok((slot, timestamp))
                },
                force_authoring,
                backoff_authoring_blocks,
                keystore: keystore_container.keystore(),
                sync_oracle: sync_service.clone(),
                justification_sync_link: sync_service.clone(),
                block_proposal_slot_portion: SlotProportion::new(2f32 / 3f32),
                max_block_proposal_slot_portion: None,
                telemetry: telemetry.as_ref().map(|x| x.handle()),
                compatibility_mode: Default::default(),
            },
        )?;
        task_manager
            .spawn_essential_handle()
            .spawn_blocking("aura", Some("block-authoring"), aura);
    }

    if enable_grandpa {
        let keystore = if role.is_authority() { Some(keystore_container.keystore()) } else { None };

        let grandpa_config = sc_consensus_grandpa::Config {
            gossip_duration: Duration::from_millis(333),
            justification_generation_period: GRANDPA_JUSTIFICATION_PERIOD,
            name: Some(name),
            observer_enabled: false,
            keystore,
            local_role: role,
            telemetry: telemetry.as_ref().map(|x| x.handle()),
            protocol_name: grandpa_protocol_name,
        };

        let grandpa_config = sc_consensus_grandpa::GrandpaParams {
            config: grandpa_config,
            link: grandpa_link,
            network,
            sync: Arc::new(sync_service),
            notification_service: grandpa_notification_service,
            voting_rule: sc_consensus_grandpa::VotingRulesBuilder::default().build(),
            prometheus_registry,
            shared_voter_state: SharedVoterState::empty(),
            telemetry: telemetry.as_ref().map(|x| x.handle()),
            offchain_tx_pool_factory: OffchainTransactionPoolFactory::new(transaction_pool),
        };

        task_manager.spawn_essential_handle().spawn_blocking(
            "grandpa-voter",
            None,
            sc_consensus_grandpa::run_grandpa_voter(grandpa_config)?,
        );
    }

    Ok(task_manager)
}

Diferencias clave vs mi reconstrucción anterior (corregidas aquí):

  • create_inherent_data_providers usa sc_consensus_aura::standalone::slot_duration_at(&*cidp_client, parent_hash)? + sp_consensus_aura::inherents::InherentDataProvider::from_timestamp_and_slot_duration (NO Slot::from_timestamp).
  • import_queue::<AuraPair, _, _, _, _, _> — 6 type params.
  • BuildNetworkParams.warp_sync_config (NO warp_sync_params).
  • FullNetworkConfiguration::new(&config.network, config.prometheus_registry().cloned()).
  • spawn_tasks.network: Arc::new(network.clone()).
  • grandpa sync: Arc::new(sync_service).
  • Service other tuple es (GrandpaBlockImport, LinkHalf, Option<Telemetry>) con tipos explícitos.

§4 — rpc.rs (pattern estándar solochain stable2509)

//! Origin Chain node RPC (Slice 4). Pattern solochain-template stable2509-3.
use std::sync::Arc;
use jsonrpsee::RpcModule;
use origin_chain_runtime::{opaque::Block, AccountId, Balance, Nonce};
use sc_transaction_pool_api::TransactionPool;
use sp_api::ProvideRuntimeApi;
use sp_block_builder::BlockBuilder;
use sp_blockchain::{Error as BlockChainError, HeaderBackend, HeaderMetadata};

pub struct FullDeps<C, P> {
    pub client: Arc<C>,
    pub pool: Arc<P>,
}

pub fn create_full<C, P>(
    deps: FullDeps<C, P>,
) -> Result<RpcModule<()>, Box<dyn std::error::Error + Send + Sync>>
where
    C: ProvideRuntimeApi<Block>
        + HeaderBackend<Block>
        + HeaderMetadata<Block, Error = BlockChainError>
        + Send + Sync + 'static,
    C::Api: substrate_frame_rpc_system::AccountNonceApi<Block, AccountId, Nonce>
        + pallet_transaction_payment_rpc::TransactionPaymentRuntimeApi<Block, Balance>
        + BlockBuilder<Block>,
    P: TransactionPool + 'static,
{
    use pallet_transaction_payment_rpc::{TransactionPayment, TransactionPaymentApiServer};
    use substrate_frame_rpc_system::{System, SystemApiServer};

    let mut module = RpcModule::new(());
    let FullDeps { client, pool } = deps;
    module.merge(System::new(client.clone(), pool).into_rpc())?;
    module.merge(TransactionPayment::new(client).into_rpc())?;
    Ok(module)
}

OJO: verificar origin_chain_runtime exporta AccountId, Balance, Nonce (root). AccountId re-exported de origin-chain-types ✓. Balance + Nonce son pub type root ✓. opaque::Block ✓.


§5 — Paso 4b: chain_spec.rs → GenericChainSpec

Reemplazar el builder serde_json + MOCK_WASM por:

use origin_chain_runtime::WASM_BINARY;
pub type ChainSpec = sc_service::GenericChainSpec;

pub fn development_config() -> Result<ChainSpec, String> {
    Ok(ChainSpec::builder(
        WASM_BINARY.ok_or("Development wasm not available")?,
        None,
    )
    .with_name("Origin Chain Development")
    .with_id("origin_chain_dev")
    .with_chain_type(sc_service::ChainType::Development)
    .with_genesis_config_preset_name("development") // usa genesis_config_presets::get_preset
    .with_protocol_id("ogc")
    .with_properties(chain_properties())
    .build())
}
// análogo local_testnet_config (Local) + mainnet_config (Live)

with_genesis_config_preset_name invoca el sp_genesis_builder::GenesisBuilder runtime API que ya está impl. Los presets development/local_testnet/mainnet_genesis ya existen en genesis_config_presets. chain_properties (OGC/18/ss58=42) reusar del builder actual.


§6 — Paso 4d: CLI → sc_cli::SubstrateCli + run

Migrar main.rs (CLI inline) a sc_cli. Patrón solochain command.rs:

impl SubstrateCli for Cli {
    fn impl_name() -> String { "Origin Chain Node".into() }
    fn impl_version() -> String { env!("CARGO_PKG_VERSION").into() }
    fn description() -> String { "Origin Metaverse settlement layer — Pilar 4".into() }
    fn author() -> String { "Charon + David Herran".into() }
    fn support_url() -> String { "https://git.originmetaverse.com".into() }
    fn copyright_start_year() -> i32 { 2026 }
    fn load_spec(&self, id: &str) -> Result<Box<dyn sc_service::ChainSpec>, String> {
        Ok(match id {
            "dev" | "development" => Box::new(chain_spec::development_config()?),
            "local" | "local_testnet" => Box::new(chain_spec::local_testnet_config()?),
            "mainnet" | "" => Box::new(chain_spec::mainnet_config()?),
            path => Box::new(chain_spec::ChainSpec::from_json_file(path.into())?),
        })
    }
}

pub fn run() -> sc_cli::Result<()> {
    let cli = <Cli as clap::Parser>::parse();
    match &cli.subcommand {
        Some(Subcommand::BuildSpec(cmd)) => {
            let runner = cli.create_runner(cmd)?;
            runner.sync_run(|config| cmd.run(config.chain_spec, config.network))
        }
        None => {
            let runner = cli.create_runner(&cli.run)?;
            runner.run_node_until_exit(|config| async move {
                service::new_full::<sc_network::NetworkWorker<_, _>>(config)
                    .map_err(sc_cli::Error::Service)
            })
        }
    }
}

Cli struct: { #[command(subcommand)] subcommand: Option<Subcommand>, #[clap(flatten)] run: sc_cli::RunCmd }. Los comandos custom actuales (info/list-presets/print-preset) reañadir como Subcommand variants OR conservar en binario aparte. Decisión: conservar BuildSpec (sc_cli::BuildSpecCmd) + run (default). info/presets → opcional re-add.


§7 — Orden de ejecución (3 ships)

  1. v0.0.47 — Paso 4a + 4b: runtime WASM_BINARY export + build.rs + chain_spec GenericChainSpec. Build runtime con wasm-build. Verificar WASM_BINARY.is_some(). (Bajo riesgo, aislado.)
  2. v0.0.48 — Paso 4c: service.rs + rpc.rs + node Cargo deps (§ABAJO). cargo build -p origin-chain-node 2>&1 | tee /tmp/b.log. Iterar leyendo /tmp/b.log chunked. NO tocar CLI aún (módulos no-usados con #[allow(dead_code)]).
  3. v0.0.49 — Paso 4d + COMPLETE: CLI sc_cli migration + run wire. ./target/release/origin-chain-node --dev --tmp produce bloques. E2E: Polkadot.js Apps ws://localhost:9944. Monorepo bump + LANDMARK + roadmap.

Node Cargo.toml deps (todos pins ya en workspace v0.0.38)

sc-cli.workspace = true
sc-service.workspace = true
sc-executor.workspace = true
sc-network.workspace = true
sc-consensus.workspace = true
sc-consensus-aura.workspace = true
sc-consensus-grandpa.workspace = true
sc-transaction-pool.workspace = true
sc-transaction-pool-api.workspace = true
sc-client-api.workspace = true
sc-basic-authorship.workspace = true
sc-telemetry.workspace = true
sc-offchain.workspace = true
sp-core.workspace = true
sp-runtime.workspace = true
sp-io.workspace = true
sp-timestamp.workspace = true
sp-consensus-aura.workspace = true
sp-api.workspace = true
sp-block-builder.workspace = true
sp-blockchain.workspace = true
jsonrpsee.workspace = true
substrate-frame-rpc-system.workspace = true
pallet-transaction-payment-rpc.workspace = true
futures.workspace = true

Verificar pins faltantes en workspace: sp-blockchain, sp-timestamp, futures, sc-client-api, sc-transaction-pool-api — si faltan, añadir al workspace (stable2509-3 versions: sp-blockchain ~41, sp-timestamp 39, futures 0.3.31). NO añadir sp-keyring (conflicto sp-core documentado) — dev accounts vía genesis presets existentes.


§8 — Risk register

Risk Mitigación
OUT_DIR/wasm_binary.rs no generado (build.rs falta) Crear build.rs runtime + verificar WASM_BINARY.is_some() Paso 4a aislado antes de service
origin_chain_runtime::RuntimeApi path (root vs apis::) impl_runtime_apis! genera en root → origin_chain_runtime::RuntimeApi. Si falla, buscar módulo generado
opaque::Block vs root Block Runtime tiene pub mod opaque; verificar exporta Block. Si no, usar root origin_chain_runtime::Block
sp-blockchain / sp-timestamp / futures faltan en workspace Añadir pins stable2509-3 al workspace antes de Ship 2
CLI migration rompe build-spec actual Ship 3 aislado; Ships 1-2 mantienen CLI actual intacto
Tooling truncation (sesión 4+) Ejecutar tooling sano; tee /tmp/b.log + reads chunked + grep -c error para counts
build_network/spawn_tasks field drift service.rs es VERBATIM stable2509-3 — coincide con workspace pins exactos

§9 — Reference checkout (si se necesita verificar)

cd /tmp && rm -rf sc-ref && mkdir sc-ref && cd sc-ref && git init -q
git remote add origin https://github.com/paritytech/polkadot-sdk
git config core.sparseCheckout true
echo "templates/solochain/node/src/*" > .git/info/sparse-checkout
git fetch --depth 1 origin polkadot-stable2509-3 && git checkout FETCH_HEAD
# (NOTA sesión 4+: `timeout` no disponible en este shell; usar git fetch directo)

El service.rs en §3 es verbatim de esa fuente. rpc.rs §4 es el pattern estándar (verificar templates/solochain/node/src/rpc.rs).


— Charon ⚓ (Pilar 4, Slice 4 plan V1 con reference verbatim stable2509-3, sesión 4+ 2026-05-27. Ejecución 3 ships próxima sesión tooling sano.)