Skip to content

Commit

Permalink
PLT-671 implement MustBeSignedBy for cardano TX (#832)
Browse files Browse the repository at this point in the history
* Add MustBeSignedBy to TX.Constraints

* add Tx.Constraint tests
  • Loading branch information
berewt authored Nov 24, 2022
1 parent bb3409d commit 7ac8444
Show file tree
Hide file tree
Showing 9 changed files with 159 additions and 76 deletions.
3 changes: 1 addition & 2 deletions plutus-contract/src/Plutus/Contract/Test.hs
Original file line number Diff line number Diff line change
Expand Up @@ -342,8 +342,7 @@ endpointAvailable contract inst = TracePredicate $
tell @(Doc Void) ("missing endpoint:" <+> fromString (symbolVal (Proxy :: Proxy l)))
pure False

tx
:: forall w s e a.
tx :: forall w s e a.
( Monoid w
)
=> Contract w s e a
Expand Down
30 changes: 15 additions & 15 deletions plutus-contract/src/Plutus/Contract/Wallet.hs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ import Control.Monad.Freer.Error (Error, throwError)
import Data.Aeson (FromJSON (parseJSON), Object, ToJSON (toJSON), Value (String), object, withObject, (.:), (.=))
import Data.Aeson.Extras qualified as JSON
import Data.Aeson.Types (Parser, parseFail)
import Data.Bifunctor (first)
import Data.Bifunctor (Bifunctor (bimap), first)
import Data.Map (Map)
import Data.Map qualified as Map
import Data.Maybe (mapMaybe)
Expand All @@ -47,9 +47,7 @@ import Ledger (DCert, Redeemer, StakingCredential, txRedeemers)
import Ledger qualified (ScriptPurpose (..))
import Ledger qualified as P
import Ledger.Ada qualified as Ada
import Ledger.Constraints (mustPayToAddress)
import Ledger.Constraints.OffChain (UnbalancedTx (unBalancedTxRequiredSignatories, unBalancedTxUtxoIndex),
unBalancedTxTx)
import Ledger.Constraints (UnbalancedTx (UnbalancedCardanoTx, UnbalancedEmulatorTx), mustPayToAddress)
import Ledger.Tx (CardanoTx, TxId (TxId), TxIn (..), TxOutRef, getCardanoTxInputs, txInRef)
import Ledger.Validation (CardanoLedgerError, fromPlutusIndex, makeTransactionBody)
import Ledger.Value (currencyMPSHash)
Expand Down Expand Up @@ -253,19 +251,20 @@ export
:: P.Params
-> UnbalancedTx
-> Either CardanoLedgerError ExportTx
export params utx =
let requiredSigners = Set.toList (unBalancedTxRequiredSignatories utx)
fromCardanoTx ctx = do
utxo <- fromPlutusIndex $ P.UtxoIndex (unBalancedTxUtxoIndex utx)
export params (UnbalancedEmulatorTx tx sigs utxos) =
let requiredSigners = Set.toList sigs
in ExportTx
<$> bimap Right (C.makeSignedTransaction []) (CardanoAPI.toCardanoTxBody params requiredSigners tx)
<*> first Right (mkInputs (P.pNetworkId params) utxos)
<*> pure (mkRedeemers tx)
export params (UnbalancedCardanoTx tx utxos) =
let fromCardanoTx ctx = do
utxo <- fromPlutusIndex $ P.UtxoIndex utxos
makeTransactionBody params utxo ctx
in ExportTx
<$> fmap (C.makeSignedTransaction [])
(either
fromCardanoTx
(first Right . CardanoAPI.toCardanoTxBody params requiredSigners)
(unBalancedTxTx utx))
<*> first Right (mkInputs (P.pNetworkId params) (unBalancedTxUtxoIndex utx))
<*> either (const $ Right []) (Right . mkRedeemers) (unBalancedTxTx utx)
<$> fmap (C.makeSignedTransaction []) (fromCardanoTx tx)
<*> first Right (mkInputs (P.pNetworkId params) utxos)
<*> pure []

mkInputs :: C.NetworkId -> Map Plutus.TxOutRef P.TxOut -> Either CardanoAPI.ToCardanoError [ExportTxInput]
mkInputs networkId = traverse (uncurry (toExportTxInput networkId)) . Map.toList
Expand All @@ -283,6 +282,7 @@ toExportTxInput networkId Plutus.TxOutRef{Plutus.txOutRefId, Plutus.txOutRefIdx}
<*> pure otherQuantities

-- TODO: Here there's hidden error of script DCert missing its redeemer - this just counts as no DCert. Don't know if bad.
-- TODO: Refactor with getGardanoTxRedeemers once we are ceady to move to Cardano Txs
mkRedeemers :: P.Tx -> [ExportTxRedeemer]
mkRedeemers = map (uncurry scriptPurposeToExportRedeemer) . Map.assocs . txRedeemers

Expand Down
6 changes: 3 additions & 3 deletions plutus-contract/src/Wallet/Emulator/Wallet.hs
Original file line number Diff line number Diff line change
Expand Up @@ -318,15 +318,15 @@ handleBalance ::
handleBalance utx = do
utxo <- get >>= ownOutputs
params@Params { pNetworkId } <- WAPI.getClientParams
let requiredSigners = Set.toList (U.unBalancedTxRequiredSignatories utx)
eitherTx = U.unBalancedTxTx utx
let eitherTx = U.unBalancedTxTx utx
plUtxo = traverse (Tx.toTxOut pNetworkId) utxo
mappedUtxo <- either (throwError . WAPI.ToCardanoError) pure plUtxo
cUtxoIndex <- handleError eitherTx $ fromPlutusIndex $ UtxoIndex $ U.unBalancedTxUtxoIndex utx <> mappedUtxo
case eitherTx of
Right _ -> do
-- Find the fixed point of fee calculation, trying maximally n times to prevent an infinite loop
let calcFee n fee = do
let requiredSigners = Set.toList (U.unBalancedTxRequiredSignatories utx)
calcFee n fee = do
tx <- handleBalanceTx utxo (utx & U.tx . Ledger.fee .~ fee)
newFee <- handleError (Right tx) $ estimateTransactionFee params cUtxoIndex requiredSigners tx
if newFee /= fee
Expand Down
154 changes: 112 additions & 42 deletions plutus-contract/test/Spec/Contract/Tx/Constraints/RequiredSigner.hs
Original file line number Diff line number Diff line change
Expand Up @@ -11,22 +11,26 @@ import Control.Monad (void)
import Data.Void (Void)
import Test.Tasty (TestTree, testGroup)

import Data.Default (Default (def))
import Data.Map as M
import Data.Maybe (fromJust)
import Data.String (fromString)
import Data.Text qualified as Text
import Ledger qualified
import Ledger.Ada qualified as Ada
import Ledger.CardanoWallet as CW
import Ledger.CardanoWallet qualified as CW
import Ledger.Constraints.OffChain qualified as Constraints hiding (requiredSignatories)
import Ledger.Constraints.OnChain.V1 qualified as Constraints
import Ledger.Constraints.OnChain.V2 qualified as Constraints
import Ledger.Constraints.TxConstraints qualified as Constraints
import Ledger.Tx qualified as Tx
import Ledger.Typed.Scripts qualified as Scripts
import Ledger.Tx.Constraints qualified as TxCons
import Plutus.Contract as Con
import Plutus.Contract.Test (assertFailedTransaction, assertValidatedTransactionCount, checkPredicateOptions,
defaultCheckOptions, mockWalletPaymentPubKey, mockWalletPaymentPubKeyHash, w1, w2)
import Plutus.Contract.Test (assertFailedTransaction, assertValidatedTransactionCount, changeInitialWalletValue,
checkPredicateOptions, defaultCheckOptions, mockWalletPaymentPubKeyHash, w1, w2)
import Plutus.Script.Utils.V2.Typed.Scripts as PSU.V2
import Plutus.Script.Utils.V2.Typed.Scripts qualified as Scripts
import Plutus.Trace qualified as Trace
import Plutus.V1.Ledger.Scripts (ScriptError (EvaluationError), unitDatum)
import Plutus.V2.Ledger.Api qualified as PV2
import PlutusTx qualified
import Prelude
import Wallet.Emulator.Wallet (signPrivateKeys, walletToMockWallet)
Expand All @@ -40,10 +44,19 @@ tests =
, otherWalletNoSigningProcess
, phase2FailureMustBeSignedBy
, withoutOffChainMustBeSignedBy
-- When we'll have enough constraints, reuse the previous tests
, cardanoTxOwnWallet
, cardanoTxOtherWalletNoSigningProcess
]

