216 lines
6.3 KiB
TypeScript
216 lines
6.3 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([
|
|
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}`);
|
|
}
|
|
}
|
|
}
|