Skip to content

Commit 34824a6

Browse files
authored
feat: only the sponsor MoveAuthenticator should be executed on the pre-consensus phase for a sponsored transaction (#11508)
# Description of change Only the sponsor MoveAuthenticator should be executed on the pre-consensus phase for a sponsored transaction. ## Links to any relevant issues fixes #11262
1 parent 7df9186 commit 34824a6

35 files changed

Lines changed: 1232 additions & 113 deletions

File tree

crates/iota-core/src/authority.rs

Lines changed: 61 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -900,13 +900,13 @@ impl AuthorityState {
900900
self.get_backing_package_store().as_ref(),
901901
)?;
902902

903-
// Load all transaction-related input objects.
904-
// Authenticator input objects and the account objects are loaded in the same
905-
// call if there are `MoveAuthenticator` signatures present in the transaction.
903+
// Load all transaction-related input objects including ones for every
904+
// `MoveAuthenticator`. Loading all objects eagerly means that any invalid
905+
// reference — missing object, wrong version, inaccessible object — causes a
906+
// pre-consensus rejection.
906907
let (tx_input_objects, tx_receiving_objects, per_authenticator_inputs) =
907908
self.read_objects_for_signing(&transaction, epoch)?;
908909

909-
// Get the `MoveAuthenticator`s, if any.
910910
let move_authenticators = transaction.move_authenticators();
911911

912912
// Check the inputs for signing.
@@ -943,6 +943,23 @@ impl AuthorityState {
943943
&self.get_object_store(),
944944
)?;
945945

946+
// Filter the authenticators and their checked inputs down to those that must
947+
// be executed pre-consensus. This is done *after* the deny-list check so
948+
// that all MoveAuthenticator input objects are covered by that check regardless
949+
// of deferral.
950+
let pre_consensus_move_authenticators =
951+
pre_consensus_move_authenticators(&transaction, protocol_config);
952+
let (move_authenticators, per_authenticator_checked_inputs): (Vec<_>, Vec<_>) =
953+
move_authenticators
954+
.into_iter()
955+
.zip(per_authenticator_checked_inputs)
956+
.filter(|(a, _)| pre_consensus_move_authenticators.contains(a))
957+
.unzip();
958+
let per_authenticator_checked_input_objects: Vec<_> = per_authenticator_checked_inputs
959+
.iter()
960+
.map(|i| &i.0)
961+
.collect();
962+
946963
// If there are `MoveAuthenticator` signatures, execute them and check if they
947964
// all succeed.
948965
if !move_authenticators.is_empty() {
@@ -981,6 +998,9 @@ impl AuthorityState {
981998
let tx_data_bytes =
982999
bcs::to_bytes(&tx_data).expect("TransactionData serialization cannot fail");
9831000

1001+
let (sender_auth_digest, sponsor_auth_digest) =
1002+
transaction.data().compute_auth_digests()?;
1003+
9841004
let (kind, signer, gas_data) = tx_data.execution_parts();
9851005

9861006
// Execute the Move authenticators.
@@ -1001,6 +1021,8 @@ impl AuthorityState {
10011021
signer,
10021022
transaction.digest().to_owned(),
10031023
tx_data_bytes,
1024+
sender_auth_digest,
1025+
sponsor_auth_digest,
10041026
&mut None,
10051027
);
10061028

@@ -1726,6 +1748,9 @@ impl AuthorityState {
17261748
let tx_data_bytes =
17271749
bcs::to_bytes(tx_data).expect("TransactionData serialization cannot fail");
17281750

1751+
let (sender_auth_digest, sponsor_auth_digest) =
1752+
certificate.data().compute_auth_digests()?;
1753+
17291754
// Check the `MoveAuthenticator` input objects.
17301755
// The `MoveAuthenticator` receiving objects are checked on the signing step.
17311756
// `max_auth_gas` is used here as a Move authenticator gas budget until it is
@@ -1793,6 +1818,8 @@ impl AuthorityState {
17931818
signer,
17941819
tx_digest,
17951820
tx_data_bytes,
1821+
sender_auth_digest,
1822+
sponsor_auth_digest,
17961823
&mut None,
17971824
)
17981825
};
@@ -6168,3 +6195,33 @@ impl NodeStateDump {
61686195
serde_json::from_reader(file).map_err(|e| anyhow::anyhow!(e))
61696196
}
61706197
}
6198+
6199+
/// Returns the [`MoveAuthenticator`]s to execute during the pre-consensus
6200+
/// phase.
6201+
///
6202+
/// When `pre_consensus_sponsor_only_move_authentication` is enabled:
6203+
/// - For sponsored transactions: only the sponsor's [`MoveAuthenticator`] is
6204+
/// returned (empty if the sponsor does not use one).
6205+
/// - For non-sponsored transactions: all [`MoveAuthenticator`]s are returned
6206+
/// (currently only the sender's).
6207+
///
6208+
/// When the flag is not set, all [`MoveAuthenticator`]s are returned for
6209+
/// compatibility.
6210+
fn pre_consensus_move_authenticators<'a>(
6211+
tx: &'a VerifiedTransaction,
6212+
protocol_config: &ProtocolConfig,
6213+
) -> Vec<&'a MoveAuthenticator> {
6214+
if protocol_config.pre_consensus_sponsor_only_move_authentication() {
6215+
if tx.transaction_data().is_sponsored_tx() {
6216+
if let Some(sponsor_move_authenticator) = tx.sponsor_move_authenticator() {
6217+
vec![sponsor_move_authenticator]
6218+
} else {
6219+
vec![]
6220+
}
6221+
} else {
6222+
tx.move_authenticators()
6223+
}
6224+
} else {
6225+
tx.move_authenticators()
6226+
}
6227+
}