mustBeSignedByContract :: Ledger.PaymentPubKey -> Ledger.PaymentPubKeyHash -> Contract () Empty ContractError ()
mustBeSignedByContract pk pkh = do
w1PubKey :: Ledger.PaymentPubKeyHash
w1PubKey = mockWalletPaymentPubKeyHash w1

w2PubKey :: Ledger.PaymentPubKeyHash
w2PubKey = mockWalletPaymentPubKeyHash w2

mustBeSignedByContract :: Ledger.PaymentPubKeyHash -> Ledger.PaymentPubKeyHash -> Contract () Empty ContractError ()
mustBeSignedByContract paymentPubKey signedPubKey = do
let lookups1 = Constraints.typedValidatorLookups mustBeSignedByTypedValidator
tx1 = Constraints.mustPayToTheScriptWithDatumInTx
()
Expand All @@ -55,17 +68,17 @@ mustBeSignedByContract pk pkh = do
let lookups2 =
Constraints.typedValidatorLookups mustBeSignedByTypedValidator
<> Constraints.unspentOutputs utxos
<> Constraints.paymentPubKey pk
<> Constraints.paymentPubKeyHash paymentPubKey
tx2 =
Constraints.collectFromTheScript utxos pkh
Constraints.collectFromTheScript utxos signedPubKey
<> Constraints.mustIncludeDatumInTx unitDatum
<> Constraints.mustBeSignedBy pkh
<> Constraints.mustBeSignedBy signedPubKey
logInfo @String $ "Required Signatories: " ++ show (Constraints.requiredSignatories tx2)
ledgerTx2 <- submitTxConstraintsWith @UnitTest lookups2 tx2
awaitTxConfirmed $ Tx.getCardanoTxId ledgerTx2

