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:
162
web3.js/src/secp256k1-program.js
Normal file
162
web3.js/src/secp256k1-program.js
Normal 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);
|
||||
}
|
Reference in New Issue
Block a user