crates/iota-e2e-tests/tests/abstract_account_tests.rs

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1111,6 +1111,7 @@ async fn test_two_move_authenticators_rejected_with_disabled_move_auth_for_spons
11111111
// Disable Move authentication for the sponsor.
11121112
let _guard = ProtocolConfig::apply_overrides_for_testing(|_, mut config| {
11131113
config.set_enable_move_authentication_for_sponsor_for_testing(false);
1114+
config.set_pre_consensus_sponsor_only_move_authentication_for_testing(false);
11141115
config
11151116
});
11161117

@@ -1172,6 +1173,7 @@ async fn test_sponsor_only_move_auth_rejected_with_disabled_move_auth_for_sponso
11721173
// Disable Move authentication for the sponsor.
11731174
let _guard = ProtocolConfig::apply_overrides_for_testing(|_, mut config| {
11741175
config.set_enable_move_authentication_for_sponsor_for_testing(false);
1176+
config.set_pre_consensus_sponsor_only_move_authentication_for_testing(false);
11751177
config
11761178
});
11771179

@@ -1347,6 +1349,199 @@ async fn test_aa_sender_and_aa_sponsor_rejected_when_sponsor_aa_fails_with_enabl
13471349
Ok(())
13481350
}
13491351

1352+
// ------------------------------------------------------------------
1353+
// --- pre_consensus_sponsor_only_move_authentication flag tests ---
1354+
// ------------------------------------------------------------------
1355+
1356+
/// With `pre_consensus_sponsor_only_move_authentication` enabled, only the
1357+
/// sponsor's MA runs pre-consensus for a sponsored TX. A sender MA signed over
1358+
/// the wrong digest is therefore accepted pre-consensus (the sponsor's
1359+
/// free-access MA passes) but fails post-consensus when the sender's MA is
1360+
/// finally executed and the signature doesn't match.
1361+
#[sim_test]
1362+
async fn test_sponsored_tx_sender_aa_fails_post_consensus_when_only_sponsor_runs_pre_consensus()
1363+
-> Result<(), anyhow::Error> {
1364+
telemetry_subscribers::init_for_testing();
1365+
let client_ip = SocketAddr::new([127, 0, 0, 1].into(), 0);
1366+
1367+
// Sender is an AA with ED25519 authentication; sponsor is an AA with
1368+
// free-access authentication.
1369+
let mut test_env = TestEnvironment::new().await;
1370+
test_env
1371+
.setup_abstract_account(AA_AUTHENTICATE_FN_NAME_ED25519)
1372+
.await?;
1373+
let sender_aa_ref = test_env.aa_ref.unwrap();
1374+
let aa_sender: IotaAddress = sender_aa_ref.object_id.into();
1375+
1376+
let sponsor_aa_ref = test_env
1377+
.create_extra_abstract_account_with(AA_AUTHENTICATE_FN_NAME_FREE_ACCESS)
1378+
.await?;
1379+
let sponsor_addr: IotaAddress = sponsor_aa_ref.object_id.into();
1380+
1381+
let rgp = test_env.test_cluster.get_reference_gas_price().await;
1382+
let sponsor_gas = test_env
1383+
.test_cluster
1384+
.fund_address_and_return_gas(rgp, Some(20_000_000_000), sponsor_addr)
1385+
.await;
1386+
1387+
let pt = test_env.craft_aa_simple_ptb(AA_MODULE_NAME)?;
1388+
let tx_data = test_env
1389+
.craft_tx_from_pt(pt, sponsor_gas, aa_sender, Some(sponsor_addr))
1390+
.await?;
1391+
1392+
// Sender's MA is signed over the wrong digest — it will fail post-consensus.
1393+
let wrong_digest = [0u8; 32];
1394+
let sender_aa_sig = test_env.create_move_authenticator_for_ed25519(&wrong_digest)?;
1395+
let sponsor_aa_sig =
1396+
test_env.create_move_authenticator_for_free_access_for_ref(sponsor_aa_ref)?;
1397+
let tx = Transaction::from_generic_sig_data(tx_data, vec![sender_aa_sig, sponsor_aa_sig]);
1398+
1399+
// Pre-consensus: only the sponsor's MA is executed (free-access → passes).
1400+
// The validator must sign the TX, producing a certificate.
1401+
let cert = test_env
1402+
.test_cluster
1403+
.create_certificate(tx, Some(client_ip))
1404+
.await?;
1405+
1406+
// Post-consensus: both MAs are executed → sender's ED25519 MA fails.
1407+
let QuorumDriverResponse { effects_cert, .. } = test_env
1408+
.test_cluster
1409+
.authority_aggregator()
1410+
.process_certificate(
1411+
HandleCertificateRequestV1::new(cert).with_events(),
1412+
Some(client_ip),
1413+
)
1414+
.await?;
1415+
1416+
let summary = effects_cert.summary_for_debug();
1417+
assert!(
1418+
summary.status.is_failure(),
1419+
"Expected TX to fail post-consensus due to the sender's MA failure"
1420+
);
1421+
assert!(
1422+
matches!(
1423+
summary.status.unwrap_err().0,
1424+
ExecutionFailureStatus::MoveAbort { .. }
1425+
),
1426+
"Expected a Move abort from the failed ED25519 authentication"
1427+
);
1428+
1429+
// Even though the TX failed, the sponsor must have paid gas. Verify that
1430+
// computation was charged and that the correct gas object (the sponsor's coin)
1431+
// was debited.
1432+
assert!(
1433+
summary.gas_used.computation_cost > 0,
1434+
"Expected computation cost > 0: the sponsor must pay gas even for a post-consensus failure"
1435+
);
1436+
assert_eq!(
1437+
effects_cert.data().gas_object().0.object_id,
1438+
sponsor_gas.object_id,
1439+
"Expected the sponsor's gas coin to be used for the failed TX"
1440+
);
1441+
1442+
Ok(())
1443+
}
1444+
1445+
/// With `pre_consensus_sponsor_only_move_authentication` disabled, both the
1446+
/// sender's and the sponsor's MAs run pre-consensus. A sender MA signed over
1447+
/// the wrong digest is therefore rejected immediately, before consensus.
1448+
#[sim_test]
1449+
async fn test_sponsored_tx_sender_aa_rejected_pre_consensus_without_sponsor_only_flag()
1450+
-> Result<(), anyhow::Error> {
1451+
telemetry_subscribers::init_for_testing();
1452+
1453+
// Disable the flag so ALL MAs run pre-consensus.
1454+
let _guard = ProtocolConfig::apply_overrides_for_testing(|_, mut config| {
1455+
config.set_pre_consensus_sponsor_only_move_authentication_for_testing(false);
1456+
config
1457+
});
1458+
1459+
let mut test_env = TestEnvironment::new().await;
1460+
test_env
1461+
.setup_abstract_account(AA_AUTHENTICATE_FN_NAME_ED25519)
1462+
.await?;
1463+
let sender_aa_ref = test_env.aa_ref.unwrap();
1464+
let aa_sender: IotaAddress = sender_aa_ref.object_id.into();
1465+
1466+
let sponsor_aa_ref = test_env
1467+
.create_extra_abstract_account_with(AA_AUTHENTICATE_FN_NAME_FREE_ACCESS)
1468+
.await?;
1469+
let sponsor_addr: IotaAddress = sponsor_aa_ref.object_id.into();
1470+
1471+
let rgp = test_env.test_cluster.get_reference_gas_price().await;
1472+
let sponsor_gas = test_env
1473+
.test_cluster
1474+
.fund_address_and_return_gas(rgp, Some(20_000_000_000), sponsor_addr)
1475+
.await;
1476+
1477+
let pt = test_env.craft_aa_simple_ptb(AA_MODULE_NAME)?;
1478+
let tx_data = test_env
1479+
.craft_tx_from_pt(pt, sponsor_gas, aa_sender, Some(sponsor_addr))
1480+
.await?;
1481+
1482+
let wrong_digest = [0u8; 32];
1483+
let sender_aa_sig = test_env.create_move_authenticator_for_ed25519(&wrong_digest)?;
1484+
let sponsor_aa_sig =
1485+
test_env.create_move_authenticator_for_free_access_for_ref(sponsor_aa_ref)?;
1486+
let tx = Transaction::from_generic_sig_data(tx_data, vec![sender_aa_sig, sponsor_aa_sig]);
1487+
1488+
// Pre-consensus: both MAs are executed → sender's MA fails → rejected
1489+
// immediately.
1490+
let err = test_env.handle_tx(tx).await.unwrap_err();
1491+
1492+
assert!(
1493+
matches!(&err, IotaError::MoveAuthenticatorExecutionFailure { .. }),
1494+
"Expected MoveAuthenticatorExecutionFailure from the sender's MA, got: {err:?}"
1495+
);
1496+
1497+
Ok(())
1498+
}
1499+
1500+
/// With `pre_consensus_sponsor_only_move_authentication` enabled, the flag
1501+
/// only skips the sender's MA pre-consensus check for *sponsored* transactions.
1502+
/// For non-sponsored transactions, the sender's MA still runs pre-consensus, so
1503+
/// a bad sender MA is still rejected before consensus.
1504+
#[sim_test]
1505+
async fn test_non_sponsored_tx_sender_aa_rejected_pre_consensus_with_sponsor_only_flag()
1506+
-> Result<(), anyhow::Error> {
1507+
telemetry_subscribers::init_for_testing();
1508+
1509+
let mut test_env = TestEnvironment::new().await;
1510+
test_env
1511+
.setup_abstract_account(AA_AUTHENTICATE_FN_NAME_ED25519)
1512+
.await?;
1513+
let aa_ref = test_env.aa_ref.unwrap();
1514+
let aa_sender: IotaAddress = aa_ref.object_id.into();
1515+
1516+
let rgp = test_env.test_cluster.get_reference_gas_price().await;
1517+
let aa_gas = test_env
1518+
.test_cluster
1519+
.fund_address_and_return_gas(rgp, Some(20_000_000_000), aa_sender)
1520+
.await;
1521+
1522+
// Non-sponsored TX — sender pays its own gas.
1523+
let pt = test_env.craft_aa_simple_ptb(AA_MODULE_NAME)?;
1524+
let tx_data = test_env
1525+
.craft_tx_from_pt(pt, aa_gas, aa_sender, None)
1526+
.await?;
1527+
1528+
// Sender's MA is signed over the wrong digest.
1529+
let wrong_digest = [0u8; 32];
1530+
let sender_aa_sig = test_env.create_move_authenticator_for_ed25519(&wrong_digest)?;
1531+
let tx = Transaction::from_generic_sig_data(tx_data, vec![sender_aa_sig]);
1532+
1533+
// Pre-consensus: non-sponsored TX → sender's MA always runs pre-consensus even
1534+
// with the sponsor-only flag → sender's MA fails → rejected immediately.
1535+
let err = test_env.handle_tx(tx).await.unwrap_err();
1536+
1537+
assert!(
1538+
matches!(&err, IotaError::MoveAuthenticatorExecutionFailure { .. }),
1539+
"Expected MoveAuthenticatorExecutionFailure for non-sponsored TX, got: {err:?}"
1540+
);
1541+
1542+
Ok(())
1543+
}
1544+
13501545
// ---------------------------------------------------
13511546
// --- Test Environment for Abstract Account tests ---
13521547
// ---------------------------------------------------

crates/iota-framework-snapshot/manifest.json

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -688,5 +688,30 @@
688688
"id": "0x000000000000000000000000000000000000000000000000000000000000107a"
689689
}
690690
]
691+
},
692+
"27": {
693+
"git_revision": "2f81916f7ea5403aeb00cebe24feb473cc98c62a",
694+
"packages": [
695+
{
696+
"name": "MoveStdlib",
697+
"path": "crates/iota-framework/packages/move-stdlib",
698+
"id": "0x0000000000000000000000000000000000000000000000000000000000000001"
699+
},
700+
{
701+
"name": "Iota",
702+
"path": "crates/iota-framework/packages/iota-framework",
703+
"id": "0x0000000000000000000000000000000000000000000000000000000000000002"
704+
},
705+
{
706+
"name": "IotaSystem",
707+
"path": "crates/iota-framework/packages/iota-system",
708+
"id": "0x0000000000000000000000000000000000000000000000000000000000000003"
709+
},
710+
{
711+
"name": "Stardust",
712+
"path": "crates/iota-framework/packages/stardust",
713+
"id": "0x000000000000000000000000000000000000000000000000000000000000107a"
714+
}
715+
]
691716
}
692717
}

0 commit comments

Comments
 (0)