Skip to content

fix: add withdrawals to redeemers pointers check#708

Closed
rodrigomd94 wants to merge 5 commits into
txpipe:mainfrom
rodrigomd94:main
Closed

fix: add withdrawals to redeemers pointers check#708
rodrigomd94 wants to merge 5 commits into
txpipe:mainfrom
rodrigomd94:main

Conversation

@rodrigomd94
Copy link
Copy Markdown
Contributor

@rodrigomd94 rodrigomd94 commented Oct 23, 2025

mk_plutus_script_redeemer_pointers didn't include a check for withdrawals, which was causing phase 1 validation to fail for transactions that include plutus scripts in the withdrawals. This is relevant for the "withdraw 0 trick".

Summary by CodeRabbit

  • New Features

    • Validation now recognizes stake reward withdrawals from script-backed stake addresses and adds corresponding Plutus redeemer pointers so those withdrawals are validated correctly.
    • Withdrawal processing now includes Reward redeemer pointers alongside inputs and minting.
  • Improvements

    • Withdrawals are deterministically ordered before processing to ensure stable, reproducible redeemer pointer generation.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Oct 23, 2025

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds deterministic handling of stake withdrawals when building Plutus redeemer pointers: withdrawals are sorted by network and stake payload, parsed as Address/StakePayload, and script-backed stake withdrawals that map to phase‑2 Plutus scripts produce Reward redeemer pointers.

Changes

Cohort / File(s) Summary
Stake withdrawal handling & redeemer pointers
pallas-validate/src/phase1/conway.rs
Extends mk_plutus_script_redeemer_pointers to iterate transaction withdrawals, parse stake addresses, detect script-backed stake credentials, and append RedeemersKey::Reward entries for matching phase‑2 Plutus scripts. Adds an unreachable branch for unexpected address forms.
Deterministic sorting of reward accounts
pallas-validate/src/phase1/conway.rs
Introduces sort_reward_accounts comparator and sorts withdrawals by network tag, stake payload kind (Script vs Stake), then by credential hash bytes to ensure stable ordering of Reward redeemer pointers.
Imports and utilities extended
pallas-validate/src/phase1/conway.rs
Adds imports (e.g., itertools::Itertools, Address, Network, StakePayload, Ordering) and supporting parsing/inspection logic; minor formatting adjustments.

Sequence Diagram(s)

sequenceDiagram
    participant Tx as Transaction
    participant Func as mk_plutus_script_redeemer_pointers
    participant Inputs as InputsAnalyzer
    participant Mint as MintAnalyzer
    participant Withdrawals as WithdrawalProcessor
    rect rgba(230,240,255,0.5)
    Tx->>Func: request redeemer pointers
    end

    Note over Func,Inputs: existing: analyze inputs
    Func->>Inputs: inspect inputs
    Inputs-->>Func: input redeemer pointers

    Note over Func,Mint: existing: analyze mint entries
    Func->>Mint: inspect mint entries
    Mint-->>Func: mint redeemer pointers

    rect rgba(240,255,240,0.5)
    Note over Func,Withdrawals: new: deterministic withdrawal processing
    Func->>Withdrawals: collect withdrawals
    Withdrawals->>Withdrawals: sort_reward_accounts (network, payload, hash)
    Withdrawals->>Withdrawals: parse stake address -> Address / StakePayload
    alt script-backed & matches phase‑2
        Withdrawals-->>Func: emit Reward redeemer pointer (RedeemersKey::Reward, index)
    else non-script or non-matching
        Withdrawals-->>Func: no pointer
    else invalid address
        Withdrawals-->>Func: unreachable (invalid reward addr)
    end
    end

    Func-->>Tx: return complete redeemer pointer set
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 I hop through bytes where reward addresses lie,
I sort by net and payload with a careful eye,
Script hashes found — a Reward pointer springs,
Stable order hums as the redeemer sings,
Conway's garden neat beneath the sky.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: adding withdrawals to the redeemers pointers check, which directly addresses the missing validation for Plutus scripts in withdrawals.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
Review rate limit: 0/1 reviews remaining, refill in 60 minutes.

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2f17967 and 34cb253.

