Skip to content

Commit

Permalink
feature: CIP36 support for Ledger (governance voting registration)
Browse files Browse the repository at this point in the history
  • Loading branch information
janmazak committed Dec 4, 2022
1 parent 8d40bd6 commit bdd7dde
Show file tree
Hide file tree
Showing 7 changed files with 130 additions and 48 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
"homepage": "https://github.com/vacuumlabs/cardano-hw-cli#readme",
"dependencies": {
"@babel/runtime": "^7.11.2",
"@cardano-foundation/ledgerjs-hw-app-cardano": "^5.1.0",
"@cardano-foundation/ledgerjs-hw-app-cardano": "https://github.com/vacuumlabs/ledgerjs-cardano-shelley/releases/download/v6.0.0-rc1/cardano-foundation-ledgerjs-hw-app-cardano-v6.0.0-rc1.tgz",
"@emurgo/cardano-serialization-lib-nodejs": "^8.0.0",
"@ledgerhq/hw-transport": "^5.12.0",
"@ledgerhq/hw-transport-node-hid-noevents": "^6.24.1",
Expand Down
2 changes: 1 addition & 1 deletion src/command-parser/parserConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -361,7 +361,7 @@ export const parserConfig = {
},
'--reward-address-signing-key': {
action: 'append',
required: true,
required: false,
dest: 'rewardAddressSigningKeyData',
type: (path: string) => parseHwSigningFile(path),
help: 'Input filepath of the reward address signing file.',
Expand Down
80 changes: 57 additions & 23 deletions src/crypto-providers/ledgerCryptoProvider.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import * as TxTypes from 'cardano-hw-interop-lib'
import Ledger, * as LedgerTypes from '@cardano-foundation/ledgerjs-hw-app-cardano'
import type Transport from '@ledgerhq/hw-transport'
import {
GovernanceVotingDelegationType,
TxOutputDestination,
} from '@cardano-foundation/ledgerjs-hw-app-cardano'
import { parseBIP32Path } from '../command-parser/parsers'
import { Errors } from '../errors'
import { isChainCodeHex, isPubKeyHex, isXPubKeyHex } from '../guards'
Expand All @@ -25,7 +29,6 @@ import {
NativeScriptType,
Network,
ParsedShowAddressArguments,
VotePublicKeyHex,
XPubKeyHex,
} from '../types'
import { partition } from '../util'
Expand All @@ -52,8 +55,6 @@ 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 @@ -729,24 +730,36 @@ export const LedgerCryptoProvider: (transport: Transport) => Promise<CryptoProvi
return createWitnesses(ledgerWitnesses, params.hwSigningFileData)
}

const prepareVoteDelegations = (
delegations: GovernanceVotingDelegation[],
): LedgerTypes.GovernanceVotingDelegation[] => (
delegations.map(({ votePublicKey, voteWeight }) => {
if (Number(voteWeight) > Number.MAX_SAFE_INTEGER) {
throw Error(Errors.InvalidGovernanceVotingWeight)
}
return {
// TODO what about using a path from signing files instead of the key?
type: GovernanceVotingDelegationType.KEY,
// TODO vote vs. voting in names
votingPublicKeyHex: votePublicKey,
weight: Number(voteWeight),
}
})
)

const prepareVoteAuxiliaryData = (
delegations: GovernanceVotingDelegation[],
hwStakeSigningFile: HwSigningData,
votingPublicKeyHex: VotePublicKeyHex,
addressParameters: _AddressParameters,
rewardsDestination: TxOutputDestination,
nonce: BigInt,
votingPurpose: BigInt,
): LedgerTypes.TxAuxiliaryData => ({
type: LedgerTypes.TxAuxiliaryDataType.CATALYST_REGISTRATION,
type: LedgerTypes.TxAuxiliaryDataType.GOVERNANCE_VOTING_REGISTRATION,
params: {
votingPublicKeyHex, // TODO update to cip36
format: LedgerTypes.GovernanceVotingRegistrationFormat.CIP_36,
delegations: prepareVoteDelegations(delegations),
stakingPath: hwStakeSigningFile.path,
rewardsDestination: {
type: addressParameters.addressType,
params: {
spendingPath: addressParameters.paymentPath as BIP32Path,
stakingPath: addressParameters.stakePath as BIP32Path,
},
},
rewardsDestination,
nonce: `${nonce}`,
votingPurpose: `${votingPurpose}`,
},
Expand Down Expand Up @@ -801,29 +814,50 @@ export const LedgerCryptoProvider: (transport: Transport) => Promise<CryptoProvi
rewardAddressSigningFiles: HwSigningData[],
): Promise<VotingRegistrationMetaDataCborHex> => {
const { data: address } : { data: Buffer } = bech32.decode(rewardAddressBech32)

let destination: TxOutputDestination
const addressParams = getAddressParameters(rewardAddressSigningFiles, address, network)
if (!addressParams) {
throw Error(Errors.AuxSigningFileNotFoundForVotingRewardAddress)
if (addressParams) {
validateVotingRegistrationAddressType(addressParams.addressType)
destination = {
type: LedgerTypes.TxOutputDestinationType.DEVICE_OWNED,
params: {
type: addressParams.addressType,
params: {
spendingPath: addressParams.paymentPath as BIP32Path,
stakingPath: addressParams.stakePath as BIP32Path,
},
},
}
} else {
destination = {
type: LedgerTypes.TxOutputDestinationType.THIRD_PARTY,
params: {
addressHex: address.toString('hex'),
},
}
}

validateVotingRegistrationAddressType(addressParams.addressType)

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

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

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

Expand Down
9 changes: 8 additions & 1 deletion src/crypto-providers/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
KeyHash,
ScriptHash,
} from 'cardano-hw-interop-lib'
import { GOVERNANCE_VOTING_PURPOSE_CATALYST, HARDENED_THRESHOLD } from '../constants'
import { HARDENED_THRESHOLD } from '../constants'
import { Errors } from '../errors'
import { isBIP32Path, isPubKeyHex } from '../guards'
import {
Expand Down Expand Up @@ -364,6 +364,13 @@ const _packRewardAddress = (
}
}

/*
* Turns binary address into address parameters. Useful for nicer UI:
* HW wallets can show key derivation paths etc.
*
* If there is not enough signing data (e.g. when the address is third-party),
* returns null.
*/
const getAddressParameters = (
hwSigningData: HwSigningData[],
address: Buffer,
Expand Down
2 changes: 1 addition & 1 deletion src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ const enum Errors {
InvalidGovernanceVotingPublicKey = 'Invalid governance vote public key',
InvalidGovernanceVotingWeight = 'Invalid governance vote weight',
InvalidGovernanceVotingDelegations = 'Invalid governance voting delegations (either a single vote public key or several vote public keys with their weights are expected)',
AuxSigningFileNotFoundForVotingRewardAddress = 'Voting rewards payment address doesn\'t match with supplied auxiliary signing keys',
AuxSigningFileNotFoundForVotingRewardAddress = 'Voting rewards payment address doesn\'t match with supplied auxiliary signing keys --- Trezor does not support third-party reward addresses yet', // TODO should be removed after the support is added
ByronSigningFilesFoundInVotingRegistration = 'Byron addresses are not allowed for voting registration',
TrezorVersionError = 'Failed to retrieve trezor version',
InvalidVotingRegistrationAddressType = 'Voting registration address type must be either BASE or REWARD',
Expand Down
76 changes: 59 additions & 17 deletions test/integration/ledger/node/votingRegistration.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,43 +7,85 @@ const { signingFiles } = require('./signingFiles')
const { addresses } = require('./addresses')
const { getTransport } = require('./speculos')

// TODO add cip36 ones
const votingRegistrations = {
withTestnetBaseAddress0: {
network: 'TESTNET_LEGACY',
auxiliarySigningFiles: [signingFiles.payment0, signingFiles.stake0],
delegations: [
{
votePublicKey: '3b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b7',
voteWeight: 2,
},
],
hwStakeSigningFile: signingFiles.stake0,
rewardAddressBech32: addresses.testnet.base0,
votePublicKeyHex: '3b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b7',
nonce: 165564,
signedVotingRegistrationMetaDataHex: 'a219ef64a40158203b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b702582066610efd336e1137c525937b76511fbcf2a0e6bcf0d340a67bcb39bc870d85e80358390014c16d7f43243bd81478e68b9db53a8528fd4fb1078d58d54a7f11241d227aefa4b773149170885aadba30aab3127cc611ddbc4999def61c041a000286bc19ef65a10158408a8cca0fbc26626c65eaf28608da21527c70c8f6136e3eb5166e8d665ef4f9f55e0b8063393d63bfa0104a1ac737a10c1c41a93a68d101a4cb0b405ec5bb6707',
votingPurpose: 0,
network: 'TESTNET_LEGACY',
auxiliarySigningFiles: [signingFiles.payment0, signingFiles.stake0],
signedVotingRegistrationMetaDataHex: 'a219ef64a501818258203b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b70202582066610efd336e1137c525937b76511fbcf2a0e6bcf0d340a67bcb39bc870d85e80358390014c16d7f43243bd81478e68b9db53a8528fd4fb1078d58d54a7f11241d227aefa4b773149170885aadba30aab3127cc611ddbc4999def61c041a000286bc050019ef65a101584031aafad1223d2ac49584e55cf1232221bdb2312b826fef7c901d5802f987a391f4a57b82416de34d60ee13b3a823a61aadec0b0cf09c529efe56922ec3c1ab0c',
},
withMainnetBaseAddress0: {
network: 'MAINNET',
auxiliarySigningFiles: [signingFiles.payment0, signingFiles.stake0],
delegations: [
{
votePublicKey: '3b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b7',
voteWeight: 2,
},
],
hwStakeSigningFile: signingFiles.stake0,
rewardAddressBech32: addresses.mainnet.base0,
votePublicKeyHex: '3b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b7',
nonce: 165564,
signedVotingRegistrationMetaDataHex: 'a219ef64a40158203b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b702582066610efd336e1137c525937b76511fbcf2a0e6bcf0d340a67bcb39bc870d85e80358390114c16d7f43243bd81478e68b9db53a8528fd4fb1078d58d54a7f11241d227aefa4b773149170885aadba30aab3127cc611ddbc4999def61c041a000286bc19ef65a101584031e0da1b744776cec0f1dd9a4668b0a92dd61d8ca62fb1f923f469ff8f2c3c30034ad9e4d23a22ee92984efb12dbf5bc1ec5bdb8aa9073fb03f7e994cb061004',
votingPurpose: 1,
network: 'MAINNET',
auxiliarySigningFiles: [signingFiles.payment0, signingFiles.stake0],
signedVotingRegistrationMetaDataHex: 'a219ef64a501818258203b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b70202582066610efd336e1137c525937b76511fbcf2a0e6bcf0d340a67bcb39bc870d85e80358390114c16d7f43243bd81478e68b9db53a8528fd4fb1078d58d54a7f11241d227aefa4b773149170885aadba30aab3127cc611ddbc4999def61c041a000286bc050119ef65a1015840fc76d0ef0e195b8d03a8988f7983c2d881b2fa1098762010d8a61270a779620a4a98361879344f1bc3bda7d33736ae3536d89aec65e4e6adaef15ccc90fa4704',
},
withTestnetRewardAddress0: {
network: 'TESTNET_LEGACY',
auxiliarySigningFiles: [signingFiles.stake0],
delegations: [
{
votePublicKey: '3b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b7',
voteWeight: 1,
},
],
hwStakeSigningFile: signingFiles.stake0,
rewardAddressBech32: addresses.testnet.reward0,
votePublicKeyHex: '3b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b7',
nonce: 165564,
signedVotingRegistrationMetaDataHex: 'a219ef64a40158203b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b702582066610efd336e1137c525937b76511fbcf2a0e6bcf0d340a67bcb39bc870d85e803581de01d227aefa4b773149170885aadba30aab3127cc611ddbc4999def61c041a000286bc19ef65a1015840d4401401a6f447c3e906c4dde2193f19a7c9085040c73f7941bb4cda212ada78a9c21f0b6e1afc54a34e17ecfc9e31bc46bb7836ffddf94b985e78f490159b06',
votingPurpose: 0,
network: 'TESTNET_LEGACY',
auxiliarySigningFiles: [signingFiles.stake0],
signedVotingRegistrationMetaDataHex: 'a219ef64a501818258203b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b70102582066610efd336e1137c525937b76511fbcf2a0e6bcf0d340a67bcb39bc870d85e803581de01d227aefa4b773149170885aadba30aab3127cc611ddbc4999def61c041a000286bc050019ef65a1015840b8d4e3315af77091fc78e1aef92d07121f8d767b65940ad46632d9a835d709aa2f64293beaa077c5c094e43e0bfc479717e6cc54842cdc28cecb2f34ec315507',
},
withMainnetRewardAddress0: {
withMultipleDelegations: {
delegations: [
{
votePublicKey: '3b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b7',
voteWeight: 1,
},
{
votePublicKey: '3eadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef',
voteWeight: 3,
},
],
hwStakeSigningFile: signingFiles.stake0,
rewardAddressBech32: addresses.mainnet.reward0,
nonce: 165564,
votingPurpose: 0,
network: 'MAINNET',
auxiliarySigningFiles: [signingFiles.stake0],
signedVotingRegistrationMetaDataHex: 'a219ef64a501828258203b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b7018258203eadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef0302582066610efd336e1137c525937b76511fbcf2a0e6bcf0d340a67bcb39bc870d85e803581de11d227aefa4b773149170885aadba30aab3127cc611ddbc4999def61c041a000286bc050019ef65a10158401500c4a09b763d5d396933f264d93e51e2aaf85ea41958d3425f1248c72b9d75215857eec227a21a14341efab85ac68882ed246a124398c9f9645b1921cd460c',
},
withThirdPartyRewardAddress: {
delegations: [
{
votePublicKey: '3b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b7',
voteWeight: 2,
},
],
hwStakeSigningFile: signingFiles.stake0,
rewardAddressBech32: addresses.mainnet.reward0,
votePublicKeyHex: '3b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b7',
rewardAddressBech32: addresses.mainnet.base0,
nonce: 165564,
signedVotingRegistrationMetaDataHex: 'a219ef64a40158203b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b702582066610efd336e1137c525937b76511fbcf2a0e6bcf0d340a67bcb39bc870d85e803581de11d227aefa4b773149170885aadba30aab3127cc611ddbc4999def61c041a000286bc19ef65a1015840269e36e855a7200606be820f369f83ee4e942594900ca20c8a28ef4a21ed77fd17bba7236096a7873ef026cc912708d9014a436f074fe300e86a9c1e4da52909',
votingPurpose: 1,
network: 'MAINNET',
auxiliarySigningFiles: [],
signedVotingRegistrationMetaDataHex: 'a219ef64a501818258203b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b70202582066610efd336e1137c525937b76511fbcf2a0e6bcf0d340a67bcb39bc870d85e80358390114c16d7f43243bd81478e68b9db53a8528fd4fb1078d58d54a7f11241d227aefa4b773149170885aadba30aab3127cc611ddbc4999def61c041a000286bc050119ef65a1015840fc76d0ef0e195b8d03a8988f7983c2d881b2fa1098762010d8a61270a779620a4a98361879344f1bc3bda7d33736ae3536d89aec65e4e6adaef15ccc90fa4704',
},
}

Expand Down
7 changes: 3 additions & 4 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,9 @@
"@babel/helper-validator-identifier" "^7.18.6"
to-fast-properties "^2.0.0"

"@cardano-foundation/ledgerjs-hw-app-cardano@^5.1.0":
version "5.1.0"
resolved "https://registry.yarnpkg.com/@cardano-foundation/ledgerjs-hw-app-cardano/-/ledgerjs-hw-app-cardano-5.1.0.tgz#69b94f7c5055bdb19aa719ff277fa5f683d11067"
integrity sha512-ucuz/XbS/0ZD0Bal/GI/kiTm9jDIl8J+A7ypEqcAcBDGicFsyWmtPotOTwuDovTsiM8+eA/5OGTFX0oRqzxstQ==
"@cardano-foundation/ledgerjs-hw-app-cardano@https://github.com/vacuumlabs/ledgerjs-cardano-shelley/releases/download/v6.0.0-rc1/cardano-foundation-ledgerjs-hw-app-cardano-v6.0.0-rc1.tgz":
version "6.0.0-rc1"
resolved "https://github.com/vacuumlabs/ledgerjs-cardano-shelley/releases/download/v6.0.0-rc1/cardano-foundation-ledgerjs-hw-app-cardano-v6.0.0-rc1.tgz#9c104f1c85762a13ed50ef1f4563a151f85dd216"
dependencies:
"@ledgerhq/hw-transport" "^5.12.0"
"@types/ledgerhq__hw-transport" "^4.21.3"
Expand Down

0 comments on commit bdd7dde

Please sign in to comment.