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>
This commit is contained in:
davidherranandClaude Opus 4.8 committed 2026-05-30 10:26:55 -05:00
1 parent c2aa04fe02
commit 28408cdf73
1 file changed
+609
@@ -0,0 +1,609 @@
# 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`):
```rust
// 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:
```rust
// 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):
```bash
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`.
```rust
//! 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)
```rust
//! 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:
```rust
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`:
```rust
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)
```toml
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)
```bash
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.)