diff --git a/web3.js/module.d.ts b/web3.js/module.d.ts index 74ea97d7f4..99441ac304 100644 --- a/web3.js/module.d.ts +++ b/web3.js/module.d.ts @@ -1039,6 +1039,13 @@ declare module '@solana/web3.js' { } // === src/secp256k1-program.js === + export type CreateSecp256k1InstructionWithEthAddressParams = { + ethAddress: Buffer | Uint8Array | Array | string; + message: Buffer | Uint8Array | Array; + signature: Buffer | Uint8Array | Array; + recoveryId: number; + }; + export type CreateSecp256k1InstructionWithPublicKeyParams = { publicKey: Buffer | Uint8Array | Array; message: Buffer | Uint8Array | Array; @@ -1054,6 +1061,14 @@ declare module '@solana/web3.js' { export class Secp256k1Program { static get programId(): PublicKey; + static publicKeyToEthAddress( + publicKey: Buffer | Uint8Array | Array, + ): Buffer; + + static createInstructionWithEthAddress( + params: CreateSecp256k1InstructionWithEthAddressParams, + ): TransactionInstruction; + static createInstructionWithPublicKey( params: CreateSecp256k1InstructionWithPublicKeyParams, ): TransactionInstruction; diff --git a/web3.js/module.flow.js b/web3.js/module.flow.js index d267a56293..45c6f06640 100644 --- a/web3.js/module.flow.js +++ b/web3.js/module.flow.js @@ -1045,6 +1045,13 @@ declare module '@solana/web3.js' { } // === src/secp256k1-program.js === + declare export type CreateSecp256k1InstructionWithEthAddressParams = {| + ethAddress: Buffer | Uint8Array | Array | string, + message: Buffer | Uint8Array | Array, + signature: Buffer | Uint8Array | Array, + recoveryId: number, + |}; + declare export type CreateSecp256k1InstructionWithPublicKeyParams = {| publicKey: Buffer | Uint8Array | Array, message: Buffer | Uint8Array | Array, @@ -1060,6 +1067,14 @@ declare module '@solana/web3.js' { declare export class Secp256k1Program { static get programId(): PublicKey; + static publicKeyToEthAddress( + publicKey: Buffer | Uint8Array | Array, + ): Buffer; + + static createInstructionWithEthAddress( + params: CreateSecp256k1InstructionWithEthAddressParams, + ): TransactionInstruction; + static createInstructionWithPublicKey( params: CreateSecp256k1InstructionWithPublicKeyParams, ): TransactionInstruction; diff --git a/web3.js/src/secp256k1-program.js b/web3.js/src/secp256k1-program.js index ed27c2a09d..51d213c2bc 100644 --- a/web3.js/src/secp256k1-program.js +++ b/web3.js/src/secp256k1-program.js @@ -13,12 +13,12 @@ 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 HASHED_PUBKEY_SERIALIZED_SIZE = 20; 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 * @property {Buffer | Uint8Array | Array} publicKey * @property {Buffer | Uint8Array | Array} 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} ethAddress + * @property {Buffer | Uint8Array | Array} message + * @property {Buffer | Uint8Array | Array} signature + * @property {number} recoveryId + */ +export type CreateSecp256k1InstructionWithEthAddressParams = {| + ethAddress: Buffer | Uint8Array | Array | string, + message: Buffer | Uint8Array | Array, + signature: Buffer | Uint8Array | Array, + recoveryId: number, +|}; + +/** + * Params for creating an secp256k1 instruction using a private key * @typedef {Object} CreateSecp256k1InstructionWithPrivateKeyParams * @property {Buffer | Uint8Array | Array} privateKey * @property {Buffer | Uint8Array | Array} message @@ -59,32 +74,72 @@ const SECP256K1_INSTRUCTION_LAYOUT = BufferLayout.struct([ export class Secp256k1Program { /** - * Public key that identifies the Secp256k program + * Public key that identifies the secp256k1 program */ static get programId(): PublicKey { 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, + ): 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( params: CreateSecp256k1InstructionWithPublicKeyParams, ): TransactionInstruction { 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( - publicKey.length === PUBLIC_KEY_BYTES, - `Public key must be ${PUBLIC_KEY_BYTES} bytes`, + ethAddress.length === ETHEREUM_ADDRESS_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 ethAddressOffset = dataStart; const signatureOffset = dataStart + ethAddress.length; @@ -106,7 +161,7 @@ export class Secp256k1Program { messageDataSize: message.length, messageInstructionIndex: 0, signature: toBuffer(signature), - ethAddress, + ethAddress: toBuffer(ethAddress), recoveryId, }, 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( params: CreateSecp256k1InstructionWithPrivateKeyParams, @@ -131,7 +187,7 @@ export class Secp256k1Program { assert( 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 { @@ -152,11 +208,3 @@ export class Secp256k1Program { } } } - -function constructEthAddress( - publicKey: Buffer | Uint8Array | Array, -): Buffer { - return Buffer.from(keccak_256.update(toBuffer(publicKey)).digest()).slice( - -HASHED_PUBKEY_SERIALIZED_SIZE, - ); -} diff --git a/web3.js/test/secp256k1-program.test.js b/web3.js/test/secp256k1-program.test.js index 76215e0f29..7e24f24d82 100644 --- a/web3.js/test/secp256k1-program.test.js +++ b/web3.js/test/secp256k1-program.test.js @@ -13,7 +13,6 @@ import { Secp256k1Program, } from '../src'; import {url} from './url'; -import {toBuffer} from '../src/util/to-buffer'; const randomPrivateKey = () => { let privateKey; @@ -25,22 +24,70 @@ const randomPrivateKey = () => { if (process.env.TEST_LIVE) { describe('secp256k1', () => { - it('create secp256k1 instruction with public key', async () => { - const privateKey = randomPrivateKey(); - const publicKey = publicKeyCreate(privateKey, false).slice(1); - const message = Buffer.from('This is a message'); - const messageHash = Buffer.from( - keccak_256.update(toBuffer(message)).digest(), - ); - const {signature, recid: recoveryId} = ecdsaSign(messageHash, privateKey); - const connection = new Connection(url, 'confirmed'); + const privateKey = randomPrivateKey(); + const publicKey = publicKeyCreate(privateKey, false).slice(1); + const ethAddress = Secp256k1Program.publicKeyToEthAddress(publicKey); + const from = new Account(); + const connection = new Connection(url, 'confirmed'); - const from = new Account(); + before(async function () { await connection.confirmTransaction( - await connection.requestAirdrop(from.publicKey, 2 * LAMPORTS_PER_SOL), - 'confirmed', + await connection.requestAirdrop(from.publicKey, 10 * LAMPORTS_PER_SOL), + ); + }); + + 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( Secp256k1Program.createInstructionWithPublicKey({ publicKey, @@ -50,33 +97,19 @@ if (process.env.TEST_LIVE) { }), ); - await sendAndConfirmTransaction(connection, transaction, [from], { - commitment: 'confirmed', - preflightCommitment: 'confirmed', - }); + await sendAndConfirmTransaction(connection, transaction, [from]); }); it('create secp256k1 instruction with private key', async () => { - const privateKey = randomPrivateKey(); - const connection = new Connection(url, 'confirmed'); - - const from = new Account(); - await connection.confirmTransaction( - await connection.requestAirdrop(from.publicKey, 2 * LAMPORTS_PER_SOL), - 'confirmed', - ); - + const message = Buffer.from('private key'); const transaction = new Transaction().add( Secp256k1Program.createInstructionWithPrivateKey({ privateKey, - message: Buffer.from('Test 123'), + message, }), ); - await sendAndConfirmTransaction(connection, transaction, [from], { - commitment: 'confirmed', - preflightCommitment: 'confirmed', - }); + await sendAndConfirmTransaction(connection, transaction, [from]); }); }); }