diff --git a/web3.js/src/ed25519-program.ts b/web3.js/src/ed25519-program.ts new file mode 100644 index 0000000000..fab806ea8a --- /dev/null +++ b/web3.js/src/ed25519-program.ts @@ -0,0 +1,140 @@ +import {Buffer} from 'buffer'; +import * as BufferLayout from '@solana/buffer-layout'; +import nacl from 'tweetnacl'; + +import {Keypair} from './keypair'; +import {PublicKey} from './publickey'; +import {TransactionInstruction} from './transaction'; +import assert from './util/assert'; + +const PRIVATE_KEY_BYTES = 64; +const PUBLIC_KEY_BYTES = 32; +const SIGNATURE_BYTES = 64; + +/** + * Params for creating an ed25519 instruction using a public key + */ +export type CreateEd25519InstructionWithPublicKeyParams = { + publicKey: Uint8Array; + message: Uint8Array; + signature: Uint8Array; + instructionIndex?: number; +}; + +/** + * Params for creating an ed25519 instruction using a private key + */ +export type CreateEd25519InstructionWithPrivateKeyParams = { + privateKey: Uint8Array; + message: Uint8Array; + instructionIndex?: number; +}; + +const ED25519_INSTRUCTION_LAYOUT = BufferLayout.struct([ + BufferLayout.u8('numSignatures'), + BufferLayout.u8('padding'), + BufferLayout.u16('signatureOffset'), + BufferLayout.u16('signatureInstructionIndex'), + BufferLayout.u16('publicKeyOffset'), + BufferLayout.u16('publicKeyInstructionIndex'), + BufferLayout.u16('messageDataOffset'), + BufferLayout.u16('messageDataSize'), + BufferLayout.u16('messageInstructionIndex'), +]); + +export class Ed25519Program { + /** + * @internal + */ + constructor() {} + + /** + * Public key that identifies the ed25519 program + */ + static programId: PublicKey = new PublicKey( + 'Ed25519SigVerify111111111111111111111111111', + ); + + /** + * Create an ed25519 instruction with a public key and signature. The + * public key must be a buffer that is 32 bytes long, and the signature + * must be a buffer of 64 bytes. + */ + static createInstructionWithPublicKey( + params: CreateEd25519InstructionWithPublicKeyParams, + ): TransactionInstruction { + const {publicKey, message, signature, instructionIndex} = params; + + assert( + publicKey.length === PUBLIC_KEY_BYTES, + `Public Key must be ${PUBLIC_KEY_BYTES} bytes but received ${publicKey.length} bytes`, + ); + + assert( + signature.length === SIGNATURE_BYTES, + `Signature must be ${SIGNATURE_BYTES} bytes but received ${signature.length} bytes`, + ); + + const publicKeyOffset = ED25519_INSTRUCTION_LAYOUT.span; + const signatureOffset = publicKeyOffset + publicKey.length; + const messageDataOffset = signatureOffset + signature.length; + const numSignatures = 1; + + const instructionData = Buffer.alloc(messageDataOffset + message.length); + + ED25519_INSTRUCTION_LAYOUT.encode( + { + numSignatures, + padding: 0, + signatureOffset, + signatureInstructionIndex: instructionIndex, + publicKeyOffset, + publicKeyInstructionIndex: instructionIndex, + messageDataOffset, + messageDataSize: message.length, + messageInstructionIndex: instructionIndex, + }, + instructionData, + ); + + instructionData.fill(publicKey, publicKeyOffset); + instructionData.fill(signature, signatureOffset); + instructionData.fill(message, messageDataOffset); + + return new TransactionInstruction({ + keys: [], + programId: Ed25519Program.programId, + data: instructionData, + }); + } + + /** + * Create an ed25519 instruction with a private key. The private key + * must be a buffer that is 64 bytes long. + */ + static createInstructionWithPrivateKey( + params: CreateEd25519InstructionWithPrivateKeyParams, + ): TransactionInstruction { + const {privateKey, message, instructionIndex} = params; + + assert( + privateKey.length === PRIVATE_KEY_BYTES, + `Private key must be ${PRIVATE_KEY_BYTES} bytes but received ${privateKey.length} bytes`, + ); + + try { + const keypair = Keypair.fromSecretKey(privateKey); + const publicKey = keypair.publicKey.toBytes(); + const signature = nacl.sign.detached(message, keypair.secretKey); + + return this.createInstructionWithPublicKey({ + publicKey, + message, + signature, + instructionIndex, + }); + } catch (error) { + throw new Error(`Error creating instruction; ${error}`); + } + } +} diff --git a/web3.js/src/index.ts b/web3.js/src/index.ts index a7901a26b3..5b6a7f18d2 100644 --- a/web3.js/src/index.ts +++ b/web3.js/src/index.ts @@ -4,6 +4,7 @@ export * from './bpf-loader-deprecated'; export * from './bpf-loader'; export * from './connection'; export * from './epoch-schedule'; +export * from './ed25519-program'; export * from './fee-calculator'; export * from './keypair'; export * from './loader'; diff --git a/web3.js/test/ed25519-program.test.ts b/web3.js/test/ed25519-program.test.ts new file mode 100644 index 0000000000..a2598f782b --- /dev/null +++ b/web3.js/test/ed25519-program.test.ts @@ -0,0 +1,54 @@ +import {Buffer} from 'buffer'; +import nacl from 'tweetnacl'; + +import { + Connection, + Keypair, + sendAndConfirmTransaction, + LAMPORTS_PER_SOL, + Transaction, + Ed25519Program, +} from '../src'; +import {url} from './url'; + +if (process.env.TEST_LIVE) { + describe('ed25519', () => { + const keypair = Keypair.generate(); + const privateKey = keypair.secretKey; + const publicKey = keypair.publicKey.toBytes(); + const from = Keypair.generate(); + const connection = new Connection(url, 'confirmed'); + + before(async function () { + await connection.confirmTransaction( + await connection.requestAirdrop(from.publicKey, 10 * LAMPORTS_PER_SOL), + ); + }); + + it('create ed25519 instruction', async () => { + const message = Buffer.from('string address'); + const signature = nacl.sign.detached(message, privateKey); + const transaction = new Transaction().add( + Ed25519Program.createInstructionWithPublicKey({ + publicKey, + message, + signature, + }), + ); + + await sendAndConfirmTransaction(connection, transaction, [from]); + }); + + it('create ed25519 instruction with private key', async () => { + const message = Buffer.from('private key'); + const transaction = new Transaction().add( + Ed25519Program.createInstructionWithPrivateKey({ + privateKey, + message, + }), + ); + + await sendAndConfirmTransaction(connection, transaction, [from]); + }); + }); +}