feat: add Secp256k1 support to solana-web3.js (#12958)

* feat: add secp256k1 instruction

* feat: use buffer-layout for encoding as well

* style: use consistent naming for types

* style: update typings and make program functions static

* fix: attempt to resolve rollup issue

* fix: expose sysvar in typings

* fix: remove decode instruction functionality (for now)
This commit is contained in:
Josh 2020-10-22 13:15:24 -07:00 committed by GitHub
parent 84d56c62ce
commit 368aeb2cee
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 380 additions and 8 deletions

4
web3.js/flow-typed/keccak.js vendored Normal file
View File

@ -0,0 +1,4 @@
declare module 'keccak' {
// TODO: Fill in types
declare module.exports: any;
}

4
web3.js/flow-typed/secp256k1.js vendored Normal file
View File

@ -0,0 +1,4 @@
declare module 'secp256k1' {
// TODO: Fill in types
declare module.exports: any;
}

26
web3.js/module.d.ts vendored
View File

@ -531,6 +531,7 @@ declare module '@solana/web3.js' {
export const SYSVAR_RENT_PUBKEY: PublicKey;
export const SYSVAR_REWARDS_PUBKEY: PublicKey;
export const SYSVAR_STAKE_HISTORY_PUBKEY: PublicKey;
export const SYSVAR_INSTRUCTIONS_PUBKEY: PublicKey;
// === src/vote-account.js ===
export const VOTE_PROGRAM_ID: PublicKey;
@ -966,6 +967,31 @@ declare module '@solana/web3.js' {
): AuthorizeNonceParams;
}
// === src/secp256k1-program.js ===
export type CreateSecp256k1InstructionWithPublicKeyParams = {
publicKey: Buffer | Uint8Array | Array<number>;
message: Buffer | Uint8Array | Array<number>;
signature: Buffer | Uint8Array | Array<number>;
recoveryId: number;
};
export type CreateSecp256k1InstructionWithPrivateKeyParams = {
privateKey: Buffer | Uint8Array | Array<number>;
message: Buffer | Uint8Array | Array<number>;
};
export class Secp256k1Program {
static get programId(): PublicKey;
static createInstructionWithPublicKey(
params: CreateSecp256k1InstructionWithPublicKeyParams,
): TransactionInstruction;
static createInstructionWithPrivateKey(
params: CreateSecp256k1InstructionWithPrivateKeyParams,
): TransactionInstruction;
}
// === src/loader.js ===
export class Loader {
static getMinNumSignatures(dataLength: number): number;

View File

@ -536,6 +536,7 @@ declare module '@solana/web3.js' {
declare export var SYSVAR_RENT_PUBKEY;
declare export var SYSVAR_REWARDS_PUBKEY;
declare export var SYSVAR_STAKE_HISTORY_PUBKEY;
declare export var SYSVAR_INSTRUCTIONS_PUBKEY;
// === src/vote-account.js ===
declare export var VOTE_PROGRAM_ID;
@ -973,6 +974,31 @@ declare module '@solana/web3.js' {
): AuthorizeNonceParams;
}
// === src/secp256k1-program.js ===
declare export type CreateSecp256k1InstructionWithPublicKeyParams = {|
publicKey: Buffer | Uint8Array | Array<number>,
message: Buffer | Uint8Array | Array<number>,
signature: Buffer | Uint8Array | Array<number>,
recoveryId: number,
|};
declare export type CreateSecp256k1InstructionWithPrivateKeyParams = {|
privateKey: Buffer | Uint8Array | Array<number>,
message: Buffer | Uint8Array | Array<number>,
|};
declare export class Secp256k1Program {
static get programId(): PublicKey;
static createInstructionWithPublicKey(
params: CreateSecp256k1InstructionWithPublicKeyParams,
): TransactionInstruction;
static createInstructionWithPrivateKey(
params: CreateSecp256k1InstructionWithPrivateKeyParams,
): TransactionInstruction;
}
// === src/loader.js ===
declare export class Loader {
static getMinNumSignatures(dataLength: number): number;

View File

@ -7163,8 +7163,7 @@
"brorand": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz",
"integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=",
"dev": true
"integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8="
},
"browser-process-hrtime": {
"version": "1.0.0",
@ -11357,7 +11356,6 @@
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.5.tgz",
"integrity": "sha512-eWI5HG9Np+eHV1KQhisXWwM+4EPPYe5dFX1UZZH7k/E3JzDEazVH+VGlZi6R94ZqImq+A3D1mCEtrFIfg/E7sA==",
"dev": true,
"requires": {
"inherits": "^2.0.3",
"minimalistic-assert": "^1.0.1"
@ -11380,7 +11378,6 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz",
"integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=",
"dev": true,
"requires": {
"hash.js": "^1.0.3",
"minimalistic-assert": "^1.0.0",
@ -14453,6 +14450,22 @@
"verror": "1.10.0"
}
},
"keccak": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/keccak/-/keccak-3.0.1.tgz",
"integrity": "sha512-epq90L9jlFWCW7+pQa6JOnKn2Xgl2mtI664seYR6MHskvI9agt7AnDqmAlp9TqU4/caMYbA08Hi5DMZAl5zdkA==",
"requires": {
"node-addon-api": "^2.0.0",
"node-gyp-build": "^4.2.0"
},
"dependencies": {
"node-gyp-build": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.2.3.tgz",
"integrity": "sha512-MN6ZpzmfNCRM+3t57PTJHgHyw/h4OWnZ6mR8P5j/uZtqQr46RRuDE/P+g3n0YR/AiYXeWixZZzaip77gdICfRg=="
}
}
},
"keypather": {
"version": "1.10.2",
"resolved": "https://registry.npmjs.org/keypather/-/keypather-1.10.2.tgz",
@ -15461,14 +15474,12 @@
"minimalistic-assert": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
"integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==",
"dev": true
"integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A=="
},
"minimalistic-crypto-utils": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz",
"integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=",
"dev": true
"integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo="
},
"minimatch": {
"version": "3.0.4",
@ -15606,6 +15617,11 @@
"resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz",
"integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ=="
},
"node-addon-api": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-2.0.2.tgz",
"integrity": "sha512-Ntyt4AIXyaLIuMHF6IOoTakB3K+RWxwtsHNRxllEoA6vPwP9o4866g6YWDLUdnucilZhmkxiHwHr11gAENw+QA=="
},
"node-emoji": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.10.0.tgz",
@ -21328,6 +21344,42 @@
"xmlchars": "^2.2.0"
}
},
"secp256k1": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/secp256k1/-/secp256k1-4.0.2.tgz",
"integrity": "sha512-UDar4sKvWAksIlfX3xIaQReADn+WFnHvbVujpcbr+9Sf/69odMwy2MUsz5CKLQgX9nsIyrjuxL2imVyoNHa3fg==",
"requires": {
"elliptic": "^6.5.2",
"node-addon-api": "^2.0.0",
"node-gyp-build": "^4.2.0"
},
"dependencies": {
"bn.js": {
"version": "4.11.9",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz",
"integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw=="
},
"elliptic": {
"version": "6.5.3",
"resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.3.tgz",
"integrity": "sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw==",
"requires": {
"bn.js": "^4.4.0",
"brorand": "^1.0.1",
"hash.js": "^1.0.0",
"hmac-drbg": "^1.0.0",
"inherits": "^2.0.1",
"minimalistic-assert": "^1.0.0",
"minimalistic-crypto-utils": "^1.0.0"
}
},
"node-gyp-build": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.2.3.tgz",
"integrity": "sha512-MN6ZpzmfNCRM+3t57PTJHgHyw/h4OWnZ6mR8P5j/uZtqQr46RRuDE/P+g3n0YR/AiYXeWixZZzaip77gdICfRg=="
}
}
},
"semantic-release": {
"version": "17.2.1",
"resolved": "https://registry.npmjs.org/semantic-release/-/semantic-release-17.2.1.tgz",

View File

@ -81,10 +81,12 @@
"crypto-hash": "^1.2.2",
"esdoc-inject-style-plugin": "^1.0.0",
"jayson": "^3.0.1",
"keccak": "^3.0.1",
"mz": "^2.7.0",
"node-fetch": "^2.2.0",
"npm-run-all": "^4.1.5",
"rpc-websockets": "^7.4.2",
"secp256k1": "^4.0.2",
"superstruct": "^0.8.3",
"tweetnacl": "^1.0.0",
"ws": "^7.0.0"

View File

@ -106,6 +106,8 @@ function generateConfig(configType) {
'superstruct',
'tweetnacl',
'url',
'secp256k1',
'keccak',
];
break;
default:

View File

@ -21,6 +21,7 @@ export {
SystemProgram,
SYSTEM_INSTRUCTION_LAYOUTS,
} from './system-program';
export {Secp256k1Program} from './secp256k1-program';
export {Transaction, TransactionInstruction} from './transaction';
export {VALIDATOR_INFO_KEY, ValidatorInfo} from './validator-info';
export {VOTE_PROGRAM_ID, VoteAccount} from './vote-account';
@ -29,6 +30,7 @@ export {
SYSVAR_RENT_PUBKEY,
SYSVAR_REWARDS_PUBKEY,
SYSVAR_STAKE_HISTORY_PUBKEY,
SYSVAR_INSTRUCTIONS_PUBKEY,
} from './sysvar';
export {sendAndConfirmTransaction} from './util/send-and-confirm-transaction';
export {sendAndConfirmRawTransaction} from './util/send-and-confirm-raw-transaction';

View File

@ -0,0 +1,162 @@
// @flow
import * as BufferLayout from 'buffer-layout';
import secp256k1 from 'secp256k1';
import createKeccakHash from 'keccak';
import assert from 'assert';
import {PublicKey} from './publickey';
import {TransactionInstruction} from './transaction';
import {toBuffer} from './util/to-buffer';
const {publicKeyCreate, ecdsaSign} = secp256k1;
const PRIVATE_KEY_BYTES = 32;
const PUBLIC_KEY_BYTES = 65;
const HASHED_PUBKEY_SERIALIZED_SIZE = 20;
const SIGNATURE_OFFSETS_SERIALIZED_SIZE = 11;
/**
* Create a Secp256k1 instruction using a public key params
* @typedef {Object} CreateSecp256k1InstructionWithPublicKeyParams
* @property {Buffer | Uint8Array | Array<number>} publicKey
* @property {Buffer | Uint8Array | Array<number>} message
* @property {Buffer | Uint8Array | Array<number>} signature
* @property {number} recoveryId
*/
export type CreateSecp256k1InstructionWithPublicKeyParams = {|
publicKey: Buffer | Uint8Array | Array<number>,
message: Buffer | Uint8Array | Array<number>,
signature: Buffer | Uint8Array | Array<number>,
recoveryId: number,
|};
/**
* Create a Secp256k1 instruction using a private key params
* @typedef {Object} CreateSecp256k1InstructionWithPrivateKeyParams
* @property {Buffer | Uint8Array | Array<number>} privateKey
* @property {Buffer | Uint8Array | Array<number>} message
*/
export type CreateSecp256k1InstructionWithPrivateKeyParams = {|
privateKey: Buffer | Uint8Array | Array<number>,
message: Buffer | Uint8Array | Array<number>,
|};
const SECP256K1_INSTRUCTION_LAYOUT = BufferLayout.struct([
BufferLayout.u8('numSignatures'),
BufferLayout.u16('signatureOffset'),
BufferLayout.u8('signatureInstructionIndex'),
BufferLayout.u16('ethAddressOffset'),
BufferLayout.u8('ethAddressInstructionIndex'),
BufferLayout.u16('messageDataOffset'),
BufferLayout.u16('messageDataSize'),
BufferLayout.u8('messageInstructionIndex'),
BufferLayout.blob(20, 'ethPublicKey'),
BufferLayout.blob(64, 'signature'),
BufferLayout.u8('recoveryId'),
]);
export class Secp256k1Program {
/**
* Public key that identifies the Secp256k program
*/
static get programId(): PublicKey {
return new PublicKey('KeccakSecp256k11111111111111111111111111111');
}
/**
* Create a secp256k1 instruction with public key
*/
static createInstructionWithPublicKey(
params: CreateSecp256k1InstructionWithPublicKeyParams,
): TransactionInstruction {
const {publicKey, message, signature, recoveryId} = params;
assert(
publicKey.length === PUBLIC_KEY_BYTES,
`Public key must be ${PUBLIC_KEY_BYTES} bytes`,
);
let ethPublicKey;
try {
ethPublicKey = constructEthPubkey(publicKey);
} catch (error) {
throw new Error(`Error constructing ethereum public key: ${error}`);
}
const dataStart = 1 + SIGNATURE_OFFSETS_SERIALIZED_SIZE;
const ethAddressOffset = dataStart;
const signatureOffset = dataStart + ethPublicKey.length;
const messageDataOffset = signatureOffset + signature.length + 1;
const numSignatures = 1;
const instructionData = Buffer.alloc(
SECP256K1_INSTRUCTION_LAYOUT.span + message.length,
);
SECP256K1_INSTRUCTION_LAYOUT.encode(
{
numSignatures: numSignatures,
signatureOffset: signatureOffset,
signatureInstructionIndex: 0,
ethAddressOffset: ethAddressOffset,
ethAddressInstructionIndex: 0,
messageDataOffset: messageDataOffset,
messageDataSize: message.length,
messageInstructionIndex: 0,
signature: toBuffer(signature),
ethPublicKey: ethPublicKey,
recoveryId: recoveryId,
},
instructionData,
);
instructionData.fill(toBuffer(message), SECP256K1_INSTRUCTION_LAYOUT.span);
return new TransactionInstruction({
keys: [],
programId: Secp256k1Program.programId,
data: instructionData,
});
}
/**
* Create a secp256k1 instruction with private key
*/
static createInstructionWithPrivateKey(
params: CreateSecp256k1InstructionWithPrivateKeyParams,
): TransactionInstruction {
const {privateKey, message} = params;
assert(
privateKey.length === PRIVATE_KEY_BYTES,
`Private key must be ${PRIVATE_KEY_BYTES} bytes`,
);
try {
const publicKey = publicKeyCreate(privateKey, false);
const messageHash = createKeccakHash('keccak256')
.update(toBuffer(message))
.digest();
const {signature, recid: recoveryId} = ecdsaSign(messageHash, privateKey);
return this.createInstructionWithPublicKey({
publicKey,
message,
signature,
recoveryId,
});
} catch (error) {
throw new Error(`Error creating instruction; ${error}`);
}
}
}
export function constructEthPubkey(
publicKey: Buffer | Uint8Array | Array<number>,
): Buffer {
return createKeccakHash('keccak256')
.update(toBuffer(publicKey.slice(1))) // throw away leading byte
.digest()
.slice(-HASHED_PUBKEY_SERIALIZED_SIZE);
}

View File

@ -20,3 +20,7 @@ export const SYSVAR_REWARDS_PUBKEY = new PublicKey(
export const SYSVAR_STAKE_HISTORY_PUBKEY = new PublicKey(
'SysvarStakeHistory1111111111111111111111111',
);
export const SYSVAR_INSTRUCTIONS_PUBKEY = new PublicKey(
'Sysvar1nstructions1111111111111111111111111',
);

View File

@ -0,0 +1,88 @@
// @flow
import createKeccakHash from 'keccak';
import secp256k1 from 'secp256k1';
import {randomBytes} from 'crypto';
import {Secp256k1Program} from '../src/secp256k1-program';
import {mockRpcEnabled} from './__mocks__/node-fetch';
import {url} from './url';
import {
Connection,
Account,
sendAndConfirmTransaction,
LAMPORTS_PER_SOL,
Transaction,
} from '../src';
const {privateKeyVerify, ecdsaSign, publicKeyCreate} = secp256k1;
if (!mockRpcEnabled) {
jest.setTimeout(20000);
}
test('live create secp256k1 instruction with public key', async () => {
if (mockRpcEnabled) {
console.log('non-live test skipped');
return;
}
const message = Buffer.from('This is a message');
let privateKey;
do {
privateKey = randomBytes(32);
} while (!privateKeyVerify(privateKey));
const publicKey = publicKeyCreate(privateKey, false);
const messageHash = createKeccakHash('keccak256').update(message).digest();
const {signature, recid: recoveryId} = ecdsaSign(messageHash, privateKey);
const instruction = Secp256k1Program.createInstructionWithPublicKey({
publicKey,
message,
signature,
recoveryId,
});
const transaction = new Transaction();
transaction.add(instruction);
const connection = new Connection(url, 'recent');
const from = new Account();
await connection.requestAirdrop(from.publicKey, 2 * LAMPORTS_PER_SOL);
await sendAndConfirmTransaction(connection, transaction, [from], {
commitment: 'single',
skipPreflight: true,
});
});
test('live create secp256k1 instruction with private key', async () => {
if (mockRpcEnabled) {
console.log('non-live test skipped');
return;
}
let privateKey;
do {
privateKey = randomBytes(32);
} while (!privateKeyVerify(privateKey));
const instruction = Secp256k1Program.createInstructionWithPrivateKey({
privateKey,
message: Buffer.from('Test 123'),
});
const transaction = new Transaction();
transaction.add(instruction);
const connection = new Connection(url, 'recent');
const from = new Account();
await connection.requestAirdrop(from.publicKey, 2 * LAMPORTS_PER_SOL);
await sendAndConfirmTransaction(connection, transaction, [from], {
commitment: 'single',
skipPreflight: true,
});
});