Keyper is still under development and NOT production ready. Future versions may introduce interface changes which are not backwards compatibile with this version.
Keyper is an ownership layer for the Nervos CKB blockchain.
Nervos LockScripts provide a high level of flexibility, but it can be challenging for wallets to support all the different variations. Keyper is specification for the efficient management of LockScripts. The Keyper reference implementation is written in TypeScript.
Keyper's interface specification provides developers with a standardized way to interact with any Keyper-enabled wallet. This can include mainstream wallets of any type, including integrated wallets in applications, web browser based wallets, and hardware wallets.
The Keyper project is divided into two sub-projects: specs and container.
The specs subproject contains all specification definitions and tool class support.
The container subproject is designed to support the loading of custom LockScripts within wallets.
A Key Manager is the component of a wallet responsible for managing the user's private keys. Private keys can be either raw private keys or HD wallet keys.
A Key Manager should integrate the @nervosnetwork/keyper-container module or the @nervosnetwork/keyper-container protocol interface in order to support the Keyper architecture.
interface PublicKey {
payload: Bytes
algorithm: SignatureAlgorithm
}
interface SignProvider {
sign(context: SignContext, message: Bytes): Promise<Bytes>
}
interface KeyManager {
addLockScript(lockScript: LockScript): void
addPublicKey(publicKey: PublicKey): void
removePublicKey(publicKey: PublicKey): void
}
interface TransactionMeta {
name: string
script: Script
deps: CellDep[]
headers?: Hash256[]
}
interface LockHashWithMeta {
hash: Hash256
meta: TransactionMeta
}
interface ContainerService {
getAllLockHashesAndMeta(): Promise<LockHashWithMeta[]>
sign(context: SignContext, rawTx: RawTransaction, config: Config): Promise<RawTransaction>
send(tx: RawTransaction): Promise<Hash256>
}
interface LockScript {
readonly name: string;
readonly codeHash: Hash256;
readonly hashType: ScriptHashType;
setProvider(provider: SignProvider): void;
script(publicKey: string): Script;
deps(): CellDep[];
headers?(): Hash256[];
signatureAlgorithm(): SignatureAlgorithm;
sign(context: SignContext, rawTx: RawTransaction, config: Config): Promise<RawTransaction>;
}
The LockScript basic information keys are name, codeHash and hashType.
The setProvider key is a callback function for implementation of the underlying signature algorithm. This is provided by Keyper container.
For example, below is the Keyper Scatter secp256k1 signature algorithm implementation:
public sign(context: SignContext, message: Bytes): Promise<Bytes> {
const key = keys[context.publicKey];
if (!key) {
throw new Error(`no key for address: ${context.address}`);
}
const privateKey = keystore.decrypt(key, context.password);
const ec = new EC('secp256k1');
const keypair = ec.keyFromPrivate(privateKey);
const msg = typeof message === 'string' ? hexToBytes(message) : message;
let { r, s, recoveryParam } = keypair.sign(msg, {
canonical: true,
});
if (recoveryParam === null){
throw new Error('Fail to sign the message');
}
const fmtR = r.toString(16).padStart(64, '0');
const fmtS = s.toString(16).padStart(64, '0');
const signature = `0x${fmtR}${fmtS}${this.padToEven(recoveryParam.toString(16))}`;
return signature;
}
The script key is a method that implments public key to Script transfer. Below is a Secp256k1 implementation:
public script(publicKey: string): Script {
const args = utils.blake160(publicKey);
return {
codeHash: this.codeHash,
hashType: this.hashType,
args: `0x${Buffer.from(args).toString("hex")}`
};
}
The deps and headers keys contain LockScript source details. Below is secp256k1 implementation:
public deps(): CellDep[] {
return [{
outPoint: {
txHash: "0x84dcb061adebff4ef93d57c975ba9058a9be939d79ea12ee68003f6492448890",
index: "0x0",
},
depType: "depGroup",
}];
}
The signatureAlgorithm key returns the supported signature algorithm for this LockScript.
The sign key is a method that implements signing functionality for a transaction. Partial signatures can be accomplished using the config parameter. Below is seck256k1 signing example:
public async sign(context: SignContext, rawTx: RawTransaction, config: Config = {index: 0, length: -1}): Promise<RawTransaction> {
const txHash = utils.rawTransactionToHash(rawTx);
if (config.length === -1) {
config.length = rawTx.witnesses.length;
}
if (config.length + config.index > rawTx.witnesses.length) {
throw new Error("request config error");
}
if (typeof rawTx.witnesses[config.index] !== 'object') {
throw new Error("first witness in the group should be type of WitnessArgs");
}
const emptyWitness = {
// @ts-ignore
...rawTx.witnesses[config.index],
lock: `0x${'0'.repeat(130)}`,
};
const serializedEmptyWitnessBytes = utils.hexToBytes(utils.serializeWitnessArgs(emptyWitness));
const serialziedEmptyWitnessSize = serializedEmptyWitnessBytes.length;
const s = utils.blake2b(32, null, null, utils.PERSONAL);
s.update(utils.hexToBytes(txHash));
s.update(utils.hexToBytes(utils.toHexInLittleEndian(`0x${numberToBN(serialziedEmptyWitnessSize).toString(16)}`, 8)));
s.update(serializedEmptyWitnessBytes);
for (let i = config.index + 1; i < config.index + config.length; i++) {
const w = rawTx.witnesses[i];
// @ts-ignore
const bytes = utils.hexToBytes(typeof w === 'string' ? w : utils.serializeWitnessArgs(w));
s.update(utils.hexToBytes(utils.toHexInLittleEndian(`0x${numberToBN(bytes.length).toString(16)}`, 8)));
s.update(bytes);
}
const message = `0x${s.digest('hex')}`;
const signd = await this.provider.sign(context, message);
// @ts-ignore
rawTx.witnesses[config.index].lock = signd;
// @ts-ignore
rawTx.witnesses[config.index] = utils.serializeWitnessArgs(rawTx.witnesses[config.index]);
return rawTx;
}
The following must be installed available to build this project.
- NPM https://docs.npmjs.com/downloading-and-installing-node-js-and-npm
- Yarn https://classic.yarnpkg.com/en/docs/install
Install all dependencies. This must be run once after cloning the repository.
yarn install
Clean old builds, install dependencies, and bootstrap the project. This should be run after cloning, and can be run repeatedly when needed.
yarn run reboot
Build all project components.
yarn run build
Test all project components.
yarn run test
To install Keyper as a dependency in another project without manually building use the following.
npm i @nervosnetwork/keyper-specs
npm i @nervosnetwork/keyper-container