Skip to content

Commit

Permalink
Re-implement command-line handler for 'governance verify-poll'
Browse files Browse the repository at this point in the history
  As well as finalize the metadata extraction logic on the API's side.
  This is mostly the same thing as before, but it now takes a signed
  transaction file for verification instead of a standalone metadata.

  ```
  ❯ cardano-cli governance verify-poll --poll-file polls/basic.json --tx-file verify/valid
  Found valid poll answer, signed by:
  [
      "f8db28823f8ebd01a2d9e24efb2f0d18e387665770274513e370b5d5"
  ]
  ```

  ```
  ❯ cardano-cli -- governance verify-poll --poll-file polls/basic.json --tx-file verify/none
  Command failed: governance verify-poll  Error: No answer found in the provided transaction's metadata.
  ```

  ```
  ❯ cardano-cli -- governance verify-poll --poll-file polls/basic.json --tx-file verify/malformed
  Command failed: governance verify-poll  Error: Malformed metadata; couldn't deserialise answer: An error occured while decoding GovernancePollAnswer.2.
  Error: missing mandatory field
  ```

  ```
  ❯ cardano-cli -- governance verify-poll --poll-file polls/basic.json --tx-file verify/invalid
  Command failed: governance verify-poll  Error: Invalid answer (42) not part of the poll.
  Accepted answers:
  0 → yes
  1 → no
  ```

  ```
  ❯ cardano-cli -- governance verify-poll --poll-file polls/basic.json --tx-file verify/mismatch
  Command failed: governance verify-poll  Error: Answer's poll doesn't match provided poll (hash mismatch).
  ```
  • Loading branch information
KtorZ committed Apr 25, 2023
1 parent 4d07eb1 commit 5ba4ef7
Show file tree
Hide file tree
Showing 5 changed files with 106 additions and 72 deletions.
129 changes: 89 additions & 40 deletions cardano-api/src/Cardano/Api/Governance/Poll.hs
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DerivingVia #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE ViewPatterns #-}

-- | An API for driving on-chain poll for SPOs.
--
Expand Down Expand Up @@ -35,24 +38,29 @@ module Cardano.Api.Governance.Poll(

import Cardano.Prelude hiding (poll)

import Control.Arrow (left)
import Data.List (lookup)
import qualified Data.Map.Strict as Map
import qualified Data.Text as Text
import qualified Data.Text.Lazy as Text.Lazy
import qualified Data.Text.Lazy.Builder as Text.Builder
import Formatting (build, sformat)

import Cardano.Api.Eras
import Cardano.Api.HasTypeProxy
import Cardano.Api.Hash
import Cardano.Api.KeysShelley
import Cardano.Api.SerialiseCBOR
import Cardano.Api.SerialiseRaw
import Cardano.Api.SerialiseTextEnvelope
import Cardano.Api.SerialiseUsing
import Cardano.Api.Tx
import Cardano.Api.TxBody
import Cardano.Api.TxMetadata
import Cardano.Api.Utils

import Cardano.Binary (DecoderError(..))
import Cardano.Ledger.Crypto (HASH, StandardCrypto)
import Cardano.Ledger.Keys (KeyHash(..), KeyRole(..))

import Cardano.Crypto.Hash (hashFromBytes, hashToBytes, hashWith)
import qualified Cardano.Crypto.Hash as Hash
Expand Down Expand Up @@ -254,9 +262,11 @@ instance SerialiseAsCBOR GovernancePollAnswer where
--

data GovernancePollError
= ErrGovernancePollMismatch
= ErrGovernancePollMismatch GovernancePollMismatchError
| ErrGovernancePollNoAnswer
| ErrGovernancePollUnauthenticated
| ErrGovernancePollMalformedAnswer DecoderError
| ErrGovernancePollInvalidAnswer GovernancePollInvalidAnswerError
| ErrGovernancePollInvalidWitness
deriving Show

data GovernancePollInvalidAnswerError = GovernancePollInvalidAnswerError
Expand All @@ -265,53 +275,92 @@ data GovernancePollInvalidAnswerError = GovernancePollInvalidAnswerError
}
deriving Show

