| Safe Haskell | Safe-Inferred |
|---|---|
| Language | GHC2021 |
Hydra.Cluster.SecurityScenarios
Description
End-to-end regression tests for the deposit-stealing class of bugs.
Synopsis
- cannotStealDepositWithoutHeadInput :: Tracer IO EndToEndLog -> FilePath -> ChainBackendOptions -> [TxId] -> IO ()
- cannotStealDepositWithCounterfeitHeadToken :: Tracer IO EndToEndLog -> FilePath -> ChainBackendOptions -> [TxId] -> IO ()
- placeVictimDeposit :: Tracer IO EndToEndLog -> ChainBackendOptions -> HydraClient -> Timing -> Integer -> IO (VerificationKey PaymentKey, TxId)
- data AttemptArgs = AttemptArgs {
- tracer :: Tracer IO EndToEndLog
- opts :: ChainBackendOptions
- depositTxIn :: TxIn
- depositTxOuts :: [(TxIn, TxOut CtxUTxO)]
- extraInputs :: [(TxIn, BuildTxWith BuildTx (Witness WitCtxTxIn))]
- collateralIn :: TxIn
- extraOutputs :: [TxOut CtxTx]
- attackerSk :: SigningKey PaymentKey
- attackerAddr :: AddressInEra
- spendable :: UTxO
- expectedTrace :: String
- attemptStealAndAssertRejected :: AttemptArgs -> IO ()
- requireSingletonUTxO :: HasCallStack => String -> UTxO -> IO (TxIn, TxOut CtxUTxO)
- assertDepositStillLocked :: ChainBackendOptions -> TxId -> Value -> IO ()
- assertWalletLovelace :: ChainBackendOptions -> VerificationKey PaymentKey -> Integer -> IO ()
- cannotRedirectExtraDepositDuringIncrement :: Tracer IO EndToEndLog -> FilePath -> ChainBackendOptions -> [TxId] -> IO ()
- placeVictimDepositNoWait :: Tracer IO EndToEndLog -> ChainBackendOptions -> HydraClient -> Integer -> IO (VerificationKey PaymentKey, TxId)
- waitForOnChainUTxO :: ChainBackendOptions -> TxIn -> IO UTxO
- findHeadContinuationUTxO :: ChainBackendOptions -> NetworkId -> HeadId -> IO (TxIn, TxOut CtxUTxO, OpenDatum)
- readDepositCommits :: TxOut CtxUTxO -> IO [Commit]
- cannotAbsorbDepositDuringClose :: Tracer IO EndToEndLog -> FilePath -> ChainBackendOptions -> [TxId] -> IO ()
- cannotStealLargerDepositDuringOwnIncrement :: Tracer IO EndToEndLog -> FilePath -> ChainBackendOptions -> [TxId] -> IO ()
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
| |
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.