diff --git a/.gitignore b/.gitignore index 65921fa..bdbdbd0 100644 --- a/.gitignore +++ b/.gitignore @@ -71,6 +71,4 @@ typings/ # dotenv environment variables file .env -# package-lock bc its specific to npm package-lock.json - diff --git a/__tests__/chain.test.ts b/__tests__/chain.test.ts index 2acc528..df7e29f 100644 --- a/__tests__/chain.test.ts +++ b/__tests__/chain.test.ts @@ -9,7 +9,9 @@ import { getAllTX } from '../lib/net'; import { extractInfo } from '../lib/chain'; import TXList from '../lib/TXList'; -import { genCommitTx, genLockTx, genUnlockTx } from '../lib/txs'; +import { genCommitTx } from '../lib/tx-commit.ts'; +import { genLockTx } from '../lib/tx-lock.ts'; +import { genUnlockTx } from '../lib/tx-unlock.ts'; describe('chain state', () => { it('finds the one current name', async () => { diff --git a/__tests__/txs.test.ts b/__tests__/txs.test.ts index 8d317d7..d2dc271 100644 --- a/__tests__/txs.test.ts +++ b/__tests__/txs.test.ts @@ -8,14 +8,27 @@ import { } from 'bcoin'; import { - genRedeemScript, - genLockTx, - genUnlockTx, - genCommitTx, - genCommitRedeemScript, serializeCommitData, deserializeCommitData, } from '../lib/txs'; + +import { + genUnlockTx, +} from '../lib/tx-unlock'; + +import { + genLockTx, +} from '../lib/tx-lock'; + +import { + genCommitTx, +} from '../lib/tx-commit'; + +import { + genRedeemScript, + genCommitRedeemScript, +} from '../lib/tx-generate'; + import { BadUserPublicKeyError, BadServicePublicKeyError, diff --git a/__tests__/verify.test.ts b/__tests__/verify.test.ts index e71850a..8e16647 100644 --- a/__tests__/verify.test.ts +++ b/__tests__/verify.test.ts @@ -1,9 +1,9 @@ import { verifyLockTX, verifyCommitTX } from '../lib/verify'; import { - genLockTx, - genCommitTx, serializeCommitData, } from '../lib/txs'; +import { genCommitTx } from '../lib/tx-commit'; +import { genLockTx } from '../lib/tx-lock'; import { keyring as KeyRing, coin as Coin, diff --git a/bin/bitname-cli.ts b/bin/bitname-cli.ts index 4804e79..e0a273c 100644 --- a/bin/bitname-cli.ts +++ b/bin/bitname-cli.ts @@ -7,7 +7,10 @@ import { crypto, util, } from 'bcoin'; -import { genLockTx, genUnlockTx, genCommitTx, getLockTxPubKey } from '../lib/txs'; +import { genUnlockTx } from '../lib/tx-unlock'; +import { genCommitTx } from '../lib/tx-commit'; +import { getLockTxPubKey } from '../lib/txs'; +import { genLockTx } from '../lib/tx-lock'; import { fundTx, getFeesSatoshiPerKB, getAllTX, getBlockHeight, getTX, postTX } from '../lib/net'; import { extractInfo } from '../lib/chain'; @@ -66,6 +69,9 @@ async function commit(argv: yargs.Arguments) { return errorNoFees(); } + // const upfrontFee = 500000; + // const delayFee = 1500000; + const commitFee = 500000; const registerFee = 500000; const escrowFee = 1000000; diff --git a/index.ts b/index.ts index c5e3626..b0b0871 100644 --- a/index.ts +++ b/index.ts @@ -1,5 +1,9 @@ export { genCommitTx, +} from './lib/tx-commit'; +export { genLockTx, +} from './lib/tx-lock'; +export { genUnlockTx, -} from './lib/txs'; +} from './lib/tx-unlock'; diff --git a/lib/tx-commit.ts b/lib/tx-commit.ts new file mode 100644 index 0000000..b6de50b --- /dev/null +++ b/lib/tx-commit.ts @@ -0,0 +1,124 @@ +import { + script as Script, + address as Address, + output as Output, + mtx as MTX, + coin as Coin, + tx as TX, + keyring as KeyRing, + crypto, +} from 'bcoin'; + +import { + genCommitRedeemScript, +} from './tx-generate'; + +import { + genP2shAddr, +} from './txs'; + +import { isURISafe } from './verify'; + +import randomBytes from 'randombytes'; + +/** + * Generate a commit transaction. + * @param coins Array of coins to fund the transaction. + * @param name Name for name/key pair. + * @param locktime Absolute locktime + * @param commitFee Commit fee, in satoshis. + * @param registerFee Registration fee, in satoshis. + * @param escrowFee Escrow fee, in satoshis. + * @param feeRate Fee rate, in satoshis/kilobyte. + * @param userRing The user's key ring. + * @param servicePubKey Service public key. + */ +function genCommitTx(coins: Coin[], + name: string, + locktime: number, + commitFee: number, + registerFee: number, + escrowFee: number, + feeRate: number, + userRing: KeyRing, + servicePubKey: Buffer): TX { + // + // Validate name and the service public key + if (name.length > 64) { + throw new Error('Name is too long'); + } + + if (!isURISafe(name)) { + throw new Error('Invalid character(s) in name'); + } + + if (!crypto.secp256k1.publicKeyVerify(servicePubKey)) { + throw new Error('Invalid service public key'); + } + + // Generate a P2SH address from a redeem script, using a random nonce + const nonce = randomBytes(32); + const redeemScript = genCommitRedeemScript(userRing.getPublicKey(), nonce, name, locktime); + const p2shAddr = genP2shAddr(redeemScript); + + // Generate service address from service public key + const servicePKH = crypto.hash160(servicePubKey); + const serviceAddr = Address.fromPubkeyhash(servicePKH); + + const lockTx = new MTX(); + + // Compute total value of coins + const total = coins.reduce((acc, cur) => acc + cur.value, 0); + + for (const coin of coins) { + lockTx.addCoin(coin); + } + + // Compute change amount + const changeVal = total - (commitFee + registerFee + escrowFee) - (4 * feeRate); + + // Add nonce OP_RETURN as output 0 + const pubkeyDataScript = Script.fromNulldata(nonce); + lockTx.addOutput(Output.fromScript(pubkeyDataScript, 0)); + + // Add service upfront fee as output 1 + lockTx.addOutput({ + address: serviceAddr, + value: commitFee, + }); + + // Add locked fee as output 2 + // Locks up the fee to register, the fee to be put in escrow, and enough for a 4kb tx at current rates + lockTx.addOutput({ + address: p2shAddr, + value: registerFee + escrowFee + 4 * feeRate, + }); + + // Add change output as 3 + lockTx.addOutput({ + address: userRing.getAddress(), + value: changeVal, + }); + + // Add coins as inputs + for (let i = 0; i < coins.length; ++i) { + const coin = coins[i]; + lockTx.scriptInput(i, coin, userRing); + } + + // Each signature is 72 bytes long + const virtSize = lockTx.getVirtualSize() + coins.length * 72; + lockTx.subtractIndex(3, Math.ceil(virtSize / 1000 * feeRate)); + + // Sign the coins + for (let i = 0; i < coins.length; ++i) { + const coin = coins[i]; + lockTx.signInput(i, coin, userRing, Script.hashType.ALL); + } + + return lockTx.toTX(); + +} +export { + genCommitTx, +}; diff --git a/lib/tx-generate.ts b/lib/tx-generate.ts new file mode 100644 index 0000000..115d5de --- /dev/null +++ b/lib/tx-generate.ts @@ -0,0 +1,112 @@ +import { + script as Script, + crypto, +} from 'bcoin'; + +import { + BadUserPublicKeyError, + BadServicePublicKeyError, +} from './errors'; + +import { + serializeCommitData, +} from './txs'; + +/** + * Generate a redeem script, removing a name/key pair from the blockchain. + * Validates `userPubkey` and `servicePubkey`. + * + * @param userPubkey The user's public key. + * @param servicePubkey The service's public key. + * @param alocktime An absolute lock time, in blocks. + */ +function genRedeemScript(userPubkey: Buffer, servicePubkey: Buffer, alocktime: number): Script { + // Validate user public key + if (!crypto.secp256k1.publicKeyVerify(userPubkey)) { + throw new BadUserPublicKeyError(); + } + + // Validate service public key + if (!crypto.secp256k1.publicKeyVerify(servicePubkey)) { + throw new BadServicePublicKeyError(); + } + + const script = new Script(null); + + script.pushSym('OP_IF'); + + // + // If spending as user, execute this branch + + // Verify that 0 <= current block size - commit block size + script.pushInt(0); + script.pushSym('OP_CHECKSEQUENCEVERIFY'); + script.pushSym('OP_DROP'); + + // Check the provided user signature + script.pushData(userPubkey); + script.pushSym('OP_CHECKSIG'); + + script.pushSym('OP_ELSE'); + + // + // Otherwise, if spending as service, execute this branch + + // Verify that alocktime <= current block size + script.pushInt(alocktime); + script.pushSym('OP_CHECKLOCKTIMEVERIFY'); + script.pushSym('OP_DROP'); + + // Check the provided service signature + script.pushData(servicePubkey); + script.pushSym('OP_CHECKSIG'); + + script.pushSym('OP_ENDIF'); + + script.compile(); + + return script; +} + +/** + * Generate a commit redeem script. + * @param userPubkey The user's public key. + * @param nonce A 256-bit buffer representing a nonce. + * @param name A name of at most 64 characters composed of URL-safe characters. + * @param locktime An absolute lock time, in blocks. + */ +function genCommitRedeemScript(userPubkey: Buffer, nonce: Buffer, name: string, locktime: number): Script { + // Validate user public key + if (!crypto.secp256k1.publicKeyVerify(userPubkey)) { + throw new BadUserPublicKeyError(); + } + + const script = new Script(); + + // Verify that at least six blocks have passed since commit + script.pushInt(6); + script.pushSym('OP_CHECKSEQUENCEVERIFY'); + script.pushSym('OP_DROP'); + + // + // Hash [256-bit nonce + 2-byte locktime (BE) + 1-byte length of name + name] + // and check against parameters. + script.pushSym('OP_HASH256'); + + const hashData = serializeCommitData(nonce, locktime, name); + const hash = crypto.hash256(hashData); + script.pushData(hash); + script.pushSym('OP_EQUALVERIFY'); + + // Check user signature + script.pushData(userPubkey); + script.pushSym('OP_CHECKSIG'); + + script.compile(); + return script; +} + +export { + genRedeemScript, + genCommitRedeemScript, +}; diff --git a/lib/tx-lock.ts b/lib/tx-lock.ts new file mode 100644 index 0000000..4dd813e --- /dev/null +++ b/lib/tx-lock.ts @@ -0,0 +1,132 @@ +import { + genRedeemScript, + genCommitRedeemScript, +} from './tx-generate'; + +import { + genP2shAddr, + serializeCommitData, +} from './txs'; + +import { + script as Script, + address as Address, + mtx as MTX, + tx as TX, + keyring as KeyRing, + crypto, +} from 'bcoin'; + +import { isURISafe, verifyCommitTX } from './verify'; + +/** + * Generate a lock transaction. + * @param commitTX The corresponding commit transaction. + * @param name The name to use. + * @param upfrontFee The upfront fee in satoshis to use the service, as + * determined by the service. + * @param lockedFee Fee incentivizing registrar to provide service, as + * determined by the service. + * @param feeRate Fee rate in satoshi/KB. + * @param userRing The user key ring. + * @param servicePubKey Service public key. + * @param locktime Absolute lock time in blocks. + */ +function genLockTx(commitTX: TX, + name: string, + upfrontFee: number, + lockedFee: number, + feeRate: number, + userRing: KeyRing, + servicePubKey: Buffer, + locktime: number): TX { + // + // Input validation + if (locktime > 500000000) { + throw new Error('Locktime must be less than 500000000 blocks'); + } + + if (name.length > 64) { + throw new Error('Name is too long'); + } + + if (!isURISafe(name)) { + throw new Error('Invalid character(s) in name'); + } + + if (!crypto.secp256k1.publicKeyVerify(servicePubKey)) { + throw new Error('Invalid service public key'); + } + + if (!verifyCommitTX(commitTX, userRing.getPublicKey(), servicePubKey, name, locktime)) { + throw new Error('Invalid commitment tx'); + } + + // Generate a P2SH address from redeem script + const redeemScript = genRedeemScript(userRing.getPublicKey(), servicePubKey, locktime); + const p2shAddr = genP2shAddr(redeemScript); + + // Generate address from service public key + const servicePKH = crypto.hash160(servicePubKey); + const serviceAddr = Address.fromPubkeyhash(servicePKH); + + const lockTx = MTX.fromOptions({ + version: 2, + }); + + lockTx.addTX(commitTX, 2); + + lockTx.setSequence(0, 6); + + const total = commitTX.outputs[2].value; + + const changeVal = total - upfrontFee - lockedFee; + + // Add upfront fee as output 0 + lockTx.addOutput({ + address: serviceAddr, + value: upfrontFee, + }); + + // Add locked fee as output 1 + lockTx.addOutput({ + address: p2shAddr, + value: lockedFee, + }); + + // Add change output as 2 + lockTx.addOutput({ + address: userRing.getAddress(), + value: changeVal, + }); + + const nonce = commitTX.outputs[0].script.code[1].data; + + const hashData = serializeCommitData(nonce, locktime, name); + + const commitRedeemScript = genCommitRedeemScript(userRing.getPublicKey(), nonce, name, locktime); + + const unlockScript = new Script(); + unlockScript.pushData(hashData); + unlockScript.pushData(commitRedeemScript.toRaw()); + unlockScript.compile(); + + lockTx.inputs[0].script = unlockScript; + + // Add constant for signature + const virtSize = lockTx.getVirtualSize() + 72; + + // Calculate fee to be paid + const fee = Math.ceil(virtSize / 1000 * feeRate); + lockTx.subtractIndex(2, fee); + + // Add signature + const sig = lockTx.signature(0, commitRedeemScript, total, userRing.getPrivateKey(), Script.hashType.ALL, 0); + unlockScript.insertData(0, sig); + unlockScript.compile(); + + return lockTx.toTX(); +} +export { + genLockTx, +}; diff --git a/lib/tx-unlock.ts b/lib/tx-unlock.ts new file mode 100644 index 0000000..ebf9491 --- /dev/null +++ b/lib/tx-unlock.ts @@ -0,0 +1,107 @@ +import { + script as Script, + mtx as MTX, + tx as TX, + keyring as KeyRing, + crypto, +} from 'bcoin'; + +import { + getLockTxTime, +} from './txs'; + +import { + genRedeemScript, +} from './tx-generate'; + +import { verifyLockTX } from './verify'; + +import { + BadLockTransactionError, +} from './errors'; + +/** + * Generate an unlock transaction. + * @param lockTx The corresponding lock transaction. + * @param commitTx The corresponding commit transaction. + * @param feeRate The fee rate in satoshi/KB. + * @param service Whether to use the script as service. + * @param ring The service key ring if `service`, otherwise user key ring. + * @param otherPubKey The user key ring if `service`, otherwise service key ring. + */ +function genUnlockTx(lockTx: TX, + commitTx: TX, + feeRate: number, + service: boolean, + ring: KeyRing, + otherPubKey: Buffer): TX { + // Disambiguate ring public key and the other public key + const servicePubKey = service ? ring.getPublicKey() : otherPubKey; + const userPubKey = !service ? ring.getPublicKey() : otherPubKey; + + // + // Input validation + if (!verifyLockTX(lockTx, commitTx, servicePubKey)) { + throw new BadLockTransactionError(); + } + + if (!crypto.secp256k1.publicKeyVerify(otherPubKey)) { + throw new Error('Invalid service public key'); + } + + const locktime = getLockTxTime(lockTx); + if (locktime === null) { + throw new Error('Could not extract locktime'); + } + + const redeemScript = genRedeemScript(userPubKey, servicePubKey, locktime); + + const val = lockTx.outputs[1].value; // the P2SH output + const unlockTx = MTX.fromOptions({ + version: 2, + }); + + unlockTx.addTX(lockTx, 1); + + const boolVal = service ? 0 : 1; + + if (service) { + unlockTx.setLocktime(locktime); + } else { + unlockTx.setSequence(0, 0); + } + + unlockTx.addOutput({ + address: ring.getAddress(), + value: val, + }); + + // Generate new script: + const unlockScript = new Script(); + unlockScript.pushData(unlockTx.signature(0, redeemScript, val, ring.getPrivateKey(), Script.hashType.ALL, 0)); + unlockScript.pushInt(boolVal); + unlockScript.pushData(redeemScript.toRaw()); + unlockScript.compile(); + + unlockTx.inputs[0].script = unlockScript; + + // Compute a fee by multiplying the size by the rate, then account for it + const virtSize = unlockTx.getVirtualSize(); + const fee = Math.ceil(virtSize / 1000 * feeRate); + unlockTx.subtractFee(fee); + + // Remake script with the new signature + const unlockScript2 = new Script(); + unlockScript2.pushData(unlockTx.signature(0, redeemScript, val, ring.getPrivateKey(), Script.hashType.ALL, 0)); + unlockScript2.pushInt(boolVal); + unlockScript2.pushData(redeemScript.toRaw()); + unlockScript2.compile(); + + unlockTx.inputs[0].script = unlockScript2; + + return unlockTx.toTX(); +} + +export { + genUnlockTx, +}; diff --git a/lib/txs.ts b/lib/txs.ts index 60d3f1f..87b6271 100644 --- a/lib/txs.ts +++ b/lib/txs.ts @@ -1,118 +1,9 @@ import { script as Script, address as Address, - output as Output, - mtx as MTX, - coin as Coin, tx as TX, - keyring as KeyRing, - crypto, } from 'bcoin'; -import randomBytes from 'randombytes'; - -import { - BadUserPublicKeyError, - BadServicePublicKeyError, - BadLockTransactionError, -} from './errors'; - -import { verifyLockTX, isURISafe, verifyCommitTX } from './verify'; - -/** - * Generate a redeem script, removing a name/key pair from the blockchain. - * Validates `userPubkey` and `servicePubkey`. - * - * @param userPubkey The user's public key. - * @param servicePubkey The service's public key. - * @param alocktime An absolute lock time, in blocks. - */ -function genRedeemScript(userPubkey: Buffer, servicePubkey: Buffer, alocktime: number): Script { - // Validate user public key - if (!crypto.secp256k1.publicKeyVerify(userPubkey)) { - throw new BadUserPublicKeyError(); - } - - // Validate service public key - if (!crypto.secp256k1.publicKeyVerify(servicePubkey)) { - throw new BadServicePublicKeyError(); - } - - const script = new Script(null); - - script.pushSym('OP_IF'); - - // - // If spending as user, execute this branch - - // Verify that 0 <= current block size - commit block size - script.pushInt(0); - script.pushSym('OP_CHECKSEQUENCEVERIFY'); - script.pushSym('OP_DROP'); - - // Check the provided user signature - script.pushData(userPubkey); - script.pushSym('OP_CHECKSIG'); - - script.pushSym('OP_ELSE'); - - // - // Otherwise, if spending as service, execute this branch - - // Verify that alocktime <= current block size - script.pushInt(alocktime); - script.pushSym('OP_CHECKLOCKTIMEVERIFY'); - script.pushSym('OP_DROP'); - - // Check the provided service signature - script.pushData(servicePubkey); - script.pushSym('OP_CHECKSIG'); - - script.pushSym('OP_ENDIF'); - - script.compile(); - - return script; -} - -/** - * Generate a commit redeem script. - * @param userPubkey The user's public key. - * @param nonce A 256-bit buffer representing a nonce. - * @param name A name of at most 64 characters composed of URL-safe characters. - * @param locktime An absolute lock time, in blocks. - */ -function genCommitRedeemScript(userPubkey: Buffer, nonce: Buffer, name: string, locktime: number): Script { - // Validate user public key - if (!crypto.secp256k1.publicKeyVerify(userPubkey)) { - throw new BadUserPublicKeyError(); - } - - const script = new Script(); - - // Verify that at least six blocks have passed since commit - script.pushInt(6); - script.pushSym('OP_CHECKSEQUENCEVERIFY'); - script.pushSym('OP_DROP'); - - // - // Hash [256-bit nonce + 2-byte locktime (BE) + 1-byte length of name + name] - // and check against parameters. - script.pushSym('OP_HASH256'); - - const hashData = serializeCommitData(nonce, locktime, name); - const hash = crypto.hash256(hashData); - script.pushData(hash); - script.pushSym('OP_EQUALVERIFY'); - - // Check user signature - script.pushData(userPubkey); - script.pushSym('OP_CHECKSIG'); - - script.compile(); - return script; -} - /** * Generate a P2SH address from a redeem script. * @param redeemScript The script to use. @@ -198,213 +89,6 @@ function deserializeCommitData(data: Buffer): ICommitData { }; } -/** - * Generate a commit transaction. - * @param coins Array of coins to fund the transaction. - * @param name Name for name/key pair. - * @param locktime Absolute locktime - * @param commitFee Commit fee, in satoshis. - * @param registerFee Registration fee, in satoshis. - * @param escrowFee Escrow fee, in satoshis. - * @param feeRate Fee rate, in satoshis/kilobyte. - * @param userRing The user's key ring. - * @param servicePubKey Service public key. - */ -function genCommitTx(coins: Coin[], - name: string, - locktime: number, - commitFee: number, - registerFee: number, - escrowFee: number, - feeRate: number, - userRing: KeyRing, - servicePubKey: Buffer): TX { - // - // Validate name and the service public key - if (name.length > 64) { - throw new Error('Name is too long'); - } - - if (!isURISafe(name)) { - throw new Error('Invalid character(s) in name'); - } - - if (!crypto.secp256k1.publicKeyVerify(servicePubKey)) { - throw new Error('Invalid service public key'); - } - - // Generate a P2SH address from a redeem script, using a random nonce - const nonce = randomBytes(32); - const redeemScript = genCommitRedeemScript(userRing.getPublicKey(), nonce, name, locktime); - const p2shAddr = genP2shAddr(redeemScript); - - // Generate service address from service public key - const servicePKH = crypto.hash160(servicePubKey); - const serviceAddr = Address.fromPubkeyhash(servicePKH); - - const lockTx = new MTX(); - - // Compute total value of coins - const total = coins.reduce((acc, cur) => acc + cur.value, 0); - - for (const coin of coins) { - lockTx.addCoin(coin); - } - - // Compute change amount - const changeVal = total - (commitFee + registerFee + escrowFee) - (4 * feeRate); - - // Add nonce OP_RETURN as output 0 - const pubkeyDataScript = Script.fromNulldata(nonce); - lockTx.addOutput(Output.fromScript(pubkeyDataScript, 0)); - - // Add service upfront fee as output 1 - lockTx.addOutput({ - address: serviceAddr, - value: commitFee, - }); - - // Add locked fee as output 2 - // Locks up the fee to register, the fee to be put in escrow, and enough for a 4kb tx at current rates - lockTx.addOutput({ - address: p2shAddr, - value: registerFee + escrowFee + 4 * feeRate, - }); - - // Add change output as 3 - lockTx.addOutput({ - address: userRing.getAddress(), - value: changeVal, - }); - - // Add coins as inputs - for (let i = 0; i < coins.length; ++i) { - const coin = coins[i]; - lockTx.scriptInput(i, coin, userRing); - } - - // Each signature is 72 bytes long - const virtSize = lockTx.getVirtualSize() + coins.length * 72; - lockTx.subtractIndex(3, Math.ceil(virtSize / 1000 * feeRate)); - - // Sign the coins - for (let i = 0; i < coins.length; ++i) { - const coin = coins[i]; - lockTx.signInput(i, coin, userRing, Script.hashType.ALL); - } - - return lockTx.toTX(); -} - -/** - * Generate a lock transaction. - * @param commitTX The corresponding commit transaction. - * @param name The name to use. - * @param upfrontFee The upfront fee in satoshis to use the service, as - * determined by the service. - * @param lockedFee Fee incentivizing registrar to provide service, as - * determined by the service. - * @param feeRate Fee rate in satoshi/KB. - * @param userRing The user key ring. - * @param servicePubKey Service public key. - * @param locktime Absolute lock time in blocks. - */ -function genLockTx(commitTX: TX, - name: string, - upfrontFee: number, - lockedFee: number, - feeRate: number, - userRing: KeyRing, - servicePubKey: Buffer, - locktime: number): TX { - // - // Input validation - if (locktime > 500000000) { - throw new Error('Locktime must be less than 500000000 blocks'); - } - - if (name.length > 64) { - throw new Error('Name is too long'); - } - - if (!isURISafe(name)) { - throw new Error('Invalid character(s) in name'); - } - - if (!crypto.secp256k1.publicKeyVerify(servicePubKey)) { - throw new Error('Invalid service public key'); - } - - if (!verifyCommitTX(commitTX, userRing.getPublicKey(), servicePubKey, name, locktime)) { - throw new Error('Invalid commitment tx'); - } - - // Generate a P2SH address from redeem script - const redeemScript = genRedeemScript(userRing.getPublicKey(), servicePubKey, locktime); - const p2shAddr = genP2shAddr(redeemScript); - - // Generate address from service public key - const servicePKH = crypto.hash160(servicePubKey); - const serviceAddr = Address.fromPubkeyhash(servicePKH); - - const lockTx = MTX.fromOptions({ - version: 2, - }); - - lockTx.addTX(commitTX, 2); - - lockTx.setSequence(0, 6); - - const total = commitTX.outputs[2].value; - - const changeVal = total - upfrontFee - lockedFee; - - // Add upfront fee as output 0 - lockTx.addOutput({ - address: serviceAddr, - value: upfrontFee, - }); - - // Add locked fee as output 1 - lockTx.addOutput({ - address: p2shAddr, - value: lockedFee, - }); - - // Add change output as 2 - lockTx.addOutput({ - address: userRing.getAddress(), - value: changeVal, - }); - - const nonce = commitTX.outputs[0].script.code[1].data; - - const hashData = serializeCommitData(nonce, locktime, name); - - const commitRedeemScript = genCommitRedeemScript(userRing.getPublicKey(), nonce, name, locktime); - - const unlockScript = new Script(); - unlockScript.pushData(hashData); - unlockScript.pushData(commitRedeemScript.toRaw()); - unlockScript.compile(); - - lockTx.inputs[0].script = unlockScript; - - // Add constant for signature - const virtSize = lockTx.getVirtualSize() + 72; - - // Calculate fee to be paid - const fee = Math.ceil(virtSize / 1000 * feeRate); - lockTx.subtractIndex(2, fee); - - // Add signature - const sig = lockTx.signature(0, commitRedeemScript, total, userRing.getPrivateKey(), Script.hashType.ALL, 0); - unlockScript.insertData(0, sig); - unlockScript.compile(); - - return lockTx.toTX(); -} - /** * Extract metadata from a script (i.e. the nonce, locktime, and name). * @param inputScript Script to read from. @@ -468,98 +152,11 @@ function getLockTxPubKey(lockTx: TX): Buffer | null { return pubKey; } -/** - * Generate an unlock transaction. - * @param lockTx The corresponding lock transaction. - * @param commitTx The corresponding commit transaction. - * @param feeRate The fee rate in satoshi/KB. - * @param service Whether to use the script as service. - * @param ring The service key ring if `service`, otherwise user key ring. - * @param otherPubKey The user key ring if `service`, otherwise service key ring. - */ -function genUnlockTx(lockTx: TX, - commitTx: TX, - feeRate: number, - service: boolean, - ring: KeyRing, - otherPubKey: Buffer): TX { - // Disambiguate ring public key and the other public key - const servicePubKey = service ? ring.getPublicKey() : otherPubKey; - const userPubKey = !service ? ring.getPublicKey() : otherPubKey; - - // - // Input validation - if (!verifyLockTX(lockTx, commitTx, servicePubKey)) { - throw new BadLockTransactionError(); - } - - if (!crypto.secp256k1.publicKeyVerify(otherPubKey)) { - throw new Error('Invalid service public key'); - } - - const locktime = getLockTxTime(lockTx); - if (locktime === null) { - throw new Error('Could not extract locktime'); - } - - const redeemScript = genRedeemScript(userPubKey, servicePubKey, locktime); - - const val = lockTx.outputs[1].value; // the P2SH output - const unlockTx = MTX.fromOptions({ - version: 2, - }); - - unlockTx.addTX(lockTx, 1); - - const boolVal = service ? 0 : 1; - - if (service) { - unlockTx.setLocktime(locktime); - } else { - unlockTx.setSequence(0, 0); - } - - unlockTx.addOutput({ - address: ring.getAddress(), - value: val, - }); - - // Generate new script: - const unlockScript = new Script(); - unlockScript.pushData(unlockTx.signature(0, redeemScript, val, ring.getPrivateKey(), Script.hashType.ALL, 0)); - unlockScript.pushInt(boolVal); - unlockScript.pushData(redeemScript.toRaw()); - unlockScript.compile(); - - unlockTx.inputs[0].script = unlockScript; - - // Compute a fee by multiplying the size by the rate, then account for it - const virtSize = unlockTx.getVirtualSize(); - const fee = Math.ceil(virtSize / 1000 * feeRate); - unlockTx.subtractFee(fee); - - // Remake script with the new signature - const unlockScript2 = new Script(); - unlockScript2.pushData(unlockTx.signature(0, redeemScript, val, ring.getPrivateKey(), Script.hashType.ALL, 0)); - unlockScript2.pushInt(boolVal); - unlockScript2.pushData(redeemScript.toRaw()); - unlockScript2.compile(); - - unlockTx.inputs[0].script = unlockScript2; - - return unlockTx.toTX(); -} - export { - genLockTx, - genUnlockTx, - genRedeemScript, genP2shAddr, getLockTxName, getLockTxTime, getLockTxPubKey, - genCommitTx, - genCommitRedeemScript, serializeCommitData, deserializeCommitData, }; diff --git a/lib/verify.ts b/lib/verify.ts index 7dd07e4..def261b 100644 --- a/lib/verify.ts +++ b/lib/verify.ts @@ -5,12 +5,14 @@ import { crypto, } from 'bcoin'; import { - genRedeemScript, - genCommitRedeemScript, getLockTxName, getLockTxTime, getLockTxPubKey, } from './txs'; +import { + genRedeemScript, + genCommitRedeemScript, +} from './tx-generate'; /** * Returns a Boolean value that indicates if URI has and only has alphanumeric leteral and '_', '-', '.' '~' diff --git a/package.json b/package.json index 35a6fb2..3201133 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,8 @@ "n64": "^0.0.18", "node-fetch": "^1.7.3", "randombytes": "^2.0.5", - "yargs": "^10.0.3" + "yargs": "^10.0.3", + "yarn": "^1.5.1" }, "scripts": { "build": "rollup -c", diff --git a/yarn.lock b/yarn.lock index 1aa88f2..b585be2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3757,6 +3757,10 @@ yargs@~3.10.0: decamelize "^1.0.0" window-size "0.1.0" +yarn@^1.5.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/yarn/-/yarn-1.5.1.tgz#e8680360e832ac89521eb80dad3a7bc27a40bab4" + yeast@0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/yeast/-/yeast-0.1.2.tgz#008e06d8094320c372dbc2f8ed76a0ca6c8ac419"