§10.1 core2 yanked unblock documentado (vendor path patch). §10.2 finding: crates.io sc-/sp- consensus ≠ git tag API bajo misma versión. 3 opciones para David: A git consensus / B diferir node / C git todo el workspace. Recomendación Charon: C como inversión pre-Phase-4. §10.3 estado 4 pasos: 4a DONE, 4b/4c/4d bloqueados por §10.2. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
29 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 queorigin_chain_runtime::opaque::Blockexiste (haypub mod opaque). SiRuntimeApino está en root, usar el path que genereimpl_runtime_apis!(normalmente root crate). El template lo tiene enapis::porque su runtime re-exporta así; el mío lo genera en root → usarorigin_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_providersusasc_consensus_aura::standalone::slot_duration_at(&*cidp_client, parent_hash)?+sp_consensus_aura::inherents::InherentDataProvider::from_timestamp_and_slot_duration(NOSlot::from_timestamp).import_queue::<AuraPair, _, _, _, _, _>— 6 type params.BuildNetworkParams.warp_sync_config(NOwarp_sync_params).FullNetworkConfiguration::new(&config.network, config.prometheus_registry().cloned()).spawn_tasks.network: Arc::new(network.clone()).- grandpa
sync: Arc::new(sync_service). Serviceother 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)
- 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.) - 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)]). - v0.0.49 — Paso 4d + COMPLETE: CLI sc_cli migration + run wire.
./target/release/origin-chain-node --dev --tmpproduce 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).
§10 — Hallazgos de ejecución (v0.0.48 + v0.0.49, 2026-05-27)
Ship 1 (v0.0.48 WASM_BINARY export) shipped limpio. Ship 2 reveló dos cosas — una resuelta, una que necesita decisión de David.
§10.1 — RESUELTO: core2 yanked (el muro de 3 sesiones)
sc-network-types 0.16.0 → cid ^0.9 (única 0.9.x = 0.9.0, yanked) → core2 ^0.4 (única 0.4.x = 0.4.0, yanked; las no-yanked son 0.3.x que no satisfacen ^0.4). Cargo no auto-selecciona versiones yanked → la resolución del node fallaba. Este era exactamente el blocker documentado en chain_spec.rs que difirió sc-service desde Slice 3.
Fix aplicado (v0.0.49): vendorizado el .crate publicado de core2 0.4.0 (yanked pero descargable de static.crates.io) en vendor/core2-0.4.0/ + [patch.crates-io] core2 = { path = "vendor/core2-0.4.0" }. Una path source es seleccionable por cargo independiente del estado yanked. El git source (technocreatives/core2 master) NO sirvió — resultó ser workspace ≤0.3.x. Resuelto: core2 0.4.0 + cid 0.9.0 ya en Cargo.lock, deps del node resuelven.
§10.2 — DECISIÓN DE DAVID: crates.io consensus crates ≠ git tag API
Las crates sc-/sp- publicadas en crates.io tienen API distinta al git tag polkadot-stable2509-3 bajo el mismo número de versión:
| Símbolo | git tag stable2509-3 | crates.io publicado |
|---|---|---|
| slot_duration_at | sc_consensus_aura::standalone::slot_duration_at |
sc_consensus_aura::slot_duration_at (root) |
| slot inherent provider | sp_consensus_aura::inherents::InherentDataProvider::from_timestamp_and_slot_duration |
NO EXISTE (Slot no implementa el trait; Slot::from_timestamp toma 1 arg) |
polkadot-sdk reusa números de versión con cambios git no publicados (el mismo patrón que pallet-revive: crates.io 0.12.2 ≠ git tag 0.10.1). El sp-consensus-aura 0.45.0 publicado no tiene un slot InherentDataProvider en absoluto → el patrón de node service Aura no es construible contra las crates publicadas.
Dos opciones (necesito tu decisión, David):
-
Opción A — Migrar sc-/sp- consensus a git sources del tag stable2509-3. Permite un node service real ahora. Riesgo: la misma cirugía duplicate-crate que golpeó pallet-revive (cargo trata git source + crates.io como crates distintos → conflictos de tipos en frame-system/sp-runtime). Mitigación: mover TODO el stack consensus (no solo aura) a git para coherencia. Scope: 1-2 sesiones cuidadosas, alto riesgo de cascada.
-
Opción B — Mantener crates.io publicadas; diferir el node service real. El node sigue siendo CLI + chain_spec builder (sin
--devproduciendo bloques) hasta una futura generación Substrate donde las publicadas alcancen al tag. Bajo riesgo. Phase 4 testnet necesitaría node real → empuja testnet. -
Opción C — Migrar TODO el workspace a git sources
polkadot-stable2509-3(no solo consensus). Coherencia total tag-vs-publicado, resuelve pallet-revive vendor + este gap de una vez. Mayor scope pero elimina la clase entera de bug "publicado ≠ tag". Probable mejor inversión a largo plazo pre-mainnet.
Recomendación Charon: Opción C como inversión pre-Phase-4, pero es decisión tuya por el scope. Mientras tanto el core2 unblock + WASM_BINARY export quedan firmes; service.rs/rpc.rs conservados como WIP (verbatim tag + 2 fixes intentados contra publicado).
§10.3 — Estado de los 4 pasos
- 4a WASM_BINARY export — ✅ DONE (v0.0.48)
- 4b chain_spec GenericChainSpec — ⏸ bloqueado por §10.2 (GenericChainSpec vive en sc-service stack)
- 4c service.rs + rpc.rs — ⏸ WIP escrito, bloqueado por §10.2 (API gap consensus)
- 4d CLI sc_cli — ⏸ pendiente 4b/4c
— Charon ⚓ (Pilar 4, Slice 4 plan V1 + §10 hallazgos ejecución v0.0.48-49, sesión 4+ 2026-05-27)