4

We're trying to understand the signed transaction vs. unsigned transaction lifecycle, and what ensure_signed actually does for you.

What it seemed was, if you submitted an unsigned transaction to our pallet that it would put the failed txn in the block because it should fail ensure_signed with BadOrigin, correct? Just like any other failure inside of an extrinsic. IF you assume this, then it's fine for Pays::Yes txns.

However in Pays::No txns, one would think submitting unsigned txns would be an attack vector. Yet running our chain locally, we can submit unsigned txns all day long, and they will never be put in a block as a failed transaction, it will be invalid, because it reaches, (we think) this generated/macro code in validate_unsigned, displaying UnknownTransaction::NoUnsignedValidator in the Polkadot dashboard, without our having to do anything extra.

And in fact if we call this extrinsic using Sudo, it fails with BadOrigin, and the failed transaction is included in a block...so I guess we shouldn't DoS ourselves.

So next, SignedExtension's default validate_unsigned has something very different:

fn validate_unsigned(
    _call: &Self::Call,
    _info: &DispatchInfoOf<Self::Call>,
    _len: usize,
) -> TransactionValidity {
    Ok(ValidTransaction::default())
}

Basically it seems that unsigned transactions are not calling the above default implementation, but instead the first one which is automagically generated for pallets regardless of whether they have a SignedExtension implemented. Following the code, this expand_outer_validate_unsigned function is called (probably self-evident by now) in construct_runtime.

Question is what is the use of the default for the trait? Are we correct in thinking that unsigned transactions are default rejected before getting to the extrinsic code, unless you explicitly allow them? What is then the real purpose of ensure_signed? We were thinking it would also prevent Root from executing such a transaction.

Lastly, for something called SignedExtension, why is there a validate_unsigned function at all? The trait name would seem to imply it's really just for signed transactions.

2 Answers 2

3

The lifecycle of a extrinsic validation is the following:

  1. The transaction pool call the Runtime API validate_transaction method
  2. There, validate_transaction from the Executive pallet is called. uxt is of type Block::Extrinsic, which for the default substrate runtime equals to UncheckedExtrinsic (from this line and this one.
  3. In Executive::validate_transaction, after calculating encoded_len, it check() the UncheckExtrinsic. At this point, UncheckExtrinsic can be either a signed or unsigned extrinsic, we do not know yet.
  4. In the check() method for the generic UncheckedExtrinsic is where is checked if the extrinsic is signed or not. In both cases, a CheckedExtrinsic is return. If signed, the signed attribute will contain the signature information, if unsigned, the signed attribute will be set to None. That field in the CheckedExtrinsic is the one that differentiate between a signed and an unsigned extrinsic.
  5. Next action in Executive:validate_transaction is to get the dispatch_info from the previously returned CheckedExtrinsic. That happens here returning the DispatchInfo struct.
  6. Finally, the CheckedExtrinsic is validated. If we pay attention to its method, we can see how depending on its signed attribute (if it exists or None), a different method will be called from its type Extra. If it exists (signed tx), it calls Extra::validate, if not (unsigned tx), it calls Extra::validate_unsigned and U::validate_unsigned. Here is where signed and unsigned extrinsic validation flow diverge.

ensure_signed prevents unsigned extrinsic to execute your dispatchable. I still would need to investigate why all your unsigned transactions are failing.

It is true that SignedExtension name might be a bit misleading since both, signed & unsigned extrinsics go through them.

1
  • Excellent summary. However, the last bit though doesn't seem to be complete. Only signed transactions use all the SignedExtension extras: check_{non_zero_sender, mortality, nonce, genesis, tx_version, spec_version, weight}. Whereas unsigned transactions only use the SignedExtension check_weight. Correct? Nov 16, 2022 at 14:00
1

I think @robert-la-ferla and I may have answered the question that I had originally. To recap, for an UnsignedExtension, looking at primitives/runtime/src/generic/checked_exstrinsic#validate, the call of Extra::validate_unsigned is where the SignedExtra list validate_unsigned functions would be called. Only check_weights of the signed extensions we are using has validate_unsigned implemented. For all other pallets, the SignedExtrinsic default validate_unsigned is called - which returns Ok(...)

To expand on your #6 point, @ignacio-palacios, as you point out the next line is the U::validate_unsigned and we believe that this calls that generated in construct_runtime.rs (by calling expand_outer_validate_unsigned), where, if the pallet whose function is being called implements validate_unsigned, that is called, otherwise it returns UnknownTransaction::NoUnsignedValidator. In short we think line 63 passes for unsigned transactions and goes on to fail in line 64.

I believe this means by default no pallet will accept unsigned extrinsics, and to accept one must implement validate_unsigned in the pallet and do whatever checks are needed to allow a given unsigned extrinsic call.

Please correct me if I'm wrong.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.