📒 Files selected for processing (1)
  • pallas-validate/src/phase1/conway.rs (3 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
pallas-validate/src/phase1/conway.rs (3)
pallas-traverse/src/tx.rs (1)
  • withdrawals (476-492)
pallas-validate/src/phase1/babbage.rs (1)
  • is_phase_2_script (989-1004)
pallas-traverse/src/witnesses.rs (3)
  • plutus_v1_scripts (82-104)
  • plutus_v2_scripts (196-213)
  • plutus_v3_scripts (215-227)
🔇 Additional comments (1)
pallas-validate/src/phase1/conway.rs (1)

15-15: LGTM!

The Address import is necessary for parsing stake addresses in the withdrawal handling logic and is consistent with the existing import patterns.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 34cb253 and 455fb0b.

📒 Files selected for processing (1)
  • pallas-validate/src/phase1/conway.rs (3 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
pallas-validate/src/phase1/conway.rs (4)
pallas-traverse/src/tx.rs (1)
  • withdrawals (476-492)
pallas-validate/src/phase2/script_context.rs (1)
  • withdrawals (413-419)
pallas-validate/src/phase1/babbage.rs (1)
  • is_phase_2_script (989-1004)
pallas-traverse/src/witnesses.rs (3)
  • plutus_v1_scripts (82-104)
  • plutus_v2_scripts (196-213)
  • plutus_v3_scripts (215-227)
🔇 Additional comments (1)
pallas-validate/src/phase1/conway.rs (1)

15-15: LGTM!

The Address import is correctly added to support parsing stake address bytes in the withdrawals check.

Comment thread pallas-validate/src/phase1/conway.rs
scarmuega
scarmuega previously approved these changes Oct 23, 2025
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (1)
pallas-validate/src/phase1/conway.rs (1)

1153-1180: Refactor to eliminate code duplication.

This function is duplicated from pallas-validate/src/phase2/script_context.rs (lines 1081-1107). Consider extracting it to a shared module to maintain a single source of truth and avoid maintenance overhead.

For example, you could move this function to a common utilities module and import it in both places:

// In a shared utilities module
pub fn sort_reward_accounts(a: &Bytes, b: &Bytes) -> Ordering {
    // ... implementation
}

Then import in both phase1/conway.rs and phase2/script_context.rs.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a0b95a7 and fd4bbcf.

📒 Files selected for processing (1)
  • pallas-validate/src/phase1/conway.rs (5 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
pallas-validate/src/phase1/conway.rs (4)
pallas-validate/src/phase2/script_context.rs (3)
  • sort_reward_accounts (1082-1108)
  • network_tag (1086-1092)
  • withdrawals (413-419)
pallas-traverse/src/tx.rs (1)
  • withdrawals (476-492)
pallas-validate/src/phase1/babbage.rs (1)
  • is_phase_2_script (989-1004)
pallas-traverse/src/witnesses.rs (3)
  • plutus_v1_scripts (82-104)
  • plutus_v2_scripts (196-213)
  • plutus_v3_scripts (215-227)
🔇 Additional comments (2)
pallas-validate/src/phase1/conway.rs (2)

15-16: LGTM!

The new imports are necessary for withdrawal handling and deterministic sorting. All imports are used appropriately in the implementation.

Also applies to: 27-27


1225-1249: LGTM!

The withdrawals handling is correctly implemented:

  • Withdrawals are deterministically sorted before processing, ensuring stable redeemer pointer generation.
  • Invalid addresses are gracefully skipped using the if let Ok(...) pattern.
  • Script-backed stake addresses are correctly identified using is_script().
  • Phase-2 script verification ensures only Plutus scripts generate Reward redeemer pointers.

This properly addresses the "withdraw 0 trick" validation issue mentioned in the PR objectives.

@rodrigomd94 rodrigomd94 requested a review from scarmuega October 23, 2025 18:59
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fd4bbcf and 955073c.

📒 Files selected for processing (1)
  • pallas-validate/src/phase1/conway.rs (5 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
pallas-validate/src/phase1/conway.rs (4)
pallas-validate/src/phase2/script_context.rs (3)
  • sort_reward_accounts (1082-1108)
  • network_tag (1086-1092)
  • withdrawals (413-419)
pallas-traverse/src/tx.rs (1)
  • withdrawals (476-492)
pallas-validate/src/phase1/babbage.rs (1)
  • is_phase_2_script (989-1004)
pallas-traverse/src/witnesses.rs (3)
  • plutus_v1_scripts (82-104)
  • plutus_v2_scripts (196-213)
  • plutus_v3_scripts (215-227)
🔇 Additional comments (2)
pallas-validate/src/phase1/conway.rs (2)

15-18: LGTM!

The new imports support the withdrawals handling functionality. All are necessary for parsing stake addresses, sorting withdrawals, and creating reward redeemer pointers.

Also applies to: 29-29


1227-1251: Logic looks correct, but depends on fixing the sort function.

The withdrawals handling logic correctly:

  • Sorts withdrawals deterministically per Cardano ledger specifications
  • Parses stake addresses with appropriate error handling
  • Filters for script-backed stake withdrawals
  • Creates Reward redeemer pointers only for phase-2 Plutus scripts

This properly addresses the "withdraw 0 trick" mentioned in the PR objectives.

However, this code depends on sort_reward_accounts not panicking on invalid addresses. Please ensure the code duplication and error handling issues in sort_reward_accounts (lines 1155-1182) are addressed first, as the current .expect() calls will cause panics before this graceful error handling can execute.

Comment on lines +1155 to +1182
// Sorting function for reward accounts (withdrawals).
fn sort_reward_accounts(a: &Bytes, b: &Bytes) -> Ordering {
let addr_a = Address::from_bytes(a).expect("invalid reward address in withdrawals.");
let addr_b = Address::from_bytes(b).expect("invalid reward address in withdrawals.");

fn network_tag(network: Network) -> u8 {
match network {
Network::Testnet => 0,
Network::Mainnet => 1,
Network::Other(tag) => tag,
}
}

if let (Address::Stake(accnt_a), Address::Stake(accnt_b)) = (addr_a, addr_b) {
if accnt_a.network() != accnt_b.network() {
return network_tag(accnt_a.network()).cmp(&network_tag(accnt_b.network()));
}

match (accnt_a.payload(), accnt_b.payload()) {
(StakePayload::Script(..), StakePayload::Stake(..)) => Ordering::Less,
(StakePayload::Stake(..), StakePayload::Script(..)) => Ordering::Greater,
(StakePayload::Script(hash_a), StakePayload::Script(hash_b)) => hash_a.cmp(hash_b),
(StakePayload::Stake(hash_a), StakePayload::Stake(hash_b)) => hash_a.cmp(hash_b),
}
} else {
unreachable!("invalid reward address in withdrawals.");
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Eliminate code duplication and fix inconsistent error handling.

This function duplicates code from pallas-validate/src/phase2/script_context.rs (lines 1081-1107). Additionally, the use of .expect() creates inconsistent error handling: if withdrawals contain invalid addresses, the sort will panic before the graceful error handling in the loop at line 1233 can process them.

Consider these solutions:

Solution 1 (preferred): Import from phase2 module

If the phase2 module is accessible from phase1, import the function:

+use crate::phase2::script_context::sort_reward_accounts;
+
-// Sorting function for reward accounts (withdrawals).
-fn sort_reward_accounts(a: &Bytes, b: &Bytes) -> Ordering {
-    let addr_a = Address::from_bytes(a).expect("invalid reward address in withdrawals.");
-    let addr_b = Address::from_bytes(b).expect("invalid reward address in withdrawals.");
-
-    fn network_tag(network: Network) -> u8 {
-        match network {
-            Network::Testnet => 0,
-            Network::Mainnet => 1,
-            Network::Other(tag) => tag,
-        }
-    }
-
-    if let (Address::Stake(accnt_a), Address::Stake(accnt_b)) = (addr_a, addr_b) {
-        if accnt_a.network() != accnt_b.network() {
-            return network_tag(accnt_a.network()).cmp(&network_tag(accnt_b.network()));
-        }
-
-        match (accnt_a.payload(), accnt_b.payload()) {
-            (StakePayload::Script(..), StakePayload::Stake(..)) => Ordering::Less,
-            (StakePayload::Stake(..), StakePayload::Script(..)) => Ordering::Greater,
-            (StakePayload::Script(hash_a), StakePayload::Script(hash_b)) => hash_a.cmp(hash_b),
-            (StakePayload::Stake(hash_a), StakePayload::Stake(hash_b)) => hash_a.cmp(hash_b),
-        }
-    } else {
-        unreachable!("invalid reward address in withdrawals.");
-    }
-}
-

Solution 2: Move to shared utilities module

If phase1 cannot access phase2, move this function to a shared location (e.g., crate::utils) and import it from both modules.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In pallas-validate/src/phase1/conway.rs around lines 1155-1182, this sorting
function duplicates code from phase2 and uses .expect() which can panic; replace
the local function with a call to the canonical implementation from
pallas-validate/src/phase2/script_context.rs (preferred) or move the shared
sorting logic into a common crate::utils module and import it from both phase1
and phase2; ensure the shared function does not call .expect() but instead
returns a Result (or otherwise propagates/handles invalid-address errors) so
invalid withdrawal addresses are handled consistently by the existing error path
rather than panicking.

pgrange added a commit to pgrange/dolos that referenced this pull request Nov 4, 2025
Using the patched version of pallas until next releases of both
pallas and dolos.

See: txpipe/pallas#708
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
pallas-validate/src/phase1/conway.rs (1)

1155-1182: ⚠️ Potential issue | 🔴 Critical

Don't let malformed withdrawals panic phase-1 validation.

sort_reward_accounts still uses expect and unreachable! on untrusted reward-account bytes. A malformed or non-stake withdrawal will abort validation during sorting before check_redeemers can return a ValidationError; please parse/validate once and propagate PostAlonzo(InputDecoding) instead of panicking.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pallas-validate/src/phase1/conway.rs` around lines 1155 - 1182,
sort_reward_accounts currently calls Address::from_bytes(...).expect(...) and
uses unreachable!() which will panic on malformed or non-stake withdrawal bytes;
change it to return a Result<Ordering, ValidationError> (or propagate an
existing error type) and parse Address::from_bytes without expect, mapping any
decode error to PostAlonzo(InputDecoding), and similarly handle the non-stake
branch by returning the same ValidationError instead of unreachable!; update all
call sites of sort_reward_accounts to propagate or handle the Result and ensure
sorting returns a ValidationError when decoding fails rather than panicking.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@pallas-validate/src/phase1/conway.rs`:
- Around line 1234-1242: The code currently passes reference_scripts (which may
include native-script hashes) into is_phase_2_script causing native reference
scripts to be treated like Plutus and emit a Reward pointer; update the call
site around stake_addr.is_script() so you pass only Plutus reference hashes
(e.g., filter reference_scripts to include only hashes present in
plutus_v1_scripts || plutus_v2_scripts || plutus_v3_scripts, or derive a
pluts_only_reference_hashes collection from get_reference_script_hashes) before
calling is_phase_2_script(stake_hash, plutus_v1_scripts, plutus_v2_scripts,
plutus_v3_scripts, pluts_only_reference_hashes), or alternatively modify
is_phase_2_script to distinguish native vs Plutus refs internally.

---

Duplicate comments:
In `@pallas-validate/src/phase1/conway.rs`:
- Around line 1155-1182: sort_reward_accounts currently calls
Address::from_bytes(...).expect(...) and uses unreachable!() which will panic on
malformed or non-stake withdrawal bytes; change it to return a Result<Ordering,
ValidationError> (or propagate an existing error type) and parse
Address::from_bytes without expect, mapping any decode error to
PostAlonzo(InputDecoding), and similarly handle the non-stake branch by
returning the same ValidationError instead of unreachable!; update all call
sites of sort_reward_accounts to propagate or handle the Result and ensure
sorting returns a ValidationError when decoding fails rather than panicking.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: cd8a7ad7-8245-461b-9453-35b056cf68ee

📥 Commits

Reviewing files that changed from the base of the PR and between 955073c and afe10b2.

📒 Files selected for processing (1)
  • pallas-validate/src/phase1/conway.rs

Comment on lines +1234 to +1242
if stake_addr.is_script() {
let script_hash = stake_addr.payload().as_hash();
if is_phase_2_script(
&script_hash,
plutus_v1_scripts,
plutus_v2_scripts,
plutus_v3_scripts,
reference_scripts,
) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Filter reference scripts to Plutus before adding Reward pointers.

is_phase_2_script treats any hash present in reference_scripts as redeemer-requiring, but get_reference_script_hashes also collects native reference scripts. For a withdrawal authorized by a native script supplied through a reference input, this branch will emit a Reward pointer and phase-1 will incorrectly fail with RedeemerMissing. Pass only Plutus reference hashes here, or make the helper distinguish native vs Plutus refs.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pallas-validate/src/phase1/conway.rs` around lines 1234 - 1242, The code
currently passes reference_scripts (which may include native-script hashes) into
is_phase_2_script causing native reference scripts to be treated like Plutus and
emit a Reward pointer; update the call site around stake_addr.is_script() so you
pass only Plutus reference hashes (e.g., filter reference_scripts to include
only hashes present in plutus_v1_scripts || plutus_v2_scripts ||
plutus_v3_scripts, or derive a pluts_only_reference_hashes collection from
get_reference_script_hashes) before calling is_phase_2_script(stake_hash,
plutus_v1_scripts, plutus_v2_scripts, plutus_v3_scripts,
pluts_only_reference_hashes), or alternatively modify is_phase_2_script to
distinguish native vs Plutus refs internally.

@scarmuega
Copy link
Copy Markdown
Member

Superseded by #761, which keeps the withdrawal redeemer-pointer logic and sorting but replaces the .expect() / unreachable!() calls in sort_reward_accounts with PostAlonzo(InputDecoding) propagation, so malformed reward accounts become a validation error rather than a node-level panic. Closing in favor of that PR.

@scarmuega scarmuega closed this May 8, 2026
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.

2 participants