From 9733b45c73878dda2f918a86b23a1a5418cc8194 Mon Sep 17 00:00:00 2001 From: Luke Nadur <19835357+intricate@users.noreply.github.com> Date: Mon, 31 Aug 2020 14:32:47 -0400 Subject: [PATCH 1/2] Add command for creating genesis key delegation certificates --- .../src/Cardano/CLI/Shelley/Commands.hs | 27 ++++ .../src/Cardano/CLI/Shelley/Parsers.hs | 153 +++++++++++++++++- .../src/Cardano/CLI/Shelley/Run/Governance.hs | 54 ++++++- 3 files changed, 229 insertions(+), 5 deletions(-) diff --git a/cardano-cli/src/Cardano/CLI/Shelley/Commands.hs b/cardano-cli/src/Cardano/CLI/Shelley/Commands.hs index d8bc76bc5f7..fd14a4bab67 100644 --- a/cardano-cli/src/Cardano/CLI/Shelley/Commands.hs +++ b/cardano-cli/src/Cardano/CLI/Shelley/Commands.hs @@ -1,3 +1,6 @@ +{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE StandaloneDeriving #-} +{-# LANGUAGE UndecidableInstances #-} -- | Shelley CLI command types module Cardano.CLI.Shelley.Commands @@ -37,6 +40,7 @@ module Cardano.CLI.Shelley.Commands , PoolMetaDataFile (..) , PrivKeyFile (..) , BlockId (..) + , VerificationKeyOrHashOrFile (..) ) where import Data.Text (Text) @@ -180,6 +184,11 @@ data QueryCmd = data GovernanceCmd = GovernanceMIRCertificate MIRPot [VerificationKeyFile] [Lovelace] OutputFile + | GovernanceGenesisKeyDelegationCertificate + (VerificationKeyOrHashOrFile GenesisKey) + (VerificationKeyOrHashOrFile GenesisDelegateKey) + (VerificationKeyOrHashOrFile VrfKey) + OutputFile | GovernanceUpdateProposal OutputFile EpochNo [VerificationKeyFile] ProtocolParametersUpdate @@ -303,3 +312,21 @@ newtype TxFile newtype VerificationKeyBase64 = VerificationKeyBase64 String deriving (Eq, Show) + +-- | Either a verification key, verification key hash, or path to a +-- verification key file. +data VerificationKeyOrHashOrFile keyrole + = VerificationKeyValue !(VerificationKey keyrole) + -- ^ A verification key. + | VerificationKeyHash !(Hash keyrole) + -- ^ A verification key hash. + | VerificationKeyFilePath !VerificationKeyFile + -- ^ A path to a verification key file. + -- Note that this file hasn't been validated at all (whether it exists, + -- contains a key of the correct type, etc.) + +deriving instance (Show (VerificationKey keyrole), Show (Hash keyrole)) + => Show (VerificationKeyOrHashOrFile keyrole) + +deriving instance (Eq (VerificationKey keyrole), Eq (Hash keyrole)) + => Eq (VerificationKeyOrHashOrFile keyrole) diff --git a/cardano-cli/src/Cardano/CLI/Shelley/Parsers.hs b/cardano-cli/src/Cardano/CLI/Shelley/Parsers.hs index 0ed22ae66d9..d338108971a 100644 --- a/cardano-cli/src/Cardano/CLI/Shelley/Parsers.hs +++ b/cardano-cli/src/Cardano/CLI/Shelley/Parsers.hs @@ -637,6 +637,9 @@ pGovernanceCmd = [ Opt.command "create-mir-certificate" (Opt.info pMIRCertificate $ Opt.progDesc "Create an MIR (Move Instantaneous Rewards) certificate") + , Opt.command "create-genesis-key-delegation-certificate" + (Opt.info pGovernanceGenesisKeyDelegationCertificate $ + Opt.progDesc "Create a genesis key delegation certificate") , Opt.command "create-update-proposal" (Opt.info pUpdateProposal $ Opt.progDesc "Create an update proposal") @@ -649,6 +652,14 @@ pGovernanceCmd = <*> some pRewardAmt <*> pOutputFile + pGovernanceGenesisKeyDelegationCertificate :: Parser GovernanceCmd + pGovernanceGenesisKeyDelegationCertificate = + GovernanceGenesisKeyDelegationCertificate + <$> pGenesisVerificationKeyOrHashOrFile + <*> pGenesisDelegateVerificationKeyOrHashOrFile + <*> pVrfVerificationKeyOrHashOrFile + <*> pOutputFile + pMIRPot :: Parser Shelley.MIRPot pMIRPot = Opt.flag' Shelley.ReservesMIR @@ -1106,6 +1117,90 @@ pGenesisVerificationKeyFile = <> Opt.completer (Opt.bashCompleter "file") ) +pGenesisVerificationKeyHash :: Parser (Hash GenesisKey) +pGenesisVerificationKeyHash = + Opt.option + (Opt.eitherReader deserialiseFromHex) + ( Opt.long "genesis-verification-key-hash" + <> Opt.metavar "STRING" + <> Opt.help "Genesis verification key hash (hex-encoded)." + ) + where + deserialiseFromHex :: String -> Either String (Hash GenesisKey) + deserialiseFromHex = + maybe (Left "Invalid genesis verification key hash.") Right + . deserialiseFromRawBytesHex (AsHash AsGenesisKey) + . BSC.pack + +pGenesisVerificationKey :: Parser (VerificationKey GenesisKey) +pGenesisVerificationKey = + Opt.option + (Opt.eitherReader deserialiseFromHex) + ( Opt.long "genesis-verification-key" + <> Opt.metavar "STRING" + <> Opt.help "Genesis verification key (hex-encoded)." + ) + where + deserialiseFromHex :: String -> Either String (VerificationKey GenesisKey) + deserialiseFromHex = + maybe (Left "Invalid genesis verification key.") Right + . deserialiseFromRawBytesHex (AsVerificationKey AsGenesisKey) + . BSC.pack + +pGenesisVerificationKeyOrHashOrFile :: Parser (VerificationKeyOrHashOrFile GenesisKey) +pGenesisVerificationKeyOrHashOrFile = + VerificationKeyValue <$> pGenesisVerificationKey + <|> VerificationKeyHash <$> pGenesisVerificationKeyHash + <|> VerificationKeyFilePath <$> pGenesisVerificationKeyFile + +pGenesisDelegateVerificationKeyFile :: Parser VerificationKeyFile +pGenesisDelegateVerificationKeyFile = + VerificationKeyFile <$> + Opt.strOption + ( Opt.long "genesis-delegate-verification-key-file" + <> Opt.metavar "FILE" + <> Opt.help "Filepath of the genesis delegate verification key." + <> Opt.completer (Opt.bashCompleter "file") + ) + +pGenesisDelegateVerificationKeyHash :: Parser (Hash GenesisDelegateKey) +pGenesisDelegateVerificationKeyHash = + Opt.option + (Opt.eitherReader deserialiseFromHex) + ( Opt.long "genesis-delegate-verification-key-hash" + <> Opt.metavar "STRING" + <> Opt.help "Genesis delegate verification key hash (hex-encoded)." + ) + where + deserialiseFromHex :: String -> Either String (Hash GenesisDelegateKey) + deserialiseFromHex = + maybe (Left "Invalid genesis delegate verification key hash.") Right + . deserialiseFromRawBytesHex (AsHash AsGenesisDelegateKey) + . BSC.pack + +pGenesisDelegateVerificationKey :: Parser (VerificationKey GenesisDelegateKey) +pGenesisDelegateVerificationKey = + Opt.option + (Opt.eitherReader deserialiseFromHex) + ( Opt.long "genesis-delegate-verification-key" + <> Opt.metavar "STRING" + <> Opt.help "Genesis delegate verification key (hex-encoded)." + ) + where + deserialiseFromHex + :: String + -> Either String (VerificationKey GenesisDelegateKey) + deserialiseFromHex = + maybe (Left "Invalid genesis delegate verification key.") Right + . deserialiseFromRawBytesHex (AsVerificationKey AsGenesisDelegateKey) + . BSC.pack + +pGenesisDelegateVerificationKeyOrHashOrFile + :: Parser (VerificationKeyOrHashOrFile GenesisDelegateKey) +pGenesisDelegateVerificationKeyOrHashOrFile = + VerificationKeyValue <$> pGenesisDelegateVerificationKey + <|> VerificationKeyHash <$> pGenesisDelegateVerificationKeyHash + <|> VerificationKeyFilePath <$> pGenesisDelegateVerificationKeyFile pKESVerificationKeyFile :: Parser VerificationKeyFile pKESVerificationKeyFile = @@ -1399,8 +1494,8 @@ pStakePoolVerificationKeyHashOrFile = StakePoolVerificationKeyFile <$> pPoolStakeVerificationKeyFile <|> StakePoolVerificationKeyHash <$> pStakePoolVerificationKeyHash -pVRFVerificationKeyFile :: Parser VerificationKeyFile -pVRFVerificationKeyFile = +pVrfVerificationKeyFile :: Parser VerificationKeyFile +pVrfVerificationKeyFile = VerificationKeyFile <$> Opt.strOption ( Opt.long "vrf-verification-key-file" @@ -1409,6 +1504,58 @@ pVRFVerificationKeyFile = <> Opt.completer (Opt.bashCompleter "file") ) +pVrfVerificationKeyHash :: Parser (Hash VrfKey) +pVrfVerificationKeyHash = + Opt.option + (Opt.eitherReader deserialiseFromHex) + ( Opt.long "vrf-verification-key-hash" + <> Opt.metavar "STRING" + <> Opt.help "VRF verification key hash (hex-encoded)." + ) + where + deserialiseFromHex :: String -> Either String (Hash VrfKey) + deserialiseFromHex = + maybe (Left "Invalid VRF verification key hash.") Right + . deserialiseFromRawBytesHex (AsHash AsVrfKey) + . BSC.pack + +pVrfVerificationKey :: Parser (VerificationKey VrfKey) +pVrfVerificationKey = + Opt.option + (Opt.eitherReader deserialiseFromBech32OrHex) + ( Opt.long "vrf-verification-key" + <> Opt.metavar "STRING" + <> Opt.help "VRF verification key (Bech32 or hex-encoded)." + ) + where + asType :: AsType (VerificationKey VrfKey) + asType = AsVerificationKey AsVrfKey + + deserialiseFromBech32OrHex + :: String + -> Either String (VerificationKey VrfKey) + deserialiseFromBech32OrHex str = + case deserialiseFromBech32 asType (Text.pack str) of + Right res -> Right res + + -- The input was valid Bech32, but some other error occurred. + Left err@(Bech32UnexpectedPrefix _ _) -> Left (displayError err) + Left err@(Bech32DataPartToBytesError _) -> Left (displayError err) + Left err@(Bech32DeserialiseFromBytesError _) -> Left (displayError err) + Left err@(Bech32WrongPrefix _ _) -> Left (displayError err) + + -- The input was not valid Bech32. Attempt to deserialize it as hex. + Left (Bech32DecodingError _) -> + case deserialiseFromRawBytesHex asType (BSC.pack str) of + Just res' -> Right res' + Nothing -> Left "Invalid VRF verification key." + +pVrfVerificationKeyOrHashOrFile :: Parser (VerificationKeyOrHashOrFile VrfKey) +pVrfVerificationKeyOrHashOrFile = + VerificationKeyValue <$> pVrfVerificationKey + <|> VerificationKeyHash <$> pVrfVerificationKeyHash + <|> VerificationKeyFilePath <$> pVrfVerificationKeyFile + pRewardAcctVerificationKeyFile :: Parser VerificationKeyFile pRewardAcctVerificationKeyFile = VerificationKeyFile <$> @@ -1577,7 +1724,7 @@ pStakePoolRegistrationCert :: Parser PoolCmd pStakePoolRegistrationCert = PoolRegistrationCert <$> pPoolStakeVerificationKeyFile - <*> pVRFVerificationKeyFile + <*> pVrfVerificationKeyFile <*> pPoolPledge <*> pPoolCost <*> pPoolMargin diff --git a/cardano-cli/src/Cardano/CLI/Shelley/Run/Governance.hs b/cardano-cli/src/Cardano/CLI/Shelley/Run/Governance.hs index febf9f89c52..f3957b7c018 100644 --- a/cardano-cli/src/Cardano/CLI/Shelley/Run/Governance.hs +++ b/cardano-cli/src/Cardano/CLI/Shelley/Run/Governance.hs @@ -14,6 +14,7 @@ import Control.Monad.Trans.Except.Extra (firstExceptT, left, newExcept import Cardano.Api.TextView (TextViewDescription (..), textShow) import Cardano.Api.Typed +import Cardano.CLI.Shelley.Commands (VerificationKeyOrHashOrFile (..)) import Cardano.CLI.Shelley.Parsers import Cardano.CLI.Types @@ -49,8 +50,12 @@ renderShelleyGovernanceError err = runGovernanceCmd :: GovernanceCmd -> ExceptT ShelleyGovernanceError IO () -runGovernanceCmd (GovernanceMIRCertificate mirpot vKeys rewards out) = runGovernanceMIRCertificate mirpot vKeys rewards out -runGovernanceCmd (GovernanceUpdateProposal out eNo genVKeys ppUp) = runGovernanceUpdateProposal out eNo genVKeys ppUp +runGovernanceCmd (GovernanceMIRCertificate mirpot vKeys rewards out) = + runGovernanceMIRCertificate mirpot vKeys rewards out +runGovernanceCmd (GovernanceGenesisKeyDelegationCertificate genVk genDelegVk vrfVk out) = + runGovernanceGenesisKeyDelegationCertificate genVk genDelegVk vrfVk out +runGovernanceCmd (GovernanceUpdateProposal out eNo genVKeys ppUp) = + runGovernanceUpdateProposal out eNo genVKeys ppUp runGovernanceMIRCertificate :: Shelley.MIRPot @@ -88,6 +93,33 @@ runGovernanceMIRCertificate mirPot vKeys rwdAmts (OutputFile oFp) = do $ readFileTextEnvelope (AsVerificationKey AsStakeKey) stVKey right . StakeCredentialByKey $ verificationKeyHash stakeVkey +runGovernanceGenesisKeyDelegationCertificate + :: VerificationKeyOrHashOrFile GenesisKey + -> VerificationKeyOrHashOrFile GenesisDelegateKey + -> VerificationKeyOrHashOrFile VrfKey + -> OutputFile + -> ExceptT ShelleyGovernanceError IO () +runGovernanceGenesisKeyDelegationCertificate genVkOrHashOrFp + genDelVkOrHashOrFp + vrfVkOrHashOrFp + (OutputFile oFp) = do + genesisVkHash <- firstExceptT GovernanceReadFileError + . newExceptT + $ readVerificationKeyOrHashOrFile AsGenesisKey genVkOrHashOrFp + genesisDelVkHash <-firstExceptT GovernanceReadFileError + . newExceptT + $ readVerificationKeyOrHashOrFile AsGenesisDelegateKey genDelVkOrHashOrFp + vrfVkHash <- firstExceptT GovernanceReadFileError + . newExceptT + $ readVerificationKeyOrHashOrFile AsVrfKey vrfVkOrHashOrFp + firstExceptT GovernanceWriteFileError + . newExceptT + $ writeFileTextEnvelope oFp (Just genKeyDelegCertDesc) + $ makeGenesisKeyDelegationCertificate genesisVkHash genesisDelVkHash vrfVkHash + where + genKeyDelegCertDesc :: TextViewDescription + genKeyDelegCertDesc = TextViewDescription "Genesis Key Delegation Certificate" + runGovernanceUpdateProposal :: OutputFile -> EpochNo @@ -107,3 +139,21 @@ runGovernanceUpdateProposal (OutputFile upFile) eNo genVerKeyFiles upPprams = do upProp = makeShelleyUpdateProposal upPprams genKeyHashes eNo firstExceptT GovernanceWriteFileError . newExceptT $ writeFileTextEnvelope upFile Nothing upProp + +-- | Read a verification key or verification key hash or verification key file +-- and return a verification key hash. +-- +-- If a filepath is provided, it will be interpreted as a text envelope +-- formatted file. +readVerificationKeyOrHashOrFile + :: Key keyrole + => AsType keyrole + -> VerificationKeyOrHashOrFile keyrole + -> IO (Either (FileError TextEnvelopeError) (Hash keyrole)) +readVerificationKeyOrHashOrFile asType verKeyOrHashOrFile = + case verKeyOrHashOrFile of + VerificationKeyHash vkHash -> pure (Right vkHash) + VerificationKeyValue vk -> pure (Right $ verificationKeyHash vk) + VerificationKeyFilePath (VerificationKeyFile fp) -> do + eitherVk <- readFileTextEnvelope (AsVerificationKey asType) fp + pure (verificationKeyHash <$> eitherVk) From 8aa273e8b1a32206d3e753cc0a7d884c38f372ba Mon Sep 17 00:00:00 2001 From: Luke Nadur <19835357+intricate@users.noreply.github.com> Date: Wed, 2 Sep 2020 13:44:05 -0400 Subject: [PATCH 2/2] Add CLI test for creating genesis key delegation certificates --- cardano-cli/cardano-cli.cabal | 1 + cardano-cli/test/Test/Golden/Shelley.hs | 3 + .../GenesisDelegationCertificate.hs | 13 --- .../GenesisKeyDelegationCertificate.hs | 79 +++++++++++++++++++ .../genesis_key_delegation_certificate | 5 ++ 5 files changed, 88 insertions(+), 13 deletions(-) delete mode 100644 cardano-cli/test/Test/Golden/Shelley/TextEnvelope/Certificates/GenesisDelegationCertificate.hs create mode 100644 cardano-cli/test/Test/Golden/Shelley/TextEnvelope/Certificates/GenesisKeyDelegationCertificate.hs create mode 100644 cardano-cli/test/data/golden/shelley/certificates/genesis_key_delegation_certificate diff --git a/cardano-cli/cardano-cli.cabal b/cardano-cli/cardano-cli.cabal index dfd5e39145c..e97b71f4b80 100644 --- a/cardano-cli/cardano-cli.cabal +++ b/cardano-cli/cardano-cli.cabal @@ -245,6 +245,7 @@ test-suite cardano-cli-golden Test.Golden.Shelley.StakeAddress.KeyGen Test.Golden.Shelley.StakeAddress.RegistrationCertificate Test.Golden.Shelley.StakePool.RegistrationCertificate + Test.Golden.Shelley.TextEnvelope.Certificates.GenesisKeyDelegationCertificate Test.Golden.Shelley.TextEnvelope.Certificates.MIRCertificate Test.Golden.Shelley.TextEnvelope.Certificates.OperationalCertificate Test.Golden.Shelley.TextEnvelope.Certificates.StakeAddressCertificates diff --git a/cardano-cli/test/Test/Golden/Shelley.hs b/cardano-cli/test/Test/Golden/Shelley.hs index 6635473ad83..fe1a347a228 100644 --- a/cardano-cli/test/Test/Golden/Shelley.hs +++ b/cardano-cli/test/Test/Golden/Shelley.hs @@ -31,6 +31,8 @@ import Test.Golden.Shelley.StakeAddress.RegistrationCertificate (golden_shelleyStakeAddressRegistrationCertificate) import Test.Golden.Shelley.StakePool.RegistrationCertificate (golden_shelleyStakePoolRegistrationCertificate) +import Test.Golden.Shelley.TextEnvelope.Certificates.GenesisKeyDelegationCertificate + (golden_shelleyGenesisKeyDelegationCertificate) import Test.Golden.Shelley.TextEnvelope.Certificates.MIRCertificate (golden_shelleyMIRCertificate) import Test.Golden.Shelley.TextEnvelope.Certificates.OperationalCertificate @@ -121,6 +123,7 @@ certificateTests = , ("golden_shelleyOperationalCertificate", golden_shelleyOperationalCertificate) , ("golden_shelleyStakePoolCertificates", golden_shelleyStakePoolCertificates) , ("golden_shelleyMIRCertificate", golden_shelleyMIRCertificate) + , ("golden_shelleyGenesisKeyDelegationCertificate", golden_shelleyGenesisKeyDelegationCertificate) ] metaDatatests :: IO Bool diff --git a/cardano-cli/test/Test/Golden/Shelley/TextEnvelope/Certificates/GenesisDelegationCertificate.hs b/cardano-cli/test/Test/Golden/Shelley/TextEnvelope/Certificates/GenesisDelegationCertificate.hs deleted file mode 100644 index 0fe3318196f..00000000000 --- a/cardano-cli/test/Test/Golden/Shelley/TextEnvelope/Certificates/GenesisDelegationCertificate.hs +++ /dev/null @@ -1,13 +0,0 @@ -{-# LANGUAGE OverloadedStrings #-} - -module Test.Golden.Shelley.TextEnvelope.Certificates.GenesisDelegation - ( golden_shelleyGenesisDelegationCertificate - ) where - -import Cardano.Prelude -import Hedgehog (Property) - -{- HLINT ignore "Use camelCase" -} - -golden_shelleyGenesisDelegationCertificate :: Property -golden_shelleyGenesisDelegationCertificate = panic "TODO" diff --git a/cardano-cli/test/Test/Golden/Shelley/TextEnvelope/Certificates/GenesisKeyDelegationCertificate.hs b/cardano-cli/test/Test/Golden/Shelley/TextEnvelope/Certificates/GenesisKeyDelegationCertificate.hs new file mode 100644 index 00000000000..14591c754b4 --- /dev/null +++ b/cardano-cli/test/Test/Golden/Shelley/TextEnvelope/Certificates/GenesisKeyDelegationCertificate.hs @@ -0,0 +1,79 @@ +{-# LANGUAGE OverloadedStrings #-} + +module Test.Golden.Shelley.TextEnvelope.Certificates.GenesisKeyDelegationCertificate + ( golden_shelleyGenesisKeyDelegationCertificate + ) where + +import Cardano.Prelude + +import Hedgehog (Property) + +import Cardano.Api.Typed (AsType (..), HasTextEnvelope (..)) + +import Test.OptParse + +{- HLINT ignore "Use camelCase" -} + +golden_shelleyGenesisKeyDelegationCertificate :: Property +golden_shelleyGenesisKeyDelegationCertificate = + propertyOnce . moduleWorkspace "tmp" $ \tempDir -> do + -- Reference certificate + referenceCertificateFilePath <- + noteInputFile $ + "test/data/golden/shelley/certificates/" + <> "genesis_key_delegation_certificate" + + -- Verification key and certificate filepaths + genesisVerKeyFilePath <- + noteTempFile tempDir "genesis-verification-key-file" + genesisDelegVerKeyFilePath <- + noteTempFile tempDir "genesis-delegate-verification-key-file" + vrfVerKeyFilePath <- noteTempFile tempDir "vrf-verification-key-file" + genesisKeyDelegCertFilePath <- + noteTempFile tempDir "genesis-key-delegation-certificate-file" + + -- Generate genesis key pair + void $ execCardanoCLI + [ "shelley","genesis","key-gen-genesis" + , "--verification-key-file", genesisVerKeyFilePath + , "--signing-key-file", "/dev/null" + ] + + -- Generate genesis delegate key pair + void $ execCardanoCLI + [ "shelley","genesis","key-gen-delegate" + , "--verification-key-file", genesisDelegVerKeyFilePath + , "--signing-key-file", "/dev/null" + , "--operational-certificate-issue-counter-file", "/dev/null" + ] + + -- Generate VRF key pair + void $ execCardanoCLI + [ "shelley","node","key-gen-VRF" + , "--verification-key-file", vrfVerKeyFilePath + , "--signing-key-file", "/dev/null" + ] + + assertFilesExist + [ genesisVerKeyFilePath + , genesisDelegVerKeyFilePath + , vrfVerKeyFilePath + ] + + -- Create genesis key delegation certificate + void $ execCardanoCLI + [ "shelley","governance","create-genesis-key-delegation-certificate" + , "--genesis-verification-key-file", genesisVerKeyFilePath + , "--genesis-delegate-verification-key-file", genesisDelegVerKeyFilePath + , "--vrf-verification-key-file", vrfVerKeyFilePath + , "--out-file", genesisKeyDelegCertFilePath + ] + + assertFilesExist [genesisKeyDelegCertFilePath] + + let certificateType = textEnvelopeType AsCertificate + + checkTextEnvelopeFormat + certificateType + referenceCertificateFilePath + genesisKeyDelegCertFilePath diff --git a/cardano-cli/test/data/golden/shelley/certificates/genesis_key_delegation_certificate b/cardano-cli/test/data/golden/shelley/certificates/genesis_key_delegation_certificate new file mode 100644 index 00000000000..ae5180283a2 --- /dev/null +++ b/cardano-cli/test/data/golden/shelley/certificates/genesis_key_delegation_certificate @@ -0,0 +1,5 @@ +{ + "type": "CertificateShelley", + "description": "Genesis Key Delegation Certificate", + "cborHex": "8405581cc3db461200fa59c81a4ecc8495446d9e42de27483ff6ee4339c9ab94581cd52ac434259f2af7fd2a538ece5ef8d80386527aa93e207473acb31c58201b9de69baec0dff8dde6e81d71f40f8b65fb3df55bb6ece5783aade88b17354d" +}