feat: support creating secp256k1 instructions with eth address (#15626)

This commit is contained in:
Justin Starry
2021-03-03 02:16:36 +08:00
committed by GitHub
parent 43663b1750
commit 7435a7b0ed
4 changed files with 167 additions and 56 deletions

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

@ -1039,6 +1039,13 @@ declare module '@solana/web3.js' {
} }
// === src/secp256k1-program.js === // === src/secp256k1-program.js ===
export type CreateSecp256k1InstructionWithEthAddressParams = {
ethAddress: Buffer | Uint8Array | Array<number> | string;
message: Buffer | Uint8Array | Array<number>;
signature: Buffer | Uint8Array | Array<number>;
recoveryId: number;
};
export type CreateSecp256k1InstructionWithPublicKeyParams = { export type CreateSecp256k1InstructionWithPublicKeyParams = {
publicKey: Buffer | Uint8Array | Array<number>; publicKey: Buffer | Uint8Array | Array<number>;
message: Buffer | Uint8Array | Array<number>; message: Buffer | Uint8Array | Array<number>;
@ -1054,6 +1061,14 @@ declare module '@solana/web3.js' {
export class Secp256k1Program { export class Secp256k1Program {
static get programId(): PublicKey; static get programId(): PublicKey;
static publicKeyToEthAddress(
publicKey: Buffer | Uint8Array | Array<number>,
): Buffer;
static createInstructionWithEthAddress(
params: CreateSecp256k1InstructionWithEthAddressParams,
): TransactionInstruction;
static createInstructionWithPublicKey( static createInstructionWithPublicKey(
params: CreateSecp256k1InstructionWithPublicKeyParams, params: CreateSecp256k1InstructionWithPublicKeyParams,
): TransactionInstruction; ): TransactionInstruction;

View File

@ -1045,6 +1045,13 @@ declare module '@solana/web3.js' {
} }
// === src/secp256k1-program.js === // === src/secp256k1-program.js ===
declare export type CreateSecp256k1InstructionWithEthAddressParams = {|
ethAddress: Buffer | Uint8Array | Array<number> | string,
message: Buffer | Uint8Array | Array<number>,
signature: Buffer | Uint8Array | Array<number>,
recoveryId: number,
|};
declare export type CreateSecp256k1InstructionWithPublicKeyParams = {| declare export type CreateSecp256k1InstructionWithPublicKeyParams = {|
publicKey: Buffer | Uint8Array | Array<number>, publicKey: Buffer | Uint8Array | Array<number>,
message: Buffer | Uint8Array | Array<number>, message: Buffer | Uint8Array | Array<number>,
@ -1060,6 +1067,14 @@ declare module '@solana/web3.js' {
declare export class Secp256k1Program { declare export class Secp256k1Program {
static get programId(): PublicKey; static get programId(): PublicKey;
static publicKeyToEthAddress(
publicKey: Buffer | Uint8Array | Array<number>,
): Buffer;
static createInstructionWithEthAddress(
params: CreateSecp256k1InstructionWithEthAddressParams,
): TransactionInstruction;
static createInstructionWithPublicKey( static createInstructionWithPublicKey(
params: CreateSecp256k1InstructionWithPublicKeyParams, params: CreateSecp256k1InstructionWithPublicKeyParams,
): TransactionInstruction; ): TransactionInstruction;

View File

@ -13,12 +13,12 @@ import {toBuffer} from './util/to-buffer';
const {publicKeyCreate, ecdsaSign} = secp256k1; const {publicKeyCreate, ecdsaSign} = secp256k1;
const PRIVATE_KEY_BYTES = 32; const PRIVATE_KEY_BYTES = 32;
const ETHEREUM_ADDRESS_BYTES = 20;
const PUBLIC_KEY_BYTES = 64; const PUBLIC_KEY_BYTES = 64;
const HASHED_PUBKEY_SERIALIZED_SIZE = 20;
const SIGNATURE_OFFSETS_SERIALIZED_SIZE = 11; const SIGNATURE_OFFSETS_SERIALIZED_SIZE = 11;
/** /**
* Create a Secp256k1 instruction using a public key params * Params for creating an secp256k1 instruction using a public key
* @typedef {Object} CreateSecp256k1InstructionWithPublicKeyParams * @typedef {Object} CreateSecp256k1InstructionWithPublicKeyParams
* @property {Buffer | Uint8Array | Array<number>} publicKey * @property {Buffer | Uint8Array | Array<number>} publicKey
* @property {Buffer | Uint8Array | Array<number>} message * @property {Buffer | Uint8Array | Array<number>} message
@ -33,7 +33,22 @@ export type CreateSecp256k1InstructionWithPublicKeyParams = {|
|}; |};
/** /**
* Create a Secp256k1 instruction using a private key params * Params for creating an secp256k1 instruction using an Ethereum address
* @typedef {Object} CreateSecp256k1InstructionWithEthAddressParams
* @property {Buffer | Uint8Array | Array<number>} ethAddress
* @property {Buffer | Uint8Array | Array<number>} message
* @property {Buffer | Uint8Array | Array<number>} signature
* @property {number} recoveryId
*/
export type CreateSecp256k1InstructionWithEthAddressParams = {|
ethAddress: Buffer | Uint8Array | Array<number> | string,
message: Buffer | Uint8Array | Array<number>,
signature: Buffer | Uint8Array | Array<number>,
recoveryId: number,
|};
/**
* Params for creating an secp256k1 instruction using a private key
* @typedef {Object} CreateSecp256k1InstructionWithPrivateKeyParams * @typedef {Object} CreateSecp256k1InstructionWithPrivateKeyParams
* @property {Buffer | Uint8Array | Array<number>} privateKey * @property {Buffer | Uint8Array | Array<number>} privateKey
* @property {Buffer | Uint8Array | Array<number>} message * @property {Buffer | Uint8Array | Array<number>} message
@ -59,32 +74,72 @@ const SECP256K1_INSTRUCTION_LAYOUT = BufferLayout.struct([
export class Secp256k1Program { export class Secp256k1Program {
/** /**
* Public key that identifies the Secp256k program * Public key that identifies the secp256k1 program
*/ */
static get programId(): PublicKey { static get programId(): PublicKey {
return new PublicKey('KeccakSecp256k11111111111111111111111111111'); return new PublicKey('KeccakSecp256k11111111111111111111111111111');
} }
/** /**
* Create a secp256k1 instruction with public key * 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(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( static createInstructionWithPublicKey(
params: CreateSecp256k1InstructionWithPublicKeyParams, params: CreateSecp256k1InstructionWithPublicKeyParams,
): TransactionInstruction { ): TransactionInstruction {
const {publicKey, message, signature, recoveryId} = params; const {publicKey, message, signature, recoveryId} = params;
return Secp256k1Program.createInstructionWithEthAddress({
ethAddress: Secp256k1Program.publicKeyToEthAddress(publicKey),
message,
signature,
recoveryId,
});
}
/**
* 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} = params;
let ethAddress = rawAddress;
if (typeof rawAddress === 'string') {
if (rawAddress.startsWith('0x')) {
ethAddress = Buffer.from(rawAddress.substr(2), 'hex');
} else {
ethAddress = Buffer.from(rawAddress, 'hex');
}
}
assert( assert(
publicKey.length === PUBLIC_KEY_BYTES, ethAddress.length === ETHEREUM_ADDRESS_BYTES,
`Public key must be ${PUBLIC_KEY_BYTES} bytes`, `Address must be ${ETHEREUM_ADDRESS_BYTES} bytes but received ${ethAddress.length} bytes`,
); );
let ethAddress;
try {
ethAddress = constructEthAddress(publicKey);
} catch (error) {
throw new Error(`Error constructing ethereum public key: ${error}`);
}
const dataStart = 1 + SIGNATURE_OFFSETS_SERIALIZED_SIZE; const dataStart = 1 + SIGNATURE_OFFSETS_SERIALIZED_SIZE;
const ethAddressOffset = dataStart; const ethAddressOffset = dataStart;
const signatureOffset = dataStart + ethAddress.length; const signatureOffset = dataStart + ethAddress.length;
@ -106,7 +161,7 @@ export class Secp256k1Program {
messageDataSize: message.length, messageDataSize: message.length,
messageInstructionIndex: 0, messageInstructionIndex: 0,
signature: toBuffer(signature), signature: toBuffer(signature),
ethAddress, ethAddress: toBuffer(ethAddress),
recoveryId, recoveryId,
}, },
instructionData, instructionData,
@ -122,7 +177,8 @@ export class Secp256k1Program {
} }
/** /**
* Create a secp256k1 instruction with private key * Create an secp256k1 instruction with a private key. The private key
* must be a buffer that is 32 bytes long.
*/ */
static createInstructionWithPrivateKey( static createInstructionWithPrivateKey(
params: CreateSecp256k1InstructionWithPrivateKeyParams, params: CreateSecp256k1InstructionWithPrivateKeyParams,
@ -131,7 +187,7 @@ export class Secp256k1Program {
assert( assert(
privateKey.length === PRIVATE_KEY_BYTES, privateKey.length === PRIVATE_KEY_BYTES,
`Private key must be ${PRIVATE_KEY_BYTES} bytes`, `Private key must be ${PRIVATE_KEY_BYTES} bytes but received ${privateKey.length} bytes`,
); );
try { try {
@ -152,11 +208,3 @@ export class Secp256k1Program {
} }
} }
} }
function constructEthAddress(
publicKey: Buffer | Uint8Array | Array<number>,
): Buffer {
return Buffer.from(keccak_256.update(toBuffer(publicKey)).digest()).slice(
-HASHED_PUBKEY_SERIALIZED_SIZE,
);
}

View File

@ -13,7 +13,6 @@ import {
Secp256k1Program, Secp256k1Program,
} from '../src'; } from '../src';
import {url} from './url'; import {url} from './url';
import {toBuffer} from '../src/util/to-buffer';
const randomPrivateKey = () => { const randomPrivateKey = () => {
let privateKey; let privateKey;
@ -25,22 +24,70 @@ const randomPrivateKey = () => {
if (process.env.TEST_LIVE) { if (process.env.TEST_LIVE) {
describe('secp256k1', () => { describe('secp256k1', () => {
it('create secp256k1 instruction with public key', async () => {
const privateKey = randomPrivateKey(); const privateKey = randomPrivateKey();
const publicKey = publicKeyCreate(privateKey, false).slice(1); const publicKey = publicKeyCreate(privateKey, false).slice(1);
const message = Buffer.from('This is a message'); const ethAddress = Secp256k1Program.publicKeyToEthAddress(publicKey);
const messageHash = Buffer.from( const from = new Account();
keccak_256.update(toBuffer(message)).digest(),
);
const {signature, recid: recoveryId} = ecdsaSign(messageHash, privateKey);
const connection = new Connection(url, 'confirmed'); const connection = new Connection(url, 'confirmed');
const from = new Account(); before(async function () {
await connection.confirmTransaction( await connection.confirmTransaction(
await connection.requestAirdrop(from.publicKey, 2 * LAMPORTS_PER_SOL), await connection.requestAirdrop(from.publicKey, 10 * LAMPORTS_PER_SOL),
'confirmed', );
});
it('create secp256k1 instruction with string address', async () => {
const message = Buffer.from('string address');
const messageHash = Buffer.from(keccak_256.update(message).digest());
const {signature, recid: recoveryId} = ecdsaSign(messageHash, privateKey);
const transaction = new Transaction().add(
Secp256k1Program.createInstructionWithEthAddress({
ethAddress: ethAddress.toString('hex'),
message,
signature,
recoveryId,
}),
); );
await sendAndConfirmTransaction(connection, transaction, [from]);
});
it('create secp256k1 instruction with 0x prefix string address', async () => {
const message = Buffer.from('0x string address');
const messageHash = Buffer.from(keccak_256.update(message).digest());
const {signature, recid: recoveryId} = ecdsaSign(messageHash, privateKey);
const transaction = new Transaction().add(
Secp256k1Program.createInstructionWithEthAddress({
ethAddress: '0x' + ethAddress.toString('hex'),
message,
signature,
recoveryId,
}),
);
await sendAndConfirmTransaction(connection, transaction, [from]);
});
it('create secp256k1 instruction with buffer address', async () => {
const message = Buffer.from('buffer address');
const messageHash = Buffer.from(keccak_256.update(message).digest());
const {signature, recid: recoveryId} = ecdsaSign(messageHash, privateKey);
const transaction = new Transaction().add(
Secp256k1Program.createInstructionWithEthAddress({
ethAddress,
message,
signature,
recoveryId,
}),
);
await sendAndConfirmTransaction(connection, transaction, [from]);
});
it('create secp256k1 instruction with public key', async () => {
const message = Buffer.from('public key');
const messageHash = Buffer.from(keccak_256.update(message).digest());
const {signature, recid: recoveryId} = ecdsaSign(messageHash, privateKey);
const transaction = new Transaction().add( const transaction = new Transaction().add(
Secp256k1Program.createInstructionWithPublicKey({ Secp256k1Program.createInstructionWithPublicKey({
publicKey, publicKey,
@ -50,33 +97,19 @@ if (process.env.TEST_LIVE) {
}), }),
); );
await sendAndConfirmTransaction(connection, transaction, [from], { await sendAndConfirmTransaction(connection, transaction, [from]);
commitment: 'confirmed',
preflightCommitment: 'confirmed',
});
}); });
it('create secp256k1 instruction with private key', async () => { it('create secp256k1 instruction with private key', async () => {
const privateKey = randomPrivateKey(); const message = Buffer.from('private key');
const connection = new Connection(url, 'confirmed');
const from = new Account();
await connection.confirmTransaction(
await connection.requestAirdrop(from.publicKey, 2 * LAMPORTS_PER_SOL),
'confirmed',
);
const transaction = new Transaction().add( const transaction = new Transaction().add(
Secp256k1Program.createInstructionWithPrivateKey({ Secp256k1Program.createInstructionWithPrivateKey({
privateKey, privateKey,
message: Buffer.from('Test 123'), message,
}), }),
); );
await sendAndConfirmTransaction(connection, transaction, [from], { await sendAndConfirmTransaction(connection, transaction, [from]);
commitment: 'confirmed',
preflightCommitment: 'confirmed',
});
}); });
}); });
} }