hydra-cluster
Safe HaskellSafe-Inferred
LanguageGHC2021

Hydra.Cluster.SecurityScenarios

Description

End-to-end regression tests for the deposit-stealing class of bugs.

Synopsis

Standalone Claim with no head input

cannotStealDepositWithoutHeadInput :: Tracer IO EndToEndLog -> FilePath -> ChainBackendOptions -> [TxId] -> IO () Source #

Regression test for the standalone-Claim deposit-stealing attack.

A third party submits a standalone tx that spends a pending deposit UTxO with the Claim redeemer, redirecting the value to their own address. The tx has no Head input at all, so the deposit validator's Claim branch fails on list.find with DepositHeadInputNotFound (D06).

Counterfeit Head state-token

cannotStealDepositWithCounterfeitHeadToken :: Tracer IO EndToEndLog -> FilePath -> ChainBackendOptions -> [TxId] -> IO () Source #

The attacker mints a counterfeit token of asset name HydraHeadV2 under a permissive minting policy they control, locks it in their own wallet, and uses that UTxO as bait alongside a victim deposit. The deposit validator's Claim branch searches the inputs for a UTxO that carries the real head's policy id; the counterfeit token's policy id does not match, so the validator fails with DepositHeadInputNotFound (D06).

Helpers

placeVictimDeposit :: Tracer IO EndToEndLog -> ChainBackendOptions -> HydraClient -> Timing -> Integer -> IO (VerificationKey PaymentKey, TxId) Source #

Place a deposit of amount lovelace from a fresh wallet into the head managed by n1. Returns the wallet's verification key (so its balance can be queried later) and the deposit transaction id.

data AttemptArgs Source #

Encapsulates the moving parts of an attempted-steal transaction.

Constructors

AttemptArgs 

Fields

  • tracer :: Tracer IO EndToEndLog
     
  • opts :: ChainBackendOptions
     
  • depositTxIn :: TxIn

    Diagnostic / naming hook for the "primary" deposit being attacked.

  • depositTxOuts :: [(TxIn, TxOut CtxUTxO)]

    Deposit-script UTxOs being attacked. The first entry is wired up as a Claim-witnessed input by the helper; additional deposit inputs (for amplification cases) must be passed through $sel:extraInputs:AttemptArgs with their own deposit-script witness. TODO: This may not need to be alist, and should be more simply a UTxO type.

  • extraInputs :: [(TxIn, BuildTxWith BuildTx (Witness WitCtxTxIn))]

    Additional non-deposit-script inputs (key witnesses or extra deposit script witnesses for amplification cases).

  • collateralIn :: TxIn
     
  • extraOutputs :: [TxOut CtxTx]
     
  • attackerSk :: SigningKey PaymentKey
     
  • attackerAddr :: AddressInEra
     
  • spendable :: UTxO
     
  • expectedTrace :: String

    Plutus error trace (e.g. D06) expected on the autobalancer's Left. The build error must contain this trace; any other Left (insufficient ada for fees, integrity-hash mismatch, era hiccup, ...) is treated as a test failure rather than success.

attemptStealAndAssertRejected :: AttemptArgs -> IO () Source #

Build a steal transaction and assert the autobalancer rejects it specifically because the deposit validator failed with expectedTrace. Any other Left (insufficient ada, integrity hash mismatch, era issue, ...) is treated as a test failure.

requireSingletonUTxO :: HasCallStack => String -> UTxO -> IO (TxIn, TxOut CtxUTxO) Source #

assertDepositStillLocked :: ChainBackendOptions -> TxId -> Value -> IO () Source #

assertWalletLovelace :: ChainBackendOptions -> VerificationKey PaymentKey -> Integer -> IO () Source #

Multi-deposit redirection during a real Increment

cannotRedirectExtraDepositDuringIncrement :: Tracer IO EndToEndLog -> FilePath -> ChainBackendOptions -> [TxId] -> IO () Source #

A malicious participant of a head constructs an Increment-shaped transaction that consumes the head's ST + the redeemer-specified deposit + ANOTHER pending deposit for the same head, and routes the extra deposit's value to attacker-controlled outputs.

Setup: 1-party head (Alice). Two unrelated depositors (V1 and V2) each deposit funds for Alice's head. Alice (the malicious participant) constructs an Increment that claims V1's deposit (which is in the snapshot Alice signs) but ALSO consumes V2's deposit and routes its value to a Kate address. V2's deposit rejects a Claim that the head's Increment redeemer does not name (DepositNotClaimedByHead, D09).

That is the only rule rejecting this: the head validator grows by exactly the deposit its redeemer names, so V2's value escaping to Kate leaves mustPreserveValue satisfied.

Increment-redirection helpers

placeVictimDepositNoWait :: Tracer IO EndToEndLog -> ChainBackendOptions -> HydraClient -> Integer -> IO (VerificationKey PaymentKey, TxId) Source #

Like placeVictimDeposit but does not wait for the hydra-node to emit CommitRecorded. The caller is expected to exit the withHydraNode bracket promptly so the node cannot submit its own honest Increment for the deposit.

waitForOnChainUTxO :: ChainBackendOptions -> TxIn -> IO UTxO Source #

Poll the chain for a UTxO at the given TxIn until it appears. Times out after ~30 seconds.

findHeadContinuationUTxO :: ChainBackendOptions -> NetworkId -> HeadId -> IO (TxIn, TxOut CtxUTxO, OpenDatum) Source #

Locate the head's continuation UTxO at v_head address, identified by its state token policy id matching the head id.

readDepositCommits :: TxOut CtxUTxO -> IO [Commit] Source #

Read the [Commit] list from a deposit-script UTxO's inline datum.

Close-with-deposit absorption

cannotAbsorbDepositDuringClose :: Tracer IO EndToEndLog -> FilePath -> ChainBackendOptions -> [TxId] -> IO () Source #

A malicious head participant closes the head with a transaction that ALSO spends a foreign deposit input, padding the closed-state continuation's value with the deposit's value (mustPreserveHeadValue uses geq, so head_out > head_in is allowed). After the contestation deadline they fanout the absorbed value to their own address. The deposit validator's head_output_is_open check requires the head's continuation datum to be the Open state, so Close Contest Fanout transitions cannot absorb deposits and the tx is rejected with HeadOutputNotOpen (D06).

The test uses the simplest of the close redeemers, CloseInitial, which doesn't require an off-chain snapshot signature: just snapshotNumber = 0 and utxoHash = the open's initialUtxoHash.

Leader self-deposit cover redirection

cannotStealLargerDepositDuringOwnIncrement :: Tracer IO EndToEndLog -> FilePath -> ChainBackendOptions -> [TxId] -> IO () Source #

The malicious snapshot leader places their own SMALL deposit alongside a much LARGER unrelated victim deposit, then constructs an Increment that claims their own small deposit (which is in the snapshot they sign) but ALSO consumes the victim's larger deposit and routes its value to a leader-controlled wallet ("their own pocket") rather than to the head.

Compared to cannotRedirectExtraDepositDuringIncrement (two unrelated depositors, redirection to a third party), this captures the threat model where the leader benefits *directly* and uses their own honest deposit as cover ("I was just incrementing my deposit"). The asymmetric amounts (small honest, large stolen) make the economic motive explicit.

The on-chain defense is the same as there: the victim's deposit rejects its own Claim because the head's Increment redeemer does not name it (DepositNotClaimedByHead, D09).

TODO: This could move into the MutationSpec or the TraceSpec.