withoutOffChainMustBeSignedByContract :: Ledger.PaymentPubKey -> Ledger.PaymentPubKeyHash -> Contract () Empty ContractError ()
withoutOffChainMustBeSignedByContract pk pkh = do
withoutOffChainMustBeSignedByContract :: Ledger.PaymentPubKeyHash -> Ledger.PaymentPubKeyHash -> Contract () Empty ContractError ()
withoutOffChainMustBeSignedByContract paymentPubKey signedPubKey = do
let lookups1 = Constraints.typedValidatorLookups mustBeSignedByTypedValidator
tx1 = Constraints.mustPayToTheScriptWithDatumInTx
()
Expand All @@ -77,62 +90,52 @@ withoutOffChainMustBeSignedByContract pk pkh = do
let lookups2 =
Constraints.typedValidatorLookups mustBeSignedByTypedValidator
<> Constraints.unspentOutputs utxos
<> Constraints.paymentPubKey pk
<> Constraints.paymentPubKeyHash paymentPubKey
tx2 =
Constraints.collectFromTheScript utxos pkh
Constraints.collectFromTheScript utxos signedPubKey
<> Constraints.mustIncludeDatumInTx unitDatum
logInfo @String $ "Required Signatories: " ++ show (Constraints.requiredSignatories tx2)
ledgerTx2 <- submitTxConstraintsWith @UnitTest lookups2 tx2
awaitTxConfirmed $ Tx.getCardanoTxId ledgerTx2

