Decision de David, 2026-10-29: «Si, del 10 % de lo que puede votar». Que puede votar: - Es la emision vigente menos el saldo que nadie puede votar ni recuperar: el pool de emision, el de subsidios y el vault de gobierno (cuentas_sin_llave en configs.rs). - Los bonds (og/props) y los depositos de pallet-revive NO se restan, porque vuelven enteros a una cuenta con llave. La revision adversaria de spec 19 lo demostro con sonda: restarlos dejaba bajar el quorum un 14 % en el genesis de mainnet, aparcando un bond en una propuesta de relleno y recuperandolo despues. Efecto: - En mainnet el quorum pasa de ~100M a ~35,1M OGC (10 % de 351M). - En la testnet de la casa apenas cambia. - Las propuestas abiertas conservan el quorum que guardaron al crearse. Pruebas: - quorums_del_runtime clasifica cada cuenta de pallet por su nombre. - aparcar_un_bond_no_baja_el_quorum reproduce la sonda con la configuracion real. - toda_cuenta_de_pallet_con_saldo_esta_clasificada vigila las cuentas modl del genesis. - 10 contrafactuales en rojo. - El E2E (paso 2b) descubre las cuentas de pallet recorriendo el storage, no copia la lista, y exige el quorum exacto. La comision se quema despues de despachar, y eso se mide en el BurnedDebt de la propia transaccion. Pesos: submit_proposal, submit_treasury_proposal y veto_proposal leen 3 cuentas mas. Medidos en la Pi: 5 procesos limpios, el nucleo calentado 3 s antes de cada uno, a 2,4 GHz. Operacion de la rp1, arrancar.sh: - en cada encendido desde el 26-oct el nodo moria al segundo porque la red no estaba; ahora espera a la ruta por defecto; - un cerrojo flock impide dos instancias; - el pid vivo se reconoce por su linea de comandos. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
226 lines
11 KiB
Rust
226 lines
11 KiB
Rust
//! Firma extrinsics como los firma un wallet, para el E2E por RPC contra un nodo vivo.
|
|
//!
|
|
//! Existe porque un E2E que solo mira bloques no ve lo que ve un usuario: el 2026-10-07 el
|
|
//! nodo producía y finalizaba bloques mientras rechazaba con «bad signature» toda firma de
|
|
//! wallet (el runtime se creía `spec 0`, ver `type Version` en `configs.rs`). Esta
|
|
//! herramienta firma igual que polkadot.js: la versión que anuncia el runtime, el hash del
|
|
//! génesis, era inmortal, sin metadata hash.
|
|
//!
|
|
//! ```text
|
|
//! cargo build -p origin-chain-runtime --example firmar_como_wallet # con SKIP_WASM_BUILD=1
|
|
//! F=target/debug/examples/firmar_como_wallet
|
|
//!
|
|
//! firmar <llamada> <nonce> <genesis_hex> [spec tx] extrinsic firmado por //Alice, en hex
|
|
//! llamadas: remark · finalize_override (propuesta 0x09…09) · uphold0 (peso 0)
|
|
//! set_code:<ruta.wasm> (sudo de //Alice, ensayo de upgrade sin fork)
|
|
//! tesoro:<monto>[:<etiqueta>] (RV17: propuesta de tesoro de //Alice que paga
|
|
//! <monto> plancks a //Bob; 14 días y el bond mínimo; la etiqueta cambia el título
|
|
//! y con él el id) · pagar:<id_hex>:<monto> (sudo de
|
|
//! treasury_disbursement a //Bob) · votar:<id_hex>:<peso> (Aye de //Alice)
|
|
//! · depositar:<monto> (treasury_deposit de //Alice al vault)
|
|
//! · finalizar:<id_hex> (finalize_proposal)
|
|
//! spec y tx: los que da `state_getRuntimeVersion`. Por defecto, los de `VERSION`
|
|
//! compilado. Tras un `set_code` hay que pasarlos: el wallet firma con la versión
|
|
//! nueva que anuncia el nodo, no con la que se compiló aquí.
|
|
//! eventos <hex de System::Events> decodifica los eventos de un bloque, con su fase
|
|
//! cuenta "<frase BIP39>" [//derivación] llave sr25519 y dirección SS58 (42) que verá un wallet
|
|
//! Substrate (substrate-bip39 → mini-secret → sr25519): vectores para la wallet Origin!
|
|
//! verificar <hex_extrinsic> <genesis_hex> pasa la firma por el check del runtime nativo
|
|
//! sobre el preset development (sin nodo)
|
|
//! ```
|
|
//!
|
|
//! Recorrido por RPC (nodo `run --dev --rpc-port 9955`):
|
|
//! `chain_getBlockHash 0` → génesis; `system_accountNextIndex` → nonce;
|
|
//! `author_submitExtrinsic` con el hex; y en el bloque que lo incluye,
|
|
//! `state_getStorage 0x26aa394eea5630e07c48ae0c9558cef780d41e5e16056765bc8461851072c9d7`
|
|
//! (System::Events) → `eventos`.
|
|
|
|
use frame_system::EventRecord;
|
|
use origin_chain_runtime::{
|
|
Runtime, RuntimeCall, RuntimeEvent, TxExtension, UncheckedExtrinsic, VERSION,
|
|
};
|
|
use parity_scale_codec::{Decode, Encode};
|
|
use sp_core::{sr25519, Pair, H256};
|
|
use sp_runtime::{
|
|
generic::{Era, SignedPayload},
|
|
MultiAddress, MultiSignature,
|
|
};
|
|
|
|
fn hex(arg: &str) -> Vec<u8> {
|
|
sp_core::bytes::from_hex(arg).unwrap_or_else(|e| panic!("hex inválido {arg}: {e:?}"))
|
|
}
|
|
|
|
fn bob() -> sp_runtime::AccountId32 {
|
|
sr25519::Pair::from_string("//Bob", None).expect("//Bob").public().into()
|
|
}
|
|
|
|
fn llamada(nombre: &str) -> RuntimeCall {
|
|
let id = origin_chain_governance::ProposalId([9u8; 32]);
|
|
match nombre {
|
|
"remark" => RuntimeCall::System(frame_system::Call::remark {
|
|
remark: b"origin".to_vec(),
|
|
}),
|
|
"finalize_override" => {
|
|
RuntimeCall::Governance(origin_chain_governance::Call::finalize_override {
|
|
proposal_id: id,
|
|
})
|
|
}
|
|
"uphold0" => RuntimeCall::Governance(origin_chain_governance::Call::uphold_council_veto {
|
|
proposal_id: id,
|
|
stake_weight: 0,
|
|
}),
|
|
otra if otra.starts_with("tesoro:") => {
|
|
let (monto, etiqueta) = match otra["tesoro:".len()..].split_once(':') {
|
|
Some((m, e)) => (m, format!("/{e}")),
|
|
None => (&otra["tesoro:".len()..], String::new()),
|
|
};
|
|
let amount: u128 = monto.parse().expect("tesoro:<monto en plancks>[:<etiqueta>]");
|
|
let titulo = [b"e2e-rv17/titulo".as_slice(), etiqueta.as_bytes()].concat();
|
|
RuntimeCall::Governance(origin_chain_governance::Call::submit_treasury_proposal {
|
|
title_hash: sp_core::blake2_256(&titulo),
|
|
body_hash: sp_core::blake2_256(b"e2e-rv17/cuerpo"),
|
|
voting_period_blocks: origin_chain_governance::MIN_VOTING_PERIOD_BLOCKS,
|
|
stake: origin_chain_governance::MIN_PROPOSAL_STAKE,
|
|
recipient: bob(),
|
|
amount,
|
|
})
|
|
}
|
|
otra if otra.starts_with("votar:") => {
|
|
let (id, peso) =
|
|
otra["votar:".len()..].split_once(':').expect("votar:<id_hex>:<peso>");
|
|
let id: [u8; 32] = hex(id).try_into().expect("el id son 32 bytes");
|
|
RuntimeCall::Governance(origin_chain_governance::Call::cast_vote {
|
|
proposal_id: origin_chain_governance::ProposalId(id),
|
|
stance: origin_chain_types::VoteStance::Aye,
|
|
stake_weight: peso.parse().expect("peso en plancks"),
|
|
})
|
|
}
|
|
otra if otra.starts_with("finalizar:") => {
|
|
let id: [u8; 32] =
|
|
hex(&otra["finalizar:".len()..]).try_into().expect("el id son 32 bytes");
|
|
RuntimeCall::Governance(origin_chain_governance::Call::finalize_proposal {
|
|
proposal_id: origin_chain_governance::ProposalId(id),
|
|
})
|
|
}
|
|
otra if otra.starts_with("depositar:") => {
|
|
RuntimeCall::Governance(origin_chain_governance::Call::treasury_deposit {
|
|
amount: otra["depositar:".len()..].parse().expect("depositar:<monto en plancks>"),
|
|
})
|
|
}
|
|
otra if otra.starts_with("pagar:") => {
|
|
let (id, amount) =
|
|
otra["pagar:".len()..].split_once(':').expect("pagar:<id_hex>:<monto>");
|
|
let id: [u8; 32] = hex(id).try_into().expect("el id son 32 bytes");
|
|
RuntimeCall::Sudo(pallet_sudo::Call::sudo {
|
|
call: Box::new(RuntimeCall::Governance(
|
|
origin_chain_governance::Call::treasury_disbursement {
|
|
proposal_id: origin_chain_governance::ProposalId(id),
|
|
recipient: bob(),
|
|
amount: amount.parse().expect("monto en plancks"),
|
|
},
|
|
)),
|
|
})
|
|
}
|
|
otra => match otra.strip_prefix("set_code:") {
|
|
Some(ruta) => {
|
|
let code = std::fs::read(ruta).unwrap_or_else(|e| panic!("{ruta}: {e}"));
|
|
RuntimeCall::Sudo(pallet_sudo::Call::sudo {
|
|
call: Box::new(RuntimeCall::System(frame_system::Call::set_code { code })),
|
|
})
|
|
}
|
|
None => panic!(
|
|
"llamada desconocida: {otra} (remark · finalize_override · uphold0 · \
|
|
set_code:<ruta> · tesoro:<monto> · pagar:<id_hex>:<monto> · \
|
|
votar:<id_hex>:<peso> · depositar:<monto> · finalizar:<id_hex>)"
|
|
),
|
|
},
|
|
}
|
|
}
|
|
|
|
fn firmar(call: RuntimeCall, nonce: u32, genesis: H256, spec: u32, tx: u32) -> UncheckedExtrinsic {
|
|
let alice = sr25519::Pair::from_string("//Alice", None).expect("//Alice");
|
|
let tx_ext: TxExtension = (
|
|
frame_system::CheckNonZeroSender::<Runtime>::new(),
|
|
frame_system::CheckSpecVersion::<Runtime>::new(),
|
|
frame_system::CheckTxVersion::<Runtime>::new(),
|
|
frame_system::CheckGenesis::<Runtime>::new(),
|
|
frame_system::CheckEra::<Runtime>::from(Era::Immortal),
|
|
frame_system::CheckNonce::<Runtime>::from(nonce),
|
|
frame_system::CheckWeight::<Runtime>::new(),
|
|
pallet_transaction_payment::ChargeTransactionPayment::<Runtime>::from(0),
|
|
frame_metadata_hash_extension::CheckMetadataHash::<Runtime>::new(false),
|
|
frame_system::WeightReclaim::<Runtime>::new(),
|
|
);
|
|
// Lo que pone el wallet: la versión que anuncia el RPC, no la que crea el storage.
|
|
let implicito = ((), spec, tx, genesis, genesis, (), (), (), None, ());
|
|
let payload = SignedPayload::<RuntimeCall, TxExtension>::from_raw(call, tx_ext, implicito);
|
|
let firma = payload.using_encoded(|p| alice.sign(p));
|
|
let (call, tx_ext, _) = payload.deconstruct();
|
|
UncheckedExtrinsic::new_signed(
|
|
call,
|
|
MultiAddress::Id(alice.public().into()),
|
|
MultiSignature::Sr25519(firma),
|
|
tx_ext,
|
|
)
|
|
}
|
|
|
|
fn main() {
|
|
let args: Vec<String> = std::env::args().collect();
|
|
let modo = args.get(1).map(String::as_str).unwrap_or("");
|
|
match modo {
|
|
"firmar" => {
|
|
let nonce: u32 = args[3].parse().expect("nonce");
|
|
let genesis = H256::from_slice(&hex(&args[4]));
|
|
let spec = args
|
|
.get(5)
|
|
.map_or(VERSION.spec_version, |v| v.parse().expect("spec"));
|
|
let tx = args
|
|
.get(6)
|
|
.map_or(VERSION.transaction_version, |v| v.parse().expect("tx"));
|
|
let xt = firmar(llamada(&args[2]), nonce, genesis, spec, tx);
|
|
// Hex completo: `HexDisplay` recorta con «...» pasados 1024 bytes, y un
|
|
// `set_code` lleva el runtime entero (MB).
|
|
println!("{}", sp_core::bytes::to_hex(&xt.encode(), false));
|
|
}
|
|
"eventos" => {
|
|
let bytes = hex(&args[2]);
|
|
let eventos = Vec::<EventRecord<RuntimeEvent, H256>>::decode(&mut &bytes[..])
|
|
.expect("System::Events");
|
|
// La fase va delante: dice a qué extrinsic pertenece cada evento.
|
|
for e in eventos {
|
|
println!("{:?} · {:?}", e.phase, e.event);
|
|
}
|
|
}
|
|
"cuenta" => {
|
|
use sp_core::crypto::Ss58Codec;
|
|
let uri = format!("{}{}", args[2], args.get(3).map(String::as_str).unwrap_or(""));
|
|
let par = sr25519::Pair::from_string(&uri, None).expect("frase o derivación inválida");
|
|
let publica = par.public();
|
|
println!("sr25519 publica 0x{}", sp_core::hexdisplay::HexDisplay::from(&publica.0));
|
|
println!("ss58 (42) {}", publica.to_ss58check());
|
|
}
|
|
"verificar" => {
|
|
use sp_runtime::traits::Checkable;
|
|
let bytes = hex(&args[2]);
|
|
let genesis = H256::from_slice(&hex(&args[3]));
|
|
let xt = UncheckedExtrinsic::decode(&mut &bytes[..]).expect("extrinsic");
|
|
let json = origin_chain_runtime::genesis_config_presets::get_preset(
|
|
&sp_genesis_builder::PresetId::from(sp_genesis_builder::DEV_RUNTIME_PRESET),
|
|
)
|
|
.expect("preset development");
|
|
let mut ext = sp_io::TestExternalities::new_empty();
|
|
ext.execute_with(|| {
|
|
frame_support::genesis_builder_helper::build_state::<
|
|
origin_chain_runtime::RuntimeGenesisConfig,
|
|
>(json)
|
|
.expect("estado del génesis");
|
|
frame_system::Pallet::<Runtime>::set_block_number(5);
|
|
frame_system::BlockHash::<Runtime>::insert(0, genesis);
|
|
let r = xt.check(&frame_system::ChainContext::<Runtime>::default());
|
|
println!("check del runtime: {:?}", r.map(|_| "firma aceptada"));
|
|
});
|
|
}
|
|
_ => panic!("modo: firmar · eventos · verificar · cuenta (ver la cabecera del fichero)"),
|
|
}
|
|
}
|