diff --git a/lbry-demo.js b/lbry-demo.js new file mode 100644 index 0000000..ba3b7eb --- /dev/null +++ b/lbry-demo.js @@ -0,0 +1,87 @@ +// This is a standalone script that you can try out in its own project. You +// should install this fork of bitcoinjs-lib, but otherwise you can get from +// normal npm. + +// TODO - maybe just put this in a README or similar file + +const ecpair = require('ecpair') +const bs58check = require('bs58check'); +const ecc = require('tiny-secp256k1') +const lbry = require('bitcoinjs-lib') +const ECPair = ecpair.ECPairFactory(ecc) +const network = lbry.networks.regtest + +// Previous data from a regtest instance + +const key = ECPair.fromPrivateKey(bs58check.decode('cRWuHDPLjySJJzen1eztGD8SYq396NjBTdEwU3WF1LRRJ8NwhYCT').slice(1, -1), network) + +// A previous transaction + +/* + "amount": 0.00000000, + "fee": -0.01400000, + "confirmations": 7, + "blockhash": "2006054341b56529181e194531217823454650c07997c75dd796b9a8fe8a8fec", + "blockindex": 1, + "blocktime": 1646858320, + "txid": "a2f0ea39d3f54f46e30a4bfac403988779ca81b132c2869764535e836ba03a18", + "walletconflicts": [ + ], + "time": 1646858274, + "timereceived": 1646858274, + "bip125-replaceable": "no", + "details": [ + { + "address": "mo4PK1TNw8ssTG6gM6JdWZfoFdasnNfqaE", + "category": "send", + "amount": -1.00000000, + "vout": 1, + "fee": -0.01400000, + "abandoned": false + }, + { + "address": "mo4PK1TNw8ssTG6gM6JdWZfoFdasnNfqaE", + "category": "receive", + "amount": 1.00000000, + "vout": 1 + } + ], + + { + "n": 0, + "name": "my_name", + "claimId": "cc2181b64c566f29733e88cee984923f4f1b8c05", + "value": "deadbeef", + "depth": 6, + "inClaimTrie": false, + "inQueue": false + } +*/ +const nonWitnessUtxo = Buffer.from('020000000285ea15a6f1294a52b6424365d0a76e939b6ca9c8e87b25d67bebab1c1cd10a4300000000484730440220664b43577c03e6e3ced92bb699ab99c8d35c34ebca4ace408f0d4b54e807e055022050decc7da867941bac47ad097e14e0cff8e33fe199267ef8d1c64fce9f1e0faf01feffffffec34291fe8e028108f2d71e18fbd4b89a2d476d3d87f48c55e48373d122e7d7a0000000048473044022003a900648a3f5092e315234b7a17f366698957427a3ae402f825d6bc0e500257022061bef51b321ba9e64a569853c2d7fd87e025a504ea100ff028bb76c6af48ee5a01feffffff024084e005000000001976a914036c5b9a022ae12360bd539825f6f2e6fb3080d088ac00e1f5050000000029b5076d795f6e616d6504deadbeef6d7576a91452baa8720ff969c4a927757f81275d2a42291c2088ac78000000', 'hex') + +// Based on p2pkh, adding claimName and claim +const payment = lbry.payments.claimName({ + pubkey: key.publicKey, + claimName: "claim", + claim: Buffer.from("new_claim") +}) + +exports.psbt = new lbry.Psbt({ network }) + +exports.psbt.addOutput({ script: payment.output, value: 99000000 }) + +// For some reason they want the entirety of the input txn (nonWitnessUtxo) as well as its hash here +exports.psbt.addInput({ + nonWitnessUtxo, + hash: 'a2f0ea39d3f54f46e30a4bfac403988779ca81b132c2869764535e836ba03a18', + index: 1 // matching vout in the input txn +}) + +exports.psbt.signInput(0, key) + +exports.result = exports.psbt + .finalizeAllInputs() + .extractTransaction() + .toHex() + +// Putting exports.result into the regtest succeeded. The claim showed up. diff --git a/src/address.js b/src/address.js index 164bf7e..0667699 100644 --- a/src/address.js +++ b/src/address.js @@ -86,7 +86,7 @@ function toBech32(data, version, prefix) { exports.toBech32 = toBech32; function fromOutputScript(output, network) { // TODO: Network - network = network || networks.bitcoin; + network = network || networks.mainnet; try { return payments.p2pkh({ output, network }).address; } catch (e) {} @@ -106,7 +106,7 @@ function fromOutputScript(output, network) { } exports.fromOutputScript = fromOutputScript; function toOutputScript(address, network) { - network = network || networks.bitcoin; + network = network || networks.mainnet; let decodeBase58; let decodeBech32; try { diff --git a/src/networks.d.ts b/src/networks.d.ts index d5590fd..15c92eb 100644 --- a/src/networks.d.ts +++ b/src/networks.d.ts @@ -10,7 +10,7 @@ interface Bip32 { public: number; private: number; } -export declare const bitcoin: Network; +export declare const mainnet: Network; export declare const regtest: Network; export declare const testnet: Network; export {}; diff --git a/src/networks.js b/src/networks.js index ea710f8..060c27b 100644 --- a/src/networks.js +++ b/src/networks.js @@ -1,36 +1,36 @@ 'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); -exports.testnet = exports.regtest = exports.bitcoin = void 0; -exports.bitcoin = { +exports.testnet = exports.regtest = exports.mainnet = void 0; +exports.mainnet = { messagePrefix: '\x18Bitcoin Signed Message:\n', - bech32: 'bc', + bech32: 'bech32', bip32: { public: 0x0488b21e, - private: 0x0488ade4, + private: 0x0488ade4, // lbry/wallet/server/coin.py XPRV_VERBYTES (unchanged) }, - pubKeyHash: 0x00, - scriptHash: 0x05, - wif: 0x80, + pubKeyHash: 0x55, + scriptHash: 0x7a, + wif: 0x1c, // lbry/wallet/server/coin.py WIF_BYTE }; exports.regtest = { messagePrefix: '\x18Bitcoin Signed Message:\n', - bech32: 'bcrt', + bech32: 'bech32', bip32: { public: 0x043587cf, - private: 0x04358394, + private: 0x04358394, // lbry/wallet/server/coin.py XPRV_VERBYTES (unchanged) }, pubKeyHash: 0x6f, scriptHash: 0xc4, - wif: 0xef, + wif: 0x1c, // lbry/wallet/server/coin.py WIF_BYTE }; exports.testnet = { messagePrefix: '\x18Bitcoin Signed Message:\n', - bech32: 'tb', + bech32: 'bech32', bip32: { public: 0x043587cf, - private: 0x04358394, + private: 0x04358394, // lbry/wallet/server/coin.py XPRV_VERBYTES (unchanged) }, pubKeyHash: 0x6f, scriptHash: 0xc4, - wif: 0xef, + wif: 0x1c, // lbry/wallet/server/coin.py WIF_BYTE }; diff --git a/src/ops.js b/src/ops.js index 9d629cd..95a6957 100644 --- a/src/ops.js +++ b/src/ops.js @@ -112,9 +112,11 @@ const OPS = { OP_CHECKSEQUENCEVERIFY: 178, OP_NOP4: 179, OP_NOP5: 180, - OP_NOP6: 181, - OP_NOP7: 182, - OP_NOP8: 183, + // LBRY custom opcodes: tx types + // Replaces OP_NOP6 - OP_NOP8 + OP_CLAIM_NAME: 181, + OP_SUPPORT_CLAIM: 182, + OP_UPDATE_CLAIM: 183, OP_NOP9: 184, OP_NOP10: 185, OP_PUBKEYHASH: 253, diff --git a/src/payments/claim_name.d.ts b/src/payments/claim_name.d.ts new file mode 100644 index 0000000..daef256 --- /dev/null +++ b/src/payments/claim_name.d.ts @@ -0,0 +1,2 @@ +import { Payment, PaymentOpts } from './index'; +export declare function claimName(a: Payment, opts?: PaymentOpts): Payment; diff --git a/src/payments/claim_name.js b/src/payments/claim_name.js new file mode 100644 index 0000000..9a46ac9 --- /dev/null +++ b/src/payments/claim_name.js @@ -0,0 +1,179 @@ +'use strict'; +// This is mostly a copy of p2pkh, as the usual claim name script is based on that +Object.defineProperty(exports, '__esModule', { value: true }); +exports.claimName = void 0; +const bcrypto = require('../crypto'); +const networks_1 = require('../networks'); +const bscript = require('../script'); +const types_1 = require('../types'); +const lazy = require('./lazy'); +const bs58check = require('bs58check'); +const OPS = bscript.OPS; +// input: {signature} {pubkey} +// output: OP_CLAIM_NAME {claim_name} {claim} OP_2DROP OP_DROP OP_DUP OP_HASH160 {hash160(pubkey)} OP_EQUALVERIFY OP_CHECKSIG +function claimName(a, opts) { + if ( + !a.address && + !a.hash && + !a.output && + !a.pubkey && + !a.input && + !a.claim && + !a.claimName + ) + throw new TypeError('Not enough data'); + opts = Object.assign({ validate: true }, opts || {}); + (0, types_1.typeforce)( + { + network: types_1.typeforce.maybe(types_1.typeforce.Object), + address: types_1.typeforce.maybe(types_1.typeforce.String), + hash: types_1.typeforce.maybe(types_1.typeforce.BufferN(20)), + output: types_1.typeforce.maybe(types_1.typeforce.Buffer), + pubkey: types_1.typeforce.maybe(types_1.isPoint), + signature: types_1.typeforce.maybe(bscript.isCanonicalScriptSignature), + input: types_1.typeforce.maybe(types_1.typeforce.Buffer), + claimName: types_1.typeforce.maybe(types_1.typeforce.String), + claim: types_1.typeforce.maybe(types_1.typeforce.Buffer), + }, + a, + ); + const _address = lazy.value(() => { + const payload = bs58check.decode(a.address); + const version = payload.readUInt8(0); + const hash = payload.slice(1); + return { version, hash }; + }); + const _chunks = lazy.value(() => { + return bscript.decompile(a.input); + }); + // We need output chunks as well, we can't just go by byte location within + // the output, because claim and claimName are of variable length. + const _outputChunks = lazy.value(() => { + return bscript.decompile(a.output); + }); + const network = a.network || networks_1.mainnet; + const o = { name: 'claim_name', network }; + lazy.prop(o, 'address', () => { + if (!o.hash) return; + const payload = Buffer.allocUnsafe(21); + payload.writeUInt8(network.pubKeyHash, 0); + o.hash.copy(payload, 1); + return bs58check.encode(payload); + }); + lazy.prop(o, 'hash', () => { + if (a.output) return _outputChunks()[7]; + if (a.address) return _address().hash; + if (a.pubkey || o.pubkey) return bcrypto.hash160(a.pubkey || o.pubkey); + }); + lazy.prop(o, 'claim', () => { + if (a.output) return _outputChunks()[2]; + if (a.claim) return a.claim; + }); + lazy.prop(o, 'claimName', () => { + if (a.output) return _outputChunks()[1].toString(); + if (a.claimName) return a.claimName; + }); + lazy.prop(o, 'output', () => { + if (!o.hash) return; + if (!o.claimName) return; + if (!o.claim) return; + return bscript.compile([ + OPS.OP_CLAIM_NAME, + Buffer.from(o.claimName), + o.claim, + OPS.OP_2DROP, + OPS.OP_DROP, + OPS.OP_DUP, + OPS.OP_HASH160, + o.hash, + OPS.OP_EQUALVERIFY, + OPS.OP_CHECKSIG, + ]); + }); + lazy.prop(o, 'pubkey', () => { + if (!a.input) return; + return _chunks()[1]; + }); + lazy.prop(o, 'signature', () => { + if (!a.input) return; + return _chunks()[0]; + }); + lazy.prop(o, 'input', () => { + if (!a.pubkey) return; + if (!a.signature) return; + return bscript.compile([a.signature, a.pubkey]); + }); + lazy.prop(o, 'witness', () => { + if (!o.input) return; + return []; + }); + // extended validation + if (opts.validate) { + let hash = Buffer.from([]); + if (a.address) { + if (_address().version !== network.pubKeyHash) + throw new TypeError('Invalid version or Network mismatch'); + if (_address().hash.length !== 20) throw new TypeError('Invalid address'); + hash = _address().hash; + } + if (a.hash) { + if (hash.length > 0 && !hash.equals(a.hash)) + throw new TypeError('Hash mismatch'); + else hash = a.hash; + } + if (a.output) { + if ( + _outputChunks().length !== 10 || + _outputChunks()[0] !== OPS.OP_CLAIM_NAME || + !Buffer.isBuffer(_outputChunks()[1]) || + !Buffer.isBuffer(_outputChunks()[2]) || + _outputChunks()[3] !== OPS.OP_2DROP || + _outputChunks()[4] !== OPS.OP_DROP || + _outputChunks()[5] !== OPS.OP_DUP || + _outputChunks()[6] !== OPS.OP_HASH160 || + !Buffer.isBuffer(_outputChunks()[7]) || + _outputChunks()[7].length !== 0x14 || + _outputChunks()[8] !== OPS.OP_EQUALVERIFY || + _outputChunks()[9] !== OPS.OP_CHECKSIG + ) + throw new TypeError('Output is invalid'); + const hash2 = _outputChunks()[7]; + if (hash.length > 0 && !hash.equals(hash2)) + throw new TypeError('Hash mismatch'); + else hash = hash2; + const claimName2 = _outputChunks()[1].toString(); + if (a.claimName && a.claimName !== claimName2) + throw new TypeError('claimName mismatch'); + const claim2 = _outputChunks()[2]; + if ( + Buffer.isBuffer(a.claim) && + a.claim.length > 0 && + !a.claim.equals(claim2) + ) + throw new TypeError('claim mismatch'); + } + if (a.pubkey) { + const pkh = bcrypto.hash160(a.pubkey); + if (hash.length > 0 && !hash.equals(pkh)) + throw new TypeError('Hash mismatch'); + else hash = pkh; + } + if (a.input) { + const chunks = _chunks(); + if (chunks.length !== 2) throw new TypeError('Input is invalid'); + if (!bscript.isCanonicalScriptSignature(chunks[0])) + throw new TypeError('Input has invalid signature'); + if (!(0, types_1.isPoint)(chunks[1])) + throw new TypeError('Input has invalid pubkey'); + if (a.signature && !a.signature.equals(chunks[0])) + throw new TypeError('Signature mismatch'); + if (a.pubkey && !a.pubkey.equals(chunks[1])) + throw new TypeError('Pubkey mismatch'); + const pkh = bcrypto.hash160(chunks[1]); + if (hash.length > 0 && !hash.equals(pkh)) + throw new TypeError('Hash mismatch'); + } + } + return Object.assign(o, a); +} +exports.claimName = claimName; diff --git a/src/payments/embed.js b/src/payments/embed.js index 4b7218f..d0f3d74 100644 --- a/src/payments/embed.js +++ b/src/payments/embed.js @@ -26,7 +26,7 @@ function p2data(a, opts) { }, a, ); - const network = a.network || networks_1.bitcoin; + const network = a.network || networks_1.mainnet; const o = { name: 'embed', network }; lazy.prop(o, 'output', () => { if (!a.data) return; diff --git a/src/payments/index.d.ts b/src/payments/index.d.ts index 1edf071..dba04dd 100644 --- a/src/payments/index.d.ts +++ b/src/payments/index.d.ts @@ -7,6 +7,7 @@ import { p2pkh } from './p2pkh'; import { p2sh } from './p2sh'; import { p2wpkh } from './p2wpkh'; import { p2wsh } from './p2wsh'; +import { claimName } from './claim_name'; export interface Payment { name?: string; network?: Network; @@ -23,6 +24,8 @@ export interface Payment { hash?: Buffer; redeem?: Payment; witness?: Buffer[]; + claim?: Buffer; + claimName?: string; } export declare type PaymentCreator = (a: Payment, opts?: PaymentOpts) => Payment; export declare type PaymentFunction = () => Payment; @@ -33,4 +36,4 @@ export interface PaymentOpts { export declare type StackElement = Buffer | number; export declare type Stack = StackElement[]; export declare type StackFunction = () => Stack; -export { embed, p2ms, p2pk, p2pkh, p2sh, p2wpkh, p2wsh }; +export { embed, p2ms, p2pk, p2pkh, p2sh, p2wpkh, p2wsh, claimName }; diff --git a/src/payments/index.js b/src/payments/index.js index c23c529..dacc38a 100644 --- a/src/payments/index.js +++ b/src/payments/index.js @@ -1,6 +1,6 @@ 'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); -exports.p2wsh = exports.p2wpkh = exports.p2sh = exports.p2pkh = exports.p2pk = exports.p2ms = exports.embed = void 0; +exports.claimName = exports.p2wsh = exports.p2wpkh = exports.p2sh = exports.p2pkh = exports.p2pk = exports.p2ms = exports.embed = void 0; const embed_1 = require('./embed'); Object.defineProperty(exports, 'embed', { enumerable: true, @@ -50,5 +50,12 @@ Object.defineProperty(exports, 'p2wsh', { return p2wsh_1.p2wsh; }, }); +const claim_name_1 = require('./claim_name'); +Object.defineProperty(exports, 'claimName', { + enumerable: true, + get: function() { + return claim_name_1.claimName; + }, +}); // TODO // witness commitment diff --git a/src/payments/p2ms.js b/src/payments/p2ms.js index 0b7e72d..402a408 100644 --- a/src/payments/p2ms.js +++ b/src/payments/p2ms.js @@ -46,7 +46,7 @@ function p2ms(a, opts) { }, a, ); - const network = a.network || networks_1.bitcoin; + const network = a.network || networks_1.mainnet; const o = { network }; let chunks = []; let decoded = false; diff --git a/src/payments/p2pk.js b/src/payments/p2pk.js index 2849530..3514f10 100644 --- a/src/payments/p2pk.js +++ b/src/payments/p2pk.js @@ -6,6 +6,9 @@ const bscript = require('../script'); const types_1 = require('../types'); const lazy = require('./lazy'); const OPS = bscript.OPS; +// NOTE We don't use this type of scriptPubKey (p2pk) for LBRY. Presumably +// because it stopped being used in Bitcoin before LBRY was created. +// TODO delete this file? and maybe others? // input: {signature} // output: {pubKey} OP_CHECKSIG function p2pk(a, opts) { @@ -25,7 +28,7 @@ function p2pk(a, opts) { const _chunks = lazy.value(() => { return bscript.decompile(a.input); }); - const network = a.network || networks_1.bitcoin; + const network = a.network || networks_1.mainnet; const o = { name: 'p2pk', network }; lazy.prop(o, 'output', () => { if (!a.pubkey) return; diff --git a/src/payments/p2pkh.js b/src/payments/p2pkh.js index 8edc8ba..37ec394 100644 --- a/src/payments/p2pkh.js +++ b/src/payments/p2pkh.js @@ -35,7 +35,7 @@ function p2pkh(a, opts) { const _chunks = lazy.value(() => { return bscript.decompile(a.input); }); - const network = a.network || networks_1.bitcoin; + const network = a.network || networks_1.mainnet; const o = { name: 'p2pkh', network }; lazy.prop(o, 'address', () => { if (!o.hash) return; diff --git a/src/payments/p2sh.js b/src/payments/p2sh.js index 8710bf1..6f7165d 100644 --- a/src/payments/p2sh.js +++ b/src/payments/p2sh.js @@ -44,7 +44,7 @@ function p2sh(a, opts) { ); let network = a.network; if (!network) { - network = (a.redeem && a.redeem.network) || networks_1.bitcoin; + network = (a.redeem && a.redeem.network) || networks_1.mainnet; } const o = { network }; const _address = lazy.value(() => { diff --git a/src/payments/p2wpkh.js b/src/payments/p2wpkh.js index 168e08f..fa7f725 100644 --- a/src/payments/p2wpkh.js +++ b/src/payments/p2wpkh.js @@ -41,7 +41,7 @@ function p2wpkh(a, opts) { data: Buffer.from(data), }; }); - const network = a.network || networks_1.bitcoin; + const network = a.network || networks_1.mainnet; const o = { name: 'p2wpkh', network }; lazy.prop(o, 'address', () => { if (!o.hash) return; diff --git a/src/payments/p2wsh.js b/src/payments/p2wsh.js index 66ee1da..632ed0d 100644 --- a/src/payments/p2wsh.js +++ b/src/payments/p2wsh.js @@ -70,7 +70,7 @@ function p2wsh(a, opts) { }); let network = a.network; if (!network) { - network = (a.redeem && a.redeem.network) || networks_1.bitcoin; + network = (a.redeem && a.redeem.network) || networks_1.mainnet; } const o = { network }; lazy.prop(o, 'address', () => { diff --git a/src/psbt.js b/src/psbt.js index 6162195..1151fc0 100644 --- a/src/psbt.js +++ b/src/psbt.js @@ -19,7 +19,7 @@ const DEFAULT_OPTS = { * A bitcoinjs Network object. This is only used if you pass an `address` * parameter to addOutput. Otherwise it is not needed and can be left default. */ - network: networks_1.bitcoin, + network: networks_1.mainnet, /** * When extractTransaction is called, the fee rate is checked. * THIS IS NOT TO BE RELIED ON. @@ -671,6 +671,7 @@ function canFinalize(input, script, scriptType) { case 'pubkey': case 'pubkeyhash': case 'witnesspubkeyhash': + case 'claimname': return hasSigs(1, input.partialSig); case 'multisig': const p2ms = payments.p2ms({ output: script }); @@ -719,6 +720,7 @@ const isP2PKH = isPaymentFactory(payments.p2pkh); const isP2WPKH = isPaymentFactory(payments.p2wpkh); const isP2WSHScript = isPaymentFactory(payments.p2wsh); const isP2SHScript = isPaymentFactory(payments.p2sh); +const isClaimName = isPaymentFactory(payments.claimName); function bip32DerivationIsMine(root) { return d => { if (!d.masterFingerprint.equals(root.fingerprint)) return false; @@ -1050,6 +1052,13 @@ function getPayment(script, scriptType, partialSig) { signature: partialSig[0].signature, }); break; + case 'claimname': + payment = payments.claimName({ + output: script, + pubkey: partialSig[0].pubkey, + signature: partialSig[0].signature, + }); + break; } return payment; } @@ -1404,6 +1413,7 @@ function classifyScript(script) { if (isP2PKH(script)) return 'pubkeyhash'; if (isP2MS(script)) return 'multisig'; if (isP2PK(script)) return 'pubkey'; + if (isClaimName(script)) return 'claimname'; return 'nonstandard'; } function range(n) { diff --git a/test/bitcoin.core.spec.ts b/test/bitcoin.core.spec.ts index 9040416..31f50f7 100644 --- a/test/bitcoin.core.spec.ts +++ b/test/bitcoin.core.spec.ts @@ -50,7 +50,7 @@ describe('Bitcoin-core', () => { const network: any = params.isTestnet ? bitcoin.networks.testnet - : bitcoin.networks.bitcoin; + : bitcoin.networks.mainnet; const version = network[typeMap[params.addrType]]; it('can export ' + expected, () => { @@ -65,8 +65,8 @@ describe('Bitcoin-core', () => { // base58KeysInvalid describe('address.fromBase58Check', () => { const allowedNetworks = [ - bitcoin.networks.bitcoin.pubKeyHash, - bitcoin.networks.bitcoin.scriptHash, + bitcoin.networks.mainnet.pubKeyHash, + bitcoin.networks.mainnet.scriptHash, bitcoin.networks.testnet.pubKeyHash, bitcoin.networks.testnet.scriptHash, ]; diff --git a/ts_src/address.ts b/ts_src/address.ts index 753589d..c17dee1 100644 --- a/ts_src/address.ts +++ b/ts_src/address.ts @@ -117,7 +117,7 @@ export function toBech32( export function fromOutputScript(output: Buffer, network?: Network): string { // TODO: Network - network = network || networks.bitcoin; + network = network || networks.mainnet; try { return payments.p2pkh({ output, network }).address as string; @@ -139,7 +139,7 @@ export function fromOutputScript(output: Buffer, network?: Network): string { } export function toOutputScript(address: string, network?: Network): Buffer { - network = network || networks.bitcoin; + network = network || networks.mainnet; let decodeBase58: Base58CheckResult | undefined; let decodeBech32: Bech32Result | undefined; diff --git a/ts_src/networks.ts b/ts_src/networks.ts index e66b08c..07b7923 100644 --- a/ts_src/networks.ts +++ b/ts_src/networks.ts @@ -14,36 +14,36 @@ interface Bip32 { private: number; } -export const bitcoin: Network = { - messagePrefix: '\x18Bitcoin Signed Message:\n', - bech32: 'bc', +export const mainnet: Network = { + messagePrefix: '\x18Bitcoin Signed Message:\n', // ? + bech32: 'bech32', // TODO is this right?! BECH32_ADDRESS lbry/wallet/orchstr8/node.py? bip32: { - public: 0x0488b21e, - private: 0x0488ade4, + public: 0x0488b21e, // lbry/wallet/server/coin.py XPUB_VERBYTES (unchanged) + private: 0x0488ade4, // lbry/wallet/server/coin.py XPRV_VERBYTES (unchanged) }, - pubKeyHash: 0x00, - scriptHash: 0x05, - wif: 0x80, + pubKeyHash: 0x55, // lbry/wallet/server/coin.py P2PKH_VERBYTE + scriptHash: 0x7a, // lbry/wallet/server/coin.py P2SH_VERBYTES + wif: 0x1c, // lbry/wallet/server/coin.py WIF_BYTE }; export const regtest: Network = { messagePrefix: '\x18Bitcoin Signed Message:\n', - bech32: 'bcrt', + bech32: 'bech32', // TODO is this right?! BECH32_ADDRESS lbry/wallet/orchstr8/node.py? bip32: { - public: 0x043587cf, - private: 0x04358394, + public: 0x043587cf, // lbry/wallet/server/coin.py XPUB_VERBYTES (unchanged) + private: 0x04358394, // lbry/wallet/server/coin.py XPRV_VERBYTES (unchanged) }, - pubKeyHash: 0x6f, - scriptHash: 0xc4, - wif: 0xef, + pubKeyHash: 0x6f, // lbry/wallet/server/coin.py P2PKH_VERBYTE (unchanged) + scriptHash: 0xc4, // lbry/wallet/server/coin.py P2SH_VERBYTES (unchanged) + wif: 0x1c, // lbry/wallet/server/coin.py WIF_BYTE }; export const testnet: Network = { messagePrefix: '\x18Bitcoin Signed Message:\n', - bech32: 'tb', + bech32: 'bech32', // TODO is this right?! BECH32_ADDRESS lbry/wallet/orchstr8/node.py? bip32: { - public: 0x043587cf, - private: 0x04358394, + public: 0x043587cf, // lbry/wallet/server/coin.py XPUB_VERBYTES (unchanged) + private: 0x04358394, // lbry/wallet/server/coin.py XPRV_VERBYTES (unchanged) }, - pubKeyHash: 0x6f, - scriptHash: 0xc4, - wif: 0xef, + pubKeyHash: 0x6f, // lbry/wallet/server/coin.py P2PKH_VERBYTE (unchanged) + scriptHash: 0xc4, // lbry/wallet/server/coin.py P2SH_VERBYTES (unchanged) + wif: 0x1c, // lbry/wallet/server/coin.py WIF_BYTE }; diff --git a/ts_src/ops.ts b/ts_src/ops.ts index 8e2c41c..22611c3 100644 --- a/ts_src/ops.ts +++ b/ts_src/ops.ts @@ -121,9 +121,13 @@ const OPS: { [key: string]: number } = { OP_NOP4: 179, OP_NOP5: 180, - OP_NOP6: 181, - OP_NOP7: 182, - OP_NOP8: 183, + + // LBRY custom opcodes: tx types + // Replaces OP_NOP6 - OP_NOP8 + OP_CLAIM_NAME: 181, + OP_SUPPORT_CLAIM: 182, + OP_UPDATE_CLAIM: 183, + OP_NOP9: 184, OP_NOP10: 185, diff --git a/ts_src/payments/claim_name.ts b/ts_src/payments/claim_name.ts new file mode 100644 index 0000000..0445b5d --- /dev/null +++ b/ts_src/payments/claim_name.ts @@ -0,0 +1,196 @@ +// This is mostly a copy of p2pkh, as the usual claim name script is based on that + +import * as bcrypto from '../crypto'; +import { mainnet as LBRY_MAINNET } from '../networks'; +import * as bscript from '../script'; +import { isPoint, typeforce as typef } from '../types'; +import { Payment, PaymentOpts, StackFunction } from './index'; +import * as lazy from './lazy'; +import * as bs58check from 'bs58check'; +const OPS = bscript.OPS; + +// input: {signature} {pubkey} +// output: OP_CLAIM_NAME {claim_name} {claim} OP_2DROP OP_DROP OP_DUP OP_HASH160 {hash160(pubkey)} OP_EQUALVERIFY OP_CHECKSIG +export function claimName(a: Payment, opts?: PaymentOpts): Payment { + if ( + !a.address && + !a.hash && + !a.output && + !a.pubkey && + !a.input && + !a.claim && + !a.claimName + ) + throw new TypeError('Not enough data'); + opts = Object.assign({ validate: true }, opts || {}); + + typef( + { + network: typef.maybe(typef.Object), + address: typef.maybe(typef.String), + hash: typef.maybe(typef.BufferN(20)), + output: typef.maybe(typef.Buffer), // NOTE: No length set since it's variable. + + pubkey: typef.maybe(isPoint), + signature: typef.maybe(bscript.isCanonicalScriptSignature), + input: typef.maybe(typef.Buffer), + + claimName: typef.maybe(typef.String), + claim: typef.maybe(typef.Buffer), + }, + a, + ); + + const _address = lazy.value(() => { + const payload = bs58check.decode(a.address!); + const version = payload.readUInt8(0); + const hash = payload.slice(1); + return { version, hash }; + }); + const _chunks = lazy.value(() => { + return bscript.decompile(a.input!); + }) as StackFunction; + + // We need output chunks as well, we can't just go by byte location within + // the output, because claim and claimName are of variable length. + const _outputChunks = lazy.value(() => { + return bscript.decompile(a.output!); + }) as StackFunction; + + const network = a.network || LBRY_MAINNET; + const o: Payment = { name: 'claim_name', network }; + + lazy.prop(o, 'address', () => { + if (!o.hash) return; + + const payload = Buffer.allocUnsafe(21); + payload.writeUInt8(network.pubKeyHash, 0); + o.hash.copy(payload, 1); + return bs58check.encode(payload); + }); + lazy.prop(o, 'hash', () => { + if (a.output) return _outputChunks()[7]; + if (a.address) return _address().hash; + if (a.pubkey || o.pubkey) return bcrypto.hash160(a.pubkey! || o.pubkey!); + }); + lazy.prop(o, 'claim', () => { + if (a.output) return _outputChunks()[2]; + if (a.claim) return a.claim; + }); + lazy.prop(o, 'claimName', () => { + if (a.output) return _outputChunks()[1].toString(); + if (a.claimName) return a.claimName; + }); + lazy.prop(o, 'output', () => { + if (!o.hash) return; + if (!o.claimName) return; + if (!o.claim) return; + return bscript.compile([ + OPS.OP_CLAIM_NAME, + Buffer.from(o.claimName), + o.claim, + OPS.OP_2DROP, + OPS.OP_DROP, + OPS.OP_DUP, + OPS.OP_HASH160, + o.hash, + OPS.OP_EQUALVERIFY, + OPS.OP_CHECKSIG, + ]); + }); + lazy.prop(o, 'pubkey', () => { + if (!a.input) return; + return _chunks()[1] as Buffer; + }); + lazy.prop(o, 'signature', () => { + if (!a.input) return; + return _chunks()[0] as Buffer; + }); + lazy.prop(o, 'input', () => { + if (!a.pubkey) return; + if (!a.signature) return; + return bscript.compile([a.signature, a.pubkey]); + }); + lazy.prop(o, 'witness', () => { + if (!o.input) return; + return []; + }); + + // extended validation + if (opts.validate) { + let hash: Buffer = Buffer.from([]); + if (a.address) { + if (_address().version !== network.pubKeyHash) + throw new TypeError('Invalid version or Network mismatch'); + if (_address().hash.length !== 20) throw new TypeError('Invalid address'); + hash = _address().hash; + } + + if (a.hash) { + if (hash.length > 0 && !hash.equals(a.hash)) + throw new TypeError('Hash mismatch'); + else hash = a.hash; + } + + if (a.output) { + if ( + _outputChunks().length !== 10 || + _outputChunks()[0] !== OPS.OP_CLAIM_NAME || + !Buffer.isBuffer(_outputChunks()[1]) || + !Buffer.isBuffer(_outputChunks()[2]) || + _outputChunks()[3] !== OPS.OP_2DROP || + _outputChunks()[4] !== OPS.OP_DROP || + _outputChunks()[5] !== OPS.OP_DUP || + _outputChunks()[6] !== OPS.OP_HASH160 || + !Buffer.isBuffer(_outputChunks()[7]) || + (_outputChunks()[7] as Buffer).length !== 0x14 || + _outputChunks()[8] !== OPS.OP_EQUALVERIFY || + _outputChunks()[9] !== OPS.OP_CHECKSIG + ) + throw new TypeError('Output is invalid'); + + const hash2 = _outputChunks()[7] as Buffer; + if (hash.length > 0 && !hash.equals(hash2)) + throw new TypeError('Hash mismatch'); + else hash = hash2; + + const claimName2 = _outputChunks()[1].toString(); + if (a.claimName && a.claimName !== claimName2) + throw new TypeError('claimName mismatch'); + + const claim2 = _outputChunks()[2] as Buffer; + if ( + Buffer.isBuffer(a.claim) && + a.claim.length > 0 && + !a.claim.equals(claim2) + ) + throw new TypeError('claim mismatch'); + } + + if (a.pubkey) { + const pkh = bcrypto.hash160(a.pubkey); + if (hash.length > 0 && !hash.equals(pkh)) + throw new TypeError('Hash mismatch'); + else hash = pkh; + } + + if (a.input) { + const chunks = _chunks(); + if (chunks.length !== 2) throw new TypeError('Input is invalid'); + if (!bscript.isCanonicalScriptSignature(chunks[0] as Buffer)) + throw new TypeError('Input has invalid signature'); + if (!isPoint(chunks[1])) throw new TypeError('Input has invalid pubkey'); + + if (a.signature && !a.signature.equals(chunks[0] as Buffer)) + throw new TypeError('Signature mismatch'); + if (a.pubkey && !a.pubkey.equals(chunks[1] as Buffer)) + throw new TypeError('Pubkey mismatch'); + + const pkh = bcrypto.hash160(chunks[1] as Buffer); + if (hash.length > 0 && !hash.equals(pkh)) + throw new TypeError('Hash mismatch'); + } + } + + return Object.assign(o, a); +} diff --git a/ts_src/payments/embed.ts b/ts_src/payments/embed.ts index c479b89..c53656d 100644 --- a/ts_src/payments/embed.ts +++ b/ts_src/payments/embed.ts @@ -1,4 +1,4 @@ -import { bitcoin as BITCOIN_NETWORK } from '../networks'; +import { mainnet as LBRY_MAINNET } from '../networks'; import * as bscript from '../script'; import { typeforce as typef } from '../types'; import { Payment, PaymentOpts, Stack } from './index'; @@ -28,7 +28,7 @@ export function p2data(a: Payment, opts?: PaymentOpts): Payment { a, ); - const network = a.network || BITCOIN_NETWORK; + const network = a.network || LBRY_MAINNET; const o = { name: 'embed', network } as Payment; lazy.prop(o, 'output', () => { diff --git a/ts_src/payments/index.ts b/ts_src/payments/index.ts index 4b7f111..8cd642b 100644 --- a/ts_src/payments/index.ts +++ b/ts_src/payments/index.ts @@ -6,6 +6,7 @@ import { p2pkh } from './p2pkh'; import { p2sh } from './p2sh'; import { p2wpkh } from './p2wpkh'; import { p2wsh } from './p2wsh'; +import { claimName } from './claim_name'; export interface Payment { name?: string; @@ -23,6 +24,8 @@ export interface Payment { hash?: Buffer; redeem?: Payment; witness?: Buffer[]; + claim?: Buffer; + claimName?: string; } export type PaymentCreator = (a: Payment, opts?: PaymentOpts) => Payment; @@ -38,7 +41,7 @@ export type StackElement = Buffer | number; export type Stack = StackElement[]; export type StackFunction = () => Stack; -export { embed, p2ms, p2pk, p2pkh, p2sh, p2wpkh, p2wsh }; +export { embed, p2ms, p2pk, p2pkh, p2sh, p2wpkh, p2wsh, claimName }; // TODO // witness commitment diff --git a/ts_src/payments/p2ms.ts b/ts_src/payments/p2ms.ts index eaa1440..91b8538 100644 --- a/ts_src/payments/p2ms.ts +++ b/ts_src/payments/p2ms.ts @@ -1,4 +1,4 @@ -import { bitcoin as BITCOIN_NETWORK } from '../networks'; +import { mainnet as LBRY_MAINNET } from '../networks'; import * as bscript from '../script'; import { isPoint, typeforce as typef } from '../types'; import { Payment, PaymentOpts, Stack } from './index'; @@ -48,7 +48,7 @@ export function p2ms(a: Payment, opts?: PaymentOpts): Payment { a, ); - const network = a.network || BITCOIN_NETWORK; + const network = a.network || LBRY_MAINNET; const o: Payment = { network }; let chunks: Stack = []; diff --git a/ts_src/payments/p2pk.ts b/ts_src/payments/p2pk.ts index 7273f53..99c325a 100644 --- a/ts_src/payments/p2pk.ts +++ b/ts_src/payments/p2pk.ts @@ -1,10 +1,14 @@ -import { bitcoin as BITCOIN_NETWORK } from '../networks'; +import { mainnet as LBRY_MAINNET } from '../networks'; import * as bscript from '../script'; import { isPoint, typeforce as typef } from '../types'; import { Payment, PaymentOpts, StackFunction } from './index'; import * as lazy from './lazy'; const OPS = bscript.OPS; +// NOTE We don't use this type of scriptPubKey (p2pk) for LBRY. Presumably +// because it stopped being used in Bitcoin before LBRY was created. +// TODO delete this file? and maybe others? + // input: {signature} // output: {pubKey} OP_CHECKSIG export function p2pk(a: Payment, opts?: PaymentOpts): Payment { @@ -28,7 +32,7 @@ export function p2pk(a: Payment, opts?: PaymentOpts): Payment { return bscript.decompile(a.input!); }) as StackFunction; - const network = a.network || BITCOIN_NETWORK; + const network = a.network || LBRY_MAINNET; const o: Payment = { name: 'p2pk', network }; lazy.prop(o, 'output', () => { diff --git a/ts_src/payments/p2pkh.ts b/ts_src/payments/p2pkh.ts index 3b5bd6f..1d1f20f 100644 --- a/ts_src/payments/p2pkh.ts +++ b/ts_src/payments/p2pkh.ts @@ -1,5 +1,5 @@ import * as bcrypto from '../crypto'; -import { bitcoin as BITCOIN_NETWORK } from '../networks'; +import { mainnet as LBRY_MAINNET } from '../networks'; import * as bscript from '../script'; import { isPoint, typeforce as typef } from '../types'; import { Payment, PaymentOpts, StackFunction } from './index'; @@ -38,7 +38,7 @@ export function p2pkh(a: Payment, opts?: PaymentOpts): Payment { return bscript.decompile(a.input!); }) as StackFunction; - const network = a.network || BITCOIN_NETWORK; + const network = a.network || LBRY_MAINNET; const o: Payment = { name: 'p2pkh', network }; lazy.prop(o, 'address', () => { diff --git a/ts_src/payments/p2sh.ts b/ts_src/payments/p2sh.ts index 9be5a8c..e978634 100644 --- a/ts_src/payments/p2sh.ts +++ b/ts_src/payments/p2sh.ts @@ -1,5 +1,5 @@ import * as bcrypto from '../crypto'; -import { bitcoin as BITCOIN_NETWORK } from '../networks'; +import { mainnet as LBRY_MAINNET } from '../networks'; import * as bscript from '../script'; import { typeforce as typef } from '../types'; import { @@ -51,7 +51,7 @@ export function p2sh(a: Payment, opts?: PaymentOpts): Payment { let network = a.network; if (!network) { - network = (a.redeem && a.redeem.network) || BITCOIN_NETWORK; + network = (a.redeem && a.redeem.network) || LBRY_MAINNET; } const o: Payment = { network }; diff --git a/ts_src/payments/p2wpkh.ts b/ts_src/payments/p2wpkh.ts index a4497fe..5717a56 100644 --- a/ts_src/payments/p2wpkh.ts +++ b/ts_src/payments/p2wpkh.ts @@ -1,5 +1,5 @@ import * as bcrypto from '../crypto'; -import { bitcoin as BITCOIN_NETWORK } from '../networks'; +import { mainnet as LBRY_MAINNET } from '../networks'; import * as bscript from '../script'; import { isPoint, typeforce as typef } from '../types'; import { Payment, PaymentOpts } from './index'; @@ -42,7 +42,7 @@ export function p2wpkh(a: Payment, opts?: PaymentOpts): Payment { }; }); - const network = a.network || BITCOIN_NETWORK; + const network = a.network || LBRY_MAINNET; const o: Payment = { name: 'p2wpkh', network }; lazy.prop(o, 'address', () => { diff --git a/ts_src/payments/p2wsh.ts b/ts_src/payments/p2wsh.ts index 00860e0..d99993e 100644 --- a/ts_src/payments/p2wsh.ts +++ b/ts_src/payments/p2wsh.ts @@ -1,5 +1,5 @@ import * as bcrypto from '../crypto'; -import { bitcoin as BITCOIN_NETWORK } from '../networks'; +import { mainnet as LBRY_MAINNET } from '../networks'; import * as bscript from '../script'; import { isPoint, typeforce as typef } from '../types'; import { Payment, PaymentOpts, StackElement, StackFunction } from './index'; @@ -74,7 +74,7 @@ export function p2wsh(a: Payment, opts?: PaymentOpts): Payment { let network = a.network; if (!network) { - network = (a.redeem && a.redeem.network) || BITCOIN_NETWORK; + network = (a.redeem && a.redeem.network) || LBRY_MAINNET; } const o: Payment = { network }; diff --git a/ts_src/psbt.ts b/ts_src/psbt.ts index b9af10f..b56b1c8 100644 --- a/ts_src/psbt.ts +++ b/ts_src/psbt.ts @@ -16,7 +16,7 @@ import { checkForInput, checkForOutput } from 'bip174/src/lib/utils'; import { fromOutputScript, toOutputScript } from './address'; import { cloneBuffer, reverseBuffer } from './bufferutils'; import { hash160 } from './crypto'; -import { bitcoin as btcNetwork, Network } from './networks'; +import { mainnet as lbryMainnet, Network } from './networks'; import * as payments from './payments'; import * as bscript from './script'; import { Output, Transaction } from './transaction'; @@ -55,7 +55,7 @@ const DEFAULT_OPTS: PsbtOpts = { * A bitcoinjs Network object. This is only used if you pass an `address` * parameter to addOutput. Otherwise it is not needed and can be left default. */ - network: btcNetwork, + network: lbryMainnet, /** * When extractTransaction is called, the fee rate is checked. * THIS IS NOT TO BE RELIED ON. @@ -899,6 +899,7 @@ function canFinalize( case 'pubkey': case 'pubkeyhash': case 'witnesspubkeyhash': + case 'claimname': return hasSigs(1, input.partialSig); case 'multisig': const p2ms = payments.p2ms({ output: script }); @@ -955,6 +956,7 @@ const isP2PKH = isPaymentFactory(payments.p2pkh); const isP2WPKH = isPaymentFactory(payments.p2wpkh); const isP2WSHScript = isPaymentFactory(payments.p2wsh); const isP2SHScript = isPaymentFactory(payments.p2sh); +const isClaimName = isPaymentFactory(payments.claimName); function bip32DerivationIsMine( root: HDSigner, @@ -1379,6 +1381,13 @@ function getPayment( signature: partialSig[0].signature, }); break; + case 'claimname': + payment = payments.claimName({ + output: script, + pubkey: partialSig[0].pubkey, + signature: partialSig[0].signature, + }); + break; } return payment!; } @@ -1844,12 +1853,14 @@ type ScriptType = | 'pubkeyhash' | 'multisig' | 'pubkey' + | 'claimname' | 'nonstandard'; function classifyScript(script: Buffer): ScriptType { if (isP2WPKH(script)) return 'witnesspubkeyhash'; if (isP2PKH(script)) return 'pubkeyhash'; if (isP2MS(script)) return 'multisig'; if (isP2PK(script)) return 'pubkey'; + if (isClaimName(script)) return 'claimname'; return 'nonstandard'; }