ownWallet :: TestTree
ownWallet =
let pk = mockWalletPaymentPubKey w1
pkh = mockWalletPaymentPubKeyHash w1
trace = do
void $ Trace.activateContractWallet w1 $ mustBeSignedByContract pk pkh
void $ Trace.waitNSlots 1
let trace = do
void $ Trace.activateContractWallet w1 $ mustBeSignedByContract w1PubKey w1PubKey
void Trace.nextSlot
in checkPredicateOptions defaultCheckOptions "own wallet's signature passes on-chain mustBeSignedBy validation" (assertValidatedTransactionCount 2) (void trace)

otherWallet :: TestTree -- must use Trace.setSigningProcess for w2
otherWallet =
let pk = mockWalletPaymentPubKey w2
pkh = mockWalletPaymentPubKeyHash w2
trace = do
Trace.setSigningProcess w1 (Just $ signPrivateKeys [paymentPrivateKey $ fromJust $ walletToMockWallet w1, paymentPrivateKey $ fromJust $ walletToMockWallet w2])
void $ Trace.activateContractWallet w1 $ mustBeSignedByContract pk pkh
void $ Trace.waitNSlots 1
let trace = do
Trace.setSigningProcess w1 (Just $ signPrivateKeys [CW.paymentPrivateKey $ fromJust $ walletToMockWallet w1, CW.paymentPrivateKey $ fromJust $ walletToMockWallet w2])
void $ Trace.activateContractWallet w1 $ mustBeSignedByContract w2PubKey w2PubKey
void Trace.nextSlot
in checkPredicateOptions defaultCheckOptions "other wallet's signature passes on-chain mustBeSignedBy validation" (assertValidatedTransactionCount 2) (void trace)

otherWalletNoSigningProcess :: TestTree
otherWalletNoSigningProcess =
let pk = mockWalletPaymentPubKey w2
pkh = mockWalletPaymentPubKeyHash w2
trace = do
void $ Trace.activateContractWallet w1 $ mustBeSignedByContract pk pkh
void $ Trace.waitNSlots 1
let trace = do
void $ Trace.activateContractWallet w1 $ mustBeSignedByContract w2PubKey w2PubKey
void Trace.nextSlot
in checkPredicateOptions defaultCheckOptions "without Trace.setSigningProcess fails phase-1 validation"
(assertFailedTransaction (\_ err -> case err of {Ledger.CardanoLedgerValidationError msg -> Text.isInfixOf "MissingRequiredSigners" msg; _ -> False }))
(void trace)

withoutOffChainMustBeSignedBy :: TestTree -- there's no "required signer" in the txbody logs but still passes phase-2 so it must be there. Raised https://github.com/input-output-hk/plutus-apps/issues/645. It'd be good to check log output for expected required signer pubkey in these tests.
withoutOffChainMustBeSignedBy =
let pk = mockWalletPaymentPubKey w1
pkh = mockWalletPaymentPubKeyHash w1
trace = do
void $ Trace.activateContractWallet w1 $ withoutOffChainMustBeSignedByContract pk pkh
void $ Trace.waitNSlots 1
let trace = do
void $ Trace.activateContractWallet w1 $ withoutOffChainMustBeSignedByContract w1PubKey w1PubKey
void Trace.nextSlot
in checkPredicateOptions defaultCheckOptions "without mustBeSignedBy off-chain constraint required signer is not included in txbody so phase-2 validation fails"
(assertFailedTransaction (\_ err -> case err of {Ledger.ScriptFailure (EvaluationError ("L4":_) _) -> True; _ -> False }))
(void trace)

phase2FailureMustBeSignedBy :: TestTree
phase2FailureMustBeSignedBy =
let pk = mockWalletPaymentPubKey w1
pkh = Ledger.PaymentPubKeyHash $ fromString "76aaef06f38cc98ed08ceb168ddb55bab2ea5df43a6847a99f086fc9" :: Ledger.PaymentPubKeyHash
trace = do
void $ Trace.activateContractWallet w1 $ withoutOffChainMustBeSignedByContract pk pkh
void $ Trace.waitNSlots 1
let trace = do
void $ Trace.activateContractWallet w1 $ withoutOffChainMustBeSignedByContract w1PubKey w2PubKey
void Trace.nextSlot
in checkPredicateOptions defaultCheckOptions "with wrong pubkey fails on-chain mustBeSignedBy constraint validation"
(assertFailedTransaction (\_ err -> case err of {Ledger.ScriptFailure (EvaluationError ("L4":_) _) -> True; _ -> False }))
(void trace)
Expand All @@ -147,7 +150,7 @@ instance Scripts.ValidatorTypes UnitTest where
type instance RedeemerType UnitTest = Ledger.PaymentPubKeyHash

