solana/web3.js/src/secp256k1-program.ts

230 lines
6.7 KiB
TypeScript

import {Buffer} from 'buffer';
import * as BufferLayout from '@solana/buffer-layout';
import secp256k1 from 'secp256k1';
import sha3 from 'js-sha3';
import {PublicKey} from './publickey';
import {TransactionInstruction} from './transaction';
import assert from './util/assert';
import {toBuffer} from './util/to-buffer';
const {publicKeyCreate, ecdsaSign} = secp256k1;
const PRIVATE_KEY_BYTES = 32;
const ETHEREUM_ADDRESS_BYTES = 20;
const PUBLIC_KEY_BYTES = 64;
const SIGNATURE_OFFSETS_SERIALIZED_SIZE = 11;
/**
* Params for creating an secp256k1 instruction using a public key
*/
export type CreateSecp256k1InstructionWithPublicKeyParams = {
publicKey: Buffer | Uint8Array | Array<number>;
message: Buffer | Uint8Array | Array<number>;
signature: Buffer | Uint8Array | Array<number>;
recoveryId: number;
instructionIndex?: number;
};
/**
* Params for creating an secp256k1 instruction using an Ethereum address
*/
export type CreateSecp256k1InstructionWithEthAddressParams = {
ethAddress: Buffer | Uint8Array | Array<number> | string;
message: Buffer | Uint8Array | Array<number>;
signature: Buffer | Uint8Array | Array<number>;
recoveryId: number;
instructionIndex?: number;
};
/**
* Params for creating an secp256k1 instruction using a private key
*/
export type CreateSecp256k1InstructionWithPrivateKeyParams = {
privateKey: Buffer | Uint8Array | Array<number>;
message: Buffer | Uint8Array | Array<number>;
instructionIndex?: number;
};
const SECP256K1_INSTRUCTION_LAYOUT = BufferLayout.struct<
Readonly<{
ethAddress: Uint8Array;
ethAddressInstructionIndex: number;
ethAddressOffset: number;
messageDataOffset: number;
messageDataSize: number;
messageInstructionIndex: number;
numSignatures: number;
recoveryId: number;
signature: Uint8Array;
signatureInstructionIndex: number;
signatureOffset: number;
}>
>([
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, 'ethAddress'),
BufferLayout.blob(64, 'signature'),
BufferLayout.u8('recoveryId'),
]);
export class Secp256k1Program {
/**
* @internal
*/
constructor() {}
/**
* Public key that identifies the secp256k1 program
*/
static programId: PublicKey = new PublicKey(
'KeccakSecp256k11111111111111111111111111111',
);
/**
* Construct an Ethereum address from a secp256k1 public key buffer.
* @param {Buffer} publicKey a 64 byte secp256k1 public key buffer
*/
static publicKeyToEthAddress(
publicKey: Buffer | Uint8Array | Array<number>,
): Buffer {
assert(
publicKey.length === PUBLIC_KEY_BYTES,
`Public key must be ${PUBLIC_KEY_BYTES} bytes but received ${publicKey.length} bytes`,
);
try {
return Buffer.from(
sha3.keccak_256.update(toBuffer(publicKey)).digest(),
).slice(-ETHEREUM_ADDRESS_BYTES);
} catch (error) {
throw new Error(`Error constructing Ethereum address: ${error}`);
}
}
/**
* Create an secp256k1 instruction with a public key. The public key
* must be a buffer that is 64 bytes long.
*/
static createInstructionWithPublicKey(
params: CreateSecp256k1InstructionWithPublicKeyParams,
): TransactionInstruction {
const {publicKey, message, signature, recoveryId, instructionIndex} =
params;
return Secp256k1Program.createInstructionWithEthAddress({
ethAddress: Secp256k1Program.publicKeyToEthAddress(publicKey),
message,
signature,
recoveryId,
instructionIndex,
});
}
/**
* Create an secp256k1 instruction with an Ethereum address. The address
* must be a hex string or a buffer that is 20 bytes long.
*/
static createInstructionWithEthAddress(
params: CreateSecp256k1InstructionWithEthAddressParams,
): TransactionInstruction {
const {
ethAddress: rawAddress,
message,
signature,
recoveryId,
instructionIndex = 0,
} = params;
let ethAddress;
if (typeof rawAddress === 'string') {
if (rawAddress.startsWith('0x')) {
ethAddress = Buffer.from(rawAddress.substr(2), 'hex');
} else {
ethAddress = Buffer.from(rawAddress, 'hex');
}
} else {
ethAddress = rawAddress;
}
assert(
ethAddress.length === ETHEREUM_ADDRESS_BYTES,
`Address must be ${ETHEREUM_ADDRESS_BYTES} bytes but received ${ethAddress.length} bytes`,
);
const dataStart = 1 + SIGNATURE_OFFSETS_SERIALIZED_SIZE;
const ethAddressOffset = dataStart;
const signatureOffset = dataStart + ethAddress.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,
signatureOffset,
signatureInstructionIndex: instructionIndex,
ethAddressOffset,
ethAddressInstructionIndex: instructionIndex,
messageDataOffset,
messageDataSize: message.length,
messageInstructionIndex: instructionIndex,
signature: toBuffer(signature),
ethAddress: toBuffer(ethAddress),
recoveryId,
},
instructionData,
);
instructionData.fill(toBuffer(message), SECP256K1_INSTRUCTION_LAYOUT.span);
return new TransactionInstruction({
keys: [],
programId: Secp256k1Program.programId,
data: instructionData,
});
}
/**
* Create an secp256k1 instruction with a private key. The private key
* must be a buffer that is 32 bytes long.
*/
static createInstructionWithPrivateKey(
params: CreateSecp256k1InstructionWithPrivateKeyParams,
): TransactionInstruction {
const {privateKey: pkey, message, instructionIndex} = params;
assert(
pkey.length === PRIVATE_KEY_BYTES,
`Private key must be ${PRIVATE_KEY_BYTES} bytes but received ${pkey.length} bytes`,
);
try {
const privateKey = toBuffer(pkey);
const publicKey = publicKeyCreate(privateKey, false).slice(1); // throw away leading byte
const messageHash = Buffer.from(
sha3.keccak_256.update(toBuffer(message)).digest(),
);
const {signature, recid: recoveryId} = ecdsaSign(messageHash, privateKey);
return this.createInstructionWithPublicKey({
publicKey,
message,
signature,
recoveryId,
instructionIndex,
});
} catch (error) {
throw new Error(`Error creating instruction; ${error}`);
}
}
}