data GovernancePollMismatchError = GovernancePollMismatchError
{ specifiedHashInAnswer :: Hash GovernancePoll
, calculatedHashFromPoll :: Hash GovernancePoll
}
deriving Show

renderGovernancePollError :: GovernancePollError -> Text
renderGovernancePollError err =
case err of
ErrGovernancePollMismatch ->
"Answer's poll doesn't match provided poll (hash mismatch)."
ErrGovernancePollInvalidAnswer invalidAnswer ->
mconcat
[ "Invalid answer ("
, textShow (invalidAnswerReceivedAnswer invalidAnswer)
, ") not part of the poll."
, "\n"
, "Accepted answers:"
, "\n"
, Text.intercalate "\n"
[ mconcat
[ textShow ix
, ""
, answer
]
| (ix, answer) <- invalidAnswerAcceptableAnswers invalidAnswer
ErrGovernancePollMismatch mismatchErr -> mconcat
[ "Answer's poll doesn't match provided poll (hash mismatch).\n"
, " Hash specified in answer: " <> textShow (specifiedHashInAnswer mismatchErr)
, "\n"
, " Hash calculated from poll: " <> textShow (calculatedHashFromPoll mismatchErr)
]
ErrGovernancePollNoAnswer ->
"No answer found in the provided transaction's metadata."
ErrGovernancePollUnauthenticated -> mconcat
[ "No (valid) signatories found for the answer. "
, "Signatories MUST be specified as extra signatories on the transaction "
, "and cannot be mere payment keys."
]
ErrGovernancePollMalformedAnswer decoderErr ->
"Malformed metadata; couldn't deserialise answer: " <> sformat build decoderErr
ErrGovernancePollInvalidAnswer invalidAnswer -> mconcat
[ "Invalid answer ("
, textShow (invalidAnswerReceivedAnswer invalidAnswer)
, ") not part of the poll."
, "\n"
, "Accepted answers:"
, "\n"
, Text.intercalate "\n"
[ mconcat
[ textShow ix
, ""
, answer
]
| (ix, answer) <- invalidAnswerAcceptableAnswers invalidAnswer
]
ErrGovernancePollInvalidWitness ->
"Invalid witness for the answer: the proof / signature doesn't hold."
]

-- | Verify a poll against a given transaction and returns the signatories
-- (verification key only) when valid.
--
-- Note: signatures aren't checked as it is assumed to have been done externally
-- (the existence of the transaction in the ledger provides this guarantee).
verifyPollAnswer
:: GovernancePoll
-> InAnyCardanoEra Tx
-> Either GovernancePollError [KeyHash 'Witness StandardCrypto]
verifyPollAnswer poll (InAnyCardanoEra _era tx) = do
answer <- extractPollAnswer (getTxBody tx)

when (hashGovernancePoll poll /= govAnsPoll answer) $
Left ErrGovernancePollMismatch

when (govAnsChoice answer >= fromIntegral (length (govPollAnswers poll))) $ do
let invalidAnswerReceivedAnswer = govAnsChoice answer
let invalidAnswerAcceptableAnswers = zip [0..] (govPollAnswers poll)
Left $ ErrGovernancePollInvalidAnswer $ GovernancePollInvalidAnswerError
{ invalidAnswerReceivedAnswer
, invalidAnswerAcceptableAnswers
}

pure [ witVKeyHash wit | (ShelleyKeyWitness _ wit) <- getTxWitnesses tx ]
-> Either GovernancePollError [Hash PaymentKey]
verifyPollAnswer poll (InAnyCardanoEra _era (getTxBody -> TxBody body)) = do
answer <- extractPollAnswer (txMetadata body)
answer `hasMatchingHash` hashGovernancePoll poll
answer `isAmongAcceptableChoices` govPollAnswers poll
extraKeyWitnesses (txExtraKeyWits body)
where
extractPollAnswer (TxBody body) =
undefined
extractPollAnswer = \case
TxMetadataNone ->
Left ErrGovernancePollNoAnswer
TxMetadataInEra _era metadata ->
left ErrGovernancePollMalformedAnswer $
deserialiseFromCBOR AsGovernancePollAnswer (serialiseToCBOR metadata)

hasMatchingHash answer calculatedHashFromPoll = do
let specifiedHashInAnswer = govAnsPoll answer
when (calculatedHashFromPoll /= specifiedHashInAnswer) $
Left $ ErrGovernancePollMismatch $
GovernancePollMismatchError
{ specifiedHashInAnswer
, calculatedHashFromPoll
}

isAmongAcceptableChoices answer answers =
when (govAnsChoice answer >= fromIntegral (length answers)) $ do
let invalidAnswerReceivedAnswer = govAnsChoice answer
let invalidAnswerAcceptableAnswers = zip [0..] answers
Left $ ErrGovernancePollInvalidAnswer $ GovernancePollInvalidAnswerError
{ invalidAnswerReceivedAnswer
, invalidAnswerAcceptableAnswers
}

extraKeyWitnesses = \case
TxExtraKeyWitnesses _era witnesses ->
pure witnesses
TxExtraKeyWitnessesNone ->
Left ErrGovernancePollUnauthenticated


-- ----------------------------------------------------------------------------
Expand Down
1 change: 1 addition & 0 deletions cardano-api/src/Cardano/Api/KeysShelley.hs
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ newtype instance Hash PaymentKey =
deriving stock (Eq, Ord)
deriving (Show, IsString) via UsingRawBytesHex (Hash PaymentKey)
deriving (ToCBOR, FromCBOR) via UsingRawBytes (Hash PaymentKey)
deriving (ToJSONKey, ToJSON, FromJSON) via UsingRawBytesHex (Hash PaymentKey)
deriving anyclass SerialiseAsCBOR

instance SerialiseAsRawBytes (Hash PaymentKey) where
Expand Down
2 changes: 1 addition & 1 deletion cardano-cli/src/Cardano/CLI/Shelley/Commands.hs
Original file line number Diff line number Diff line change
Expand Up @@ -415,7 +415,7 @@ data GovernanceCmd
(Maybe Word) -- Answer index
| GovernanceVerifyPoll
FilePath -- Poll file
FilePath -- Metadata JSON file
FilePath -- Tx file
deriving Show

renderGovernanceCmd :: GovernanceCmd -> Text
Expand Down
11 changes: 1 addition & 10 deletions cardano-cli/src/Cardano/CLI/Shelley/Parsers.hs
Original file line number Diff line number Diff line change
Expand Up @@ -1124,7 +1124,7 @@ pGovernanceCmd =
pGovernanceVerifyPoll =
GovernanceVerifyPoll
<$> pPollFile
<*> pPollMetadataFile
<*> pTxFileIn


pPollQuestion :: Parser Text
Expand Down Expand Up @@ -1168,15 +1168,6 @@ pPollNonce =
<> Opt.help "An (optional) nonce for non-replayability."
)

pPollMetadataFile :: Parser FilePath
pPollMetadataFile =
Opt.strOption
( Opt.long "metadata-file"
<> Opt.metavar "FILE"
<> Opt.help "Filepath of the metadata file, in (detailed) JSON format."
<> Opt.completer (Opt.bashCompleter "file")
)

pTransferAmt :: Parser Lovelace
pTransferAmt =
Opt.option (readerFromParsecParser parseLovelace)
Expand Down
35 changes: 14 additions & 21 deletions cardano-cli/src/Cardano/CLI/Shelley/Run/Governance.hs
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,10 @@ import Cardano.CLI.Shelley.Key (InputDecodeError, VerificationKeyOrHas
readVerificationKeyOrHashOrFile, readVerificationKeyOrHashOrTextEnvFile)
import Cardano.CLI.Shelley.Parsers
import Cardano.CLI.Types
import Cardano.CLI.Shelley.Run.Key (SomeSigningKey(..), readSigningKeyFile)
import Cardano.CLI.Shelley.Run.Transaction (ShelleyTxCmdError, readFileTxMetadata,
import Cardano.CLI.Shelley.Run.Transaction (ShelleyTxCmdError, readFileTx,
renderShelleyTxCmdError)

import Cardano.Binary (DecoderError)
import Cardano.Ledger.Crypto (StandardCrypto)
import Cardano.Ledger.Keys (SignKeyDSIGN, SignKeyVRF)
import qualified Cardano.Ledger.Shelley.TxBody as Shelley

data ShelleyGovernanceCmdError
Expand All @@ -59,7 +56,7 @@ data ShelleyGovernanceCmdError
!Int
-- ^ Maximum answer index
| ShelleyGovernanceCmdPollInvalidChoice
| ShelleyGovernanceCmdMetadataError !ShelleyTxCmdError
| ShelleyGovernanceCmdTxCmdError !ShelleyTxCmdError
| ShelleyGovernanceCmdDecoderError !DecoderError
| ShelleyGovernanceCmdVerifyPollError !GovernancePollError

Expand Down Expand Up @@ -88,8 +85,8 @@ renderShelleyGovernanceError err =
"Poll answer out of bounds. Choices are between 0 and " <> textShow nMax
ShelleyGovernanceCmdPollInvalidChoice ->
"Invalid choice. Please choose from the available answers."
ShelleyGovernanceCmdMetadataError metadataError ->
renderShelleyTxCmdError metadataError
ShelleyGovernanceCmdTxCmdError txCmdErr ->
renderShelleyTxCmdError txCmdErr
ShelleyGovernanceCmdDecoderError decoderError ->
"Unable to decode metadata: " <> sformat build decoderError
ShelleyGovernanceCmdVerifyPollError pollError ->
Expand All @@ -106,8 +103,8 @@ runGovernanceCmd (GovernanceUpdateProposal out eNo genVKeys ppUp mCostModelFp) =
runGovernanceUpdateProposal out eNo genVKeys ppUp mCostModelFp
runGovernanceCmd (GovernanceCreatePoll prompt choices nonce out) =
runGovernanceCreatePoll prompt choices nonce out
runGovernanceCmd (GovernanceAnswerPoll poll sk ix) =
runGovernanceAnswerPoll poll sk ix
runGovernanceCmd (GovernanceAnswerPoll poll ix) =
runGovernanceAnswerPoll poll ix
runGovernanceCmd (GovernanceVerifyPoll poll metadata) =
runGovernanceVerifyPoll poll metadata

Expand Down Expand Up @@ -323,20 +320,16 @@ runGovernanceVerifyPoll
:: FilePath
-> FilePath
-> ExceptT ShelleyGovernanceCmdError IO ()
runGovernanceVerifyPoll pollFile metadataFile = do
runGovernanceVerifyPoll pollFile txFile = do
poll <- firstExceptT ShelleyGovernanceCmdTextEnvReadError . newExceptT $
readFileTextEnvelope AsGovernancePoll pollFile

metadata <- firstExceptT ShelleyGovernanceCmdMetadataError $
readFileTxMetadata TxMetadataJsonDetailedSchema (MetadataFileJSON metadataFile)
tx <- firstExceptT ShelleyGovernanceCmdTxCmdError $
readFileTx txFile

answer <- firstExceptT ShelleyGovernanceCmdDecoderError . newExceptT $ pure $
deserialiseFromCBOR AsGovernancePollAnswer (serialiseToCBOR metadata)
signatories <- firstExceptT ShelleyGovernanceCmdVerifyPollError . newExceptT $ pure $
verifyPollAnswer poll tx

witness <- firstExceptT ShelleyGovernanceCmdDecoderError . newExceptT $ pure $
deserialiseFromCBOR AsGovernancePollWitness (serialiseToCBOR metadata)

firstExceptT ShelleyGovernanceCmdVerifyPollError . newExceptT $ pure $
verifyPollAnswer poll answer witness

liftIO $ BSC.hPutStrLn stderr "Ok."
liftIO $ do
BSC.hPutStrLn stderr "Found valid poll answer, signed by: "
BSC.hPutStrLn stdout (prettyPrintJSON signatories)

0 comments on commit 5ba4ef7

Please sign in to comment.