{-# INLINEABLE mustBeSignedByValidator #-}
mustBeSignedByValidator :: () -> Ledger.PaymentPubKeyHash -> Ledger.ScriptContext -> Bool
mustBeSignedByValidator :: () -> Ledger.PaymentPubKeyHash -> PV2.ScriptContext -> Bool
mustBeSignedByValidator _ pkh = Constraints.checkScriptContext @Void @Void (Constraints.mustBeSignedBy pkh)

mustBeSignedByTypedValidator :: Scripts.TypedValidator UnitTest
Expand All @@ -156,3 +159,70 @@ mustBeSignedByTypedValidator = Scripts.mkTypedValidator @UnitTest
$$(PlutusTx.compile [|| wrap ||])
where
wrap = Scripts.mkUntypedValidator


-- plutus-tx-constraints tests
-- all below to be covered by the above tests when the corresponding constraints will be implemented
-- for CardanoTx


cardanoTxOwnWalletContract
:: Ledger.PaymentPubKeyHash
-> Ledger.PaymentPubKeyHash
-> Contract () EmptySchema ContractError ()
cardanoTxOwnWalletContract paymentPubKey signedPubKey = do
let mkTx lookups constraints = either (error . show) id $ TxCons.mkTx @UnitTest def lookups constraints

utxos <- ownUtxos
let get3 (a:b:c:_) = (a, b, c)
get3 _ = error "Spec.Contract.TxConstraints.get3: not enough inputs"
((utxoRef, utxo), (utxoRefForBalance1, _), (utxoRefForBalance2, _)) = get3 $ M.toList utxos
lookups1 = Constraints.typedValidatorLookups mustBeSignedByTypedValidator
<> Constraints.unspentOutputs utxos
tx1 = Constraints.mustPayToTheScriptWithDatumInTx
()
(Ada.lovelaceValueOf 25_000_000)
<> Constraints.mustSpendPubKeyOutput utxoRefForBalance1
<> Constraints.mustUseOutputAsCollateral utxoRefForBalance1
ledgerTx1 <- submitTxConstraintsWith lookups1 tx1
awaitTxConfirmed $ Tx.getCardanoTxId ledgerTx1

-- Trying to unlock the Ada in the script address
scriptUtxos <- utxosAt (Ledger.scriptHashAddress $ Scripts.validatorHash mustBeSignedByTypedValidator)
utxos' <- ownUtxos
let lookups2 =
Constraints.typedValidatorLookups mustBeSignedByTypedValidator
<> Constraints.unspentOutputs (M.singleton utxoRef utxo <> scriptUtxos <> utxos')
<> Constraints.paymentPubKeyHash paymentPubKey
tx2 =
Constraints.collectFromTheScript utxos signedPubKey
<> Constraints.mustIncludeDatumInTx unitDatum
<> Constraints.mustUseOutputAsCollateral utxoRefForBalance2
<> Constraints.mustSpendPubKeyOutput utxoRefForBalance2
<> Constraints.mustBeSignedBy signedPubKey
logInfo @String $ "Required Signatories: " ++ show (Constraints.requiredSignatories tx2)
submitTxConfirmed $ mkTx lookups2 tx2

cardanoTxOwnWallet :: TestTree
cardanoTxOwnWallet =
let trace = do
void $ Trace.activateContractWallet w1 $ cardanoTxOwnWalletContract w1PubKey w1PubKey
void Trace.nextSlot
in checkPredicateOptions
(changeInitialWalletValue w1 (const $ Ada.adaValueOf 1000) defaultCheckOptions)
"own wallet's signature passes on-chain mustBeSignedBy validation with cardano tx"
(assertValidatedTransactionCount 2)
(void trace)

cardanoTxOtherWalletNoSigningProcess :: TestTree
cardanoTxOtherWalletNoSigningProcess =
let trace = do
void $ Trace.activateContractWallet w1 $ cardanoTxOwnWalletContract w2PubKey w2PubKey
void Trace.nextSlot
in checkPredicateOptions
-- Needed to manually balance the transaction
-- We may remove it once PLT-321
(changeInitialWalletValue w1 (const $ Ada.adaValueOf 1000) defaultCheckOptions)
"without Trace.setSigningProcess fails phase-1 validation"
(assertFailedTransaction (\_ err -> case err of {Ledger.CardanoLedgerValidationError msg -> Text.isInfixOf "MissingRequiredSigners" msg; _ -> False }))
(void trace)
20 changes: 10 additions & 10 deletions plutus-ledger-constraints/src/Ledger/Constraints/OffChain.hs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ module Ledger.Constraints.OffChain(
, ownPaymentPubKeyHash
, ownStakingCredential
, paymentPubKey
, paymentPubKeyHash
-- * Constraints resolution
, SomeLookupsAndConstraints(..)
, UnbalancedTx(..)
Expand Down Expand Up @@ -249,7 +250,12 @@ otherData dt =
-- | A script lookups value with a payment public key
paymentPubKey :: PaymentPubKey -> ScriptLookups a
paymentPubKey (PaymentPubKey pk) =
mempty { slPaymentPubKeyHashes = Set.singleton (PaymentPubKeyHash $ pubKeyHash pk) }
paymentPubKeyHash (PaymentPubKeyHash $ pubKeyHash pk)

-- | A script lookups value with a payment public key
paymentPubKeyHash :: PaymentPubKeyHash -> ScriptLookups a
paymentPubKeyHash pkh =
mempty { slPaymentPubKeyHashes = Set.singleton pkh }

-- | A script lookups value with a payment public key hash.
--
Expand Down Expand Up @@ -291,13 +297,8 @@ data UnbalancedTx
-- Simply refers to 'slTxOutputs' of 'ScriptLookups'.
}
| UnbalancedCardanoTx
{ unBalancedCardanoBuildTx :: C.CardanoBuildTx
, unBalancedTxRequiredSignatories :: Set PaymentPubKeyHash
-- ^ These are all the payment public keys that should be used to request the
-- signatories from the user's wallet. The signatories are what is required to
-- sign the transaction before submitting it to the blockchain. Transaction
-- validation will fail if the transaction is not signed by the required wallet.
, unBalancedTxUtxoIndex :: Map TxOutRef TxOut
{ unBalancedCardanoBuildTx :: C.CardanoBuildTx
, unBalancedTxUtxoIndex :: Map TxOutRef TxOut
-- ^ Utxo lookups that are used for adding inputs to the 'UnbalancedTx'.
-- Simply refers to 'slTxOutputs' of 'ScriptLookups'.
}
Expand Down Expand Up @@ -325,10 +326,9 @@ instance Pretty UnbalancedTx where
, hang 2 $ vsep $ "Requires signatures:" : (pretty <$> Set.toList rs)
, hang 2 $ vsep $ "Utxo index:" : (pretty <$> Map.toList utxo)
]
pretty (UnbalancedCardanoTx utx rs utxo) =
pretty (UnbalancedCardanoTx utx utxo) =
vsep
[ hang 2 $ vsep ["Tx (cardano-api Representation):", pretty utx]
, hang 2 $ vsep $ "Requires signatures:" : (pretty <$> Set.toList rs)
, hang 2 $ vsep $ "Utxo index:" : (pretty <$> Map.toList utxo)
]

Expand Down
1 change: 1 addition & 0 deletions plutus-ledger/src/Ledger/Tx.hs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ module Ledger.Tx
, getCardanoTxCollateralInputs
, getCardanoTxOutRefs
, getCardanoTxOutputs
, getCardanoTxRedeemers
, getCardanoTxSpentOutputs
, getCardanoTxProducedOutputs
, getCardanoTxReturnCollateral
Expand Down
Loading

0 comments on commit 7ac8444

Please sign in to comment.