Skip to content

Commit cd9bb41

Browse files
authored
Merge pull request #43 from mlabs-haskell/gergely/manual-sign
Add the option to manually sign transactions
2 parents 53ec8a7 + 7345a74 commit cd9bb41

File tree

7 files changed

+219
-65
lines changed

7 files changed

+219
-65
lines changed

src/BotPlutusInterface/CardanoCLI.hs

Lines changed: 41 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,17 @@ module BotPlutusInterface.CardanoCLI (
1616
queryTip,
1717
) where
1818

19-
import BotPlutusInterface.Effects (PABEffect, ShellArgs (..), callCommand, uploadDir)
19+
import BotPlutusInterface.Effects (PABEffect, ShellArgs (..), callCommand, printLog, uploadDir)
2020
import BotPlutusInterface.Files (
21+
DummyPrivKey (FromSKey, FromVKey),
2122
datumJsonFilePath,
2223
policyScriptFilePath,
2324
redeemerJsonFilePath,
2425
signingKeyFilePath,
2526
txFilePath,
2627
validatorScriptFilePath,
2728
)
28-
import BotPlutusInterface.Types (PABConfig, Tip)
29+
import BotPlutusInterface.Types (LogLevel (Warn), PABConfig, Tip)
2930
import BotPlutusInterface.UtxoParser qualified as UtxoParser
3031
import Cardano.Api.Shelley (NetworkId (Mainnet, Testnet), NetworkMagic (..), serialiseAddress)
3132
import Codec.Serialise qualified as Codec
@@ -54,7 +55,7 @@ import Ledger (Slot (Slot), SlotRange)
5455
import Ledger qualified
5556
import Ledger.Ada qualified as Ada
5657
import Ledger.Address (Address (..))
57-
import Ledger.Crypto (PubKey, PubKeyHash)
58+
import Ledger.Crypto (PubKey, PubKeyHash (getPubKeyHash))
5859
import Ledger.Interval (
5960
Extended (Finite),
6061
Interval (Interval),
@@ -198,17 +199,27 @@ buildTx ::
198199
forall (w :: Type) (effs :: [Type -> Type]).
199200
Member (PABEffect w) effs =>
200201
PABConfig ->
202+
Map PubKeyHash DummyPrivKey ->
201203
PubKeyHash ->
202204
BuildMode ->
203205
Tx ->
204206
Eff effs ()
205-
buildTx pabConf ownPkh buildMode tx =
207+
buildTx pabConf privKeys ownPkh buildMode tx =
206208
callCommand @w $ ShellArgs "cardano-cli" opts (const ())
207209
where
208210
ownAddr = Ledger.pubKeyHashAddress (Ledger.PaymentPubKeyHash ownPkh) Nothing
209211
requiredSigners =
210212
concatMap
211-
(\pubKey -> ["--required-signer", signingKeyFilePath pabConf (Ledger.pubKeyHash pubKey)])
213+
( \pubKey ->
214+
let pkh = Ledger.pubKeyHash pubKey
215+
in case Map.lookup pkh privKeys of
216+
Just (FromSKey _) ->
217+
["--required-signer", signingKeyFilePath pabConf pkh]
218+
Just (FromVKey _) ->
219+
["--required-signer-hash", encodeByteString $ fromBuiltin $ getPubKeyHash pkh]
220+
Nothing ->
221+
[]
222+
)
212223
(Map.keys (Ledger.txSignatures tx))
213224
opts =
214225
mconcat
@@ -237,27 +248,39 @@ signTx ::
237248
forall (w :: Type) (effs :: [Type -> Type]).
238249
Member (PABEffect w) effs =>
239250
PABConfig ->
251+
Map PubKeyHash DummyPrivKey ->
240252
Tx ->
241253
[PubKey] ->
242-
Eff effs ()
243-
signTx pabConf tx pubKeys =
244-
callCommand @w $
245-
ShellArgs
246-
"cardano-cli"
247-
( mconcat
248-
[ ["transaction", "sign"]
249-
, ["--tx-body-file", txFilePath pabConf "raw" tx]
250-
, signingKeyFiles
251-
, ["--out-file", txFilePath pabConf "signed" tx]
252-
]
253-
)
254-
(const ())
254+
Eff effs (Either Text ())
255+
signTx pabConf privKeys tx pubKeys =
256+
let skeys = Map.filter (\case FromSKey _ -> True; FromVKey _ -> False) privKeys
257+
in if all ((`Map.member` skeys) . Ledger.pubKeyHash) pubKeys
258+
then callCommand @w $ ShellArgs "cardano-cli" opts (const (Right ()))
259+
else do
260+
let err =
261+
Text.unlines
262+
[ "Not all required signatures have signing key files. Please sign and submit the tx manually:"
263+
, "Tx file: " <> txFilePath pabConf "raw" tx
264+
, "Signatories (pkh): "
265+
<> Text.unwords
266+
(map (encodeByteString . fromBuiltin . getPubKeyHash . Ledger.pubKeyHash) pubKeys)
267+
]
268+
printLog @w Warn (Text.unpack err)
269+
pure $ Left err
255270
where
256271
signingKeyFiles =
257272
concatMap
258273
(\pubKey -> ["--signing-key-file", signingKeyFilePath pabConf (Ledger.pubKeyHash pubKey)])
259274
pubKeys
260275

276+
opts =
277+
mconcat
278+
[ ["transaction", "sign"]
279+
, ["--tx-body-file", txFilePath pabConf "raw" tx]
280+
, signingKeyFiles
281+
, ["--out-file", txFilePath pabConf "signed" tx]
282+
]
283+
261284
-- Signs and writes a tx (uses the tx body written to disk as input)
262285
submitTx ::
263286
forall (w :: Type) (effs :: [Type -> Type]).

src/BotPlutusInterface/Contract.hs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import Control.Monad.Freer.Extras.Log (handleLogIgnore)
2323
import Control.Monad.Freer.Extras.Modify (raiseEnd)
2424
import Control.Monad.Freer.Writer (Writer (Tell))
2525
import Data.Aeson (ToJSON, Value)
26+
import Data.Either (isRight)
2627
import Data.Kind (Type)
2728
import Data.Map qualified as Map
2829
import Data.Row (Row)
@@ -193,14 +194,15 @@ writeBalancedTx contractEnv (Right tx) = do
193194
Right _ -> do
194195
let ownPkh = contractEnv.cePABConfig.pcOwnPubKeyHash
195196
let requiredSigners = Map.keys $ tx ^. Tx.signatures
197+
privKeys <- either (error . Text.unpack) id <$> Files.readPrivateKeys @w contractEnv.cePABConfig
196198

197199
CardanoCLI.uploadFiles @w contractEnv.cePABConfig
198200

199-
CardanoCLI.buildTx @w contractEnv.cePABConfig ownPkh CardanoCLI.BuildAuto tx
200-
CardanoCLI.signTx @w contractEnv.cePABConfig tx requiredSigners
201+
CardanoCLI.buildTx @w contractEnv.cePABConfig privKeys ownPkh CardanoCLI.BuildAuto tx
202+
res <- CardanoCLI.signTx @w contractEnv.cePABConfig privKeys tx requiredSigners
201203

202204
result <-
203-
if contractEnv.cePABConfig.pcDryRun
205+
if contractEnv.cePABConfig.pcDryRun || isRight res
204206
then pure Nothing
205207
else CardanoCLI.submitTx @w contractEnv.cePABConfig tx
206208

src/BotPlutusInterface/Files.hs

Lines changed: 74 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,21 @@
22

33
module BotPlutusInterface.Files (
44
policyScriptFilePath,
5+
DummyPrivKey (FromSKey, FromVKey),
6+
unDummyPrivateKey,
57
validatorScriptFilePath,
68
readPrivateKeys,
79
signingKeyFilePath,
810
txFilePath,
9-
readPrivateKey,
1011
writeAll,
1112
writePolicyScriptFile,
1213
redeemerJsonFilePath,
14+
mkDummyPrivateKey,
1315
writeRedeemerJsonFile,
1416
writeValidatorScriptFile,
1517
datumJsonFilePath,
16-
fromCardanoPaymentKey,
18+
skeyToDummyPrivKey,
19+
vkeyToDummyPrivKey,
1720
writeDatumJsonFile,
1821
) where
1922

@@ -27,8 +30,9 @@ import BotPlutusInterface.Effects (
2730
)
2831
import BotPlutusInterface.Types (PABConfig)
2932
import Cardano.Api (
30-
AsType (AsPaymentKey, AsSigningKey),
33+
AsType (AsPaymentKey, AsSigningKey, AsVerificationKey),
3134
FileError,
35+
Key (VerificationKey),
3236
PaymentKey,
3337
SigningKey,
3438
getVerificationKey,
@@ -51,14 +55,15 @@ import Data.ByteString.Lazy qualified as LazyByteString
5155
import Data.ByteString.Short qualified as ShortByteString
5256
import Data.Either.Combinators (mapLeft)
5357
import Data.Kind (Type)
58+
import Data.List (sortOn)
5459
import Data.Map (Map)
5560
import Data.Map qualified as Map
56-
import Data.Maybe (mapMaybe)
61+
import Data.Maybe (catMaybes, mapMaybe)
5762
import Data.Set qualified as Set
5863
import Data.Text (Text)
5964
import Data.Text qualified as Text
6065
import Ledger qualified
61-
import Ledger.Crypto (PrivateKey, PubKeyHash (PubKeyHash))
66+
import Ledger.Crypto (PrivateKey, PubKey (PubKey), PubKeyHash (PubKeyHash))
6267
import Ledger.Tx (Tx)
6368
import Ledger.Tx qualified as Tx
6469
import Ledger.TxId qualified as TxId
@@ -67,16 +72,18 @@ import Plutus.V1.Ledger.Api (
6772
CurrencySymbol,
6873
Datum (getDatum),
6974
DatumHash (..),
75+
LedgerBytes (LedgerBytes),
7076
MintingPolicy,
7177
Redeemer (getRedeemer),
7278
RedeemerHash (..),
7379
Script,
7480
Validator,
7581
ValidatorHash (..),
82+
toBuiltin,
7683
)
7784
import PlutusTx (ToData, toData)
7885
import PlutusTx.Builtins (fromBuiltin)
79-
import System.FilePath (isExtensionOf)
86+
import System.FilePath (takeExtension, (</>))
8087
import Prelude
8188

8289
-- | Filename of a minting policy script
@@ -167,50 +174,92 @@ readPrivateKeys ::
167174
forall (w :: Type) (effs :: [Type -> Type]).
168175
Member (PABEffect w) effs =>
169176
PABConfig ->
170-
Eff effs (Either Text (Map PubKeyHash PrivateKey))
177+
Eff effs (Either Text (Map PubKeyHash DummyPrivKey))
171178
readPrivateKeys pabConf = do
172179
files <- listDirectory @w $ Text.unpack pabConf.pcSigningKeyFileDir
173-
let sKeyFiles =
174-
map (\filename -> Text.unpack pabConf.pcSigningKeyFileDir ++ "/" ++ filename) $
175-
filter ("skey" `isExtensionOf`) files
176-
privKeys <- mapM (readPrivateKey @w) sKeyFiles
180+
181+
privKeys <-
182+
catMaybes
183+
<$> mapM
184+
( \filename ->
185+
let fullPath = Text.unpack pabConf.pcSigningKeyFileDir </> filename
186+
in case takeExtension filename of
187+
".vkey" -> Just <$> readVerificationKey @w fullPath
188+
".skey" -> Just <$> readSigningKey @w fullPath
189+
_ -> pure Nothing
190+
)
191+
files
192+
177193
pure $ toPrivKeyMap <$> sequence privKeys
178194
where
179-
toPrivKeyMap :: [PrivateKey] -> Map PubKeyHash PrivateKey
195+
toPrivKeyMap :: [DummyPrivKey] -> Map PubKeyHash DummyPrivKey
180196
toPrivKeyMap =
181197
foldl
182198
( \pKeyMap pKey ->
183-
let pkh = Ledger.pubKeyHash $ Ledger.toPublicKey pKey
199+
let pkh = Ledger.pubKeyHash $ Ledger.toPublicKey $ unDummyPrivateKey pKey
184200
in Map.insert pkh pKey pKeyMap
185201
)
186202
Map.empty
203+
. sortOn keyPriority
204+
205+
keyPriority :: DummyPrivKey -> Int
206+
keyPriority (FromSKey _) = 1
207+
keyPriority (FromVKey _) = 0
187208

188-
readPrivateKey ::
209+
data DummyPrivKey
210+
= FromSKey PrivateKey
211+
| FromVKey PrivateKey
212+
213+
unDummyPrivateKey :: DummyPrivKey -> PrivateKey
214+
unDummyPrivateKey (FromSKey key) = key
215+
unDummyPrivateKey (FromVKey key) = key
216+
217+
readSigningKey ::
189218
forall (w :: Type) (effs :: [Type -> Type]).
190219
Member (PABEffect w) effs =>
191220
FilePath ->
192-
Eff effs (Either Text PrivateKey)
193-
readPrivateKey filePath = do
221+
Eff effs (Either Text DummyPrivKey)
222+
readSigningKey filePath = do
194223
pKey <- mapLeft (Text.pack . show) <$> readFileTextEnvelope @w (AsSigningKey AsPaymentKey) filePath
195-
pure $ fromCardanoPaymentKey =<< pKey
224+
pure $ skeyToDummyPrivKey =<< pKey
225+
226+
readVerificationKey ::
227+
forall (w :: Type) (effs :: [Type -> Type]).
228+
Member (PABEffect w) effs =>
229+
FilePath ->
230+
Eff effs (Either Text DummyPrivKey)
231+
readVerificationKey filePath = do
232+
pKey <- mapLeft (Text.pack . show) <$> readFileTextEnvelope @w (AsVerificationKey AsPaymentKey) filePath
233+
pure $ vkeyToDummyPrivKey =<< pKey
234+
235+
vkeyToDummyPrivKey :: VerificationKey PaymentKey -> Either Text DummyPrivKey
236+
vkeyToDummyPrivKey =
237+
fmap FromVKey . vkeyToDummyPrivKey'
238+
239+
skeyToDummyPrivKey :: SigningKey PaymentKey -> Either Text DummyPrivKey
240+
skeyToDummyPrivKey =
241+
fmap FromSKey . vkeyToDummyPrivKey' . getVerificationKey
196242

197243
{- | Warning! This implementation is not correct!
198244
This private key is derived from a normal signing key which uses a 32 byte private key compared
199245
to the extended key which is 64 bytes. Also, the extended key includes a chain index value
200246
201-
This keys sole purpose is to be able to derive a public key from it, which is then used for
247+
This key's sole purpose is to be able to derive a public key from it, which is then used for
202248
mapping to a signing key file for the CLI
203249
-}
204-
fromCardanoPaymentKey :: SigningKey PaymentKey -> Either Text PrivateKey
205-
fromCardanoPaymentKey sKey =
206-
let dummyPrivKeySuffix = ByteString.replicate 32 0
250+
vkeyToDummyPrivKey' :: VerificationKey PaymentKey -> Either Text PrivateKey
251+
vkeyToDummyPrivKey' =
252+
mkDummyPrivateKey . PubKey . LedgerBytes . toBuiltin . serialiseToRawBytes
253+
254+
mkDummyPrivateKey :: PubKey -> Either Text PrivateKey
255+
mkDummyPrivateKey (PubKey (LedgerBytes pubkey)) =
256+
let dummyPrivKey = ByteString.replicate 32 0
257+
dummyPrivKeySuffix = ByteString.replicate 32 0
207258
dummyChainCode = ByteString.replicate 32 1
208-
vKey = getVerificationKey sKey
209-
privkeyBS = serialiseToRawBytes sKey
210-
pubkeyBS = serialiseToRawBytes vKey
259+
pubkeyBS = fromBuiltin pubkey
211260
in mapLeft Text.pack $
212261
Crypto.xprv $
213-
mconcat [privkeyBS, dummyPrivKeySuffix, pubkeyBS, dummyChainCode]
262+
mconcat [dummyPrivKey, dummyPrivKeySuffix, pubkeyBS, dummyChainCode]
214263

215264
serialiseScript :: Script -> PlutusScript PlutusScriptV1
216265
serialiseScript =

src/BotPlutusInterface/PreBalance.hs

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ module BotPlutusInterface.PreBalance (
77

88
import BotPlutusInterface.CardanoCLI qualified as CardanoCLI
99
import BotPlutusInterface.Effects (PABEffect, createDirectoryIfMissing, printLog)
10+
import BotPlutusInterface.Files (DummyPrivKey, unDummyPrivateKey)
1011
import BotPlutusInterface.Files qualified as Files
1112
import BotPlutusInterface.Types (LogLevel (Debug), PABConfig)
1213
import Cardano.Api.Shelley (Lovelace (Lovelace), ProtocolParameters (protocolParamUTxOCostPerWord))
@@ -28,7 +29,7 @@ import Ledger qualified
2829
import Ledger.Ada qualified as Ada
2930
import Ledger.Address (Address (..))
3031
import Ledger.Constraints.OffChain (UnbalancedTx (..), fromScriptOutput)
31-
import Ledger.Crypto (PrivateKey, PubKeyHash)
32+
import Ledger.Crypto (PubKeyHash)
3233
import Ledger.Interval (
3334
Extended (Finite, NegInf, PosInf),
3435
Interval (Interval),
@@ -86,7 +87,7 @@ preBalanceTxIO pabConf ownPkh unbalancedTx =
8687
where
8788
loop ::
8889
Map TxOutRef TxOut ->
89-
Map PubKeyHash PrivateKey ->
90+
Map PubKeyHash DummyPrivKey ->
9091
[PubKeyHash] ->
9192
[(TxOut, Integer)] ->
9293
Tx ->
@@ -105,7 +106,7 @@ preBalanceTxIO pabConf ownPkh unbalancedTx =
105106
hoistEither $ preBalanceTx pabConf.pcProtocolParams minUtxos 0 utxoIndex ownPkh privKeys requiredSigs tx
106107

107108
lift $ createDirectoryIfMissing @w False (Text.unpack pabConf.pcTxFileDir)
108-
lift $ CardanoCLI.buildTx @w pabConf ownPkh (CardanoCLI.BuildRaw 0) txWithoutFees
109+
lift $ CardanoCLI.buildTx @w pabConf privKeys ownPkh (CardanoCLI.BuildRaw 0) txWithoutFees
109110
fees <- newEitherT $ CardanoCLI.calculateMinFee @w pabConf txWithoutFees
110111

111112
lift $ printLog @w Debug $ "Fees: " ++ show fees
@@ -132,7 +133,7 @@ preBalanceTx ::
132133
Integer ->
133134
Map TxOutRef TxOut ->
134135
PubKeyHash ->
135-
Map PubKeyHash PrivateKey ->
136+
Map PubKeyHash DummyPrivKey ->
136137
[PubKeyHash] ->
137138
Tx ->
138139
Either Text Tx
@@ -261,12 +262,12 @@ balanceNonAdaOuts ownPkh utxos tx =
261262
{- | Add the required signatorioes to the transaction. Be aware the the signature itself is invalid,
262263
and will be ignored. Only the pub key hashes are used, mapped to signing key files on disk.
263264
-}
264-
addSignatories :: PubKeyHash -> Map PubKeyHash PrivateKey -> [PubKeyHash] -> Tx -> Either Text Tx
265+
addSignatories :: PubKeyHash -> Map PubKeyHash DummyPrivKey -> [PubKeyHash] -> Tx -> Either Text Tx
265266
addSignatories ownPkh privKeys pkhs tx =
266267
foldM
267268
( \tx' pkh ->
268269
case Map.lookup pkh privKeys of
269-
Just privKey -> Right $ Tx.addSignature' privKey tx'
270+
Just privKey -> Right $ Tx.addSignature' (unDummyPrivateKey privKey) tx'
270271
Nothing -> Left "Signing key not found."
271272
)
272273
tx

0 commit comments

Comments
 (0)