feat(iota-core, iota-types): add validator attestation#11528
feat(iota-core, iota-types): add validator attestation#11528vekkiokonio wants to merge 48 commits into
Conversation
|
|
|
|
Co-authored-by: Roman Overko <63564739+roman1e2f5p8s@users.noreply.github.com>
….com:iotaledger/iota into protocol-research/feat/validator-attestation
| /// of `ConsensusTransactionKind::UserTransactionV2`. | ||
| #[derive(Clone, Debug, Serialize, Deserialize)] | ||
| pub struct AttestedTransaction { | ||
| pub transaction: Box<Transaction>, |
There was a problem hiding this comment.
I would remove this inner Box, keep the outer - the pattern is already there in the codebase to Box any variant carrying significant content:
CertifiedTransaction(Box<CertifiedTransaction>),
CheckpointSignature(Box<CheckpointSignatureMessage>),
UserTransactionV1(Box<Transaction>),
UserTransactionV2(Box<AttestedTransaction>),| UserTransactionV1(Box<Transaction>), | ||
| /// Attested user transaction. Carries a gas attestation produced either by | ||
| /// the proposing validator or a registered third-party attestor. | ||
| UserTransactionV2(Box<AttestedTransaction>), |
There was a problem hiding this comment.
Or perhaps the Box wrapping inside of AttestedTransaction is unnecessary?
this
| warn!( | ||
| ?digest, | ||
| error = ?e, | ||
| "UserTransactionV1 failed validity_check post-consensus, dropping" | ||
| ); |
There was a problem hiding this comment.
this applies to V2 as well, so the warning message should be updated
suggestion
warn!(
?digest,
kind = if attestation.is_some() { "UserTransactionV2" } else { "UserTransactionV1" },
error = ?e,
"user transaction failed validity_check post-consensus, dropping"
);| // Gated behind `enable_white_flag_flow`. | ||
| ConsensusTransactionKind::UserTransactionV1(_) => { | ||
| ConsensusTransactionKind::UserTransactionV1(_) | ||
| | ConsensusTransactionKind::UserTransactionV2(_) => { |
There was a problem hiding this comment.
V2 is gated by enable_validator_attestation, which transitively requires enable_white_flag_flow, but it is not the same flag
| let committee = epoch_store.committee(); | ||
| let attestor_index = committee | ||
| .names() | ||
| .position(|n| n == &state.name) |
There was a problem hiding this comment.
this search will run for every single tx, why doing that if validator position does not change within an epoch? either cache authority index if attestor_index field is needed indeed, or remove the field entirely
There was a problem hiding this comment.
This search is now a hashmap lookup so not worth caching
| let all_certificates = transactions | ||
| .iter() | ||
| .all(|tx| matches!(tx.kind, ConsensusTransactionKind::CertifiedTransaction(_))); | ||
| let all_v1 = transactions | ||
| .iter() | ||
| .all(|tx| matches!(tx.kind, ConsensusTransactionKind::UserTransactionV1(_))); | ||
| let all_v2 = transactions | ||
| .iter() | ||
| .all(|tx| matches!(tx.kind, ConsensusTransactionKind::UserTransactionV2(_))); |
There was a problem hiding this comment.
could be done in a single scan rather than three
also could catch future user transaction V3, etc
| || if epoch_store.protocol_config().enable_white_flag_flow() { | ||
| // In the certificate-less mode, `UserTransactionV1` kind corresponds | ||
| // to user transactions. | ||
| // In the certificate-less mode, `UserTransactionV1`/`UserTransactionV2` |
There was a problem hiding this comment.
update other such comments in this file
roman1e2f5p8s
left a comment
There was a problem hiding this comment.
formatting and clippy warning should be fixed
| ( | ||
| "UserTransactionV1", | ||
| ConsensusTransactionKind::UserTransactionV1(Box::new(signed_tx)), | ||
| ), |
There was a problem hiding this comment.
V2 should be exercised here as well?
| starfish_config::AuthorityIndex::from(tx.0.certificate_author_index as u8); | ||
| let error = match attestation { | ||
| Attestation::Validator { attestor_index, .. } => { | ||
| if *attestor_index != block_author { |
There was a problem hiding this comment.
A malicious validator may create invalid attestations with
attestor_indexof a different authority.
but why is that even possible in the first place? isn't it because the field exists?
| ) -> IotaResult<ConsensusTransactionResult> { | ||
| let _scope = monitored_scope("HandleConsensusTransaction"); | ||
| let VerifiedSequencedConsensusTransaction(SequencedConsensusTransaction { | ||
| certificate_author_index: _, |
There was a problem hiding this comment.
this is where certificate_author_index could be remembered and propagated later in this function to execution phase, so attestor_index seems redundant (also exploitable) in block-signer attestation
| SequencedConsensusTransactionKind::External(ConsensusTransaction { | ||
| kind: ConsensusTransactionKind::UserTransactionV2(a), | ||
| .. | ||
| }) => &a.transaction, |
There was a problem hiding this comment.
so attestation data is discarded here? propagation to execution will be a next step?
| let executable_tx = VerifiedExecutableTransaction::new_unchecked( | ||
| ExecutableTransaction::new_from_data_and_sig( | ||
| transaction.data().clone(), | ||
| CertificateProof::ConsensusOrdered(self.epoch()), |
There was a problem hiding this comment.
in this PR, the attestation data is dropped, but in the future, it will have to be propagated to execution anyway.
this is exactly where certificate_author_index could be propagated to execution, so no attestor_index field is needed at all
| starfish_config::AuthorityIndex::from(tx.0.certificate_author_index as u8); | ||
| let error = match attestation { | ||
| Attestation::Validator { attestor_index, .. } => { | ||
| if *attestor_index != block_author { |
There was a problem hiding this comment.
Since after consensus the block author is lost
not lost, but explicitly discarded; see https://github.com/iotaledger/iota/pull/11528/changes#r3288664436 and https://github.com/iotaledger/iota/pull/11528/changes#r3288710190
the same mechanism that will propagate attestation data to execution could naturally carry certificate_author_index or block author along it
Co-authored-by: Roman Overko <63564739+roman1e2f5p8s@users.noreply.github.com>
Co-authored-by: Roman Overko <63564739+roman1e2f5p8s@users.noreply.github.com>
Co-authored-by: Roman Overko <63564739+roman1e2f5p8s@users.noreply.github.com>
Co-authored-by: Roman Overko <63564739+roman1e2f5p8s@users.noreply.github.com>
Co-authored-by: Roman Overko <63564739+roman1e2f5p8s@users.noreply.github.com>
….com:iotaledger/iota into protocol-research/feat/validator-attestation
Co-authored-by: Roman Overko <63564739+roman1e2f5p8s@users.noreply.github.com>
….com:iotaledger/iota into protocol-research/feat/validator-attestation
|
|
||
| let authenticator_gas_budget = protocol_config.max_auth_gas(); | ||
| let (exec_gas_status, per_auth_checked, auth_and_tx_checked) = | ||
| iota_transaction_checks::check_transaction_and_move_authenticator_input( |
There was a problem hiding this comment.
Here you are executing the same checks for the second time (with a small change that I will describe later):
iota_transaction_checks::check_transaction_and_move_authenticator_inputhere is equivalent to the check perfomed inself.run_validation_checks -> self.check_transaction_inputs_for_validation -> iota_transaction_checks::check_move_authenticator_input_for_validation + iota_transaction_checks::check_transaction_input- the only change is that
iota_transaction_checks::check_transaction_and_move_authenticator_inputcallscheck_transaction_input_innerwith the last parameter (is_execute_transaction_to_effects) set to true, whileiota_transaction_checks::check_transaction_inputcalls the same function but with that param set to false. - I suggest to remove this
iota_transaction_checks::check_transaction_and_move_authenticator_inputinvocation here and modifyself.run_validation_checksto support a new parameter, i.e.,is_execute_transaction_to_effects, that is set to true forattest_transactionand false forhandle_transaction_validation_checks, then propagate this parameter down toiota_transaction_checks::check_transaction_input
| /// [`VerifiedExecutableTransaction`] is not yet available. Produces an | ||
| /// execution-mode gas status (full `transaction_gas_budget`, not the | ||
| /// reduced signing-time auth budget). | ||
| pub fn check_transaction_and_move_authenticator_input( |
There was a problem hiding this comment.
check_transaction_and_move_authenticator_input can be removed. See this #11528 (comment) comment
| }; | ||
|
|
||
| // Step 7: build AttestationData. | ||
| let estimated_computation_cost = effects.gas_cost_summary().computation_cost; |
There was a problem hiding this comment.
The advantage of letting OOG transaction pass is that it elimenates that as an attack vector on attestors i.e. sending transactions with insufficient gas to expend attestor resources without paying.
This is not applicable for AA TXs:
- I use the gas coin of an AA account,
- make the authenticator go OOG (it will abort the execution, but charge estimated_computation_cost)
- and then repeat this to drain any gas coin that I don't own
| // Build AttestationData. | ||
| let estimated_computation_cost = effects.gas_cost_summary().computation_cost; | ||
|
|
||
| // Collect all input object refs seen during execution. Start from the |
There was a problem hiding this comment.
Why are you collecting object refs with versions being the pre-execution ones? You will find the same info already in the TxData that is signed (through its digest) together with the attestation data. So it seems a duplication to me. The objects that will appear here and not in the TxData would only be the dynamic fields used during the execution, but I don't see why thisdynamic fields' information might be necessary.
Description of change
This PR implements the Validator Attestation (Phase 1) — the mechanism by which a proposing validator certifies, pre-consensus, that a transaction has passed validation and provides an estimated computation cost for scheduler hints.
Note: related changes to sequencer and execution will be part of a separate PR.
What changed
New data structures (
iota-types)attestation.rs:Attestation(two variants —Validator,Explicit),AttestationData(versioned, carries estimated computation cost and observed shared-object versions),AttestedTransaction(transaction + attestation bundle). BCS round-trip tests included.messages_consensus.rs: newUserTransactionV2(Box<AttestedTransaction>)variant inConsensusTransactionKind, carrying the bundled attestation.error.rs:AttestationAuthorMismatchandExplicitAttestationNotSupportederror variants.iota-protocol-config:enable_validator_attestationfeature flag. Attestation is only active whenenable_white_flag_flowis also on; the two flags are enforced as co-dependent.Pre-consensus attestation (
authority.rs,validator_v2.rs)attest_transaction: validates the transaction (deny checks, gas, ownership, coin deny list, full dry-run) and returns(AttestationData, Vec<ObjectRef>). It subsumeshandle_transaction_validation_checks, so the two paths are mutually exclusive.submit_single_txrestructured: whenenable_validator_attestationis on, callsattest_transactioninstead ofhandle_transaction_validation_checks. The pre-consensus soft lock is acquired after attestation (owned objects are returned byattest_transaction, so no extra pass is needed). The transaction is submitted asUserTransactionV2with the attestation embedded.Batch homogeneity (
consensus_adapter.rs)submit_batchenforces that all transactions in a soft-bundle are of the same kind: all-V1, all-V2, or all-certificates. Mixed batches are rejected withInvalidTxKindInSoftBundle.Post-consensus attestor verification (
post_consensus_validation.rs)UserTransactionV2match arm separated into pure classification; digest is pushed toall_user_tx_digestsbefore any check, so dropped V2 transactions still release pre-consensus soft locks.attestor_indexinAttestation::Validatormatchescertificate_author_index(block author).UserTransactionV2transactions skip Check # 6 (handle_transaction_validation_checks) — the attestor already performed those checks before producing the attestation.consensus_validator.rs: comment updated to clarify that attestor verification happens inpost_consensus_validation, not during block ingestion (where block-author context is unavailable).Unit tests
validator_v2_tests.rs: unit tests for the V2 submission path — happy path (transaction is attested and accepted) and rejection path (attestation fails with an invalid transaction).post_consensus_validation_tests.rs: unit tests forUserTransactionV2handling in post-consensus validation — correct attestor accepted, mismatched attestor rejected, andUserTransactionV2skipping duplicate validation checks.End-to-end test (iota-e2e-tests)
attestation_tests.rs:test_aa_tx_accepted_via_v2_attestation_pathsubmits a Move abstract account transaction through the V2 gRPC path with bothenable_white_flag_flowandenable_validator_attestationactive. Exercises theauthenticate_then_execute_transaction_to_effectsbranch ofattest_transaction(MoveAuthenticator dry-run path).Links to any relevant issues
Closes https://github.com/iotaledger/iota-private/issues/384
Closes https://github.com/iotaledger/iota-private/issues/385
Part of Epic https://github.com/iotaledger/iota-private/issues/383
How the change has been tested
BCS round-trip tests for
AttestationData,Attestation::Validator,Attestation::Explicit, andAttestedTransactionare included inattestation.rs. Unit tests for the V2 submission and post-consensus validation paths are invalidator_v2_tests.rsandpost_consensus_validation_tests.rs. An end-to-end test exercising the full attestation pipeline is inattestation_tests.rs.Release Notes
UserTransactionV2consensus transaction kind andenable_validator_attestationprotocol config flag (requiresenable_white_flag_flow).