Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

NativeScript signing #1599

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages/key-management/src/InMemoryKeyAgent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ export class InMemoryKeyAgent extends KeyAgentBase implements KeyAgent {

async signTransaction(
txBody: Serialization.TransactionBody,
{ txInKeyPathMap, knownAddresses }: SignTransactionContext,
{ txInKeyPathMap, knownAddresses, scripts }: SignTransactionContext,
{ additionalKeyPaths = [] }: SignTransactionOptions = {}
): Promise<Cardano.Signatures> {
// Possible optimization is casting strings to OpaqueString types directly and skipping validation
Expand All @@ -143,7 +143,7 @@ export class InMemoryKeyAgent extends KeyAgentBase implements KeyAgent {
const dRepKeyHash = (
await Crypto.Ed25519PublicKey.fromHex(await this.derivePublicKey(DREP_KEY_DERIVATION_PATH)).hash()
).hex();
const derivationPaths = ownSignatureKeyPaths(body, knownAddresses, txInKeyPathMap, dRepKeyHash);
const derivationPaths = ownSignatureKeyPaths(body, knownAddresses, txInKeyPathMap, dRepKeyHash, scripts);
const keyPaths = uniqBy([...derivationPaths, ...additionalKeyPaths], ({ role, index }) => `${role}.${index}`);
// TODO:
// if (keyPaths.length === 0) {
Expand Down
1 change: 1 addition & 0 deletions packages/key-management/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ export interface SignTransactionContext {
handleResolutions?: HandleResolution[];
dRepKeyHashHex?: Crypto.Ed25519KeyHashHex;
sender?: MessageSender;
scripts?: Cardano.Script[];
}

export type SignDataContext = Cip8SignDataContext & { sender?: MessageSender };
Expand Down
81 changes: 79 additions & 2 deletions packages/key-management/src/util/ownSignatureKeyPaths.ts
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,81 @@ const getRequiredSignersKeyPaths = (
return paths;
};

const checkStakeCredential = (address: GroupedAddress, keyHash: Crypto.Ed25519KeyHashHex): SignatureCheck =>
address.stakeKeyDerivationPath &&
Cardano.RewardAccount.toHash(address.rewardAccount) === Crypto.Hash28ByteBase16.fromEd25519KeyHashHex(keyHash)
? { derivationPaths: [address.stakeKeyDerivationPath], requiresForeignSignatures: false }
: { derivationPaths: [], requiresForeignSignatures: true };

const checkPaymentCredential = (address: GroupedAddress, keyHash: Crypto.Ed25519KeyHashHex): SignatureCheck => {
const paymentCredential = Cardano.Address.fromBech32(address.address)?.asBase()?.getPaymentCredential();
return paymentCredential?.type === Cardano.CredentialType.KeyHash &&
paymentCredential.hash === Crypto.Hash28ByteBase16.fromEd25519KeyHashHex(keyHash)
? {
derivationPaths: [{ index: address.index, role: Number(address.type) }],
requiresForeignSignatures: false
}
: { derivationPaths: [], requiresForeignSignatures: true };
};

const combineSignatureChecks = (a: SignatureCheck, b: SignatureCheck): SignatureCheck => ({
derivationPaths: [...a.derivationPaths, ...b.derivationPaths],
requiresForeignSignatures: a.requiresForeignSignatures || b.requiresForeignSignatures
});

const processSignatureScript = (
script: Cardano.RequireSignatureScript,
groupedAddresses: GroupedAddress[]
): SignatureCheck => {
let signatureCheck: SignatureCheck = { derivationPaths: [], requiresForeignSignatures: false };

for (const address of groupedAddresses) {
if (address.stakeKeyDerivationPath) {
signatureCheck = checkStakeCredential(address, script.keyHash);
}
signatureCheck = combineSignatureChecks(signatureCheck, checkPaymentCredential(address, script.keyHash));
}

return signatureCheck;
};

const getNativeScriptKeyPaths = (
groupedAddresses: GroupedAddress[],
nativeScripts?: Cardano.Script[]
): SignatureCheck => {
const signatureCheck: SignatureCheck = { derivationPaths: [], requiresForeignSignatures: false };
if (!nativeScripts?.length) return signatureCheck;

const processScript = (script: Cardano.Script): SignatureCheck => {
if (!Cardano.isNativeScript(script)) {
return { derivationPaths: [], requiresForeignSignatures: false };
}

switch (script.kind) {
case Cardano.NativeScriptKind.RequireSignature: {
return processSignatureScript(script as Cardano.RequireSignatureScript, groupedAddresses);
}
case Cardano.NativeScriptKind.RequireAllOf:
case Cardano.NativeScriptKind.RequireAnyOf:
case Cardano.NativeScriptKind.RequireNOf: {
const scriptWithScripts = script as Cardano.RequireAllOfScript | Cardano.RequireAnyOfScript;
return scriptWithScripts.scripts.reduce<SignatureCheck>(
(acc, subScript) => combineSignatureChecks(acc, processScript(subScript)),
{ derivationPaths: [], requiresForeignSignatures: false }
);
}
case Cardano.NativeScriptKind.RequireTimeBefore:
case Cardano.NativeScriptKind.RequireTimeAfter:
return { derivationPaths: [], requiresForeignSignatures: false };
}
};

return nativeScripts.reduce<SignatureCheck>(
(acc, script) => combineSignatureChecks(acc, processScript(script)),
signatureCheck
);
};

/** Check if there are certificates that require DRep credentials and if we own them */
export const getDRepCredentialKeyPaths = ({
dRepKeyHash,
Expand Down Expand Up @@ -357,7 +432,8 @@ export const ownSignatureKeyPaths = (
txBody: Cardano.TxBody,
knownAddresses: GroupedAddress[],
txInKeyPathMap: TxInKeyPathMap,
dRepKeyHash?: Crypto.Ed25519KeyHashHex
dRepKeyHash?: Crypto.Ed25519KeyHashHex,
scripts?: Cardano.Script[]
): AccountKeyDerivationPath[] => {
// TODO: add `proposal_procedure` witnesses.

Expand All @@ -368,7 +444,8 @@ export const ownSignatureKeyPaths = (
...getStakeCredentialKeyPaths(knownAddresses, txBody).derivationPaths,
...getDRepCredentialKeyPaths({ dRepKeyHash, txBody }).derivationPaths,
...getRequiredSignersKeyPaths(knownAddresses, txBody.requiredExtraSignatures),
...getVotingProcedureKeyPaths({ dRepKeyHash, groupedAddresses: knownAddresses, txBody }).derivationPaths
...getVotingProcedureKeyPaths({ dRepKeyHash, groupedAddresses: knownAddresses, txBody }).derivationPaths,
...getNativeScriptKeyPaths(knownAddresses, scripts).derivationPaths
],
isEqual
);
Expand Down
4 changes: 2 additions & 2 deletions packages/key-management/src/util/stubSignTransaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,15 @@ export interface StubSignTransactionProps {

export const stubSignTransaction = async ({
txBody,
context: { knownAddresses, txInKeyPathMap, dRepKeyHashHex: dRepKeyHash },
context: { knownAddresses, txInKeyPathMap, dRepKeyHashHex: dRepKeyHash, scripts },
signTransactionOptions: { extraSigners, additionalKeyPaths = [] } = {}
}: StubSignTransactionProps): Promise<Cardano.Signatures> => {
const mockSignature = Crypto.Ed25519SignatureHex(
// eslint-disable-next-line max-len
'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'
);
const signatureKeyPaths = uniqWith(
[...ownSignatureKeyPaths(txBody, knownAddresses, txInKeyPathMap, dRepKeyHash), ...additionalKeyPaths],
[...ownSignatureKeyPaths(txBody, knownAddresses, txInKeyPathMap, dRepKeyHash, scripts), ...additionalKeyPaths],
deepEquals
);

Expand Down
3 changes: 2 additions & 1 deletion packages/key-management/test/InMemoryKeyAgent.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,8 @@ describe('InMemoryKeyAgent', () => {
knownAddresses,
txInKeyPathMap
});
expect(ownSignatureKeyPaths).toBeCalledWith(body.toCore(), knownAddresses, txInKeyPathMap, expect.anything());
const expectedArgs = [body.toCore(), knownAddresses, txInKeyPathMap, expect.anything(), undefined] as const;
expect(ownSignatureKeyPaths).toBeCalledWith(...expectedArgs);
expect(witnessSet.size).toBe(2);
expect(typeof [...witnessSet.values()][0]).toBe('string');
});
Expand Down
74 changes: 65 additions & 9 deletions packages/key-management/test/util/ownSignaturePaths.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,32 +16,45 @@ const toStakeCredential = (stakeKeyHash: Crypto.Hash28ByteBase16): Cardano.Crede
type: Cardano.CredentialType.KeyHash
});

const createBaseGroupedAddress = (
address: Cardano.PaymentAddress,
rewardAccount: Cardano.RewardAccount,
type: AddressType,
index: number
) => ({
address,
index,
rewardAccount,
type
});

const createGroupedAddress = (
address: Cardano.PaymentAddress,
rewardAccount: Cardano.RewardAccount,
type: AddressType,
index: number,
stakeKeyDerivationPath: AccountKeyDerivationPath
// eslint-disable-next-line max-params
): GroupedAddress =>
) =>
({
address,
index,
rewardAccount,
stakeKeyDerivationPath,
type
...createBaseGroupedAddress(address, rewardAccount, type, index),
stakeKeyDerivationPath
} as GroupedAddress);

describe('KeyManagement.util.ownSignaturePaths', () => {
const ownRewardAccount = Cardano.RewardAccount('stake_test1uqfu74w3wh4gfzu8m6e7j987h4lq9r3t7ef5gaw497uu85qsqfy27');
const createTestAddresses = () => {
const otherRewardAccount = Cardano.RewardAccount('stake_test1uqrw9tjymlm8wrwq7jk68n6v7fs9qz8z0tkdkve26dylmfc2ux2hj');
const ownRewardAccount = Cardano.RewardAccount('stake_test1uqfu74w3wh4gfzu8m6e7j987h4lq9r3t7ef5gaw497uu85qsqfy27');
const address1 = Cardano.PaymentAddress(
'addr_test1qra788mu4sg8kwd93ns9nfdh3k4ufxwg4xhz2r3n064tzfgxu2hyfhlkwuxupa9d5085eunq2qywy7hvmvej456flkns6cy45x'
);
const address2 = Cardano.PaymentAddress(
'addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3jcu5d8ps7zex2k2xt3uqxgjqnnj83ws8lhrn648jjxtwq2ytjqp'
);

return { address1, address2, otherRewardAccount, ownRewardAccount };
};

describe('KeyManagement.util.ownSignaturePaths', () => {
const { address1, address2, ownRewardAccount, otherRewardAccount } = createTestAddresses();
const ownStakeKeyHash = Cardano.RewardAccount.toHash(ownRewardAccount);
const ownStakeCredential = {
hash: ownStakeKeyHash,
Expand Down Expand Up @@ -578,4 +591,47 @@ describe('KeyManagement.util.ownSignaturePaths', () => {
]);
});
});

describe('Native scripts', () => {
it('includes derivation paths from native scripts when scripts are provided', async () => {
const txBody: Cardano.TxBody = {
fee: BigInt(0),
inputs: [{}, {}, {}] as Cardano.TxIn[],
outputs: []
};

const scripts: Cardano.Script[] = [
{
__type: Cardano.ScriptType.Native,
keyHash: Ed25519KeyHashHex(ownStakeKeyHash),
kind: Cardano.NativeScriptKind.RequireSignature
}
];

expect(util.ownSignatureKeyPaths(txBody, [knownAddress1], {}, undefined, scripts)).toEqual([
{
index: 0,
role: KeyRole.Stake
}
]);
});

it('does not include derivation paths from native scripts with foreign key hashes', async () => {
const txBody: Cardano.TxBody = {
fee: BigInt(0),
inputs: [{}, {}, {}] as Cardano.TxIn[],
outputs: []
};

const scripts: Cardano.Script[] = [
{
__type: Cardano.ScriptType.Native,
keyHash: Ed25519KeyHashHex(otherStakeKeyHash),
kind: Cardano.NativeScriptKind.RequireSignature
}
];

expect(util.ownSignatureKeyPaths(txBody, [knownAddress1], {}, undefined, scripts)).toEqual([]);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ describe('KeyManagement.util.stubSignTransaction', () => {
})
).size
).toBe(2);
expect(ownSignatureKeyPaths).toBeCalledWith(txBody, knownAddresses, txInKeyPathMap, dRepKeyHash);
const expectedArgs = [txBody, knownAddresses, txInKeyPathMap, dRepKeyHash, undefined] as const;
expect(ownSignatureKeyPaths).toBeCalledWith(...expectedArgs);
});
});