Skip to content

Commit

Permalink
feature: CIP36 support Trezor (governance voting registration)
Browse files Browse the repository at this point in the history
  • Loading branch information
davidmisiak authored and janmazak committed Dec 4, 2022
1 parent ecfb5ca commit 8d40bd6
Show file tree
Hide file tree
Showing 21 changed files with 371 additions and 149 deletions.
12 changes: 7 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,20 +119,22 @@ cardano-hw-cli node issue-op-cert
--hw-signing-file FILE Input filepath of the hardware wallet signing file.
```

## Catalyst voting registration
## Governance voting registration
```
cardano-hw-cli catalyst voting-key-registration-metadata
cardano-hw-cli governance voting-registration-metadata
--mainnet | --testnet-magic NATURAL Use the mainnet magic id or specify testnet magic id.
--vote-public-key FILE Input filepath of vote public key in ed25519extended.
--vote-public-key FILE Input filepath to vote public key in ed25519extended format (one or more keys can be provided).
--vote-weight WEIGHT Voting power weight assigned to vote public key.
--stake-signing-key FILE Input filepath of the hardware wallet stake signing file, which will be used to to sign the voting registration.
--reward-address REWARDADDRESS Staking address which will receive voting rewards.
--nonce NONCE Current slot number.
--voting-purpose VOTINGPURPOSE Voting purpose (optional)
--reward-address-signing-key FILE Input filepath of the reward address signing files.
--metadata-cbor-out-file FILE Output filepath of metadata cbor.
--derivation-type TYPE Derivation type - currently applies only to Trezor. Options: LEDGER, ICARUS or ICARUS_TREZOR (default).
```

see [Catalyst voting registration example](docs/catalyst-voting-registration-example.md)
see [Governance voting registration example](docs/governance-voting-registration-example.md)

## Policy id generation
```
Expand All @@ -157,7 +159,7 @@ cardano-hw-cli device version
# Examples/Guides
- [Basic transaction](docs/transaction-example.md)
- [Stake delegation](docs/delegation-example.md)
- [Catalyst voting registration](docs/catalyst-voting-registration-example.md)
- [Governance voting registration](docs/governance-voting-registration-example.md)
- [Stake pool registration](docs/pool-registration.md)
- [Token minting](docs/token-minting.md)
- [Multisig transactions](docs/multisig-transactions.md)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
# Requirements
Ledger users: Cardano Ledger app versions 2.3.1 and higher is needed in order to register your vote. In order to install app 2.3.1 you need Ledger firmware 2.0.0 or higher. Both these updates can be done in Ledger Live > Manager section. If you don't see the option to update the firmware to 2.0.0, please re-install Ledger Live.

# Create catalyst voting keys
To get private/public voting key you must use another tool, you can use jcli to generate your private/public voting key:
[TODO add Ledger app and Trezor Firmware versions]

# Create governance voting keys

