Skip to content

Commit e3dbfb8

Browse files
Merge pull request #724 from input-output-hk/feat/lw-6529-keyagent-update-to-support-abitrary-index-stake-key-derivation
[LW-6529]: feat: key agent 'deriveAddress' now takes an additional parameter 'stakeKeyDerivationIndex'
2 parents 4ca46de + cbfd3c1 commit e3dbfb8

File tree

14 files changed

+152
-48
lines changed

14 files changed

+152
-48
lines changed

packages/e2e/src/scripts/mnemonic.ts

+7-4
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,13 @@ import { localNetworkChainId } from '../util';
2626
}
2727
);
2828

29-
const derivedAddress = await keyAgentFromMnemonic.deriveAddress({
30-
index: 0,
31-
type: AddressType.External
32-
});
29+
const derivedAddress = await keyAgentFromMnemonic.deriveAddress(
30+
{
31+
index: 0,
32+
type: AddressType.External
33+
},
34+
0
35+
);
3336

3437
console.log('');
3538
console.log(` Mnemonic: ${mnemonic}`);

packages/e2e/test/local-network/register-pool.test.ts

+14-8
Original file line numberDiff line numberDiff line change
@@ -78,10 +78,13 @@ describe('local-network/register-pool', () => {
7878
const poolKeyHash = await bip32Ed25519.getPubKeyHash(poolPubKey);
7979
const poolId = Cardano.PoolId.fromKeyHash(poolKeyHash);
8080
const poolRewardAccount = (
81-
await poolKeyAgent.deriveAddress({
82-
index: 0,
83-
type: AddressType.External
84-
})
81+
await poolKeyAgent.deriveAddress(
82+
{
83+
index: 0,
84+
type: AddressType.External
85+
},
86+
0
87+
)
8588
).rewardAccount;
8689

8790
const registrationCert: Cardano.PoolRegistrationCertificate = {
@@ -160,10 +163,13 @@ describe('local-network/register-pool', () => {
160163
const poolKeyHash = await bip32Ed25519.getPubKeyHash(poolPubKey);
161164
const poolId = Cardano.PoolId.fromKeyHash(poolKeyHash);
162165
const poolRewardAccount = (
163-
await poolKeyAgent.deriveAddress({
164-
index: 0,
165-
type: AddressType.External
166-
})
166+
await poolKeyAgent.deriveAddress(
167+
{
168+
index: 0,
169+
type: AddressType.External
170+
},
171+
0
172+
)
167173
).rewardAccount;
168174

169175
const registrationCert: Cardano.PoolRegistrationCertificate = {

packages/e2e/test/long-running/cache-invalidation.test.ts

+7-4
Original file line numberDiff line numberDiff line change
@@ -59,10 +59,13 @@ describe('cache invalidation', () => {
5959
const poolKeyHash = await bip32Ed25519.getPubKeyHash(poolPubKey);
6060
const poolId = Cardano.PoolId.fromKeyHash(poolKeyHash);
6161
const poolRewardAccount = (
62-
await poolKeyAgent.deriveAddress({
63-
index: 0,
64-
type: AddressType.External
65-
})
62+
await poolKeyAgent.deriveAddress(
63+
{
64+
index: 0,
65+
type: AddressType.External
66+
},
67+
0
68+
)
6669
).rewardAccount;
6770

6871
const registrationCert: Cardano.PoolRegistrationCertificate = {

packages/governance/test/integration/cip36KeyAgents.test.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ describe('cip36', () => {
3737

3838
it('can create cip36 voting registration metadata', async () => {
3939
// Just ensuring we have some address. SingleAddressWallet already does this internally.
40-
await walletKeyAgent.deriveAddress({ index: 0, type: AddressType.External });
40+
await walletKeyAgent.deriveAddress({ index: 0, type: AddressType.External }, 0);
4141
const paymentAddress = walletKeyAgent.knownAddresses[0].address;
4242
// InMemoryKeyAgent uses this derivation path for stake key.
4343
const stakeKey = await walletKeyAgent.derivePublicKey(util.STAKE_KEY_DERIVATION_PATH);

packages/key-management/src/KeyAgentBase.ts

+18-6
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import {
1313
import { Cardano } from '@cardano-sdk/core';
1414
import { Hash28ByteBase16 } from '@cardano-sdk/crypto';
1515
import { HexBlob } from '@cardano-sdk/util';
16-
import { STAKE_KEY_DERIVATION_PATH } from './util';
1716

1817
export abstract class KeyAgentBase implements KeyAgent {
1918
readonly #serializableData: SerializableKeyAgentData;
@@ -58,8 +57,22 @@ export abstract class KeyAgentBase implements KeyAgent {
5857
/**
5958
* See https://github.com/cardano-foundation/CIPs/tree/master/CIP-1852#specification
6059
*/
61-
async deriveAddress({ index, type }: AccountAddressDerivationPath): Promise<GroupedAddress> {
62-
const knownAddress = this.knownAddresses.find((addr) => addr.type === type && addr.index === index);
60+
async deriveAddress(
61+
{ index, type }: AccountAddressDerivationPath,
62+
stakeKeyDerivationIndex: number
63+
): Promise<GroupedAddress> {
64+
const stakeKeyDerivationPath = {
65+
index: stakeKeyDerivationIndex,
66+
role: KeyRole.Stake
67+
};
68+
69+
const knownAddress = this.knownAddresses.find(
70+
(addr) =>
71+
addr.type === type &&
72+
addr.index === index &&
73+
addr.stakeKeyDerivationPath?.index === stakeKeyDerivationPath.index
74+
);
75+
6376
if (knownAddress) return knownAddress;
6477
const derivedPublicPaymentKey = await this.derivePublicKey({
6578
index,
@@ -68,8 +81,7 @@ export abstract class KeyAgentBase implements KeyAgent {
6881

6982
const derivedPublicPaymentKeyHash = await this.#bip32Ed25519.getPubKeyHash(derivedPublicPaymentKey);
7083

71-
// Possible optimization: memoize/cache stakeKeyCredential, because it's always the same
72-
const publicStakeKey = await this.derivePublicKey(STAKE_KEY_DERIVATION_PATH);
84+
const publicStakeKey = await this.derivePublicKey(stakeKeyDerivationPath);
7385
const publicStakeKeyHash = await this.#bip32Ed25519.getPubKeyHash(publicStakeKey);
7486

7587
const stakeCredential = { hash: Hash28ByteBase16(publicStakeKeyHash), type: Cardano.CredentialType.KeyHash };
@@ -88,7 +100,7 @@ export abstract class KeyAgentBase implements KeyAgent {
88100
index,
89101
networkId: this.chainId.networkId,
90102
rewardAccount: Cardano.RewardAccount(rewardAccount.toBech32()),
91-
stakeKeyDerivationPath: STAKE_KEY_DERIVATION_PATH,
103+
stakeKeyDerivationPath,
92104
type
93105
};
94106

packages/key-management/src/cip8/cip30signData.ts

+12-2
Original file line numberDiff line numberDiff line change
@@ -51,14 +51,24 @@ const getAddressBytes = (signWith: Cardano.PaymentAddress | Cardano.RewardAccoun
5151

5252
const getDerivationPath = async (signWith: Cardano.PaymentAddress | Cardano.RewardAccount, keyAgent: AsyncKeyAgent) => {
5353
const isRewardAccount = signWith.startsWith('stake');
54+
55+
const knownAddresses = await firstValueFrom(keyAgent.knownAddresses$);
56+
5457
if (isRewardAccount) {
55-
return STAKE_KEY_DERIVATION_PATH;
58+
const knownRewardAddress = knownAddresses.find(({ rewardAccount }) => rewardAccount === signWith);
59+
60+
if (!knownRewardAddress)
61+
throw new Cip30DataSignError(Cip30DataSignErrorCode.ProofGeneration, 'Unknown reward address');
62+
63+
return knownRewardAddress.stakeKeyDerivationPath || STAKE_KEY_DERIVATION_PATH;
5664
}
57-
const knownAddresses = await firstValueFrom(keyAgent.knownAddresses$);
65+
5866
const knownAddress = knownAddresses.find(({ address }) => address === signWith);
67+
5968
if (!knownAddress) {
6069
throw new Cip30DataSignError(Cip30DataSignErrorCode.ProofGeneration, 'Unknown address');
6170
}
71+
6272
return { index: knownAddress.index, role: knownAddress.type as number as KeyRole };
6373
};
6474

packages/key-management/src/types.ts

+9-2
Original file line numberDiff line numberDiff line change
@@ -160,10 +160,17 @@ export interface KeyAgent {
160160
* @throws AuthenticationError
161161
*/
162162
derivePublicKey(derivationPath: AccountKeyDerivationPath): Promise<Crypto.Ed25519PublicKeyHex>;
163+
163164
/**
164-
* @throws AuthenticationError
165+
* Derives an address from the given payment key and stake key derivation path.
166+
*
167+
* @param paymentKeyDerivationPath The payment key derivation path.
168+
* @param stakeKeyDerivationIndex The stake key index. This field is optional. If not provided it defaults to index 0.
165169
*/
166-
deriveAddress(derivationPath: AccountAddressDerivationPath): Promise<GroupedAddress>;
170+
deriveAddress(
171+
paymentKeyDerivationPath: AccountAddressDerivationPath,
172+
stakeKeyDerivationIndex: number
173+
): Promise<GroupedAddress>;
167174
/**
168175
* @throws AuthenticationError
169176
*/

packages/key-management/src/util/createAsyncKeyAgent.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ import { BehaviorSubject } from 'rxjs';
44
export const createAsyncKeyAgent = (keyAgent: KeyAgent, onShutdown?: () => void): AsyncKeyAgent => {
55
const knownAddresses$ = new BehaviorSubject(keyAgent.knownAddresses);
66
return {
7-
async deriveAddress(derivationPath) {
7+
async deriveAddress(derivationPath, stakeKeyDerivationIndex: number) {
88
const numAddresses = keyAgent.knownAddresses.length;
9-
const address = await keyAgent.deriveAddress(derivationPath);
9+
const address = await keyAgent.deriveAddress(derivationPath, stakeKeyDerivationIndex);
1010
if (keyAgent.knownAddresses.length > numAddresses) {
1111
knownAddresses$.next(keyAgent.knownAddresses);
1212
}

packages/key-management/test/InMemoryKeyAgent.test.ts

+15-9
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ describe('InMemoryKeyAgent', () => {
107107
});
108108

109109
test('deriveAddress', async () => {
110-
const address = await keyAgent.deriveAddress({ index: 1, type: AddressType.Internal });
110+
const address = await keyAgent.deriveAddress({ index: 1, type: AddressType.Internal }, 0);
111111
expect(address).toBeDefined();
112112
});
113113

@@ -257,10 +257,13 @@ describe('InMemoryKeyAgent', () => {
257257
},
258258
{ bip32Ed25519, inputResolver, logger: dummyLogger }
259259
);
260-
const derivedAddress = await keyAgentFromEncryptedKey.deriveAddress({
261-
index: 1,
262-
type: AddressType.External
263-
});
260+
const derivedAddress = await keyAgentFromEncryptedKey.deriveAddress(
261+
{
262+
index: 1,
263+
type: AddressType.External
264+
},
265+
0
266+
);
264267
expect(derivedAddress.rewardAccount).toEqual(daedelusStakeAddress);
265268
});
266269

@@ -273,10 +276,13 @@ describe('InMemoryKeyAgent', () => {
273276
},
274277
{ bip32Ed25519, inputResolver, logger: dummyLogger }
275278
);
276-
const derivedAddress = await keyAgentFromMnemonic.deriveAddress({
277-
index: 1,
278-
type: AddressType.External
279-
});
279+
const derivedAddress = await keyAgentFromMnemonic.deriveAddress(
280+
{
281+
index: 1,
282+
type: AddressType.External
283+
},
284+
0
285+
);
280286
expect(derivedAddress.rewardAccount).toEqual(daedelusStakeAddress);
281287
});
282288
});

packages/key-management/test/KeyAgentBase.test.ts

+57-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
/* eslint-disable sonarjs/no-duplicate-string */
22
import * as Crypto from '@cardano-sdk/crypto';
3-
import { AddressType, KeyAgentBase, KeyAgentType, KeyRole, SerializableInMemoryKeyAgentData } from '../src';
3+
import {
4+
AccountKeyDerivationPath,
5+
AddressType,
6+
KeyAgentBase,
7+
KeyAgentType,
8+
KeyRole,
9+
SerializableInMemoryKeyAgentData
10+
} from '../src';
411
import { CML, Cardano } from '@cardano-sdk/core';
512
import { dummyLogger } from 'ts-log';
613

@@ -52,7 +59,7 @@ describe('KeyAgentBase', () => {
5259

5360
const index = 1;
5461
const type = AddressType.External;
55-
const address = await keyAgent.deriveAddress({ index, type });
62+
const address = await keyAgent.deriveAddress({ index, type }, 0);
5663

5764
expect(address.index).toBe(index);
5865
expect(address.type).toBe(type);
@@ -64,11 +71,58 @@ describe('KeyAgentBase', () => {
6471
// creates a new array obj
6572
expect(keyAgent.knownAddresses).not.toBe(initialAddresses);
6673

67-
const sameAddress = await keyAgent.deriveAddress({ index, type });
74+
const sameAddress = await keyAgent.deriveAddress({ index, type }, 0);
6875
expect(sameAddress.address).toEqual(address.address);
6976
expect(keyAgent.knownAddresses.length).toEqual(1);
7077
});
7178

79+
test('deriveAddress derives the address with stake key of the given index', async () => {
80+
const keyMap = new Map<number, string>([
81+
[0, '0000000000000000000000000000000000000000000000000000000000000000'],
82+
[1, '1111111111111111111111111111111111111111111111111111111111111111'],
83+
[2, '2222222222222222222222222222222222222222222222222222222222222222'],
84+
[3, '3333333333333333333333333333333333333333333333333333333333333333'],
85+
[4, '4444444444444444444444444444444444444444444444444444444444444444']
86+
]);
87+
88+
keyAgent.derivePublicKey = jest.fn((x: AccountKeyDerivationPath) =>
89+
Promise.resolve(Crypto.Ed25519PublicKeyHex(keyMap.get(x.index)!))
90+
);
91+
92+
const index = 0;
93+
const type = AddressType.External;
94+
const addresses = [
95+
await keyAgent.deriveAddress({ index, type }, 0),
96+
await keyAgent.deriveAddress({ index, type }, 1),
97+
await keyAgent.deriveAddress({ index, type }, 2),
98+
await keyAgent.deriveAddress({ index, type }, 3)
99+
];
100+
101+
expect(addresses[0].stakeKeyDerivationPath).toEqual({ index: 0, role: KeyRole.Stake });
102+
expect(addresses[0].rewardAccount).toEqual('stake_test1uruaegs6djpxaj9vkn8njh9uys63jdaluetqkf5r4w95zhc8cxn3h');
103+
expect(addresses[0].address).toEqual(
104+
'addr_test1qruaegs6djpxaj9vkn8njh9uys63jdaluetqkf5r4w95zhlemj3p5myzdmy2edx089wtcfp4rymmlejkpvng82utg90s4cadlm'
105+
);
106+
107+
expect(addresses[1].stakeKeyDerivationPath).toEqual({ index: 1, role: KeyRole.Stake });
108+
expect(addresses[1].rewardAccount).toEqual('stake_test1uzx0qqs06evy77cnpk6u5q3fc50exjpp5t4s0swl2ykc4jsmh8tej');
109+
expect(addresses[1].address).toEqual(
110+
'addr_test1qruaegs6djpxaj9vkn8njh9uys63jdaluetqkf5r4w95zhuv7qpql4jcfaa3xrd4egpzn3gljdyzrghtqlqa75fd3t9qqnvgeq'
111+
);
112+
113+
expect(addresses[2].stakeKeyDerivationPath).toEqual({ index: 2, role: KeyRole.Stake });
114+
expect(addresses[2].rewardAccount).toEqual('stake_test1uqcnxxxatdgmqdmz0rhg72kn3n0egek5s0nqcvfy9ztyltc9cpuz4');
115+
expect(addresses[2].address).toEqual(
116+
'addr_test1qruaegs6djpxaj9vkn8njh9uys63jdaluetqkf5r4w95zhe3xvvd6k63kqmky78w3u4d8rxlj3ndfqlxpscjg2ykf7hs8qc48l'
117+
);
118+
119+
expect(addresses[3].stakeKeyDerivationPath).toEqual({ index: 3, role: KeyRole.Stake });
120+
expect(addresses[3].rewardAccount).toEqual('stake_test1urj8hvwxxz0t6pnfttj9ne5leu74shjlg83a8kxww9ft2fqtdhssu');
121+
expect(addresses[3].address).toEqual(
122+
'addr_test1qruaegs6djpxaj9vkn8njh9uys63jdaluetqkf5r4w95zhly0wcuvvy7h5rxjkhyt8nflneatp097s0r60vvuu2jk5jq73efq0'
123+
);
124+
});
125+
72126
test('derivePublicKey', async () => {
73127
const externalPublicKey = await keyAgent.derivePublicKey({ index: 1, role: KeyRole.External });
74128
expect(typeof externalPublicKey).toBe('string');

packages/key-management/test/cip8/cip30signData.test.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ describe('cip30signData', () => {
1515
const keyAgentReady = testKeyAgent();
1616
keyAgent = await keyAgentReady;
1717
asyncKeyAgent = await testAsyncKeyAgent(undefined, undefined, keyAgentReady);
18-
address = await asyncKeyAgent.deriveAddress(addressDerivationPath);
18+
address = await asyncKeyAgent.deriveAddress(addressDerivationPath, 0);
1919
});
2020

2121
const signAndDecode = async (signWith: Cardano.PaymentAddress | Cardano.RewardAccount) => {

packages/key-management/test/util/createAsyncKeyAgent.test.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@ describe('createAsyncKeyAgent maps KeyAgent to AsyncKeyAgent', () => {
2929
expect(await asyncKeyAgent.getChainId()).toEqual(keyAgent.chainId);
3030
});
3131
it('deriveAddress/signBlob/signTransaction are unchanged', async () => {
32-
await expect(asyncKeyAgent.deriveAddress(addressDerivationPath)).resolves.toEqual(
33-
await keyAgent.deriveAddress(addressDerivationPath)
32+
await expect(asyncKeyAgent.deriveAddress(addressDerivationPath, 0)).resolves.toEqual(
33+
await keyAgent.deriveAddress(addressDerivationPath, 0)
3434
);
3535
const keyDerivationPath = { index: 0, role: 0 };
3636
const blob = HexBlob('abc123');
@@ -48,7 +48,7 @@ describe('createAsyncKeyAgent maps KeyAgent to AsyncKeyAgent', () => {
4848
});
4949
it('knownAddresses$ is emits initial addresses and after new address derivation', async () => {
5050
await expect(firstValueFrom(asyncKeyAgent.knownAddresses$)).resolves.toEqual(keyAgent.knownAddresses);
51-
await asyncKeyAgent.deriveAddress(addressDerivationPath);
51+
await asyncKeyAgent.deriveAddress(addressDerivationPath, 0);
5252
await expect(firstValueFrom(asyncKeyAgent.knownAddresses$)).resolves.toEqual(keyAgent.knownAddresses);
5353
});
5454
it('stops emitting addresses$ after shutdown', (done) => {
@@ -59,6 +59,6 @@ describe('createAsyncKeyAgent maps KeyAgent to AsyncKeyAgent', () => {
5959
throw new Error('Should not emit');
6060
}
6161
});
62-
void asyncKeyAgent.deriveAddress(addressDerivationPath);
62+
void asyncKeyAgent.deriveAddress(addressDerivationPath, 0);
6363
});
6464
});

packages/wallet/src/SingleAddressWallet/SingleAddressWallet.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -260,7 +260,7 @@ export class SingleAddressWallet implements ObservableWallet {
260260
if (addresses.length === 0) {
261261
this.#logger.debug('No addresses available; deriving one');
262262
void keyAgent
263-
.deriveAddress({ index: 0, type: AddressType.External })
263+
.deriveAddress({ index: 0, type: AddressType.External }, 0)
264264
.catch(() => this.#logger.error('Failed to derive address'));
265265
}
266266
}

packages/wallet/test/hardware/LedgerKeyAgent.test.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,10 @@ describe('LedgerKeyAgent', () => {
3434
dependencies
3535
),
3636
createWallet: async (ledgerKeyAgent) => {
37-
const { address, rewardAccount } = await ledgerKeyAgent.deriveAddress({ index: 0, type: AddressType.External });
37+
const { address, rewardAccount } = await ledgerKeyAgent.deriveAddress(
38+
{ index: 0, type: AddressType.External },
39+
0
40+
);
3841
const assetProvider = mocks.mockAssetProvider();
3942
const stakePoolProvider = createStubStakePoolProvider();
4043
const networkInfoProvider = mocks.mockNetworkInfoProvider();

0 commit comments

Comments
 (0)