From bb143e761e8f7bda205973c7237e1b42a2a4c3cb Mon Sep 17 00:00:00 2001 From: ivan Date: Fri, 11 Jun 2021 10:55:37 +0800 Subject: [PATCH 01/13] start over using updated main --- package-lock.json | 34 ++- packages/jellyfish-crypto/package.json | 3 + packages/jellyfish-crypto/src/aes256.ts | 54 ++++ .../jellyfish-crypto/src/browserify-aes.d.ts | 16 ++ packages/jellyfish-crypto/src/index.ts | 1 + .../jest.config.js | 12 + .../package.json | 50 ++++ .../src/index.ts | 1 + .../src/tx_signature.ts | 240 ++++++++++++++++++ .../tsconfig.build.json | 16 ++ packages/jellyfish-transaction/src/tx.ts | 10 + .../jellyfish-transaction/src/tx_segwit.ts | 3 +- .../jellyfish-transaction/src/tx_signature.ts | 12 +- 13 files changed, 431 insertions(+), 21 deletions(-) create mode 100644 packages/jellyfish-crypto/src/aes256.ts create mode 100644 packages/jellyfish-crypto/src/browserify-aes.d.ts create mode 100644 packages/jellyfish-transaction-signature/jest.config.js create mode 100644 packages/jellyfish-transaction-signature/package.json create mode 100644 packages/jellyfish-transaction-signature/src/index.ts create mode 100644 packages/jellyfish-transaction-signature/src/tx_signature.ts create mode 100644 packages/jellyfish-transaction-signature/tsconfig.build.json diff --git a/package-lock.json b/package-lock.json index 92dc81a22e..49e62734a7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13265,6 +13265,15 @@ "integrity": "sha512-1HcDas8SEj4z1Wc696tH56G8OlRaH/sqZOynNNB+HF0WOeXPaxTtbYzJY2oEfiUxjSKjhCKr+MvR7dCHcEelug==", "dev": true }, + "node_modules/@types/randombytes": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/randombytes/-/randombytes-2.0.0.tgz", + "integrity": "sha512-bz8PhAVlwN72vqefzxa14DKNT8jK/mV66CSjwdVQM/k3Th3EPKfUtdMniwZgMedQTFuywAsfjnZsg+pEnltaMA==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/stack-utils": { "version": "2.0.0", "dev": true, @@ -14318,7 +14327,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", - "dev": true, "dependencies": { "buffer-xor": "^1.0.3", "cipher-base": "^1.0.0", @@ -14505,8 +14513,7 @@ "node_modules/buffer-xor": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", - "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=", - "dev": true + "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=" }, "node_modules/builtin-status-codes": { "version": "3.0.0", @@ -17701,7 +17708,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==", - "dev": true, "dependencies": { "md5.js": "^1.3.4", "safe-buffer": "^5.1.1" @@ -29370,8 +29376,10 @@ "dependencies": { "bech32": "^2.0.0", "bip66": "^1.1.5", + "browserify-aes": "^1.2.0", "bs58": "^4.0.1", "create-hash": "^1.2.0", + "randombytes": "^2.1.0", "tiny-secp256k1": "^1.1.6", "wif": "^2.0.6" }, @@ -29379,6 +29387,7 @@ "@types/bech32": "^1.1.2", "@types/bs58": "^4.0.1", "@types/create-hash": "^1.2.2", + "@types/randombytes": "^2.0.0", "@types/tiny-secp256k1": "^1.0.0", "@types/wif": "^2.0.2" } @@ -30673,12 +30682,15 @@ "@types/bech32": "^1.1.2", "@types/bs58": "^4.0.1", "@types/create-hash": "^1.2.2", + "@types/randombytes": "^2.0.0", "@types/tiny-secp256k1": "^1.0.0", "@types/wif": "^2.0.2", "bech32": "^2.0.0", "bip66": "^1.1.5", + "browserify-aes": "^1.2.0", "bs58": "^4.0.1", "create-hash": "^1.2.0", + "randombytes": "^2.1.0", "tiny-secp256k1": "^1.1.6", "wif": "^2.0.6" } @@ -40105,6 +40117,15 @@ "integrity": "sha512-1HcDas8SEj4z1Wc696tH56G8OlRaH/sqZOynNNB+HF0WOeXPaxTtbYzJY2oEfiUxjSKjhCKr+MvR7dCHcEelug==", "dev": true }, + "@types/randombytes": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/randombytes/-/randombytes-2.0.0.tgz", + "integrity": "sha512-bz8PhAVlwN72vqefzxa14DKNT8jK/mV66CSjwdVQM/k3Th3EPKfUtdMniwZgMedQTFuywAsfjnZsg+pEnltaMA==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/stack-utils": { "version": "2.0.0", "dev": true @@ -40842,7 +40863,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", - "dev": true, "requires": { "buffer-xor": "^1.0.3", "cipher-base": "^1.0.0", @@ -40982,8 +41002,7 @@ "buffer-xor": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", - "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=", - "dev": true + "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=" }, "builtin-status-codes": { "version": "3.0.0", @@ -43331,7 +43350,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==", - "dev": true, "requires": { "md5.js": "^1.3.4", "safe-buffer": "^5.1.1" diff --git a/packages/jellyfish-crypto/package.json b/packages/jellyfish-crypto/package.json index 8f9fc04e76..afc06d8e2f 100644 --- a/packages/jellyfish-crypto/package.json +++ b/packages/jellyfish-crypto/package.json @@ -37,8 +37,10 @@ "dependencies": { "bech32": "^2.0.0", "bip66": "^1.1.5", + "browserify-aes": "^1.2.0", "bs58": "^4.0.1", "create-hash": "^1.2.0", + "randombytes": "^2.1.0", "tiny-secp256k1": "^1.1.6", "wif": "^2.0.6" }, @@ -46,6 +48,7 @@ "@types/bech32": "^1.1.2", "@types/bs58": "^4.0.1", "@types/create-hash": "^1.2.2", + "@types/randombytes": "^2.0.0", "@types/tiny-secp256k1": "^1.0.0", "@types/wif": "^2.0.2" } diff --git a/packages/jellyfish-crypto/src/aes256.ts b/packages/jellyfish-crypto/src/aes256.ts new file mode 100644 index 0000000000..31eb0dae8c --- /dev/null +++ b/packages/jellyfish-crypto/src/aes256.ts @@ -0,0 +1,54 @@ +import randomBytes from 'randombytes' +import aes from 'browserify-aes' +import { SHA256 } from './hash' + +const CIPHER_ALGORITHM = 'aes-256-ctr' + +/** + * Encrypt a clear-text message using AES-256 plus a random Initialization Vector. + * @see https://github.com/JamesMGreene/node-aes256 + * + * @param {Buffer} key A passphrase of any length to used to generate a symmetric session key. + * @param {Buffer} data The clear-text message or buffer to be encrypted. + * @param {(lengthOfBytes: number) => Buffer} rng Initialization vector generator, default using `crypto` or browserify `random-bytes` package + * @returns {Buffer} + */ +function encrypt (key: Buffer, data: Buffer, rng?: (lengthOfBytes: number) => Buffer): Buffer { + const sha256 = SHA256(key) + const initVector = rng === undefined ? randomBytes(16) : rng(16) + + if (initVector.length !== 16) { + throw new Error('Initialization vector must be 16 bytes long') + } + const cipher = aes.createCipheriv(CIPHER_ALGORITHM, sha256, initVector) + const ciphertext = cipher.update(data) + return Buffer.concat([initVector, ciphertext, cipher.final()]) +} + +/** + * Decrypt an encrypted message back to clear-text using AES-256 plus a random Initialization Vector. + * @see https://github.com/JamesMGreene/node-aes256 + * + * @param {Buffer} key A passphrase of any length to used to generate a symmetric session key. + * @param {Buffer} encrypted The encrypted message to be decrypted. + * @returns {Buffer} The original plain-text message or buffer. + */ +function decrypt (key: Buffer, encrypted: Buffer): Buffer { + if (encrypted.length < 17) { + throw new Error('Provided "encrypted" must decrypt to a non-empty string or buffer') + } + + const sha256 = SHA256(key) + const initVector = encrypted.slice(0, 16) + const decipher = aes.createDecipheriv(CIPHER_ALGORITHM, sha256, initVector) + + const ciphertext = encrypted.slice(16) + const deciphered = decipher.update(ciphertext) + const decipherFinal = decipher.final() + return Buffer.concat([deciphered, decipherFinal]) +} + +export const AES256 = { + encrypt, + decrypt +} diff --git a/packages/jellyfish-crypto/src/browserify-aes.d.ts b/packages/jellyfish-crypto/src/browserify-aes.d.ts new file mode 100644 index 0000000000..b251340588 --- /dev/null +++ b/packages/jellyfish-crypto/src/browserify-aes.d.ts @@ -0,0 +1,16 @@ +/** + * No DefinitelyTyped declarations found, declaring our own here. + * For ./aes256 package to work without crypto. + * @warning this is bare minimum, not fully typed as native crypto package. + */ + +declare module 'browserify-aes' { + interface Cipher { + update: (data: Buffer) => Buffer + final: () => Buffer + } + interface Decipher extends Cipher {} + + export function createCipheriv (algorithm: string, password: Buffer, iv: Buffer): Cipher + export function createDecipheriv (algorithm: string, password: Buffer, iv: Buffer): Decipher +} diff --git a/packages/jellyfish-crypto/src/index.ts b/packages/jellyfish-crypto/src/index.ts index bf916807d8..528b3ac1b8 100644 --- a/packages/jellyfish-crypto/src/index.ts +++ b/packages/jellyfish-crypto/src/index.ts @@ -1,3 +1,4 @@ +export * from './aes256' export * from './bech32' export * from './bs58' export * from './der' diff --git a/packages/jellyfish-transaction-signature/jest.config.js b/packages/jellyfish-transaction-signature/jest.config.js new file mode 100644 index 0000000000..11d9802ff4 --- /dev/null +++ b/packages/jellyfish-transaction-signature/jest.config.js @@ -0,0 +1,12 @@ +module.exports = { + testEnvironment: 'node', + testMatch: [ + '**/__tests__/**/*.test.ts' + ], + transform: { + '^.+\\.ts$': 'ts-jest' + }, + verbose: true, + clearMocks: true, + testTimeout: 120000 +} diff --git a/packages/jellyfish-transaction-signature/package.json b/packages/jellyfish-transaction-signature/package.json new file mode 100644 index 0000000000..8cb1665361 --- /dev/null +++ b/packages/jellyfish-transaction-signature/package.json @@ -0,0 +1,50 @@ +{ + "private": false, + "name": "@defichain/jellyfish-transaction-signature", + "version": "0.0.0", + "description": "A collection of TypeScript + JavaScript tools and libraries for DeFi Blockchain developers to build decentralized finance on Bitcoin", + "keywords": [ + "DeFiChain", + "DeFi", + "Blockchain", + "API", + "Bitcoin" + ], + "repository": "DeFiCh/jellyfish", + "bugs": "https://github.com/DeFiCh/jellyfish/issues", + "license": "MIT", + "contributors": [ + { + "name": "DeFiChain Foundation", + "email": "engineering@defichain.com", + "url": "https://defichain.com/" + }, + { + "name": "DeFi Blockchain Contributors" + }, + { + "name": "DeFi Jellyfish Contributors" + } + ], + "main": "dist", + "types": "dist", + "files": [ + "dist" + ], + "scripts": { + "build": "tsc -b ./tsconfig.build.json" + }, + "peerDependencies": { + "bignumber.js": "^9.0.1" + }, + "dependencies": { + "@defichain/jellyfish-crypto": "0.0.0", + "@defichain/jellyfish-transaction": "0.0.0", + "smart-buffer": "^4.1.0" + }, + "devDependencies": { + "@defichain/jellyfish-api-core": "0.0.0", + "@defichain/jellyfish-api-jsonrpc": "0.0.0", + "@defichain/testcontainers": "0.0.0" + } +} diff --git a/packages/jellyfish-transaction-signature/src/index.ts b/packages/jellyfish-transaction-signature/src/index.ts new file mode 100644 index 0000000000..d65cfcaa85 --- /dev/null +++ b/packages/jellyfish-transaction-signature/src/index.ts @@ -0,0 +1 @@ +export * from './tx_signature' diff --git a/packages/jellyfish-transaction-signature/src/tx_signature.ts b/packages/jellyfish-transaction-signature/src/tx_signature.ts new file mode 100644 index 0000000000..6256f4fead --- /dev/null +++ b/packages/jellyfish-transaction-signature/src/tx_signature.ts @@ -0,0 +1,240 @@ +import { EllipticPair, dSHA256, HASH160 } from '@defichain/jellyfish-crypto' +import { SmartBuffer } from 'smart-buffer' +import { + Script, + Transaction, + TransactionSegWit, + Vin, + Vout, + Witness, + OP_CODES, + OP_PUSHDATA, + CWitnessProgram, + WitnessProgram, + DeFiTransactionConstants, + CVoutV4 +} from '@defichain/jellyfish-transaction' + +export enum SIGHASH { + ALL = 0x01, + NONE = 0x02, + SINGLE = 0x03, + ANYONECANPAY = 0x80, + ALL_ANYONECANPAY = SIGHASH.ALL | SIGHASH.ANYONECANPAY, + NONE_ANYONECANPAY = SIGHASH.NONE | SIGHASH.ANYONECANPAY, + SINGLE_ANYONECANPAY = SIGHASH.SINGLE | SIGHASH.ANYONECANPAY, +} + +export interface SignInputOption { + /** + * Prevout of this input + */ + prevout: Vout + /** + * EllipticPair to generate a signature with + */ + ellipticPair: EllipticPair + /** + * Optionally provide a witness script, + * or it will be guessed if it can be guessed. + */ + witnessScript?: Script +} + +export interface SignOption { + sigHashType?: SIGHASH + validate?: { + version?: boolean + lockTime?: boolean + } +} + +function hashPrevouts (transaction: Transaction, sigHashType: SIGHASH): string { + if (sigHashType !== SIGHASH.ALL) { + throw new Error('currently only SIGHASH.ALL is supported') + } + + const buffer = new SmartBuffer() + for (const vin of transaction.vin) { + const txid = Buffer.from(vin.txid, 'hex').reverse() + buffer.writeBuffer(txid) + buffer.writeUInt32LE(vin.index) + } + return dSHA256(buffer.toBuffer()).toString('hex') +} + +function hashSequence (transaction: Transaction, sigHashType: SIGHASH): string { + if (sigHashType !== SIGHASH.ALL) { + throw new Error('currently only SIGHASH.ALL is supported') + } + + const buffer = new SmartBuffer() + for (const vin of transaction.vin) { + buffer.writeUInt32LE(vin.sequence) + } + return dSHA256(buffer.toBuffer()).toString('hex') +} + +function hashOutputs (transaction: Transaction, sigHashType: SIGHASH): string { + if (sigHashType !== SIGHASH.ALL) { + throw new Error('currently only SIGHASH.ALL is supported') + } + + const buffer = new SmartBuffer() + transaction.vout.forEach(vout => (new CVoutV4(vout)).toBuffer(buffer)) + return dSHA256(buffer.toBuffer()).toString('hex') +} + +/** + * + * The witness must consist of exactly 2 items. + * The '0' in scriptPubKey indicates the following push is a version 0 witness program. + * The length of 20 indicates that it is a P2WPKH type. + * + * @param {SignInputOption} signInputOption to check is is V0 P2WPKH + */ +async function isV0P2WPKH (signInputOption: SignInputOption): Promise { + const stack = signInputOption.prevout.script.stack + + if (stack.length === 2 && stack[1] instanceof OP_PUSHDATA && (stack[1] as OP_PUSHDATA).length() === 20) { + const pubkey: Buffer = await signInputOption.ellipticPair.publicKey() + const pubkeyHashHex = HASH160(pubkey).toString('hex') + const pushDataHex = (stack[1] as OP_PUSHDATA).hex + + if (pubkeyHashHex === pushDataHex) { + return true + } + + throw new Error('invalid input option - attempting to sign a mismatch vout and elliptic pair is not allowed') + } + + return false +} + +/** + * If script is not provided, it needs to be guessed + * + * @param {Vin} vin of the script + * @param {SignInputOption} signInputOption to sign the vin + */ +async function getScriptCode (vin: Vin, signInputOption: SignInputOption): Promise