diff --git a/web3.js/flow-typed/keccak.js b/web3.js/flow-typed/keccak.js new file mode 100644 index 0000000000..1d0829304c --- /dev/null +++ b/web3.js/flow-typed/keccak.js @@ -0,0 +1,4 @@ +declare module 'keccak' { + // TODO: Fill in types + declare module.exports: any; +} diff --git a/web3.js/flow-typed/secp256k1.js b/web3.js/flow-typed/secp256k1.js new file mode 100644 index 0000000000..84743e1e83 --- /dev/null +++ b/web3.js/flow-typed/secp256k1.js @@ -0,0 +1,4 @@ +declare module 'secp256k1' { + // TODO: Fill in types + declare module.exports: any; +} diff --git a/web3.js/module.d.ts b/web3.js/module.d.ts index 8c46895cbc..e54fa89a9b 100644 --- a/web3.js/module.d.ts +++ b/web3.js/module.d.ts @@ -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; + message: Buffer | Uint8Array | Array; + signature: Buffer | Uint8Array | Array; + recoveryId: number; + }; + + export type CreateSecp256k1InstructionWithPrivateKeyParams = { + privateKey: Buffer | Uint8Array | Array; + message: Buffer | Uint8Array | Array; + }; + + 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; diff --git a/web3.js/module.flow.js b/web3.js/module.flow.js index e4ae94d7ca..c4c6c1b8f2 100644 --- a/web3.js/module.flow.js +++ b/web3.js/module.flow.js @@ -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, + message: Buffer | Uint8Array | Array, + signature: Buffer | Uint8Array | Array, + recoveryId: number, + |}; + + declare export type CreateSecp256k1InstructionWithPrivateKeyParams = {| + privateKey: Buffer | Uint8Array | Array, + message: Buffer | Uint8Array | Array, + |}; + + 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; diff --git a/web3.js/package-lock.json b/web3.js/package-lock.json index 47c6c97702..cd196f9e4b 100644 --- a/web3.js/package-lock.json +++ b/web3.js/package-lock.json @@ -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", diff --git a/web3.js/package.json b/web3.js/package.json index 1685e2dfa2..3c8c331781 100644 --- a/web3.js/package.json +++ b/web3.js/package.json @@ -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" diff --git a/web3.js/rollup.config.js b/web3.js/rollup.config.js index ab72cd0141..3083db49c6 100644 --- a/web3.js/rollup.config.js +++ b/web3.js/rollup.config.js @@ -106,6 +106,8 @@ function generateConfig(configType) { 'superstruct', 'tweetnacl', 'url', + 'secp256k1', + 'keccak', ]; break; default: diff --git a/web3.js/src/index.js b/web3.js/src/index.js index 78947b7066..db4d3897cf 100644 --- a/web3.js/src/index.js +++ b/web3.js/src/index.js @@ -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'; diff --git a/web3.js/src/secp256k1-program.js b/web3.js/src/secp256k1-program.js new file mode 100644 index 0000000000..e0cc6e9e56 --- /dev/null +++ b/web3.js/src/secp256k1-program.js @@ -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} publicKey + * @property {Buffer | Uint8Array | Array} message + * @property {Buffer | Uint8Array | Array} signature + * @property {number} recoveryId + */ +export type CreateSecp256k1InstructionWithPublicKeyParams = {| + publicKey: Buffer | Uint8Array | Array, + message: Buffer | Uint8Array | Array, + signature: Buffer | Uint8Array | Array, + recoveryId: number, +|}; + +/** + * Create a Secp256k1 instruction using a private key params + * @typedef {Object} CreateSecp256k1InstructionWithPrivateKeyParams + * @property {Buffer | Uint8Array | Array} privateKey + * @property {Buffer | Uint8Array | Array} message + */ +export type CreateSecp256k1InstructionWithPrivateKeyParams = {| + privateKey: Buffer | Uint8Array | Array, + message: Buffer | Uint8Array | Array, +|}; + +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, +): Buffer { + return createKeccakHash('keccak256') + .update(toBuffer(publicKey.slice(1))) // throw away leading byte + .digest() + .slice(-HASHED_PUBKEY_SERIALIZED_SIZE); +} diff --git a/web3.js/src/sysvar.js b/web3.js/src/sysvar.js index f259834671..07b4170898 100644 --- a/web3.js/src/sysvar.js +++ b/web3.js/src/sysvar.js @@ -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', +); diff --git a/web3.js/test/secp256k1-program.test.js b/web3.js/test/secp256k1-program.test.js new file mode 100644 index 0000000000..8dc533c681 --- /dev/null +++ b/web3.js/test/secp256k1-program.test.js @@ -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, + }); +});