Skip to content

Commit

Permalink
Ledger wallet Catalyst voting registration
Browse files Browse the repository at this point in the history
  • Loading branch information
yushih committed May 23, 2021
1 parent 2d65bd0 commit 50f0937
Show file tree
Hide file tree
Showing 13 changed files with 2,690 additions and 162 deletions.
21 changes: 14 additions & 7 deletions packages/yoroi-extension/app/api/ada/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,16 @@ export type CreateVotingRegTxRequest = {|
...CreateVotingRegTxRequestCommon,
trezorTWallet: {|
votingPublicKey: string,
nonce: number,
|}
|} | {|
...CreateVotingRegTxRequestCommon,
ledgerNanoWallet: {|
votingPublicKey: string,
stakingKeyPath: Array<number>,
stakingKey: string,
rewardAddress: string,
nonce: number,
|}
|};

Expand Down Expand Up @@ -1412,7 +1422,7 @@ export default class AdaApi {
throw new Error(`${nameof(this.createVotingRegTx)} no internal addresses left. Should never happen`);
}
let trxMetadata;
if (request.trezorTWallet) {
if (request.trezorTWallet || request.ledgerNanoWallet) {
trxMetadata = undefined;
} else {
// Mnemonic wallet
Expand Down Expand Up @@ -1452,12 +1462,9 @@ export default class AdaApi {
wits: new Set(),
},
trezorTCatalystRegistrationTxSignData:
request.trezorTWallet ?
{
votingPublicKey: request.trezorTWallet.votingPublicKey,
nonce: request.absSlotNumber,
} :
undefined,
request.trezorTWallet ? request.trezorTWallet : undefined,
ledgerNanoCatalystRegistrationTxSignData:
request.ledgerNanoWallet ? request.ledgerNanoWallet: undefined,
});
} catch (error) {
Logger.error(`${nameof(AdaApi)}::${nameof(this.createVotingRegTx)} error: ` + stringifyError(error));
Expand Down
49 changes: 35 additions & 14 deletions packages/yoroi-extension/app/api/ada/lib/cardanoCrypto/catalyst.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,21 @@ export const CatalystLabels = Object.freeze({
DATA: 61284,
SIG: 61285,
});
export function generateRegistration(request: {|
stakePrivateKey: RustModule.WalletV4.PrivateKey,
catalystPrivateKey: RustModule.WalletV4.PrivateKey,
receiverAddress: Buffer,
slotNumber: number,
|}): RustModule.WalletV4.GeneralTransactionMetadata {

function prefix0x(hex: string): string {
if (hex.startsWith('0x')) {
return hex;
}
return '0x' + hex;
}

export function generateRegistrationMetadata(
votingPublicKey: string,
stakingPublicKey: string,
rewardAddress: string,
nonce: number,
signer: Uint8Array => string,
): RustModule.WalletV4.GeneralTransactionMetadata {

/**
* Catalyst follows a certain standard to prove the voting power
Expand All @@ -32,10 +41,10 @@ export function generateRegistration(request: {|

const registrationData = RustModule.WalletV4.encode_json_str_to_metadatum(
JSON.stringify({
'1': `0x${Buffer.from(request.catalystPrivateKey.to_public().as_bytes()).toString('hex')}`,
'2': `0x${Buffer.from(request.stakePrivateKey.to_public().as_bytes()).toString('hex')}`,
'3': `0x${Buffer.from(request.receiverAddress).toString('hex')}`,
'4': request.slotNumber,
'1': prefix0x(votingPublicKey),
'2': prefix0x(stakingPublicKey),
'3': prefix0x(rewardAddress),
'4': nonce,
}),
RustModule.WalletV4.MetadataJsonSchema.BasicConversions
);
Expand All @@ -48,19 +57,31 @@ export function generateRegistration(request: {|
const hashedMetadata = blake2b(256 / 8).update(
generalMetadata.to_bytes()
).digest('binary');
const catalystSignature = request.stakePrivateKey
.sign(hashedMetadata)
.to_hex();

generalMetadata.insert(
RustModule.WalletV4.BigNum.from_str(CatalystLabels.SIG.toString()),
RustModule.WalletV4.encode_json_str_to_metadatum(
JSON.stringify({
'1': `0x${catalystSignature}`,
'1': prefix0x(signer(hashedMetadata)),
}),
RustModule.WalletV4.MetadataJsonSchema.BasicConversions
)
);

return generalMetadata;
}

export function generateRegistration(request: {|
stakePrivateKey: RustModule.WalletV4.PrivateKey,
catalystPrivateKey: RustModule.WalletV4.PrivateKey,
receiverAddress: Buffer,
slotNumber: number,
|}): RustModule.WalletV4.GeneralTransactionMetadata {
return generateRegistrationMetadata(
Buffer.from(request.catalystPrivateKey.to_public().as_bytes()).toString('hex'),
Buffer.from(request.stakePrivateKey.to_public().as_bytes()).toString('hex'),
Buffer.from(request.receiverAddress).toString('hex'),
request.slotNumber,
(hashedMetadata) => request.stakePrivateKey.sign(hashedMetadata).to_hex(),
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,14 @@ type NetworkSettingSnapshot = {|
+KeyDeposit: BigNumber,
|};

type LedgerNanoCatalystRegistrationTxSignData = {|
votingPublicKey: string,
stakingKeyPath: Array<number>,
stakingKey: string,
rewardAddress: string,
nonce: number,
|};

export class HaskellShelleyTxSignRequest
implements ISignRequest<RustModule.WalletV4.TransactionBuilder> {

Expand All @@ -47,8 +55,10 @@ implements ISignRequest<RustModule.WalletV4.TransactionBuilder> {
|};
trezorTCatalystRegistrationTxSignData: void | {|
votingPublicKey: string,
nonce: BigNumber,
nonce: number,
|};
ledgerNanoCatalystRegistrationTxSignData:
void | LedgerNanoCatalystRegistrationTxSignData;

constructor(data: {|
senderUtxos: Array<CardanoAddressedUtxo>,
Expand All @@ -62,8 +72,10 @@ implements ISignRequest<RustModule.WalletV4.TransactionBuilder> {
|},
trezorTCatalystRegistrationTxSignData?: void | {|
votingPublicKey: string,
nonce: BigNumber,
nonce: number,
|};
ledgerNanoCatalystRegistrationTxSignData?:
void | LedgerNanoCatalystRegistrationTxSignData;
|}) {
this.senderUtxos = data.senderUtxos;
this.unsignedTx = data.unsignedTx;
Expand All @@ -73,6 +85,8 @@ implements ISignRequest<RustModule.WalletV4.TransactionBuilder> {
this.neededStakingKeyHashes = data.neededStakingKeyHashes;
this.trezorTCatalystRegistrationTxSignData =
data.trezorTCatalystRegistrationTxSignData;
this.ledgerNanoCatalystRegistrationTxSignData =
data.ledgerNanoCatalystRegistrationTxSignData;
}

txId(): string {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,13 @@ import type {
Address, Value, Addressing,
} from '../../lib/storage/models/PublicDeriver/interfaces';
import { HaskellShelleyTxSignRequest } from './HaskellShelleyTxSignRequest';
import { AddressType, CertificateType, TransactionSigningMode, TxOutputDestinationType, } from '@cardano-foundation/ledgerjs-hw-app-cardano';
import {
AddressType,
CertificateType,
TransactionSigningMode,
TxOutputDestinationType,
TxAuxiliaryDataType,
} from '@cardano-foundation/ledgerjs-hw-app-cardano';
import { RustModule } from '../../lib/cardanoCrypto/rustLoader';
import { toHexOrBase58 } from '../../lib/storage/bridge/utils';
import {
Expand Down Expand Up @@ -75,6 +81,28 @@ export async function createLedgerSignTxPayload(request: {|
}

const ttl = txBody.ttl();

let auxiliaryData = undefined;
if (request.signRequest.ledgerNanoCatalystRegistrationTxSignData) {
const { votingPublicKey, stakingKeyPath, nonce } =
request.signRequest.ledgerNanoCatalystRegistrationTxSignData;

auxiliaryData = {
type: TxAuxiliaryDataType.CATALYST_REGISTRATION,
params: {
votingPublicKeyHex: votingPublicKey.replace(/^0x/, ''),
stakingPath: stakingKeyPath,
rewardsDestination: {
type: AddressType.REWARD,
params: {
stakingPath: stakingKeyPath,
},
},
nonce,
}
};
}

return {
signingMode: TransactionSigningMode.ORDINARY_TRANSACTION,
tx: {
Expand All @@ -88,7 +116,7 @@ export async function createLedgerSignTxPayload(request: {|
},
withdrawals: ledgerWithdrawal.length === 0 ? null : ledgerWithdrawal,
certificates: ledgerCertificates.length === 0 ? null : ledgerCertificates,
auxiliaryData: undefined,
auxiliaryData,
validityIntervalStart: undefined,
}
};
Expand Down Expand Up @@ -358,7 +386,7 @@ export function toLedgerAddressParameters(request: {|
return {
type: AddressType.REWARD,
params: {
spendingPath: request.path, // reward addresses use spending path
stakingPath: request.path, // reward addresses use spending path
},
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ test('Generate address parameters', async () => {
})).toEqual(({
type: AddressType.REWARD,
params: {
spendingPath: stakingKeyPath,
stakingPath: stakingKeyPath,
}
}: DeviceOwnedAddress));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ export default class VotingPage extends Component<Props> {
if(selected == null){
throw new Error(`${nameof(VotingPage)} no wallet selected`);
}

if (selected.getParent().getWalletType() === WalletTypeOption.HARDWARE_WALLET) {
return <UnsupportedWallet />;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ export default class HWVerifyAddressStore extends Store<StoresMap, ActionsMap> {
try {
this.ledgerConnect = new LedgerConnect({
locale: this.stores.profile.currentLocale,
connectorUrl: 'https://emurgo.github.io/yoroi-extension-ledger-connect-vnext/#/v3',
connectorUrl: 'https://emurgo.github.io/yoroi-extension-ledger-connect-vnext/catalyst/#/v3.1',
});
await prepareLedgerConnect(this.ledgerConnect);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ export default class LedgerConnectStore
try {
const ledgerConnect = new LedgerConnect({
locale: this.stores.profile.currentLocale,
connectorUrl: 'https://emurgo.github.io/yoroi-extension-ledger-connect-vnext/#/v3',
connectorUrl: 'https://emurgo.github.io/yoroi-extension-ledger-connect-vnext/catalyst/#/v3.1',
});
this.ledgerConnect = ledgerConnect;
await prepareLedgerConnect(ledgerConnect);
Expand Down
57 changes: 44 additions & 13 deletions packages/yoroi-extension/app/stores/ada/VotingStore.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,14 @@ import {
asHasUtxoChains,
asGetSigningKey,
asGetAllAccounting,
asGetStakingKey,
} from '../../api/ada/lib/storage/models/PublicDeriver/traits';
import {
isCardanoHaskell,
getCardanoHaskellBaseConfig,
} from '../../api/ada/lib/storage/database/prepackaged/networks';
import { genTimeToSlot } from '../../api/ada/lib/storage/bridge/timeUtils';
import { unwrapStakingKey } from '../../api/ada/lib/storage/bridge/utils';
import { generatePrivateKeyForCatalyst } from '../../api/ada/lib/cardanoCrypto/cryptoWallet';
import {
isLedgerNanoWallet,
Expand Down Expand Up @@ -226,18 +228,50 @@ export default class VotingStore extends Store<StoresMap, ActionsMap> {
throw new Error(`${nameof(this._createTransaction)} should never happen`);
}

const config = fullConfig.reduce((acc, next) => Object.assign(acc, next), {});
const nonce = timeToSlot({ time: new Date() }).slot;

let votingRegTxPromise;

if (isTrezorTWallet(publicDeriver.getParent())) {
if (
publicDeriver.getParent().getWalletType() === WalletTypeOption.HARDWARE_WALLET
) {
const votingPublicKey = `0x${Buffer.from(catalystPrivateKey.to_public().as_bytes()).toString('hex')}`;

votingRegTxPromise = this.createVotingRegTx.execute({
publicDeriver: withHasUtxoChains,
absSlotNumber,
trezorTWallet: {
votingPublicKey
},
}).promise;
if (isTrezorTWallet(publicDeriver.getParent())) {
votingRegTxPromise = this.createVotingRegTx.execute({
publicDeriver: withHasUtxoChains,
absSlotNumber,
trezorTWallet: { votingPublicKey, nonce },
}).promise;
} else if (isLedgerNanoWallet(publicDeriver.getParent())) {
const withStakingKey = asGetStakingKey(publicDeriver);
if (!withStakingKey) {
throw new Error(`${nameof(this._createTransaction)} can't get staking key`);
}
const stakingKeyResp = await withStakingKey.getStakingKey();
const stakingKey = unwrapStakingKey(stakingKeyResp.addr.Hash);

const rewardAddress = RustModule.WalletV4.RewardAddress.new(
Number.parseInt(config.ChainNetworkId, 10),
stakingKey,
);

votingRegTxPromise = this.createVotingRegTx.execute({
publicDeriver: withHasUtxoChains,
absSlotNumber,
ledgerNanoWallet: {
votingPublicKey,
stakingKeyPath: stakingKeyResp.addressing.path,
stakingKey: Buffer.from(stakingKey.to_bytes()).toString('hex'),
rewardAddress: Buffer.from(rewardAddress.to_address().to_bytes()).toString('hex'),
nonce,
},
}).promise;
} else {
throw new Error(`${nameof(this._createTransaction)} unexpected hardware wallet type`);
}

} else if (
publicDeriver.getParent().getWalletType() === WalletTypeOption.WEB_WALLET
) {
Expand All @@ -258,7 +292,7 @@ export default class VotingStore extends Store<StoresMap, ActionsMap> {
publicDeriver: withStakingKey,
password: spendingPassword,
});
const config = fullConfig.reduce((acc, next) => Object.assign(acc, next), {});

const rewardAddress = RustModule.WalletV4.RewardAddress.new(
Number.parseInt(config.ChainNetworkId, 10),
RustModule.WalletV4.StakeCredential.from_keyhash(stakingKey.to_public().hash()),
Expand All @@ -268,10 +302,7 @@ export default class VotingStore extends Store<StoresMap, ActionsMap> {
stakePrivateKey: stakingKey,
catalystPrivateKey,
receiverAddress: Buffer.from(rewardAddress.to_address().to_bytes()),
slotNumber: timeToSlot({
// add current slot to registration to avoid replay attacks
time: new Date(),
}).slot,
slotNumber: nonce,
});

votingRegTxPromise = this.createVotingRegTx.execute({
Expand Down
Loading

0 comments on commit 50f0937

Please sign in to comment.