Skip to content

Accounts Refactor PR 2: Adding target_account#2771

Open
citizen-stig wants to merge 8 commits intonikolai/accounts-refactor-part-1from
nikolai/accounts-refactor-part-2
Open

Accounts Refactor PR 2: Adding target_account#2771
citizen-stig wants to merge 8 commits intonikolai/accounts-refactor-part-1from
nikolai/accounts-refactor-part-2

Conversation

@citizen-stig
Copy link
Copy Markdown
Member

@citizen-stig citizen-stig commented Apr 21, 2026

Description

Adds a signed target_address: Option<S::Address> field to V1 (multisig) transactions, threaded into AuthorizationData.address. resolve_context branches on it: Some(X) verifies (X, credential_id) ∈ account_owners via PR #2770 's is_authorized;
None keeps today's resolve_sender_address flow.
V0 is byte-identical. CHAIN HASH BREAKING for V1.

PR #2770 froze writes to the legacy credential_id → address map; InsertCredentialId now only marks (sender, credential) authorized, without rerouting that credential's default.
That means a rotated multisig (new keys → new credential_id.into()) has no way to keep acting as the original address. target_address is the signed channel that lets a signer declare which authorized address to execute as.

Design tradeoffs

  • V1 wire break, no grace period. CHAIN_HASH_OVERRIDES covers signature-level grace only (post-deserialize). Adding a borsh field breaks deserialization of pre-upgrade V1 bytes regardless of chain hash, so a grace period isn't meaningful. Matches the repo's prior convention (58b6168 CHAIN HASH BREAKING, 49ac451 V1 introduction) — ship the break.
  • V0 untouched. Advanced routing is opt-in via V1. A V0 single-sig user who wants multi-address routing wraps their key as a 1-of-1 V1.
  • Unauthorized Some(X) is Skipped, not Reverted. We haven't resolved a sender yet, so there's no account to charge — sequencer pays inclusion cost, user is unaffected. Matches the existing pre-execution-error receipt category (same path as bad signatures). Error string is preserved in the receipt.
  • Read-only check. The Some(X) path never writes to account_owners. Auto-registration only happens on the None path.
  • Explicit builder parameters. to_multisig_tx and sign_utx_v1_in_place take the target as an argument rather than defaulting to None with callers assigning the public field afterwards — avoids a half-baked API in an SDK recommended pattern.

Audit invariants

  1. V0 wire format byte-identical to pre-PR.
  2. V1's credential_address field (multisig malleability commitment) is untouched.
  3. target_address is inside the V1 signed bytes — post-sign tampering invalidates the signature (test: test_v1_target_tamper_breaks_signature).
  4. Some(_) path does exactly one read of account_owners; never writes.
  5. Failed Some(_) → TxEffect::Skipped, sequencer penalized, user never charged.

Notable side-effects

  • test_multisig_signature_verification in the EIP-712 integration test was already silently broken by PR 2770 (the multisig credential no longer routed to admin after InsertCredentialId, leaving gas on an unfunded default). Fixed here by signing with target_address = Some(admin.address()) — doubles as an EIP-712-meets-target-address smoke test.

  • I have updated CHANGELOG.md with a new entry if my PR makes any breaking changes or fixes a bug. If my PR removes a feature or changes its behavior, I provide help for users on how to migrate to the new behavior.
  • I have carefully reviewed all my Cargo.toml changes before opening the PRs. (Are all new dependencies necessary? Is any module dependency leaked into the full-node (hint: it shouldn't)?)

Linked Issues

Testing

New tests are added

Docs

Updated doc-strings and comments

@citizen-stig citizen-stig force-pushed the nikolai/accounts-refactor-part-2 branch from 1201dc0 to 5008b08 Compare April 22, 2026 08:20
@citizen-stig citizen-stig force-pushed the nikolai/accounts-refactor-part-2 branch from 920da01 to 736575b Compare April 22, 2026 12:25
@citizen-stig citizen-stig marked this pull request as ready for review April 22, 2026 13:12
devin-ai-integration[bot]

This comment was marked as resolved.

Comment on lines 362 to 368
state: &mut impl StateAccessor,
execution_context: ExecutionContext,
) -> anyhow::Result<Context<S>> {
let sender = self.accounts.resolve_sender_address(
&auth_data.default_address,
&auth_data.credential_id,
state,
)?;
let sender = self.resolve_sender(auth_data, state)?;
// The tx sender & sequencer are the same entity
Ok(Context::new(
sender,
Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration Bot Apr 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚩 resolve_unregistered_context now routes through resolve_sender, changing unregistered sequencer path semantics

Before this PR, resolve_unregistered_context always called resolve_sender_address(default_address, credential_id, state). After this PR, it calls resolve_sender(auth_data, state) which, when auth_data.address is Some(X), performs a strict is_authorized check and uses X as both the sender and sequencer_rollup_address. The comment says "The tx sender & sequencer are the same entity", so using a target-routed address as the sequencer rollup address may be unintended. In practice, V0 transactions always set address: None and this path is primarily for sequencer registration, making this edge case unlikely to be triggered. But it's worth noting that the semantic contract of resolve_unregistered_context has broadened.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The broadening is intentional. auth_data.address is a signed user declaration carried inside V1 bytes — enforcing is_authorized(X, credential_id) uniformly across resolve_context and resolve_unregistered_context keeps the V1 target_address invariant in one place. A V1 self-registering sequencer with target_address = Some(X) should still be required to prove the multisig credential is authorized for X; the alternative (skipping the check during registration only) would make the signed target_address field weaker in registration paths than in normal execution, which is a foot-gun. V0 sets address = None and is byte-identical to pre-PR behavior.

Added a doc comment above resolve_unregistered_context capturing this invariant in-code.

Comment thread crates/full-node/sov-rollup-apis/src/endpoints/simulate.rs Outdated
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant