feat: accept pre-encoded extension values, validated by round trip
A dapp hands a wallet an `era` it encoded itself, as opaque bytes. There is no
way to render those as a variant without knowing the era algorithm, which is
exactly the kind of knowledge this crate refuses to hold — so they are accepted
as `Supplied::Raw`.
Not on trust, though. Raw bytes are decoded against the type the runtime
declares and re-encoded, and anything that does not come back identical is
refused: a short read, trailing bytes, a non-canonical compact. Appending them
unchecked would mean signing a payload whose shape nobody verified, and the only
report of that is `BadProof` from a node — which is also what a wrong key looks
like.
Also makes a single-field struct transparent whether or not its field is named,
so `CheckMetadataHash { mode }` takes `"Disabled"` the way `AccountId32([u8;32])`
takes its hex. Both wrappers are the runtime's choice, and the registry is what
says they are there.
Refs #3
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012uDUodEcRbBwNRi3UCmw8f
This commit is contained in:
@@ -13,6 +13,23 @@ use scale_value::{Composite, Primitive, Value, ValueDef};
|
||||
|
||||
use crate::runtime::{CodecError, Runtime};
|
||||
|
||||
/// A value for one half of one signed extension.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Supplied {
|
||||
/// Interpreted against the type the runtime declares — see
|
||||
/// [`Runtime::json_to_value`].
|
||||
Json(serde_json::Value),
|
||||
/// Already SCALE-encoded by whoever is asking for the signature.
|
||||
///
|
||||
/// A dapp hands the wallet an `era` as opaque bytes, having encoded it
|
||||
/// itself, and there is no way to render those as JSON without knowing the
|
||||
/// era algorithm — which is exactly the kind of knowledge this crate refuses
|
||||
/// to hold. So they are accepted, but **not on trust**: [`Runtime::encode_half`]
|
||||
/// decodes them against the declared type and re-encodes them, and anything
|
||||
/// that does not round-trip is rejected rather than signed.
|
||||
Raw(Vec<u8>),
|
||||
}
|
||||
|
||||
/// What the caller knows about one signed extension.
|
||||
///
|
||||
/// Both halves are optional because most extensions need neither: a zero-sized
|
||||
@@ -21,8 +38,8 @@ use crate::runtime::{CodecError, Runtime};
|
||||
/// is not an error; *omitting* one for a non-zero-sized type is.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct ExtensionValue {
|
||||
pub extra: Option<serde_json::Value>,
|
||||
pub additional: Option<serde_json::Value>,
|
||||
pub extra: Option<Supplied>,
|
||||
pub additional: Option<Supplied>,
|
||||
}
|
||||
|
||||
/// The two byte strings a signed extrinsic needs from its extensions.
|
||||
@@ -150,7 +167,7 @@ impl Runtime {
|
||||
fn encode_half(
|
||||
&self,
|
||||
ty: u32,
|
||||
supplied: Option<&serde_json::Value>,
|
||||
supplied: Option<&Supplied>,
|
||||
identifier: &str,
|
||||
out: &mut Vec<u8>,
|
||||
) -> Result<(), CodecError> {
|
||||
@@ -160,8 +177,44 @@ impl Runtime {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let value = supplied.ok_or_else(|| CodecError::MissingExtension(identifier.to_string()))?;
|
||||
let converted = self.json_to_value(value, ty)?;
|
||||
let converted = match supplied.ok_or_else(|| {
|
||||
CodecError::MissingExtension(identifier.to_string())
|
||||
})? {
|
||||
Supplied::Json(value) => self.json_to_value(value, ty)?,
|
||||
// Round-tripped rather than appended. Bytes that decode against the
|
||||
// declared type and re-encode to themselves are the *only* bytes the
|
||||
// runtime could have meant; anything else — a short read, trailing
|
||||
// bytes, a non-canonical compact — is a disagreement about the format
|
||||
// that would otherwise be signed and only surface as `BadProof`.
|
||||
Supplied::Raw(bytes) => {
|
||||
let mut cursor = &bytes[..];
|
||||
let value = self
|
||||
.decode_checked(ty, &mut cursor)
|
||||
.map_err(|e| CodecError::Encode(format!("{identifier}: {e}")))?;
|
||||
|
||||
if !cursor.is_empty() {
|
||||
return Err(CodecError::Encode(format!(
|
||||
"{identifier}: {} trailing bytes",
|
||||
cursor.len()
|
||||
)));
|
||||
}
|
||||
|
||||
let mut check = Vec::new();
|
||||
|
||||
scale_value::scale::encode_as_type(&value, ty, self.types(), &mut check)
|
||||
.map_err(|e| CodecError::Encode(format!("{identifier}: {e}")))?;
|
||||
|
||||
if check != *bytes {
|
||||
return Err(CodecError::Encode(format!(
|
||||
"{identifier}: supplied bytes do not round-trip against the runtime's type"
|
||||
)));
|
||||
}
|
||||
|
||||
out.extend_from_slice(bytes);
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
scale_value::scale::encode_as_type(&converted, ty, self.types(), out)
|
||||
.map_err(|e| CodecError::Encode(format!("{identifier}: {e}")))
|
||||
|
||||
Reference in New Issue
Block a user