You may want to keep your voting keys stored on a Ledger Nano hardware device (the key derivation schema is described in [CIP-36](https://cips.cardano.org/cips/cip36/)). Consequently, you will have to sign all voting on the HW device storing the keys. For Trezor, this is not supported.

It is possible to register and use keys generated in other ways, e.g. as follows:
```
wget https://github.com/input-output-hk/jormungandr/releases/download/v0.9.3/jormungandr-0.9.3-x86_64-unknown-linux-gnu-generic.tar.gz
tar -xf jormungandr-0.9.3-x86_64-unknown-linux-gnu-generic.tar.gz
./jcli key generate --type ed25519extended > catalyst-vote.skey
./jcli key to-public < catalyst-vote.skey > catalyst-vote.pkey
```

# Create catalyst voting registration metadata
# Create governance voting registration metadata

Generate stake hardware wallet signing file and verification file with `cardano-hw-cli`:
```
cardano-hw-cli address key-gen \
Expand All @@ -19,22 +24,22 @@ cardano-hw-cli address key-gen \
--hw-signing-file stake.hwsfile
```

Get slot number from `cardano-cli`, use slot number as `nonce` in catalyst voting registration command:
Get slot number from `cardano-cli`, use slot number as `nonce` in governance voting registration command:
```
cardano-cli query tip --mainnet
```

Get stake address from `cardano-cli`, use it as `reward-address` and `reward-address-signing-key` in catalyst voting registration command:
Get stake address from `cardano-cli`, use it as `reward-address` and `reward-address-signing-key` in governance voting registration command:
```
cardano-cli stake-address build \
--stake-verification-key-file stake.vkey \
--out-file stake.addr \
--mainnet
```

Create catalyst voting registration metadata with `cardano-hw-cli`:
Create governance voting registration metadata with `cardano-hw-cli`:
```
cardano-hw-cli catalyst voting-key-registration-metadata \
cardano-hw-cli governance voting-registration-metadata \
--mainnet \
--vote-public-key catalyst-vote.pkey \
--reward-address $(cat stake.addr) \
Expand All @@ -43,6 +48,24 @@ cardano-hw-cli catalyst voting-key-registration-metadata \
--reward-address-signing-key stake.hwsfile \
--metadata-cbor-out-file voting_registration.cbor
```
(You should add `--voting-purpose` to change the voting purpose to something other than Catalyst.)

Alternatively, in case you want to split your voting power among several voting keys, the keys and their voting power weights can be specified like this:
```
cardano-hw-cli governance voting-registration-metadata \
--mainnet \
--vote-public-key catalyst-vote1.pkey \
--vote-weight 1 \
--vote-public-key catalyst-vote2.pkey \
--vote-weight 10 \
--reward-address $(cat stake.addr) \
--stake-signing-key stake.hwsfile \
--nonce 29747977 \
--reward-address-signing-key stake.hwsfile \
--metadata-cbor-out-file voting_registration.cbor
```

Note: The governance registration auxiliary data are formatted according to [CIP-36](https://cips.cardano.org/cips/cip36/).

# Create and submit transaction
Create raw transaction with `cardano-cli`, if you don't know how to create simple transaction, check https://github.com/vacuumlabs/cardano-hw-cli/blob/develop/docs/transaction-example.md
Expand Down Expand Up @@ -86,4 +109,4 @@ Submit transaction to blockchain with with `cardano-cli`:
cardano-cli transaction submit \
--tx-file tx.signed \
--mainnet
```
```
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
"@emurgo/cardano-serialization-lib-nodejs": "^8.0.0",
"@ledgerhq/hw-transport": "^5.12.0",
"@ledgerhq/hw-transport-node-hid-noevents": "^6.24.1",
"@trezor/connect": "^9.0.1",
"@trezor/connect": "^9.0.4",
"argparse": "^2.0.1",
"bignumber": "^1.1.0",
"borc": "^2.1.2",
Expand Down
10 changes: 5 additions & 5 deletions scripts/autocomplete.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ _cardano_hw_cli_completions()
{
case $COMP_CWORD in
1)
COMPREPLY=( $(compgen -W "device version address key transaction node catalyst" "${COMP_WORDS[1]}") )
COMPREPLY=( $(compgen -W "device version address key transaction node governance" "${COMP_WORDS[1]}") )
;;
2)
if [ "${COMP_WORDS[1]}" = "address" ]; then
Expand All @@ -20,8 +20,8 @@ _cardano_hw_cli_completions()
if [ "${COMP_WORDS[1]}" = "node" ]; then
COMPREPLY=( $(compgen -W "key-gen issue-op-cert" "${COMP_WORDS[2]}") )
fi
if [ "${COMP_WORDS[1]}" = "catalyst" ]; then
COMPREPLY=( $(compgen -W "voting-key-registration-metadata" "${COMP_WORDS[2]}") )
if [ "${COMP_WORDS[1]}" = "governance" ]; then
COMPREPLY=( $(compgen -W "voting-registration-metadata" "${COMP_WORDS[2]}") )
fi
;;
*)
Expand Down Expand Up @@ -58,8 +58,8 @@ _cardano_hw_cli_completions()
if [ "${COMP_WORDS[2]}" = "issue-op-cert" ]; then
COMPREPLY=( $(compgen -W "--kes-verification-key-file --kes-period --operational-certificate-issue-counter-file --hw-signing-file --out-file" -- "${COMP_WORDS[-1]}") )
fi
if [ "${COMP_WORDS[2]}" = "voting-key-registration-metadata" ]; then
COMPREPLY=( $(compgen -W "--mainnet --testnet-magic --vote-public-key --reward-address --stake-signing-key --nonce --reward-address-signing-key --metadata-cbor-out-file --derivation-type" -- "${COMP_WORDS[-1]}") )
if [ "${COMP_WORDS[2]}" = "voting-registration-metadata" ]; then
COMPREPLY=( $(compgen -W "--mainnet --testnet-magic --vote-public-key --reward-address --stake-signing-key --nonce --voting-purpose --reward-address-signing-key --metadata-cbor-out-file --derivation-type" -- "${COMP_WORDS[-1]}") )
fi
;;
esac
Expand Down
2 changes: 1 addition & 1 deletion src/command-parser/commandParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export enum CommandType {
DERIVE_NATIVE_SCRIPT_HASH = 'transaction.policyid',
SIGN_OPERATIONAL_CERTIFICATE = 'node.issue-op-cert',
NODE_KEY_GEN = 'node.key-gen',
CATALYST_VOTING_KEY_REGISTRATION_METADATA = 'catalyst.voting-key-registration-metadata',
GOVERNANCE_VOTING_REGISTRATION_METADATA = 'governance.voting-registration-metadata',
}

const initParser = (parser: ArgumentParser | ArgumentGroup, config: any): void => {
Expand Down
23 changes: 19 additions & 4 deletions src/command-parser/parserConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -306,8 +306,8 @@ export const parserConfig = {
'key-gen': nodeKeyGenArgs,
'issue-op-cert': opCertSigningArgs,
},
'catalyst': {
'voting-key-registration-metadata': {
'governance': {
'voting-registration-metadata': {
'--mainnet': {
nargs: '?',
dest: 'network',
Expand All @@ -323,9 +323,18 @@ export const parserConfig = {
},
'--vote-public-key': {
required: true,
dest: 'votePublicKey',
dest: 'votePublicKeys',
action: 'append',
type: (path: string) => parseVotePubFile(path),
help: 'Input filepath to vote public key in ed25519extended.',
help: 'Input filepath to vote public key in ed25519extended format (one or more keys can be provided).',
},
'--vote-weight': {
required: false, // we append 1 in commandExecutor.ts if there is only a single vote public key and its weight is not specified
dest: 'voteWeights',
action: 'append',
type: (weight: string) => BigInt(weight),
default: [],
help: 'Voting power weight assigned to vote public key.',
},
'--stake-signing-key': {
required: true,
Expand All @@ -344,6 +353,12 @@ export const parserConfig = {
type: (nonce: string) => BigInt(nonce),
help: 'Current slot number.',
},
'--voting-purpose': {
required: false,
dest: 'votingPurpose',
type: (votingPurpose: string) => BigInt(votingPurpose),
help: 'Voting purpose.',
},
'--reward-address-signing-key': {
action: 'append',
required: true,
Expand Down
4 changes: 2 additions & 2 deletions src/command-parser/parsers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -196,9 +196,9 @@ export const parseVotePubFile = (path: string): VotePublicKeyHex => {
const hexString = bech32.decode(data).data.toString('hex')
if (isVotePublicKeyHex(hexString)) return hexString
} catch (e) {
throw Error(Errors.InvalidCatalystVotePublicKey)
throw Error(Errors.InvalidGovernanceVotingPublicKey)
}
throw Error(Errors.InvalidCatalystVotePublicKey)
throw Error(Errors.InvalidGovernanceVotingPublicKey)
}

const SCRIPT_HASH_LENGTH = 28
Expand Down
36 changes: 28 additions & 8 deletions src/commandExecutor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,10 @@ import {
ParsedVerificationKeyArguments,
ParsedOpCertArguments,
ParsedNodeKeyGenArguments,
ParsedCatalystVotingKeyRegistrationMetadataArguments,
ParsedGovernanceVotingKeyRegistrationMetadataArguments,
Cbor,
NativeScriptDisplayFormat,
GovernanceVotingDelegation,
} from './types'
import { LedgerCryptoProvider } from './crypto-providers/ledgerCryptoProvider'
import { TrezorCryptoProvider } from './crypto-providers/trezorCryptoProvider'
Expand All @@ -39,7 +40,7 @@ import { Errors } from './errors'
import { parseOpCertIssueCounterFile } from './command-parser/parsers'
import { validateSigning, validateWitnessing } from './crypto-providers/signingValidation'
import { validateRawTxBeforeSigning, validateTxBeforeSigning } from './transaction/transactionValidation'
import { cardanoEraToSignedType } from './constants'
import { cardanoEraToSignedType, GOVERNANCE_VOTING_PURPOSE_CATALYST } from './constants'
import { WitnessOutput } from './transaction/types'

const promiseTimeout = <T> (promise: Promise<T>, ms: number): Promise<T> => {
Expand Down Expand Up @@ -262,20 +263,39 @@ const CommandExecutor = async () => {
writeOutputData(args.issueCounterFile, constructOpCertIssueCounterOutput(issueCounter))
}

const createCatalystVotingKeyRegistrationMetadata = async (
args: ParsedCatalystVotingKeyRegistrationMetadataArguments,
const createGovernanceVotingKeyRegistrationMetadata = async (
args: ParsedGovernanceVotingKeyRegistrationMetadataArguments,
) => {
if (!areHwSigningDataNonByron([...args.rewardAddressSigningKeyData, args.hwStakeSigningFileData])) {
throw Error(Errors.ByronSigningFilesFoundInVotingRegistration)
}

const votePublicKeyCount = args.votePublicKeys.length
const voteWeightCount = args.voteWeights.length
if (votePublicKeyCount === 1 && voteWeightCount === 0) {
// delegate the whole voting power to the single vote public key
args.voteWeights.push(BigInt(1))
} else if (votePublicKeyCount > 0 && votePublicKeyCount === voteWeightCount) {
// the vote public keys and vote weights are provided correctly
// nothing to do
} else {
throw Error(Errors.InvalidGovernanceVotingDelegations)
}
const delegations: GovernanceVotingDelegation[] = args.votePublicKeys.map((votePublicKey, index) => ({
votePublicKey,
voteWeight: args.voteWeights[index],
}))

const votingPurpose = args.votingPurpose || GOVERNANCE_VOTING_PURPOSE_CATALYST

const votingRegistrationMetaData = await cryptoProvider.signVotingRegistrationMetaData(
args.rewardAddressSigningKeyData,
delegations,
args.hwStakeSigningFileData,
args.rewardAddress,
args.votePublicKey,
args.network,
args.nonce,
votingPurpose,
args.network,
args.rewardAddressSigningKeyData,
args.derivationType,
)

Expand All @@ -292,7 +312,7 @@ const CommandExecutor = async () => {
createTxWitnesses,
createNodeSigningKeyFiles,
createSignedOperationalCertificate,
createCatalystVotingKeyRegistrationMetadata,
createGovernanceVotingRegistrationMetadata: createGovernanceVotingKeyRegistrationMetadata,
}
}

Expand Down
3 changes: 3 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ export const NETWORKS: {[key: string]: Network} = {

export const HARDENED_THRESHOLD = 0x80000000

// the 'Catalyst' value for voting_purpose in https://cips.cardano.org/cips/cip36/
export const GOVERNANCE_VOTING_PURPOSE_CATALYST = 0

export enum PathLabel {
PAYMENT = 'Payment',
STAKE = 'Stake',
Expand Down
23 changes: 16 additions & 7 deletions src/crypto-providers/ledgerCryptoProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
} from '../transaction/types'
import {
BIP32Path,
GovernanceVotingDelegation,
HexString,
HwSigningData,
NativeScript,
Expand Down Expand Up @@ -51,6 +52,8 @@ import {

const { bech32 } = require('cardano-crypto.js')

// TODO remove
// @ts-ignore
export const LedgerCryptoProvider: (transport: Transport) => Promise<CryptoProvider> = async (transport) => {
const ledger = new Ledger(transport)

Expand Down Expand Up @@ -731,10 +734,11 @@ export const LedgerCryptoProvider: (transport: Transport) => Promise<CryptoProvi
votingPublicKeyHex: VotePublicKeyHex,
addressParameters: _AddressParameters,
nonce: BigInt,
votingPurpose: BigInt,
): LedgerTypes.TxAuxiliaryData => ({
type: LedgerTypes.TxAuxiliaryDataType.CATALYST_REGISTRATION,
params: {
votingPublicKeyHex,
votingPublicKeyHex, // TODO update to cip36
stakingPath: hwStakeSigningFile.path,
rewardsDestination: {
type: addressParameters.addressType,
Expand All @@ -744,6 +748,7 @@ export const LedgerCryptoProvider: (transport: Transport) => Promise<CryptoProvi
},
},
nonce: `${nonce}`,
votingPurpose: `${votingPurpose}`,
},
})

Expand Down Expand Up @@ -787,12 +792,13 @@ export const LedgerCryptoProvider: (transport: Transport) => Promise<CryptoProvi
})

const signVotingRegistrationMetaData = async (
rewardAddressSigningFiles: HwSigningData[],
hwStakeSigningFile: HwSigningData,
delegations: GovernanceVotingDelegation[],
hwStakeSigningFile: HwSigningData, // describes stake_credential
rewardAddressBech32: string,
votePublicKeyHex: VotePublicKeyHex,
network: Network,
nonce: BigInt,
votingPurpose: BigInt,
network: Network,
rewardAddressSigningFiles: HwSigningData[],
): Promise<VotingRegistrationMetaDataCborHex> => {
const { data: address } : { data: Buffer } = bech32.decode(rewardAddressBech32)
const addressParams = getAddressParameters(rewardAddressSigningFiles, address, network)
Expand All @@ -802,17 +808,20 @@ export const LedgerCryptoProvider: (transport: Transport) => Promise<CryptoProvi

validateVotingRegistrationAddressType(addressParams.addressType)

const ledgerAuxData = prepareVoteAuxiliaryData(hwStakeSigningFile, votePublicKeyHex, addressParams, nonce)
const ledgerAuxData = prepareVoteAuxiliaryData(hwStakeSigningFile, votePublicKeyHex, addressParams, nonce, votingPurpose)
const dummyTx = prepareDummyTx(network, ledgerAuxData)

const response = await ledger.signTransaction(dummyTx)
if (!response.auxiliaryDataSupplement) throw Error(Errors.MissingAuxiliaryDataSupplement)

return encodeVotingRegistrationMetaData(
hwStakeSigningFile,
// TODO remove
// @ts-ignore
votePublicKeyHex,
hwStakeSigningFile,
address,
nonce,
votingPurpose,
response.auxiliaryDataSupplement.auxiliaryDataHashHex as HexString,
response.auxiliaryDataSupplement.catalystRegistrationSignatureHex as HexString,
)
Expand Down
Loading

0 comments on commit 8d40bd6

Please sign in to comment.