From f9a739e1db2ba5ee4dde11f90a514aabcdaddf1b Mon Sep 17 00:00:00 2001
From: Daniel Cousens <github@dcousens.com>
Date: Tue, 5 Jun 2018 17:24:47 +1000
Subject: [PATCH] add payments p2ms, p2pk, p2pkh, p2sh, p2wpkh, p2wsh

---
 src/payments/index.js     |  19 ++
 src/payments/lazy.js      |  30 +++
 src/payments/p2ms.js      | 140 ++++++++++++++
 src/payments/p2pk.js      |  80 ++++++++
 src/payments/p2pkh.js     | 127 +++++++++++++
 src/payments/p2sh.js      | 176 ++++++++++++++++++
 src/payments/p2wpkh.js    | 124 +++++++++++++
 src/payments/p2wsh.js     | 154 ++++++++++++++++
 src/payments/package.json |  40 ++++
 src/script.js             |   4 +-
 test/fixtures/p2ms.json   | 378 ++++++++++++++++++++++++++++++++++++++
 test/fixtures/p2pk.json   | 152 +++++++++++++++
 test/fixtures/p2pkh.json  | 214 +++++++++++++++++++++
 test/fixtures/p2sh.json   | 346 ++++++++++++++++++++++++++++++++++
 test/fixtures/p2wpkh.json | 195 ++++++++++++++++++++
 test/fixtures/p2wsh.json  | 326 ++++++++++++++++++++++++++++++++
 test/payments.js          |  65 +++++++
 test/payments.utils.js    | 128 +++++++++++++
 18 files changed, 2696 insertions(+), 2 deletions(-)
 create mode 100644 src/payments/index.js
 create mode 100644 src/payments/lazy.js
 create mode 100644 src/payments/p2ms.js
 create mode 100644 src/payments/p2pk.js
 create mode 100644 src/payments/p2pkh.js
 create mode 100644 src/payments/p2sh.js
 create mode 100644 src/payments/p2wpkh.js
 create mode 100644 src/payments/p2wsh.js
 create mode 100644 src/payments/package.json
 create mode 100644 test/fixtures/p2ms.json
 create mode 100644 test/fixtures/p2pk.json
 create mode 100644 test/fixtures/p2pkh.json
 create mode 100644 test/fixtures/p2sh.json
 create mode 100644 test/fixtures/p2wpkh.json
 create mode 100644 test/fixtures/p2wsh.json
 create mode 100644 test/payments.js
 create mode 100644 test/payments.utils.js

diff --git a/src/payments/index.js b/src/payments/index.js
new file mode 100644
index 0000000..9e869f5
--- /dev/null
+++ b/src/payments/index.js
@@ -0,0 +1,19 @@
+const p2ms = require('./p2ms')
+const p2pk = require('./p2pk')
+const p2pkh = require('./p2pkh')
+const p2sh = require('./p2sh')
+const p2wpkh = require('./p2wpkh')
+const p2wsh = require('./p2wsh')
+
+module.exports = {
+  p2ms: p2ms,
+  p2pk: p2pk,
+  p2pkh: p2pkh,
+  p2sh: p2sh,
+  p2wpkh: p2wpkh,
+  p2wsh: p2wsh
+}
+
+// TODO
+// OP_RETURN
+// witness commitment
diff --git a/src/payments/lazy.js b/src/payments/lazy.js
new file mode 100644
index 0000000..8538752
--- /dev/null
+++ b/src/payments/lazy.js
@@ -0,0 +1,30 @@
+function prop (object, name, f) {
+  Object.defineProperty(object, name, {
+    configurable: true,
+    enumerable: true,
+    get: function () {
+      let value = f.call(this)
+      this[name] = value
+      return value
+    },
+    set: function (value) {
+      Object.defineProperty(this, name, {
+        configurable: true,
+        enumerable: true,
+        value: value,
+        writable: true
+      })
+    }
+  })
+}
+
+function value (f) {
+  let value
+  return function () {
+    if (value !== undefined) return value
+    value = f()
+    return value
+  }
+}
+
+module.exports = { prop, value }
diff --git a/src/payments/p2ms.js b/src/payments/p2ms.js
new file mode 100644
index 0000000..9bce722
--- /dev/null
+++ b/src/payments/p2ms.js
@@ -0,0 +1,140 @@
+let lazy = require('./lazy')
+let typef = require('typeforce')
+let OPS = require('bitcoin-ops')
+let ecc = require('tiny-secp256k1')
+
+let bscript = require('../script')
+let BITCOIN_NETWORK = require('../networks').bitcoin
+let OP_INT_BASE = OPS.OP_RESERVED // OP_1 - 1
+
+function stacksEqual (a, b) {
+  if (a.length !== b.length) return false
+
+  return a.every(function (x, i) {
+    return x.equals(b[i])
+  })
+}
+
+// input: OP_0 [signatures ...]
+// output: m [pubKeys ...] n OP_CHECKMULTISIG
+function p2ms (a, opts) {
+  if (
+    !a.output &&
+    !(a.pubkeys && a.m !== undefined)
+  ) throw new TypeError('Not enough data')
+  opts = opts || { validate: true }
+
+  function isAcceptableSignature (x) {
+    return bscript.isCanonicalScriptSignature(x) || (opts.allowIncomplete && (x === OPS.OP_0))
+  }
+
+  typef({
+    network: typef.maybe(typef.Object),
+    m: typef.maybe(typef.Number),
+    n: typef.maybe(typef.Number),
+    output: typef.maybe(typef.Buffer),
+    pubkeys: typef.maybe(typef.arrayOf(ecc.isPoint)),
+
+    signatures: typef.maybe(typef.arrayOf(isAcceptableSignature)),
+    input: typef.maybe(typef.Buffer)
+  }, a)
+
+  let network = a.network || BITCOIN_NETWORK
+  let o = { network }
+
+  let chunks
+  let decoded = false
+  function decode (output) {
+    if (decoded) return
+    decoded = true
+    chunks = bscript.decompile(output)
+    let om = chunks[0] - OP_INT_BASE
+    let on = chunks[chunks.length - 2] - OP_INT_BASE
+    o.m = om
+    o.n = on
+    o.pubkeys = chunks.slice(1, -2)
+  }
+
+  lazy.prop(o, 'output', function () {
+    if (!a.m) return
+    if (!o.n) return
+    if (!a.pubkeys) return
+    return bscript.compile([].concat(
+      OP_INT_BASE + a.m,
+      a.pubkeys,
+      OP_INT_BASE + o.n,
+      OPS.OP_CHECKMULTISIG
+    ))
+  })
+  lazy.prop(o, 'm', function () {
+    if (!o.output) return
+    decode(o.output)
+    return o.m
+  })
+  lazy.prop(o, 'n', function () {
+    if (!o.pubkeys) return
+    return o.pubkeys.length
+  })
+  lazy.prop(o, 'pubkeys', function () {
+    if (!a.output) return
+    decode(a.output)
+    return o.pubkeys
+  })
+  lazy.prop(o, 'signatures', function () {
+    if (!a.input) return
+    return bscript.decompile(a.input).slice(1)
+  })
+  lazy.prop(o, 'input', function () {
+    if (!a.signatures) return
+    return bscript.compile([OPS.OP_0].concat(a.signatures))
+  })
+  lazy.prop(o, 'witness', function () {
+    if (!o.input) return
+    return []
+  })
+
+  // extended validation
+  if (opts.validate) {
+    if (a.output) {
+      decode(a.output)
+      if (!typef.Number(chunks[0])) throw new TypeError('Output is invalid')
+      if (!typef.Number(chunks[chunks.length - 2])) throw new TypeError('Output is invalid')
+      if (chunks[chunks.length - 1] !== OPS.OP_CHECKMULTISIG) throw new TypeError('Output is invalid')
+
+      if (
+        o.m <= 0 ||
+        o.n > 16 ||
+        o.m > o.n ||
+        o.n !== chunks.length - 3) throw new TypeError('Output is invalid')
+      if (!o.pubkeys.every(x => ecc.isPoint(x))) throw new TypeError('Output is invalid')
+
+      if (a.m !== undefined && a.m !== o.m) throw new TypeError('m mismatch')
+      if (a.n !== undefined && a.n !== o.n) throw new TypeError('n mismatch')
+      if (a.pubkeys && !stacksEqual(a.pubkeys, o.pubkeys)) throw new TypeError('Pubkeys mismatch')
+    }
+
+    if (a.pubkeys) {
+      if (a.n !== undefined && a.n !== a.pubkeys.length) throw new TypeError('Pubkey count mismatch')
+      o.n = a.pubkeys.length
+
+      if (o.n < o.m) throw new TypeError('Pubkey count cannot be less than m')
+    }
+
+    if (a.signatures) {
+      if (a.signatures.length < o.m) throw new TypeError('Not enough signatures provided')
+      if (a.signatures.length > o.m) throw new TypeError('Too many signatures provided')
+    }
+
+    if (a.input) {
+      if (a.input[0] !== OPS.OP_0) throw new TypeError('Input is invalid')
+      if (o.signatures.length === 0 || !o.signatures.every(isAcceptableSignature)) throw new TypeError('Input has invalid signature(s)')
+
+      if (a.signatures && !stacksEqual(a.signatures.equals(o.signatures))) throw new TypeError('Signature mismatch')
+      if (a.m !== undefined && a.m !== a.signatures.length) throw new TypeError('Signature count mismatch')
+    }
+  }
+
+  return Object.assign(o, a)
+}
+
+module.exports = p2ms
diff --git a/src/payments/p2pk.js b/src/payments/p2pk.js
new file mode 100644
index 0000000..b0408aa
--- /dev/null
+++ b/src/payments/p2pk.js
@@ -0,0 +1,80 @@
+let lazy = require('./lazy')
+let typef = require('typeforce')
+let OPS = require('bitcoin-ops')
+let ecc = require('tiny-secp256k1')
+
+let bscript = require('../script')
+let BITCOIN_NETWORK = require('../networks').bitcoin
+
+// input: {signature}
+// output: {pubKey} OP_CHECKSIG
+function p2pk (a, opts) {
+  if (
+    !a.output &&
+    !a.pubkey
+  ) throw new TypeError('Not enough data')
+  opts = opts || { validate: true }
+
+  typef({
+    network: typef.maybe(typef.Object),
+    output: typef.maybe(typef.Buffer),
+    pubkey: typef.maybe(ecc.isPoint),
+
+    signature: typef.maybe(bscript.isCanonicalScriptSignature),
+    input: typef.maybe(typef.Buffer)
+  }, a)
+
+  let _chunks = lazy.value(function () { return bscript.decompile(a.input) })
+
+  let network = a.network || BITCOIN_NETWORK
+  let o = { network }
+
+  lazy.prop(o, 'output', function () {
+    if (!a.pubkey) return
+    return bscript.compile([
+      a.pubkey,
+      OPS.OP_CHECKSIG
+    ])
+  })
+  lazy.prop(o, 'pubkey', function () {
+    if (!a.output) return
+    return a.output.slice(1, -1)
+  })
+  lazy.prop(o, 'signature', function () {
+    if (!a.input) return
+    return _chunks()[0]
+  })
+  lazy.prop(o, 'input', function () {
+    if (!a.signature) return
+    return bscript.compile([a.signature])
+  })
+  lazy.prop(o, 'witness', function () {
+    if (!o.input) return
+    return []
+  })
+
+  // extended validation
+  if (opts.validate) {
+    if (a.pubkey && a.output) {
+      if (!a.pubkey.equals(o.pubkey)) throw new TypeError('Pubkey mismatch')
+    }
+
+    if (a.output) {
+      if (a.output[a.output.length - 1] !== OPS.OP_CHECKSIG) throw new TypeError('Output is invalid')
+      if (!ecc.isPoint(o.pubkey)) throw new TypeError('Output pubkey is invalid')
+    }
+
+    if (a.signature) {
+      if (a.input && !a.input.equals(o.input)) throw new TypeError('Input mismatch')
+    }
+
+    if (a.input) {
+      if (_chunks().length !== 1) throw new TypeError('Input is invalid')
+      if (!bscript.isCanonicalScriptSignature(_chunks()[0])) throw new TypeError('Input has invalid signature')
+    }
+  }
+
+  return Object.assign(o, a)
+}
+
+module.exports = p2pk
diff --git a/src/payments/p2pkh.js b/src/payments/p2pkh.js
new file mode 100644
index 0000000..9d0733d
--- /dev/null
+++ b/src/payments/p2pkh.js
@@ -0,0 +1,127 @@
+let lazy = require('./lazy')
+let typef = require('typeforce')
+let OPS = require('bitcoin-ops')
+let ecc = require('tiny-secp256k1')
+
+let baddress = require('../address')
+let bcrypto = require('../crypto')
+let bscript = require('../script')
+let BITCOIN_NETWORK = require('../networks').bitcoin
+
+// input: {signature} {pubkey}
+// output: OP_DUP OP_HASH160 {hash160(pubkey)} OP_EQUALVERIFY OP_CHECKSIG
+function p2pkh (a, opts) {
+  if (
+    !a.address &&
+    !a.hash &&
+    !a.output &&
+    !a.pubkey &&
+    !a.input
+  ) throw new TypeError('Not enough data')
+  opts = opts || { validate: true }
+
+  typef({
+    network: typef.maybe(typef.Object),
+    address: typef.maybe(typef.String),
+    hash: typef.maybe(typef.BufferN(20)),
+    output: typef.maybe(typef.BufferN(25)),
+
+    pubkey: typef.maybe(ecc.isPoint),
+    signature: typef.maybe(bscript.isCanonicalScriptSignature),
+    input: typef.maybe(typef.Buffer)
+  }, a)
+
+  let _address = lazy.value(function () { return baddress.fromBase58Check(a.address) })
+  let _chunks = lazy.value(function () { return bscript.decompile(a.input) })
+
+  let network = a.network || BITCOIN_NETWORK
+  let o = { network }
+
+  lazy.prop(o, 'address', function () {
+    if (!o.hash) return
+    return baddress.toBase58Check(o.hash, network.pubKeyHash)
+  })
+  lazy.prop(o, 'hash', function () {
+    if (a.output) return a.output.slice(3, 23)
+    if (a.address) return _address().hash
+    if (a.pubkey || o.pubkey) return bcrypto.hash160(a.pubkey || o.pubkey)
+  })
+  lazy.prop(o, 'output', function () {
+    if (!o.hash) return
+    return bscript.compile([
+      OPS.OP_DUP,
+      OPS.OP_HASH160,
+      o.hash,
+      OPS.OP_EQUALVERIFY,
+      OPS.OP_CHECKSIG
+    ])
+  })
+  lazy.prop(o, 'pubkey', function () {
+    if (!a.input) return
+    return _chunks()[1]
+  })
+  lazy.prop(o, 'signature', function () {
+    if (!a.input) return
+    return _chunks()[0]
+  })
+  lazy.prop(o, 'input', function () {
+    if (!a.pubkey) return
+    if (!a.signature) return
+    return bscript.compile([a.signature, a.pubkey])
+  })
+  lazy.prop(o, 'witness', function () {
+    if (!o.input) return
+    return []
+  })
+
+  // extended validation
+  if (opts.validate) {
+    let hash
+    if (a.address) {
+      if (_address().version !== network.pubKeyHash) throw new TypeError('Network mismatch')
+      if (_address().hash.length !== 20) throw new TypeError('Invalid address')
+      else hash = _address().hash
+    }
+
+    if (a.hash) {
+      if (hash && !hash.equals(a.hash)) throw new TypeError('Hash mismatch')
+      else hash = a.hash
+    }
+
+    if (a.output) {
+      if (
+        a.output.length !== 25 ||
+        a.output[0] !== OPS.OP_DUP ||
+        a.output[1] !== OPS.OP_HASH160 ||
+        a.output[2] !== 0x14 ||
+        a.output[23] !== OPS.OP_EQUALVERIFY ||
+        a.output[24] !== OPS.OP_CHECKSIG) throw new TypeError('Output is invalid')
+
+      if (hash && !hash.equals(a.output.slice(3, 23))) throw new TypeError('Hash mismatch')
+      else hash = a.output.slice(3, 23)
+    }
+
+    if (a.pubkey) {
+      let pkh = bcrypto.hash160(a.pubkey)
+      if (hash && !hash.equals(pkh)) throw new TypeError('Hash mismatch')
+      else hash = pkh
+    }
+
+    if (a.input) {
+      let 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 (!ecc.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')
+
+      let pkh = bcrypto.hash160(chunks[1])
+      if (hash && !hash.equals(pkh)) throw new TypeError('Hash mismatch')
+    }
+  }
+
+  return Object.assign(o, a)
+}
+
+module.exports = p2pkh
diff --git a/src/payments/p2sh.js b/src/payments/p2sh.js
new file mode 100644
index 0000000..c67d921
--- /dev/null
+++ b/src/payments/p2sh.js
@@ -0,0 +1,176 @@
+const lazy = require('./lazy')
+const typef = require('typeforce')
+const OPS = require('bitcoin-ops')
+
+const baddress = require('../address')
+const bcrypto = require('../crypto')
+const bscript = require('../script')
+const BITCOIN_NETWORK = require('../networks').bitcoin
+
+function stacksEqual (a, b) {
+  if (a.length !== b.length) return false
+
+  return a.every(function (x, i) {
+    return x.equals(b[i])
+  })
+}
+
+// input: [redeemScriptSig ...] {redeemScript}
+// witness: <?>
+// output: OP_HASH160 {hash160(redeemScript)} OP_EQUAL
+function p2sh (a, opts) {
+  if (
+    !a.address &&
+    !a.hash &&
+    !a.output &&
+    !a.redeem &&
+    !a.input
+  ) throw new TypeError('Not enough data')
+  opts = opts || { validate: true }
+
+  typef({
+    network: typef.maybe(typef.Object),
+
+    address: typef.maybe(typef.String),
+    hash: typef.maybe(typef.BufferN(20)),
+    output: typef.maybe(typef.BufferN(23)),
+
+    redeem: typef.maybe({
+      network: typef.maybe(typef.Object),
+      output: typef.Buffer,
+      input: typef.maybe(typef.Buffer),
+      witness: typef.maybe(typef.arrayOf(typef.Buffer))
+    }),
+    input: typef.maybe(typef.Buffer),
+    witness: typef.maybe(typef.arrayOf(typef.Buffer))
+  }, a)
+
+  const network = a.network || BITCOIN_NETWORK
+  const o = { network }
+
+  const _address = lazy.value(function () { return baddress.fromBase58Check(a.address) })
+  const _chunks = lazy.value(function () { return bscript.decompile(a.input) })
+  const _redeem = lazy.value(function () {
+    const chunks = _chunks()
+    return {
+      network: network,
+      output: chunks[chunks.length - 1],
+      input: bscript.compile(chunks.slice(0, -1)),
+      witness: a.witness || []
+    }
+  })
+
+  // output dependents
+  lazy.prop(o, 'address', function () {
+    if (!o.hash) return
+    return baddress.toBase58Check(o.hash, network.scriptHash)
+  })
+  lazy.prop(o, 'hash', function () {
+    // in order of least effort
+    if (a.output) return a.output.slice(2, 22)
+    if (a.address) return _address().hash
+    if (o.redeem && o.redeem.output) return bcrypto.hash160(o.redeem.output)
+  })
+  lazy.prop(o, 'output', function () {
+    if (!o.hash) return
+    return bscript.compile([
+      OPS.OP_HASH160,
+      o.hash,
+      OPS.OP_EQUAL
+    ])
+  })
+
+  // input dependents
+  lazy.prop(o, 'redeem', function () {
+    if (!a.input) return
+    return _redeem()
+  })
+  lazy.prop(o, 'input', function () {
+    if (!a.redeem || !a.redeem.input) return
+    return bscript.compile([].concat(
+      bscript.decompile(a.redeem.input),
+      a.redeem.output
+    ))
+  })
+  lazy.prop(o, 'witness', function () {
+    if (o.redeem && o.redeem.witness) return o.redeem.witness
+    if (o.input) return []
+  })
+
+  if (opts.validate) {
+    let hash
+    if (a.address) {
+      if (_address().version !== network.scriptHash) throw new TypeError('Network mismatch')
+      if (_address().hash.length !== 20) throw new TypeError('Invalid address')
+      else hash = _address().hash
+    }
+
+    if (a.hash) {
+      if (hash && !hash.equals(a.hash)) throw new TypeError('Hash mismatch')
+      else hash = a.hash
+    }
+
+    if (a.output) {
+      if (
+        a.output.length !== 23 ||
+        a.output[0] !== OPS.OP_HASH160 ||
+        a.output[1] !== 0x14 ||
+        a.output[22] !== OPS.OP_EQUAL) throw new TypeError('Output is invalid')
+      const hash2 = a.output.slice(2, 22)
+      if (hash && !hash.equals(hash2)) throw new TypeError('Hash mismatch')
+      else hash = hash2
+    }
+
+    // inlined to prevent 'no-inner-declarations' failing
+    const checkRedeem = function (redeem) {
+      // is the redeem output empty/invalid?
+      const decompile = bscript.decompile(redeem.output)
+      if (!decompile || decompile.length < 1) throw new TypeError('Redeem.output too short')
+
+      // match hash against other sources
+      const hash2 = bcrypto.hash160(redeem.output)
+      if (hash && !hash.equals(hash2)) throw new TypeError('Hash mismatch')
+      else hash = hash2
+
+      if (redeem.input) {
+        const hasInput = redeem.input.length > 0
+        const hasWitness = redeem.witness && redeem.witness.length > 0
+        if (!hasInput && !hasWitness) throw new TypeError('Empty input')
+        if (hasInput && hasWitness) throw new TypeError('Input and witness provided')
+        if (hasInput) {
+          const richunks = bscript.decompile(redeem.input)
+          if (!bscript.isPushOnly(richunks)) throw new TypeError('Non push-only scriptSig')
+        }
+      }
+    }
+
+    if (a.input) {
+      const chunks = _chunks()
+      if (!chunks || chunks.length < 1) throw new TypeError('Input too short')
+      if (!Buffer.isBuffer(_redeem().output)) throw new TypeError('Input is invalid')
+
+      checkRedeem(_redeem())
+    }
+
+    if (a.redeem) {
+      if (a.redeem.network && a.redeem.network !== network) throw new TypeError('Network mismatch')
+      if (o.redeem) {
+        if (a.redeem.output && !a.redeem.output.equals(o.redeem.output)) throw new TypeError('Redeem.output mismatch')
+        if (a.redeem.input && !a.redeem.input.equals(o.redeem.input)) throw new TypeError('Redeem.input mismatch')
+      }
+
+      checkRedeem(a.redeem)
+    }
+
+    if (a.witness) {
+      if (
+        a.redeem &&
+        a.redeem.witness &&
+        !stacksEqual(a.redeem.witness, a.witness)) throw new TypeError('Witness and redeem.witness mismatch')
+    }
+  }
+
+  return Object.assign(o, a)
+}
+
+module.exports = p2sh
diff --git a/src/payments/p2wpkh.js b/src/payments/p2wpkh.js
new file mode 100644
index 0000000..f2bdeb4
--- /dev/null
+++ b/src/payments/p2wpkh.js
@@ -0,0 +1,124 @@
+let lazy = require('./lazy')
+let typef = require('typeforce')
+let OPS = require('bitcoin-ops')
+let ecc = require('tiny-secp256k1')
+
+let baddress = require('../address')
+let bcrypto = require('../crypto')
+let bscript = require('../script')
+let BITCOIN_NETWORK = require('../networks').bitcoin
+
+let EMPTY_BUFFER = Buffer.alloc(0)
+
+// witness: {signature} {pubKey}
+// input: <>
+// output: OP_0 {pubKeyHash}
+function p2wpkh (a, opts) {
+  if (
+    !a.address &&
+    !a.hash &&
+    !a.output &&
+    !a.pubkey &&
+    !a.witness
+  ) throw new TypeError('Not enough data')
+  opts = opts || { validate: true }
+
+  typef({
+    address: typef.maybe(typef.String),
+    hash: typef.maybe(typef.BufferN(20)),
+    input: typef.maybe(typef.BufferN(0)),
+    network: typef.maybe(typef.Object),
+    output: typef.maybe(typef.BufferN(22)),
+    pubkey: typef.maybe(ecc.isPoint),
+    signature: typef.maybe(bscript.isCanonicalScriptSignature),
+    witness: typef.maybe(typef.arrayOf(typef.Buffer))
+  }, a)
+
+  let _address = lazy.value(function () { return baddress.fromBech32(a.address) })
+
+  let network = a.network || BITCOIN_NETWORK
+  let o = { network }
+
+  lazy.prop(o, 'address', function () {
+    if (!o.hash) return
+    return baddress.toBech32(o.hash, 0x00, network.bech32)
+  })
+  lazy.prop(o, 'hash', function () {
+    if (a.output) return a.output.slice(2, 22)
+    if (a.address) return _address().data
+    if (a.pubkey || o.pubkey) return bcrypto.hash160(a.pubkey || o.pubkey)
+  })
+  lazy.prop(o, 'output', function () {
+    if (!o.hash) return
+    return bscript.compile([
+      OPS.OP_0,
+      o.hash
+    ])
+  })
+  lazy.prop(o, 'pubkey', function () {
+    if (a.pubkey) return a.pubkey
+    if (!a.witness) return
+    return a.witness[1]
+  })
+  lazy.prop(o, 'signature', function () {
+    if (!a.witness) return
+    return a.witness[0]
+  })
+  lazy.prop(o, 'input', function () {
+    if (!o.witness) return
+    return EMPTY_BUFFER
+  })
+  lazy.prop(o, 'witness', function () {
+    if (!a.pubkey) return
+    if (!a.signature) return
+    return [a.signature, a.pubkey]
+  })
+
+  // extended validation
+  if (opts.validate) {
+    let hash
+    if (a.address) {
+      if (network && network.bech32 !== _address().prefix) throw new TypeError('Network mismatch')
+      if (_address().version !== 0x00) throw new TypeError('Invalid version')
+      if (_address().data.length !== 20) throw new TypeError('Invalid data')
+      if (hash && !hash.equals(_address().data)) throw new TypeError('Hash mismatch')
+      else hash = _address().data
+    }
+
+    if (a.pubkey) {
+      let pkh = bcrypto.hash160(a.pubkey)
+      if (hash && !hash.equals(pkh)) throw new TypeError('Hash mismatch')
+      else hash = pkh
+    }
+
+    if (a.hash) {
+      if (hash && !hash.equals(a.hash)) throw new TypeError('Hash mismatch')
+      else hash = a.hash
+    }
+
+    if (a.output) {
+      if (
+        a.output.length !== 22 ||
+        a.output[0] !== OPS.OP_0 ||
+        a.output[1] !== 0x14) throw new TypeError('Output is invalid')
+      if (hash && !hash.equals(a.output.slice(2))) throw new TypeError('Hash mismatch')
+      else hash = a.output.slice(2)
+    }
+
+    if (a.witness) {
+      if (a.witness.length !== 2) throw new TypeError('Input is invalid')
+      if (!bscript.isCanonicalScriptSignature(a.witness[0])) throw new TypeError('Input has invalid signature')
+      if (!ecc.isPoint(a.witness[1])) throw new TypeError('Input has invalid pubkey')
+
+      if (a.signature && !a.signature.equals(a.witness[0])) throw new TypeError('Signature mismatch')
+      if (a.pubkey && !a.pubkey.equals(a.witness[1])) throw new TypeError('Pubkey mismatch')
+
+      let pkh = bcrypto.hash160(a.witness[1])
+      if (hash && !hash.equals(pkh)) throw new TypeError('Hash mismatch')
+    }
+  }
+
+  return Object.assign(o, a)
+}
+
+module.exports = p2wpkh
diff --git a/src/payments/p2wsh.js b/src/payments/p2wsh.js
new file mode 100644
index 0000000..8e2e4fe
--- /dev/null
+++ b/src/payments/p2wsh.js
@@ -0,0 +1,154 @@
+let lazy = require('./lazy')
+let typef = require('typeforce')
+let OPS = require('bitcoin-ops')
+
+let baddress = require('../address')
+let bcrypto = require('../crypto')
+let bscript = require('../script')
+let BITCOIN_NETWORK = require('../networks').bitcoin
+
+let EMPTY_BUFFER = Buffer.alloc(0)
+
+function stacksEqual (a, b) {
+  if (a.length !== b.length) return false
+
+  return a.every(function (x, i) {
+    return x.equals(b[i])
+  })
+}
+
+// input: <>
+// witness: [redeemScriptSig ...] {redeemScript}
+// output: OP_0 {sha256(redeemScript)}
+function p2wsh (a, opts) {
+  if (
+    !a.address &&
+    !a.hash &&
+    !a.output &&
+    !a.redeem &&
+    !a.witness
+  ) throw new TypeError('Not enough data')
+  opts = opts || { validate: true }
+
+  typef({
+    network: typef.maybe(typef.Object),
+
+    address: typef.maybe(typef.String),
+    hash: typef.maybe(typef.BufferN(32)),
+    output: typef.maybe(typef.BufferN(34)),
+
+    redeem: typef.maybe({
+      input: typef.maybe(typef.Buffer),
+      network: typef.maybe(typef.Object),
+      output: typef.Buffer,
+      witness: typef.maybe(typef.arrayOf(typef.Buffer))
+    }),
+    input: typef.maybe(typef.BufferN(0)),
+    witness: typef.maybe(typef.arrayOf(typef.Buffer))
+  }, a)
+
+  let _address = lazy.value(function () { return baddress.fromBech32(a.address) })
+  let _rchunks = lazy.value(function () { return bscript.decompile(a.redeem.input) })
+
+  let network = a.network || BITCOIN_NETWORK
+  let o = { network }
+
+  lazy.prop(o, 'address', function () {
+    if (!o.hash) return
+    return baddress.toBech32(o.hash, 0x00, network.bech32)
+  })
+  lazy.prop(o, 'hash', function () {
+    if (a.output) return a.output.slice(2)
+    if (a.address) return baddress.fromBech32(a.address).data
+    if (o.redeem && o.redeem.output) return bcrypto.sha256(o.redeem.output)
+  })
+  lazy.prop(o, 'output', function () {
+    if (!o.hash) return
+    return bscript.compile([
+      OPS.OP_0,
+      o.hash
+    ])
+  })
+  lazy.prop(o, 'redeem', function () {
+    if (!a.witness) return
+    return {
+      output: a.witness[a.witness.length - 1],
+      input: EMPTY_BUFFER,
+      witness: a.witness.slice(0, -1)
+    }
+  })
+  lazy.prop(o, 'input', function () {
+    if (!o.witness) return
+    return EMPTY_BUFFER
+  })
+  lazy.prop(o, 'witness', function () {
+    // transform redeem input to witness stack?
+    if (a.redeem && a.redeem.input && a.redeem.input.length > 0) {
+      let stack = bscript.toStack(_rchunks())
+
+      // assign, and blank the existing input
+      o.redeem = Object.assign({ witness: stack }, a.redeem)
+      o.redeem.input = EMPTY_BUFFER
+      return [].concat(stack, a.redeem.output)
+    }
+
+    if (!a.redeem) return
+    if (!a.redeem.witness) return
+    return [].concat(a.redeem.witness, a.redeem.output)
+  })
+
+  // extended validation
+  if (opts.validate) {
+    let hash
+    if (a.address) {
+      if (_address().prefix !== network.bech32) throw new TypeError('Network mismatch')
+      if (_address().version !== 0x00) throw new TypeError('Invalid version')
+      if (_address().data.length !== 32) throw new TypeError('Invalid data')
+      else hash = _address().data
+    }
+
+    if (a.hash) {
+      if (hash && !hash.equals(a.hash)) throw new TypeError('Hash mismatch')
+      else hash = a.hash
+    }
+
+    if (a.output) {
+      if (
+        a.output.length !== 34 ||
+        a.output[0] !== OPS.OP_0 ||
+        a.output[1] !== 0x20) throw new TypeError('Output is invalid')
+      let hash2 = a.output.slice(2)
+      if (hash && !hash.equals(hash2)) throw new TypeError('Hash mismatch')
+      else hash = hash2
+    }
+
+    if (a.redeem) {
+      if (a.redeem.network && a.redeem.network !== network) throw new TypeError('Network mismatch')
+
+      // is there two redeem sources?
+      if (
+        a.redeem.input &&
+        a.redeem.input.length > 0 &&
+        a.redeem.witness) throw new TypeError('Ambiguous witness source')
+
+      // is the redeem output non-empty?
+      if (bscript.decompile(a.redeem.output).length === 0) throw new TypeError('Redeem.output is invalid')
+
+      // match hash against other sources
+      let hash2 = bcrypto.sha256(a.redeem.output)
+      if (hash && !hash.equals(hash2)) throw new TypeError('Hash mismatch')
+      else hash = hash2
+
+      if (a.redeem.input && !bscript.isPushOnly(_rchunks())) throw new TypeError('Non push-only scriptSig')
+      if (a.witness && a.redeem.witness && !stacksEqual(a.witness, a.redeem.witness)) throw new TypeError('Witness and redeem.witness mismatch')
+    }
+
+    if (a.witness) {
+      if (a.redeem && !a.redeem.output.equals(a.witness[a.witness.length - 1])) throw new TypeError('Witness and redeem.output mismatch')
+    }
+  }
+
+  return Object.assign(o, a)
+}
+
+module.exports = p2wsh
diff --git a/src/payments/package.json b/src/payments/package.json
new file mode 100644
index 0000000..9383db2
--- /dev/null
+++ b/src/payments/package.json
@@ -0,0 +1,40 @@
+{
+  "name": "bitcoinjs-playground",
+  "version": "1.0.0",
+  "description": "Go nuts!",
+  "main": "_testnet.js",
+  "scripts": {
+    "test": "echo \"Error: no test specified\" && exit 1"
+  },
+  "repository": {
+    "type": "git",
+    "url": "git+https://github.com/bitcoinjs/bitcoinjs-playground.git"
+  },
+  "author": "",
+  "license": "ISC",
+  "bugs": {
+    "url": "https://github.com/bitcoinjs/bitcoinjs-playground/issues"
+  },
+  "homepage": "https://github.com/bitcoinjs/bitcoinjs-playground#readme",
+  "dependencies": {
+    "async": "^2.5.0",
+    "bech32": "^1.1.3",
+    "bip21": "^2.0.1",
+    "bip32-utils": "^0.11.1",
+    "bip38": "^2.0.2",
+    "bip39": "^2.5.0",
+    "bip69": "^2.1.1",
+    "bitcoin-ops": "^1.4.1",
+    "bitcoinjs-lib": "^3.3.2",
+    "bs58": "^4.0.1",
+    "bs58check": "^2.1.1",
+    "cb-http-client": "^0.2.3",
+    "coinselect": "^3.1.11",
+    "dhttp": "^2.4.2",
+    "merkle-lib": "^2.0.10",
+    "mocha": "^5.0.5",
+    "tape": "^4.9.0",
+    "typeforce": "^1.11.4",
+    "utxo": "^2.0.4"
+  }
+}
diff --git a/src/script.js b/src/script.js
index 30efa0a..ad7c4e4 100644
--- a/src/script.js
+++ b/src/script.js
@@ -98,11 +98,11 @@ function decompile (buffer) {
     if ((opcode > OPS.OP_0) && (opcode <= OPS.OP_PUSHDATA4)) {
       const d = pushdata.decode(buffer, i)
 
-      // did reading a pushDataInt fail? empty script
+      // did reading a pushDataInt fail?
       if (d === null) return null
       i += d.size
 
-      // attempt to read too much data? empty script
+      // attempt to read too much data?
       if (i + d.number > buffer.length) return null
 
       const data = buffer.slice(i, i + d.number)
diff --git a/test/fixtures/p2ms.json b/test/fixtures/p2ms.json
new file mode 100644
index 0000000..d5bb0c8
--- /dev/null
+++ b/test/fixtures/p2ms.json
@@ -0,0 +1,378 @@
+{
+  "valid": [
+    {
+      "description": "output from output",
+      "arguments": {
+        "output": "OP_2 030000000000000000000000000000000000000000000000000000000000000001 030000000000000000000000000000000000000000000000000000000000000002 OP_2 OP_CHECKMULTISIG"
+      },
+      "expected": {
+        "m": 2,
+        "n": 2,
+        "output": "OP_2 030000000000000000000000000000000000000000000000000000000000000001 030000000000000000000000000000000000000000000000000000000000000002 OP_2 OP_CHECKMULTISIG",
+        "pubkeys": [
+          "030000000000000000000000000000000000000000000000000000000000000001",
+          "030000000000000000000000000000000000000000000000000000000000000002"
+        ],
+        "signatures": null,
+        "input": null,
+        "witness": null
+      }
+    },
+    {
+      "description": "output from m/pubkeys",
+      "arguments": {
+        "m": 1,
+        "pubkeys": [
+          "030000000000000000000000000000000000000000000000000000000000000001",
+          "030000000000000000000000000000000000000000000000000000000000000002"
+        ]
+      },
+      "expected": {
+        "m": 1,
+        "n": 2,
+        "output": "OP_1 030000000000000000000000000000000000000000000000000000000000000001 030000000000000000000000000000000000000000000000000000000000000002 OP_2 OP_CHECKMULTISIG",
+        "pubkeys": [
+          "030000000000000000000000000000000000000000000000000000000000000001",
+          "030000000000000000000000000000000000000000000000000000000000000002"
+        ],
+        "signatures": null,
+        "input": null,
+        "witness": null
+      }
+    },
+    {
+      "description": "input/output from m/pubkeys/signatures",
+      "arguments": {
+        "m": 2,
+        "pubkeys": [
+          "030000000000000000000000000000000000000000000000000000000000000001",
+          "030000000000000000000000000000000000000000000000000000000000000002",
+          "030000000000000000000000000000000000000000000000000000000000000003"
+        ],
+        "signatures": [
+          "300602010002010001",
+          "300602010102010001"
+        ]
+      },
+      "expected": {
+        "m": 2,
+        "n": 3,
+        "output": "OP_2 030000000000000000000000000000000000000000000000000000000000000001 030000000000000000000000000000000000000000000000000000000000000002 030000000000000000000000000000000000000000000000000000000000000003 OP_3 OP_CHECKMULTISIG",
+        "pubkeys": [
+          "030000000000000000000000000000000000000000000000000000000000000001",
+          "030000000000000000000000000000000000000000000000000000000000000002",
+          "030000000000000000000000000000000000000000000000000000000000000003"
+        ],
+        "signatures": [
+          "300602010002010001",
+          "300602010102010001"
+        ],
+        "input": "OP_0 300602010002010001 300602010102010001",
+        "witness": []
+      }
+    },
+    {
+      "description": "input/output from output/signatures",
+      "arguments": {
+        "output": "OP_2 030000000000000000000000000000000000000000000000000000000000000001 030000000000000000000000000000000000000000000000000000000000000002 030000000000000000000000000000000000000000000000000000000000000003 OP_3 OP_CHECKMULTISIG",
+        "signatures": [
+          "300602010002010001",
+          "300602010102010001"
+        ]
+      },
+      "expected": {
+        "m": 2,
+        "n": 3,
+        "output": "OP_2 030000000000000000000000000000000000000000000000000000000000000001 030000000000000000000000000000000000000000000000000000000000000002 030000000000000000000000000000000000000000000000000000000000000003 OP_3 OP_CHECKMULTISIG",
+        "pubkeys": [
+          "030000000000000000000000000000000000000000000000000000000000000001",
+          "030000000000000000000000000000000000000000000000000000000000000002",
+          "030000000000000000000000000000000000000000000000000000000000000003"
+        ],
+        "signatures": [
+          "300602010002010001",
+          "300602010102010001"
+        ],
+        "input": "OP_0 300602010002010001 300602010102010001",
+        "witness": []
+      }
+    },
+    {
+      "description": "input/output from input/output",
+      "arguments": {
+        "output": "OP_2 030000000000000000000000000000000000000000000000000000000000000001 030000000000000000000000000000000000000000000000000000000000000002 030000000000000000000000000000000000000000000000000000000000000003 OP_3 OP_CHECKMULTISIG",
+        "input": "OP_0 300602010002010001 300602010102010001"
+      },
+      "expected": {
+        "m": 2,
+        "n": 3,
+        "output": "OP_2 030000000000000000000000000000000000000000000000000000000000000001 030000000000000000000000000000000000000000000000000000000000000002 030000000000000000000000000000000000000000000000000000000000000003 OP_3 OP_CHECKMULTISIG",
+        "pubkeys": [
+          "030000000000000000000000000000000000000000000000000000000000000001",
+          "030000000000000000000000000000000000000000000000000000000000000002",
+          "030000000000000000000000000000000000000000000000000000000000000003"
+        ],
+        "signatures": [
+          "300602010002010001",
+          "300602010102010001"
+        ],
+        "input": "OP_0 300602010002010001 300602010102010001",
+        "witness": []
+      }
+    },
+    {
+      "description": "input/output from input/output, even if incomplete",
+      "arguments": {
+        "output": "OP_2 030000000000000000000000000000000000000000000000000000000000000001 030000000000000000000000000000000000000000000000000000000000000002 OP_2 OP_CHECKMULTISIG",
+        "input": "OP_0 OP_0 300602010102010001"
+      },
+      "options": {
+        "allowIncomplete": true
+      },
+      "expected": {
+        "m": 2,
+        "n": 2,
+        "output": "OP_2 030000000000000000000000000000000000000000000000000000000000000001 030000000000000000000000000000000000000000000000000000000000000002 OP_2 OP_CHECKMULTISIG",
+        "pubkeys": [
+          "030000000000000000000000000000000000000000000000000000000000000001",
+          "030000000000000000000000000000000000000000000000000000000000000002"
+        ],
+        "signatures": [
+          0,
+          "300602010102010001"
+        ],
+        "input": "OP_0 OP_0 300602010102010001",
+        "witness": []
+      }
+    },
+    {
+      "description": "input/output from output/signatures, even if incomplete",
+      "arguments": {
+        "output": "OP_2 030000000000000000000000000000000000000000000000000000000000000001 030000000000000000000000000000000000000000000000000000000000000002 OP_2 OP_CHECKMULTISIG",
+        "signatures": [
+          0,
+          "300602010102010001"
+        ]
+      },
+      "options": {
+        "allowIncomplete": true
+      },
+      "expected": {
+        "m": 2,
+        "n": 2,
+        "output": "OP_2 030000000000000000000000000000000000000000000000000000000000000001 030000000000000000000000000000000000000000000000000000000000000002 OP_2 OP_CHECKMULTISIG",
+        "pubkeys": [
+          "030000000000000000000000000000000000000000000000000000000000000001",
+          "030000000000000000000000000000000000000000000000000000000000000002"
+        ],
+        "signatures": [
+          0,
+          "300602010102010001"
+        ],
+        "input": "OP_0 OP_0 300602010102010001",
+        "witness": []
+      }
+    }
+  ],
+  "invalid": [
+    {
+      "exception": "Not enough data",
+      "arguments": {}
+    },
+    {
+      "exception": "Not enough data",
+      "arguments": {
+        "m": 2
+      }
+    },
+    {
+      "exception": "Not enough data",
+      "arguments": {
+        "pubkeys": [
+          "030000000000000000000000000000000000000000000000000000000000000001",
+          "030000000000000000000000000000000000000000000000000000000000000002"
+        ]
+      }
+    },
+    {
+      "description": "Non OP_INT chunk (m)",
+      "exception": "Output is invalid",
+      "arguments": {
+        "output": "OP_RESERVED"
+      }
+    },
+    {
+      "description": "Non OP_INT chunk (n)",
+      "exception": "Output is invalid",
+      "arguments": {
+        "output": "OP_1 OP_RESERVED"
+      }
+    },
+    {
+      "description": "Missing OP_CHECKMULTISIG",
+      "exception": "Output is invalid",
+      "arguments": {
+        "output": "OP_1 OP_2 OP_RESERVED"
+      }
+    },
+    {
+      "description": "m is 0",
+      "exception": "Output is invalid",
+      "arguments": {
+        "output": "OP_0 OP_2 OP_CHECKMULTISIG"
+      }
+    },
+    {
+      "description": "n is 0 (m > n)",
+      "exception": "Output is invalid",
+      "arguments": {
+        "output": "OP_2 OP_0 OP_CHECKMULTISIG"
+      }
+    },
+    {
+      "description": "m > n",
+      "exception": "Output is invalid",
+      "arguments": {
+        "output": "OP_3 OP_2 OP_CHECKMULTISIG"
+      }
+    },
+    {
+      "description": "n !== output pubkeys",
+      "exception": "Output is invalid",
+      "arguments": {
+        "output": "OP_1 030000000000000000000000000000000000000000000000000000000000000001 OP_2 OP_CHECKMULTISIG"
+      }
+    },
+    {
+      "description": "Non-canonical output public key",
+      "exception": "Output is invalid",
+      "arguments": {
+        "output": "OP_1 ffff OP_1 OP_CHECKMULTISIG"
+      }
+    },
+    {
+      "exception": "n mismatch",
+      "arguments": {
+        "n": 2,
+        "output": "OP_1 030000000000000000000000000000000000000000000000000000000000000001 OP_1 OP_CHECKMULTISIG"
+      }
+    },
+    {
+      "exception": "m mismatch",
+      "arguments": {
+        "m": 2,
+        "output": "OP_1 030000000000000000000000000000000000000000000000000000000000000001 OP_1 OP_CHECKMULTISIG"
+      }
+    },
+    {
+      "exception": "Pubkeys mismatch",
+      "arguments": {
+        "pubkeys": [
+          "030000000000000000000000000000000000000000000000000000000000000001"
+        ],
+        "output": "OP_1 030000000000000000000000000000000000000000000000000000000000000002 OP_1 OP_CHECKMULTISIG"
+      }
+    },
+    {
+      "exception": "Pubkey count mismatch",
+      "arguments": {
+        "m": 2,
+        "n": 3,
+        "pubkeys": [
+          "030000000000000000000000000000000000000000000000000000000000000001",
+          "030000000000000000000000000000000000000000000000000000000000000002"
+        ]
+      }
+    },
+    {
+      "exception": "Pubkey count cannot be less than m",
+      "arguments": {
+        "m": 4,
+        "pubkeys": [
+          "030000000000000000000000000000000000000000000000000000000000000001",
+          "030000000000000000000000000000000000000000000000000000000000000001"
+        ]
+      }
+    },
+    {
+      "exception": "Not enough signatures provided",
+      "arguments": {
+        "m": 2,
+        "pubkeys": [
+          "030000000000000000000000000000000000000000000000000000000000000001",
+          "030000000000000000000000000000000000000000000000000000000000000001"
+        ],
+        "signatures": [
+          "300602010002010001"
+        ]
+      }
+    },
+    {
+      "exception": "Too many signatures provided",
+      "arguments": {
+        "m": 2,
+        "pubkeys": [
+          "030000000000000000000000000000000000000000000000000000000000000001",
+          "030000000000000000000000000000000000000000000000000000000000000001"
+        ],
+        "signatures": [
+          "300602010002010001",
+          "300602010002010001",
+          "300602010002010001"
+        ]
+      }
+    },
+    {
+      "description": "Missing OP_0",
+      "exception": "Input is invalid",
+      "arguments": {
+        "m": 2,
+        "pubkeys": [
+          "030000000000000000000000000000000000000000000000000000000000000001",
+          "030000000000000000000000000000000000000000000000000000000000000001"
+        ],
+        "input": "OP_RESERVED"
+      }
+    },
+    {
+      "exception": "Input has invalid signature\\(s\\)",
+      "arguments": {
+        "m": 1,
+        "pubkeys": [
+          "030000000000000000000000000000000000000000000000000000000000000001"
+        ],
+        "input": "OP_0 ffffffffffffffff"
+      }
+    }
+  ],
+  "dynamic": {
+    "depends": {
+      "m": [ "output" ],
+      "n": [ "output", [ "m", "pubkeys" ] ],
+      "output": [ "output", [ "m", "pubkeys" ] ],
+      "pubkeys": [ "output" ],
+      "signatures": [ ["input", "output"] ],
+      "input": [ ["signatures", "output"] ],
+      "witness": [ ["input", "output"] ]
+    },
+    "details": [
+      {
+        "description": "p2ms",
+        "m": 2,
+        "n": 3,
+        "output": "OP_2 030000000000000000000000000000000000000000000000000000000000000001 030000000000000000000000000000000000000000000000000000000000000002 030000000000000000000000000000000000000000000000000000000000000003 OP_3 OP_CHECKMULTISIG",
+        "pubkeys": [
+          "030000000000000000000000000000000000000000000000000000000000000001",
+          "030000000000000000000000000000000000000000000000000000000000000002",
+          "030000000000000000000000000000000000000000000000000000000000000003"
+        ],
+        "signatures": [
+          "300602010002010001",
+          "300602010102010001"
+        ],
+        "input": "OP_0 300602010002010001 300602010102010001",
+        "witness": []
+      }
+    ]
+  }
+}
diff --git a/test/fixtures/p2pk.json b/test/fixtures/p2pk.json
new file mode 100644
index 0000000..ff0bbcd
--- /dev/null
+++ b/test/fixtures/p2pk.json
@@ -0,0 +1,152 @@
+{
+  "valid": [
+    {
+      "description": "output from output",
+      "arguments": {
+        "output": "030000000000000000000000000000000000000000000000000000000000000001 OP_CHECKSIG"
+      },
+      "expected": {
+        "pubkey": "030000000000000000000000000000000000000000000000000000000000000001",
+        "signatures": null,
+        "input": null,
+        "witness": null
+      }
+    },
+    {
+      "description": "output from pubkey",
+      "arguments": {
+        "pubkey": "030000000000000000000000000000000000000000000000000000000000000001"
+      },
+      "expected": {
+        "output": "030000000000000000000000000000000000000000000000000000000000000001 OP_CHECKSIG",
+        "signatures": null,
+        "input": null,
+        "witness": null
+      }
+    },
+    {
+      "description": "input/output from output/signature",
+      "arguments": {
+        "output": "030000000000000000000000000000000000000000000000000000000000000001 OP_CHECKSIG",
+        "signature": "300602010002010001"
+      },
+      "expected": {
+        "pubkey": "030000000000000000000000000000000000000000000000000000000000000001",
+        "input": "300602010002010001",
+        "witness": []
+      }
+    },
+    {
+      "description": "input/output from pubkey/signature",
+      "arguments": {
+        "pubkey": "030000000000000000000000000000000000000000000000000000000000000001",
+        "signature": "300602010002010001"
+      },
+      "expected": {
+        "output": "030000000000000000000000000000000000000000000000000000000000000001 OP_CHECKSIG",
+        "input": "300602010002010001",
+        "witness": []
+      }
+    },
+    {
+      "description": "input/output from input/output",
+      "arguments": {
+        "output": "030000000000000000000000000000000000000000000000000000000000000001 OP_CHECKSIG",
+        "input": "300602010002010001"
+      },
+      "expected": {
+        "pubkey": "030000000000000000000000000000000000000000000000000000000000000001",
+        "signature": "300602010002010001",
+        "witness": []
+      }
+    }
+  ],
+  "invalid": [
+    {
+      "exception": "Not enough data",
+      "arguments": {}
+    },
+    {
+      "exception": "Not enough data",
+      "arguments": {
+        "input": "300602010002010001"
+      }
+    },
+    {
+      "exception": "Not enough data",
+      "arguments": {
+        "signature": "300602010002010001"
+      }
+    },
+    {
+      "description": "Non-canonical signature",
+      "exception": "Expected property \"signature\" of type \\?isCanonicalScriptSignature, got Buffer",
+      "arguments": {
+        "pubkey": "030000000000000000000000000000000000000000000000000000000000000001",
+        "signature": "3044"
+      }
+    },
+    {
+      "description": "Unexpected OP_RESERVED",
+      "exception": "Output is invalid",
+      "arguments": {
+        "output": "OP_RESERVED"
+      }
+    },
+    {
+      "description": "Non-canonical output public key",
+      "exception": "Output pubkey is invalid",
+      "arguments": {
+        "output": "ffff OP_CHECKSIG"
+      }
+    },
+    {
+      "description": "Unexpected OP_0 (at end)",
+      "exception": "Output is invalid",
+      "arguments": {
+        "output": "030000000000000000000000000000000000000000000000000000000000000001 OP_CHECKSIG OP_0"
+      }
+    },
+    {
+      "exception": "Pubkey mismatch",
+      "arguments": {
+        "pubkey": "030000000000000000000000000000000000000000000000000000000000000001",
+        "output": "030000000000000000000000000000000000000000000000000000000000000002 OP_CHECKSIG"
+      }
+    },
+    {
+      "description": "Too many chunks",
+      "exception": "Input is invalid",
+      "arguments": {
+        "pubkey": "030000000000000000000000000000000000000000000000000000000000000001",
+        "input": "300602010002010001 OP_RESERVED"
+      }
+    },
+    {
+      "exception": "Input has invalid signature",
+      "arguments": {
+        "pubkey": "030000000000000000000000000000000000000000000000000000000000000001",
+        "input": "ffffffffffffffff"
+      }
+    }
+  ],
+  "dynamic": {
+    "depends": {
+      "output":  [ "pubkey" ],
+      "pubkey": [ "output" ],
+      "signature": [ ["input", "output"] ],
+      "input": [ ["signature", "output"] ],
+      "witness": [ ["input", "output"] ]
+    },
+    "details": [
+      {
+        "description": "p2pk",
+        "output": "030000000000000000000000000000000000000000000000000000000000000001 OP_CHECKSIG",
+        "pubkey": "030000000000000000000000000000000000000000000000000000000000000001",
+        "signature": "300602010002010001",
+        "input": "300602010002010001",
+        "witness": []
+      }
+    ]
+  }
+}
diff --git a/test/fixtures/p2pkh.json b/test/fixtures/p2pkh.json
new file mode 100644
index 0000000..7d47152
--- /dev/null
+++ b/test/fixtures/p2pkh.json
@@ -0,0 +1,214 @@
+{
+  "valid": [
+    {
+      "description": "output from address",
+      "arguments": {
+        "address": "134D6gYy8DsR5m4416BnmgASuMBqKvogQh"
+      },
+      "expected": {
+        "hash": "168b992bcfc44050310b3a94bd0771136d0b28d1",
+        "output": "OP_DUP OP_HASH160 168b992bcfc44050310b3a94bd0771136d0b28d1 OP_EQUALVERIFY OP_CHECKSIG",
+        "signature": null,
+        "input": null,
+        "witness": null
+      }
+    },
+    {
+      "description": "output from hash",
+      "arguments": {
+        "hash": "168b992bcfc44050310b3a94bd0771136d0b28d1"
+      },
+      "expected": {
+        "address": "134D6gYy8DsR5m4416BnmgASuMBqKvogQh",
+        "output": "OP_DUP OP_HASH160 168b992bcfc44050310b3a94bd0771136d0b28d1 OP_EQUALVERIFY OP_CHECKSIG",
+        "signature": null,
+        "input": null,
+        "witness": null
+      }
+    },
+    {
+      "description": "output from output",
+      "arguments": {
+        "output": "OP_DUP OP_HASH160 168b992bcfc44050310b3a94bd0771136d0b28d1 OP_EQUALVERIFY OP_CHECKSIG"
+      },
+      "expected": {
+        "address": "134D6gYy8DsR5m4416BnmgASuMBqKvogQh",
+        "hash": "168b992bcfc44050310b3a94bd0771136d0b28d1",
+        "signature": null,
+        "input": null,
+        "witness": null
+      }
+    },
+    {
+      "description": "output from pubkey",
+      "arguments": {
+        "pubkey": "030000000000000000000000000000000000000000000000000000000000000001"
+      },
+      "expected": {
+        "address": "134D6gYy8DsR5m4416BnmgASuMBqKvogQh",
+        "hash": "168b992bcfc44050310b3a94bd0771136d0b28d1",
+        "output": "OP_DUP OP_HASH160 168b992bcfc44050310b3a94bd0771136d0b28d1 OP_EQUALVERIFY OP_CHECKSIG",
+        "signature": null,
+        "input": null,
+        "witness": null
+      }
+    },
+    {
+      "description": "input/output from pubkey/signature",
+      "arguments": {
+        "pubkey": "030000000000000000000000000000000000000000000000000000000000000001",
+        "signature": "300602010002010001"
+      },
+      "expected": {
+        "address": "134D6gYy8DsR5m4416BnmgASuMBqKvogQh",
+        "hash": "168b992bcfc44050310b3a94bd0771136d0b28d1",
+        "output": "OP_DUP OP_HASH160 168b992bcfc44050310b3a94bd0771136d0b28d1 OP_EQUALVERIFY OP_CHECKSIG",
+        "input": "300602010002010001 030000000000000000000000000000000000000000000000000000000000000001",
+        "witness": []
+      }
+    },
+    {
+      "description": "input/output from input",
+      "arguments": {
+        "input": "300602010002010001 030000000000000000000000000000000000000000000000000000000000000001"
+      },
+      "expected": {
+        "address": "134D6gYy8DsR5m4416BnmgASuMBqKvogQh",
+        "hash": "168b992bcfc44050310b3a94bd0771136d0b28d1",
+        "output": "OP_DUP OP_HASH160 168b992bcfc44050310b3a94bd0771136d0b28d1 OP_EQUALVERIFY OP_CHECKSIG",
+        "pubkey": "030000000000000000000000000000000000000000000000000000000000000001",
+        "signature": "300602010002010001",
+        "witness": []
+      }
+    }
+  ],
+  "invalid": [
+    {
+      "exception": "Not enough data",
+      "arguments": {}
+    },
+    {
+      "exception": "Not enough data",
+      "arguments": {
+        "signature": "300602010002010001"
+      }
+    },
+    {
+      "description": "Unexpected OP_RESERVED",
+      "exception": "Output is invalid",
+      "arguments": {
+        "output": "OP_DUP OP_HASH160 168b992bcfc44050310b3a94bd0771136d0b28d1 OP_EQUALVERIFY OP_RESERVED"
+      }
+    },
+    {
+      "description": "Unexpected OP_DUP",
+      "exception": "Output is invalid",
+      "arguments": {
+        "output": "OP_DUP OP_DUP 168b992bcfc44050310b3a94bd0771136d0b28d137 OP_EQUALVERIFY"
+      }
+    },
+    {
+      "description": "Hash too short (too many chunks)",
+      "exception": "Output is invalid",
+      "arguments": {
+        "output": "OP_DUP OP_DUP 168b992bcfc44050310b3a94bd0771136d0b28d1 OP_TRUE OP_EQUALVERIFY"
+      }
+    },
+    {
+      "description": "Non-minimally encoded (non BIP62 compliant)",
+      "exception": "Expected property \"output\" of type Buffer\\(Length: 25\\), got Buffer\\(Length: 26\\)",
+      "arguments": {
+        "outputHex": "76a94c14aa4d7985c57e011a8b3dd8e0e5a73aaef41629c588ac"
+      }
+    },
+    {
+      "exception": "Pubkey mismatch",
+      "arguments": {
+        "pubkey": "030000000000000000000000000000000000000000000000000000000000000001",
+        "input": "300602010002010001 030000000000000000000000000000000000000000000000000000000000000002"
+      }
+    },
+    {
+      "exception": "Input has invalid signature",
+      "arguments": {
+        "input": "ffffffffffffffffff 030000000000000000000000000000000000000000000000000000000000000001"
+      }
+    },
+    {
+      "exception": "Input has invalid pubkey",
+      "arguments": {
+        "input": "300602010002010001 ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
+      }
+    },
+    {
+      "description": "Input has unexpected data",
+      "exception": "Input is invalid",
+      "arguments": {
+        "input": "300602010002010001 030000000000000000000000000000000000000000000000000000000000000001 ffff"
+      }
+    },
+    {
+      "description": "H(pubkey) != H",
+      "exception": "Hash mismatch",
+      "arguments": {
+        "pubkey": "030000000000000000000000000000000000000000000000000000000000000001",
+        "hash": "ffffffffffffffffffffffffffffffffffffffff"
+      }
+    },
+    {
+      "description": "address.hash != H",
+      "exception": "Hash mismatch",
+      "arguments": {
+        "address": "134D6gYy8DsR5m4416BnmgASuMBqKvogQh",
+        "hash": "ffffffffffffffffffffffffffffffffffffffff"
+      }
+    },
+    {
+      "description": "address.hash != output.hash",
+      "exception": "Hash mismatch",
+      "arguments": {
+        "address": "134D6gYy8DsR5m4416BnmgASuMBqKvogQh",
+        "output": "OP_DUP OP_HASH160 ffffffffffffffffffffffffffffffffffffffff OP_EQUALVERIFY OP_CHECKSIG"
+      }
+    },
+    {
+      "description": "output.hash != H",
+      "exception": "Hash mismatch",
+      "arguments": {
+        "output": "OP_DUP OP_HASH160 168b992bcfc44050310b3a94bd0771136d0b28d1 OP_EQUALVERIFY OP_CHECKSIG",
+        "hash": "ffffffffffffffffffffffffffffffffffffffff"
+      }
+    },
+    {
+      "description": "H(input.pubkey) != H",
+      "exception": "Hash mismatch",
+      "arguments": {
+        "hash": "ffffffffffffffffffffffffffffffffffffffff",
+        "input": "300602010002010001 030000000000000000000000000000000000000000000000000000000000000001"
+      }
+    }
+  ],
+  "dynamic": {
+    "depends": {
+      "address": [ "address", "output", "hash", "pubkey", "input" ],
+      "hash": [ "address", "output", "hash", "pubkey", "input" ],
+      "output": [ "address", "output", "hash", "pubkey", "input" ],
+      "pubkey": [ "input" ],
+      "signature": [ "input" ],
+      "input": [ [ "pubkey", "signature" ] ],
+      "witness": [ "input" ]
+    },
+    "details": [
+      {
+        "description": "p2pkh",
+        "address": "134D6gYy8DsR5m4416BnmgASuMBqKvogQh",
+        "hash": "168b992bcfc44050310b3a94bd0771136d0b28d1",
+        "output": "OP_DUP OP_HASH160 168b992bcfc44050310b3a94bd0771136d0b28d1 OP_EQUALVERIFY OP_CHECKSIG",
+        "pubkey": "030000000000000000000000000000000000000000000000000000000000000001",
+        "signature": "300602010002010001",
+        "input": "300602010002010001 030000000000000000000000000000000000000000000000000000000000000001",
+        "witness": []
+      }
+    ]
+  }
+}
diff --git a/test/fixtures/p2sh.json b/test/fixtures/p2sh.json
new file mode 100644
index 0000000..c87c8a8
--- /dev/null
+++ b/test/fixtures/p2sh.json
@@ -0,0 +1,346 @@
+{
+  "valid": [
+    {
+      "description": "p2sh-*, out (from address)",
+      "arguments": {
+        "address": "3GETYP4cuSesh2zsPEEYVZqnRedwe4FwUT"
+      },
+      "expected": {
+        "hash": "9f840a5fc02407ef0ad499c2ec0eb0b942fb0086",
+        "output": "OP_HASH160 9f840a5fc02407ef0ad499c2ec0eb0b942fb0086 OP_EQUAL",
+        "redeem": null,
+        "input": null,
+        "witness": null
+      }
+    },
+    {
+      "description": "p2sh-*, out (from hash)",
+      "arguments": {
+        "hash": "9f840a5fc02407ef0ad499c2ec0eb0b942fb0086"
+      },
+      "expected": {
+        "address": "3GETYP4cuSesh2zsPEEYVZqnRedwe4FwUT",
+        "output": "OP_HASH160 9f840a5fc02407ef0ad499c2ec0eb0b942fb0086 OP_EQUAL",
+        "redeem": null,
+        "input": null,
+        "witness": null
+      }
+    },
+    {
+      "description": "p2sh-*, out (from output)",
+      "arguments": {
+        "output": "OP_HASH160 9f840a5fc02407ef0ad499c2ec0eb0b942fb0086 OP_EQUAL"
+      },
+      "expected": {
+        "address": "3GETYP4cuSesh2zsPEEYVZqnRedwe4FwUT",
+        "hash": "9f840a5fc02407ef0ad499c2ec0eb0b942fb0086",
+        "redeem": null,
+        "input": null,
+        "witness": null
+      }
+    },
+    {
+      "description": "p2sh-p2pkh, out (from redeem)",
+      "arguments": {
+        "redeem": {
+          "address": "this is P2PKH context, unknown and ignored by P2SH",
+          "output": "OP_DUP OP_HASH160 c30afa58ae0673b00a45b5c17dff4633780f1400 OP_EQUALVERIFY OP_CHECKSIG"
+        }
+      },
+      "expected": {
+        "address": "3GETYP4cuSesh2zsPEEYVZqnRedwe4FwUT",
+        "hash": "9f840a5fc02407ef0ad499c2ec0eb0b942fb0086",
+        "output": "OP_HASH160 9f840a5fc02407ef0ad499c2ec0eb0b942fb0086 OP_EQUAL",
+        "input": null,
+        "witness": null
+      }
+    },
+    {
+      "description": "p2sh-p2wpkh, out (from redeem)",
+      "arguments": {
+        "redeem": {
+          "hash": "this is P2WPKH context, unknown and ignored by P2SH",
+          "output": "OP_0 c30afa58ae0673b00a45b5c17dff4633780f1400"
+        }
+      },
+      "expected": {
+        "address": "325CuTNSYmvurXaBmhNFer5zDkKnDXZggu",
+        "hash": "0432515d8fe8de31be8207987fc6d67b29d5e7cc",
+        "output": "OP_HASH160 0432515d8fe8de31be8207987fc6d67b29d5e7cc OP_EQUAL",
+        "input": null,
+        "witness": null
+      }
+    },
+    {
+      "description": "p2sh-p2pk, out (from redeem)",
+      "arguments": {
+        "redeem": {
+          "output": "03e15819590382a9dd878f01e2f0cbce541564eb415e43b440472d883ecd283058 OP_CHECKSIG",
+          "pubkey": "this is P2WPKH context, unknown and ignored by P2SH"
+        }
+      },
+      "expected": {
+        "address": "36TibC8RrPB9WrBdPoGXhHqDHJosyFVtVQ",
+        "hash": "3454c084887afe854e80221c69d6282926f809c4",
+        "output": "OP_HASH160 3454c084887afe854e80221c69d6282926f809c4 OP_EQUAL",
+        "input": null,
+        "witness": null
+      }
+    },
+    {
+      "description": "p2sh-p2pkh, in and out (from redeem)",
+      "arguments": {
+        "redeem": {
+          "output": "OP_DUP OP_HASH160 c30afa58ae0673b00a45b5c17dff4633780f1400 OP_EQUALVERIFY OP_CHECKSIG",
+          "input": "3045022100e4fce9ec72b609a2df1dc050c20dcf101d27faefb3e686b7a4cb067becdd5e8e022071287fced53806b08cf39b5ad58bbe614775b3776e98a9f8760af0d4d1d47a9501 03e15819590382a9dd878f01e2f0cbce541564eb415e43b440472d883ecd283058"
+        }
+      },
+      "expected": {
+        "address": "3GETYP4cuSesh2zsPEEYVZqnRedwe4FwUT",
+        "hash": "9f840a5fc02407ef0ad499c2ec0eb0b942fb0086",
+        "output": "OP_HASH160 9f840a5fc02407ef0ad499c2ec0eb0b942fb0086 OP_EQUAL",
+        "input": "3045022100e4fce9ec72b609a2df1dc050c20dcf101d27faefb3e686b7a4cb067becdd5e8e022071287fced53806b08cf39b5ad58bbe614775b3776e98a9f8760af0d4d1d47a9501 03e15819590382a9dd878f01e2f0cbce541564eb415e43b440472d883ecd283058 76a914c30afa58ae0673b00a45b5c17dff4633780f140088ac",
+        "witness": []
+      }
+    },
+    {
+      "description": "p2sh-p2wpkh, in and out (from redeem w/ witness)",
+      "arguments": {
+        "redeem": {
+          "output": "OP_0 c30afa58ae0673b00a45b5c17dff4633780f1400",
+          "input": "",
+          "witness": [
+            "3045022100e4fce9ec72b609a2df1dc050c20dcf101d27faefb3e686b7a4cb067becdd5e8e022071287fced53806b08cf39b5ad58bbe614775b3776e98a9f8760af0d4d1d47a9501",
+            "03e15819590382a9dd878f01e2f0cbce541564eb415e43b440472d883ecd283058"
+          ]
+        }
+      },
+      "expected": {
+        "address": "325CuTNSYmvurXaBmhNFer5zDkKnDXZggu",
+        "hash": "0432515d8fe8de31be8207987fc6d67b29d5e7cc",
+        "output": "OP_HASH160 0432515d8fe8de31be8207987fc6d67b29d5e7cc OP_EQUAL",
+        "input": "0014c30afa58ae0673b00a45b5c17dff4633780f1400",
+        "witness": [
+          "3045022100e4fce9ec72b609a2df1dc050c20dcf101d27faefb3e686b7a4cb067becdd5e8e022071287fced53806b08cf39b5ad58bbe614775b3776e98a9f8760af0d4d1d47a9501",
+          "03e15819590382a9dd878f01e2f0cbce541564eb415e43b440472d883ecd283058"
+        ]
+      }
+    },
+    {
+      "description": "p2sh-p2pk, in and out (from input)",
+      "arguments": {
+        "input": "3045022100e4fce9ec72b609a2df1dc050c20dcf101d27faefb3e686b7a4cb067becdd5e8e022071287fced53806b08cf39b5ad58bbe614775b3776e98a9f8760af0d4d1d47a9501 2103e15819590382a9dd878f01e2f0cbce541564eb415e43b440472d883ecd283058ac"
+      },
+      "expected": {
+        "address": "36TibC8RrPB9WrBdPoGXhHqDHJosyFVtVQ",
+        "hash": "3454c084887afe854e80221c69d6282926f809c4",
+        "output": "OP_HASH160 3454c084887afe854e80221c69d6282926f809c4 OP_EQUAL",
+        "redeem": {
+          "output": "03e15819590382a9dd878f01e2f0cbce541564eb415e43b440472d883ecd283058 OP_CHECKSIG",
+          "input": "3045022100e4fce9ec72b609a2df1dc050c20dcf101d27faefb3e686b7a4cb067becdd5e8e022071287fced53806b08cf39b5ad58bbe614775b3776e98a9f8760af0d4d1d47a9501",
+          "witness": []
+        },
+        "witness": []
+      }
+    },
+    {
+      "description": "p2sh-p2wpkh, in and out (from input AND witness)",
+      "arguments": {
+        "input": "0014c30afa58ae0673b00a45b5c17dff4633780f1400",
+        "witness": [
+          "3045022100e4fce9ec72b609a2df1dc050c20dcf101d27faefb3e686b7a4cb067becdd5e8e022071287fced53806b08cf39b5ad58bbe614775b3776e98a9f8760af0d4d1d47a9501",
+          "03e15819590382a9dd878f01e2f0cbce541564eb415e43b440472d883ecd283058"
+        ]
+      },
+      "expected": {
+        "address": "325CuTNSYmvurXaBmhNFer5zDkKnDXZggu",
+        "hash": "0432515d8fe8de31be8207987fc6d67b29d5e7cc",
+        "output": "OP_HASH160 0432515d8fe8de31be8207987fc6d67b29d5e7cc OP_EQUAL",
+        "redeem": {
+          "output": "OP_0 c30afa58ae0673b00a45b5c17dff4633780f1400",
+          "input": "",
+          "witness": [
+            "3045022100e4fce9ec72b609a2df1dc050c20dcf101d27faefb3e686b7a4cb067becdd5e8e022071287fced53806b08cf39b5ad58bbe614775b3776e98a9f8760af0d4d1d47a9501",
+            "03e15819590382a9dd878f01e2f0cbce541564eb415e43b440472d883ecd283058"
+          ]
+        }
+      }
+    }
+  ],
+  "invalid": [
+    {
+      "exception": "Not enough data",
+      "arguments": {}
+    },
+    {
+      "description": "Non-minimally encoded (non BIP62 compliant)",
+      "exception": "Expected property \"output\" of type Buffer\\(Length: 23\\), got Buffer\\(Length: 24\\)",
+      "arguments": {
+        "outputHex": "a94c14c286a1af0947f58d1ad787385b1c2c4a976f9e7187"
+      }
+    },
+    {
+      "description": "Expected OP_HASH160",
+      "exception": "Output is invalid",
+      "arguments": {
+        "output": "OP_HASH256 ffffffffffffffffffffffffffffffffffffffff OP_EQUAL"
+      }
+    },
+    {
+      "description": "Unexpected OP_RESERVED",
+      "exception": "Output is invalid",
+      "arguments": {
+        "output": "OP_HASH256 ffffffffffffffffffffffffffffffffffffff OP_EQUAL OP_RESERVED"
+      }
+    },
+    {
+      "description": "address.hash != H",
+      "exception": "Hash mismatch",
+      "arguments": {
+        "address": "325CuTNSYmvurXaBmhNFer5zDkKnDXZggu",
+        "hash": "ffffffffffffffffffffffffffffffffffffffff"
+      }
+    },
+    {
+      "description": "address.hash != output.hash",
+      "exception": "Hash mismatch",
+      "arguments": {
+        "address": "325CuTNSYmvurXaBmhNFer5zDkKnDXZggu",
+        "output": "OP_HASH160 ffffffffffffffffffffffffffffffffffffffff OP_EQUAL"
+      }
+    },
+    {
+      "description": "output.hash != H",
+      "exception": "Hash mismatch",
+      "arguments": {
+        "hash": "ffffffffffffffffffffffffffffffffffffffff",
+        "output": "OP_HASH160 0432515d8fe8de31be8207987fc6d67b29d5e7cc OP_EQUAL"
+      }
+    },
+    {
+      "description": "H(redeem.output) != H",
+      "exception": "Hash mismatch",
+      "arguments": {
+        "hash": "ffffffffffffffffffffffffffffffffffffffff",
+        "redeem": {
+          "output": "03e15819590382a9dd878f01e2f0cbce541564eb415e43b440472d883ecd283058 OP_CHECKSIG"
+        }
+      }
+    },
+    {
+      "exception": "Input too short",
+      "arguments": {
+        "input": ""
+      }
+    },
+    {
+      "exception": "Input too short",
+      "arguments": {
+        "inputHex": "01ff02ff"
+      }
+    },
+    {
+      "exception": "Input is invalid",
+      "arguments": {
+        "input": "OP_0 OP_0"
+      }
+    },
+    {
+      "exception": "Redeem.input mismatch",
+      "arguments": {
+        "input": "OP_0 02ffff",
+        "redeem": {
+          "input": "OP_CHECKSIG",
+          "output": "ffff"
+        }
+      }
+    },
+    {
+      "exception": "Redeem.output mismatch",
+      "arguments": {
+        "input": "OP_0 02ffff",
+        "redeem": {
+          "input": "OP_0",
+          "output": "fff3"
+        }
+      }
+    },
+    {
+      "exception": "Redeem.output too short",
+      "arguments": {
+        "redeem": {
+          "input": "OP_0",
+          "output": ""
+        }
+      }
+    },
+    {
+      "exception": "Redeem.output too short",
+      "arguments": {
+        "inputHex": "021000"
+      }
+    },
+    {
+      "exception": "Hash mismatch",
+      "arguments": {
+        "hash": "ffffffffffffffffffffffffffffffffffffffff",
+        "redeem": {
+          "input": "OP_0",
+          "output": "ffff"
+        }
+      }
+    },
+    {
+      "exception": "Empty input",
+      "arguments": {
+        "inputHex": "01ff"
+      }
+    }
+  ],
+  "dynamic": {
+    "depends": {
+      "address": [ "address", "output", "hash", "redeem.output", [ "input", "witness" ] ],
+      "hash": [ "address", "output", "hash", "redeem.output", [ "input", "witness" ] ],
+      "output":  [ "address", "output", "hash", "redeem.output", [ "input", "witness" ] ],
+      "redeem.output": [ [ "input", "witness" ] ],
+      "redeem.input": [ [ "input", "witness" ] ],
+      "redeem.witness": [ [ "input", "witness" ] ],
+      "input": [ "redeem" ],
+      "witness": [ "redeem" ]
+    },
+    "details": [
+      {
+        "description": "p2sh-p2pkh",
+        "address": "3GETYP4cuSesh2zsPEEYVZqnRedwe4FwUT",
+        "hash": "9f840a5fc02407ef0ad499c2ec0eb0b942fb0086",
+        "output": "OP_HASH160 9f840a5fc02407ef0ad499c2ec0eb0b942fb0086 OP_EQUAL",
+        "redeem": {
+          "output": "OP_DUP OP_HASH160 c30afa58ae0673b00a45b5c17dff4633780f1400 OP_EQUALVERIFY OP_CHECKSIG",
+          "input": "3045022100e4fce9ec72b609a2df1dc050c20dcf101d27faefb3e686b7a4cb067becdd5e8e022071287fced53806b08cf39b5ad58bbe614775b3776e98a9f8760af0d4d1d47a9501 03e15819590382a9dd878f01e2f0cbce541564eb415e43b440472d883ecd283058",
+          "witness": []
+        },
+        "input": "3045022100e4fce9ec72b609a2df1dc050c20dcf101d27faefb3e686b7a4cb067becdd5e8e022071287fced53806b08cf39b5ad58bbe614775b3776e98a9f8760af0d4d1d47a9501 03e15819590382a9dd878f01e2f0cbce541564eb415e43b440472d883ecd283058 76a914c30afa58ae0673b00a45b5c17dff4633780f140088ac",
+        "witness": []
+      },
+      {
+        "description": "p2sh-p2wpkh",
+        "address": "325CuTNSYmvurXaBmhNFer5zDkKnDXZggu",
+        "hash": "0432515d8fe8de31be8207987fc6d67b29d5e7cc",
+        "output": "OP_HASH160 0432515d8fe8de31be8207987fc6d67b29d5e7cc OP_EQUAL",
+        "redeem": {
+          "output": "OP_0 c30afa58ae0673b00a45b5c17dff4633780f1400",
+          "input": "",
+          "witness": [
+            "3045022100e4fce9ec72b609a2df1dc050c20dcf101d27faefb3e686b7a4cb067becdd5e8e022071287fced53806b08cf39b5ad58bbe614775b3776e98a9f8760af0d4d1d47a9501",
+            "03e15819590382a9dd878f01e2f0cbce541564eb415e43b440472d883ecd283058"
+          ]
+        },
+        "input": "0014c30afa58ae0673b00a45b5c17dff4633780f1400",
+        "witness": [
+          "3045022100e4fce9ec72b609a2df1dc050c20dcf101d27faefb3e686b7a4cb067becdd5e8e022071287fced53806b08cf39b5ad58bbe614775b3776e98a9f8760af0d4d1d47a9501",
+          "03e15819590382a9dd878f01e2f0cbce541564eb415e43b440472d883ecd283058"
+        ]
+      }
+    ]
+  }
+}
diff --git a/test/fixtures/p2wpkh.json b/test/fixtures/p2wpkh.json
new file mode 100644
index 0000000..84e0c08
--- /dev/null
+++ b/test/fixtures/p2wpkh.json
@@ -0,0 +1,195 @@
+{
+  "valid": [
+    {
+      "description": "output from address",
+      "arguments": {
+        "address": "bc1qafk4yhqvj4wep57m62dgrmutldusqde8adh20d"
+      },
+      "expected": {
+        "hash": "ea6d525c0c955d90d3dbd29a81ef8bfb79003727",
+        "output": "OP_0 ea6d525c0c955d90d3dbd29a81ef8bfb79003727",
+        "signature": null,
+        "input": null,
+        "witness": null
+      }
+    },
+    {
+      "description": "output from hash",
+      "arguments": {
+        "hash": "ea6d525c0c955d90d3dbd29a81ef8bfb79003727"
+      },
+      "expected": {
+        "address": "bc1qafk4yhqvj4wep57m62dgrmutldusqde8adh20d",
+        "output": "OP_0 ea6d525c0c955d90d3dbd29a81ef8bfb79003727",
+        "signature": null,
+        "input": null,
+        "witness": null
+      }
+    },
+    {
+      "description": "output from output",
+      "arguments": {
+        "output": "OP_0 ea6d525c0c955d90d3dbd29a81ef8bfb79003727"
+      },
+      "expected": {
+        "address": "bc1qafk4yhqvj4wep57m62dgrmutldusqde8adh20d",
+        "hash": "ea6d525c0c955d90d3dbd29a81ef8bfb79003727",
+        "signature": null,
+        "input": null,
+        "witness": null
+      }
+    },
+    {
+      "description": "output from pubkey",
+      "arguments": {
+        "pubkey": "030000000000000000000000000000000000000000000000000000000000000001"
+      },
+      "expected": {
+        "address": "bc1qz69ej270c3q9qvgt822t6pm3zdksk2x35j2jlm",
+        "hash": "168b992bcfc44050310b3a94bd0771136d0b28d1",
+        "output": "OP_0 168b992bcfc44050310b3a94bd0771136d0b28d1",
+        "signature": null,
+        "input": null,
+        "witness": null
+      }
+    },
+    {
+      "description": "witness/output from pubkey/signature",
+      "arguments": {
+        "pubkey": "030000000000000000000000000000000000000000000000000000000000000001",
+        "signature": "300602010002010001"
+      },
+      "expected": {
+        "address": "bc1qz69ej270c3q9qvgt822t6pm3zdksk2x35j2jlm",
+        "hash": "168b992bcfc44050310b3a94bd0771136d0b28d1",
+        "output": "OP_0 168b992bcfc44050310b3a94bd0771136d0b28d1",
+        "input": "",
+        "witness": [
+          "300602010002010001",
+          "030000000000000000000000000000000000000000000000000000000000000001"
+        ]
+      }
+    },
+    {
+      "description": "witness/output from witness",
+      "arguments": {
+        "witness": [
+          "300602010002010001",
+          "030000000000000000000000000000000000000000000000000000000000000001"
+        ]
+      },
+      "expected": {
+        "address": "bc1qz69ej270c3q9qvgt822t6pm3zdksk2x35j2jlm",
+        "hash": "168b992bcfc44050310b3a94bd0771136d0b28d1",
+        "output": "OP_0 168b992bcfc44050310b3a94bd0771136d0b28d1",
+        "pubkey": "030000000000000000000000000000000000000000000000000000000000000001",
+        "signature": "300602010002010001",
+        "input": ""
+      }
+    }
+  ],
+  "invalid": [
+    {
+      "exception": "Not enough data",
+      "arguments": {}
+    },
+    {
+      "exception": "Not enough data",
+      "arguments": {
+        "signature": "300602010002010001"
+      }
+    },
+    {
+      "exception": "Output is invalid",
+      "description": "Unexpected OP",
+      "arguments": {
+        "output": "OP_RESERVED ea6d525c0c955d90d3dbd29a81ef8bfb79003727"
+      }
+    },
+    {
+      "exception": "Pubkey mismatch",
+      "arguments": {
+        "pubkey": "030000000000000000000000000000000000000000000000000000000000000001",
+        "witness": [
+          "300602010002010001",
+          "030000000000000000000000000000000000000000000000000000000000000002"
+        ]
+      }
+    },
+    {
+      "exception": "Hash mismatch",
+      "arguments": {
+        "pubkey": "030000000000000000000000000000000000000000000000000000000000000001",
+        "hash": "ffffffffffffffffffffffffffffffffffffffff"
+      }
+    },
+    {
+      "exception": "Hash mismatch",
+      "arguments": {
+        "address": "bc1qafk4yhqvj4wep57m62dgrmutldusqde8adh20d",
+        "hash": "ffffffffffffffffffffffffffffffffffffffff"
+      }
+    },
+    {
+      "exception": "Hash mismatch",
+      "arguments": {
+        "output": "OP_0 ea6d525c0c955d90d3dbd29a81ef8bfb79003727",
+        "hash": "ffffffffffffffffffffffffffffffffffffffff"
+      }
+    },
+    {
+      "exception": "Hash mismatch",
+      "arguments": {
+        "hash": "ffffffffffffffffffffffffffffffffffffffff",
+        "witness": [
+          "300602010002010001",
+          "030000000000000000000000000000000000000000000000000000000000000001"
+        ]
+      }
+    },
+    {
+      "exception": "Input has invalid signature",
+      "arguments": {
+        "witness": [
+          "ffffffffffffffffff",
+          "030000000000000000000000000000000000000000000000000000000000000001"
+        ]
+      }
+    },
+    {
+      "exception": "Input has invalid pubkey",
+      "arguments": {
+        "witness": [
+          "300602010002010001",
+          "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
+        ]
+      }
+    }
+  ],
+  "dynamic": {
+    "depends": {
+      "address": [ "address", "output", "hash", "pubkey", "witness" ],
+      "hash": [ "address", "output", "hash", "pubkey", "witness" ],
+      "output": [ "address", "output", "hash", "pubkey", "witness" ],
+      "pubkey": [ "witness" ],
+      "signature": [ "witness" ],
+      "input": [ "witness" ],
+      "witness": [ [ "pubkey", "signature" ] ]
+    },
+    "details": [
+      {
+        "description": "p2wpkh",
+        "address": "bc1qz69ej270c3q9qvgt822t6pm3zdksk2x35j2jlm",
+        "hash": "168b992bcfc44050310b3a94bd0771136d0b28d1",
+        "output": "OP_0 168b992bcfc44050310b3a94bd0771136d0b28d1",
+        "pubkey": "030000000000000000000000000000000000000000000000000000000000000001",
+        "signature": "300602010002010001",
+        "input": "",
+        "witness": [
+          "300602010002010001",
+          "030000000000000000000000000000000000000000000000000000000000000001"
+        ]
+      }
+    ]
+  }
+}
diff --git a/test/fixtures/p2wsh.json b/test/fixtures/p2wsh.json
new file mode 100644
index 0000000..c123d64
--- /dev/null
+++ b/test/fixtures/p2wsh.json
@@ -0,0 +1,326 @@
+{
+  "valid": [
+    {
+      "description": "p2wsh-*, out (from address)",
+      "arguments": {
+        "address": "bc1q6rgl33d3s9dugudw7n68yrryajkr3ha9q8q24j20zs62se4q9tsqdy0t2q"
+      },
+      "expected": {
+        "hash": "d0d1f8c5b1815bc471aef4f4720c64ecac38dfa501c0aac94f1434a866a02ae0",
+        "output": "OP_0 d0d1f8c5b1815bc471aef4f4720c64ecac38dfa501c0aac94f1434a866a02ae0",
+        "redeem": null,
+        "input": null,
+        "witness": null
+      }
+    },
+    {
+      "description": "p2wsh-*, out (from hash)",
+      "arguments": {
+        "hash": "d0d1f8c5b1815bc471aef4f4720c64ecac38dfa501c0aac94f1434a866a02ae0"
+      },
+      "expected": {
+        "address": "bc1q6rgl33d3s9dugudw7n68yrryajkr3ha9q8q24j20zs62se4q9tsqdy0t2q",
+        "output": "OP_0 d0d1f8c5b1815bc471aef4f4720c64ecac38dfa501c0aac94f1434a866a02ae0",
+        "redeem": null,
+        "input": null,
+        "witness": null
+      }
+    },
+    {
+      "description": "p2wsh-*, out (from output)",
+      "arguments": {
+        "output": "OP_0 d0d1f8c5b1815bc471aef4f4720c64ecac38dfa501c0aac94f1434a866a02ae0"
+      },
+      "expected": {
+        "address": "bc1q6rgl33d3s9dugudw7n68yrryajkr3ha9q8q24j20zs62se4q9tsqdy0t2q",
+        "hash": "d0d1f8c5b1815bc471aef4f4720c64ecac38dfa501c0aac94f1434a866a02ae0",
+        "redeem": null,
+        "input": null,
+        "witness": null
+      }
+    },
+    {
+      "description": "p2wsh-p2pkh, out (from redeem)",
+      "arguments": {
+        "redeem": {
+          "address": "this is P2PKH context, unknown and ignored by p2wsh",
+          "output": "OP_DUP OP_HASH160 c30afa58ae0673b00a45b5c17dff4633780f1400 OP_EQUALVERIFY OP_CHECKSIG"
+        }
+      },
+      "expected": {
+        "address": "bc1qusxlgq9quu27ucxs7a2fg8nv0pycdzvxsjk9npyupupxw3y892ss2cq5ar",
+        "hash": "e40df400a0e715ee60d0f754941e6c784986898684ac59849c0f026744872aa1",
+        "output": "OP_0 e40df400a0e715ee60d0f754941e6c784986898684ac59849c0f026744872aa1",
+        "input": null,
+        "witness": null
+      }
+    },
+    {
+      "description": "p2wsh-p2wpkh, out (from redeem)",
+      "arguments": {
+        "redeem": {
+          "hash": "this is P2WPKH context, unknown and ignored by p2wsh",
+          "output": "OP_0 c30afa58ae0673b00a45b5c17dff4633780f1400"
+        }
+      },
+      "expected": {
+        "address": "bc1qpsl7el8wcx22f3fpdt3lm2wmzug7yyx2q3n8wzgtf37kps9tqy7skc7m3e",
+        "hash": "0c3fecfceec194a4c5216ae3fda9db1711e210ca046677090b4c7d60c0ab013d",
+        "output": "OP_0 0c3fecfceec194a4c5216ae3fda9db1711e210ca046677090b4c7d60c0ab013d",
+        "input": null,
+        "witness": null
+      }
+    },
+    {
+      "description": "p2wsh-p2pk, out (from redeem)",
+      "arguments": {
+        "redeem": {
+          "output": "03e15819590382a9dd878f01e2f0cbce541564eb415e43b440472d883ecd283058 OP_CHECKSIG",
+          "pubkey": "this is P2WPKH context, unknown and ignored by p2wsh"
+        }
+      },
+      "expected": {
+        "address": "bc1q6rgl33d3s9dugudw7n68yrryajkr3ha9q8q24j20zs62se4q9tsqdy0t2q",
+        "hash": "d0d1f8c5b1815bc471aef4f4720c64ecac38dfa501c0aac94f1434a866a02ae0",
+        "output": "OP_0 d0d1f8c5b1815bc471aef4f4720c64ecac38dfa501c0aac94f1434a866a02ae0",
+        "input": null,
+        "witness": null
+      }
+    },
+    {
+      "description": "p2wsh-p2pkh, in and out (from redeem, transformed to witness)",
+      "arguments": {
+        "redeem": {
+          "output": "OP_DUP OP_HASH160 c30afa58ae0673b00a45b5c17dff4633780f1400 OP_EQUALVERIFY OP_CHECKSIG",
+          "input": "3045022100e4fce9ec72b609a2df1dc050c20dcf101d27faefb3e686b7a4cb067becdd5e8e022071287fced53806b08cf39b5ad58bbe614775b3776e98a9f8760af0d4d1d47a9501 03e15819590382a9dd878f01e2f0cbce541564eb415e43b440472d883ecd283058"
+        }
+      },
+      "expected": {
+        "address": "bc1qusxlgq9quu27ucxs7a2fg8nv0pycdzvxsjk9npyupupxw3y892ss2cq5ar",
+        "hash": "e40df400a0e715ee60d0f754941e6c784986898684ac59849c0f026744872aa1",
+        "output": "OP_0 e40df400a0e715ee60d0f754941e6c784986898684ac59849c0f026744872aa1",
+        "redeem": {
+          "input": ""
+        },
+        "input": "",
+        "witness": [
+          "3045022100e4fce9ec72b609a2df1dc050c20dcf101d27faefb3e686b7a4cb067becdd5e8e022071287fced53806b08cf39b5ad58bbe614775b3776e98a9f8760af0d4d1d47a9501",
+          "03e15819590382a9dd878f01e2f0cbce541564eb415e43b440472d883ecd283058",
+          "76a914c30afa58ae0673b00a45b5c17dff4633780f140088ac"
+        ]
+      }
+    },
+    {
+      "description": "p2wsh-p2wpkh, in and out (from redeem w/ witness)",
+      "arguments": {
+        "redeem": {
+          "output": "OP_0 c30afa58ae0673b00a45b5c17dff4633780f1400",
+          "input": "",
+          "witness": [
+            "3045022100e4fce9ec72b609a2df1dc050c20dcf101d27faefb3e686b7a4cb067becdd5e8e022071287fced53806b08cf39b5ad58bbe614775b3776e98a9f8760af0d4d1d47a9501",
+            "03e15819590382a9dd878f01e2f0cbce541564eb415e43b440472d883ecd283058"
+          ]
+        }
+      },
+      "expected": {
+        "address": "bc1qpsl7el8wcx22f3fpdt3lm2wmzug7yyx2q3n8wzgtf37kps9tqy7skc7m3e",
+        "hash": "0c3fecfceec194a4c5216ae3fda9db1711e210ca046677090b4c7d60c0ab013d",
+        "output": "OP_0 0c3fecfceec194a4c5216ae3fda9db1711e210ca046677090b4c7d60c0ab013d",
+        "input": "",
+        "witness": [
+          "3045022100e4fce9ec72b609a2df1dc050c20dcf101d27faefb3e686b7a4cb067becdd5e8e022071287fced53806b08cf39b5ad58bbe614775b3776e98a9f8760af0d4d1d47a9501",
+          "03e15819590382a9dd878f01e2f0cbce541564eb415e43b440472d883ecd283058",
+          "0014c30afa58ae0673b00a45b5c17dff4633780f1400"
+        ]
+      }
+    },
+    {
+      "description": "p2wsh-p2pk, in and out (from witness)",
+      "arguments": {
+        "witness": [
+          "3045022100e4fce9ec72b609a2df1dc050c20dcf101d27faefb3e686b7a4cb067becdd5e8e022071287fced53806b08cf39b5ad58bbe614775b3776e98a9f8760af0d4d1d47a9501",
+          "2103e15819590382a9dd878f01e2f0cbce541564eb415e43b440472d883ecd283058ac"
+        ]
+      },
+      "expected": {
+        "address": "bc1q6rgl33d3s9dugudw7n68yrryajkr3ha9q8q24j20zs62se4q9tsqdy0t2q",
+        "hash": "d0d1f8c5b1815bc471aef4f4720c64ecac38dfa501c0aac94f1434a866a02ae0",
+        "output": "OP_0 d0d1f8c5b1815bc471aef4f4720c64ecac38dfa501c0aac94f1434a866a02ae0",
+        "redeem": {
+          "output": "03e15819590382a9dd878f01e2f0cbce541564eb415e43b440472d883ecd283058 OP_CHECKSIG",
+          "input": "",
+          "witness": [
+            "3045022100e4fce9ec72b609a2df1dc050c20dcf101d27faefb3e686b7a4cb067becdd5e8e022071287fced53806b08cf39b5ad58bbe614775b3776e98a9f8760af0d4d1d47a9501"
+          ]
+        },
+        "input": ""
+      }
+    },
+    {
+      "description": "p2wsh-p2wpkh, in and out (from witness)",
+      "arguments": {
+        "witness": [
+          "3045022100e4fce9ec72b609a2df1dc050c20dcf101d27faefb3e686b7a4cb067becdd5e8e022071287fced53806b08cf39b5ad58bbe614775b3776e98a9f8760af0d4d1d47a9501",
+          "03e15819590382a9dd878f01e2f0cbce541564eb415e43b440472d883ecd283058",
+          "0014c30afa58ae0673b00a45b5c17dff4633780f1400"
+        ]
+      },
+      "expected": {
+        "address": "bc1qpsl7el8wcx22f3fpdt3lm2wmzug7yyx2q3n8wzgtf37kps9tqy7skc7m3e",
+        "hash": "0c3fecfceec194a4c5216ae3fda9db1711e210ca046677090b4c7d60c0ab013d",
+        "output": "OP_0 0c3fecfceec194a4c5216ae3fda9db1711e210ca046677090b4c7d60c0ab013d",
+        "redeem": {
+          "output": "OP_0 c30afa58ae0673b00a45b5c17dff4633780f1400",
+          "input": "",
+          "witness": [
+            "3045022100e4fce9ec72b609a2df1dc050c20dcf101d27faefb3e686b7a4cb067becdd5e8e022071287fced53806b08cf39b5ad58bbe614775b3776e98a9f8760af0d4d1d47a9501",
+            "03e15819590382a9dd878f01e2f0cbce541564eb415e43b440472d883ecd283058"
+          ]
+        }
+      }
+    }
+  ],
+  "invalid": [
+    {
+      "exception": "Not enough data",
+      "arguments": {}
+    },
+    {
+      "description": "address.hash != H",
+      "exception": "Hash mismatch",
+      "arguments": {
+        "address": "bc1qpsl7el8wcx22f3fpdt3lm2wmzug7yyx2q3n8wzgtf37kps9tqy7skc7m3e",
+        "hash": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
+      }
+    },
+    {
+      "description": "address.hash != output.hash",
+      "exception": "Hash mismatch",
+      "arguments": {
+        "address": "bc1qpsl7el8wcx22f3fpdt3lm2wmzug7yyx2q3n8wzgtf37kps9tqy7skc7m3e",
+        "output": "OP_0 ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
+      }
+    },
+    {
+      "description": "output.hash != H",
+      "exception": "Hash mismatch",
+      "arguments": {
+        "hash": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
+        "output": "OP_0 d0d1f8c5b1815bc471aef4f4720c64ecac38dfa501c0aac94f1434a866a02ae0"
+      }
+    },
+    {
+      "description": "H(redeem.output) != H",
+      "exception": "Hash mismatch",
+      "arguments": {
+        "hash": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
+        "redeem": {
+          "output": "03e15819590382a9dd878f01e2f0cbce541564eb415e43b440472d883ecd283058 OP_CHECKSIG"
+        }
+      }
+    },
+    {
+      "exception": "Output is invalid",
+      "arguments": {
+        "output": "OP_HASH256 ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff OP_EQUAL"
+      }
+    },
+    {
+      "exception": "Redeem.output is invalid",
+      "arguments": {
+        "redeem": {
+          "output": ""
+        }
+      }
+    },
+    {
+      "exception": "Non push-only scriptSig",
+      "arguments": {
+        "redeem": {
+          "output": "OP_TRUE",
+          "input": "OP_HASH256"
+        }
+      }
+    },
+    {
+      "exception": "Witness and redeem.output mismatch",
+      "arguments": {
+        "redeem": {
+          "output": "OP_TRUE",
+          "input": "OP_0"
+        },
+        "witness": [
+          "02ffff"
+        ]
+      }
+    },
+    {
+      "exception": "Witness and redeem.witness mismatch",
+      "arguments": {
+        "redeem": {
+          "output": "OP_TRUE",
+          "witness": [
+            "01"
+          ]
+        },
+        "witness": [
+          "00",
+          "02ffff"
+        ]
+      }
+    }
+  ],
+  "dynamic": {
+    "depends": {
+      "address": [ "address", "output", "hash", "redeem.output", "witness" ],
+      "hash": [ "address", "output", "hash", "redeem.output", "witness" ],
+      "output":  [ "address", "output", "hash", "redeem.output", "witness" ],
+      "redeem.output": [ "witness" ],
+      "redeem.input": [ [ "input", "witness" ], "witness" ],
+      "input": [ "witness" ],
+      "witness": [ "redeem" ]
+    },
+    "details": [
+      {
+        "description": "p2wsh-p2pkh",
+        "disabled": [
+          "redeem.input"
+        ],
+        "address": "bc1qusxlgq9quu27ucxs7a2fg8nv0pycdzvxsjk9npyupupxw3y892ss2cq5ar",
+        "hash": "e40df400a0e715ee60d0f754941e6c784986898684ac59849c0f026744872aa1",
+        "output": "OP_0 e40df400a0e715ee60d0f754941e6c784986898684ac59849c0f026744872aa1",
+        "redeem": {
+          "output": "OP_DUP OP_HASH160 c30afa58ae0673b00a45b5c17dff4633780f1400 OP_EQUALVERIFY OP_CHECKSIG",
+          "input": "3045022100e4fce9ec72b609a2df1dc050c20dcf101d27faefb3e686b7a4cb067becdd5e8e022071287fced53806b08cf39b5ad58bbe614775b3776e98a9f8760af0d4d1d47a9501 03e15819590382a9dd878f01e2f0cbce541564eb415e43b440472d883ecd283058",
+          "witness": null
+        },
+        "input": "",
+        "witness": [
+          "3045022100e4fce9ec72b609a2df1dc050c20dcf101d27faefb3e686b7a4cb067becdd5e8e022071287fced53806b08cf39b5ad58bbe614775b3776e98a9f8760af0d4d1d47a9501",
+          "03e15819590382a9dd878f01e2f0cbce541564eb415e43b440472d883ecd283058",
+          "76a914c30afa58ae0673b00a45b5c17dff4633780f140088ac"
+        ]
+      },
+      {
+        "description": "p2wsh-p2wpkh",
+        "address": "bc1qpsl7el8wcx22f3fpdt3lm2wmzug7yyx2q3n8wzgtf37kps9tqy7skc7m3e",
+        "hash": "0c3fecfceec194a4c5216ae3fda9db1711e210ca046677090b4c7d60c0ab013d",
+        "output": "OP_0 0c3fecfceec194a4c5216ae3fda9db1711e210ca046677090b4c7d60c0ab013d",
+        "redeem": {
+          "output": "OP_0 c30afa58ae0673b00a45b5c17dff4633780f1400",
+          "input": "",
+          "witness": [
+            "3045022100e4fce9ec72b609a2df1dc050c20dcf101d27faefb3e686b7a4cb067becdd5e8e022071287fced53806b08cf39b5ad58bbe614775b3776e98a9f8760af0d4d1d47a9501",
+            "03e15819590382a9dd878f01e2f0cbce541564eb415e43b440472d883ecd283058"
+          ]
+        },
+        "input": "",
+        "witness": [
+          "3045022100e4fce9ec72b609a2df1dc050c20dcf101d27faefb3e686b7a4cb067becdd5e8e022071287fced53806b08cf39b5ad58bbe614775b3776e98a9f8760af0d4d1d47a9501",
+          "03e15819590382a9dd878f01e2f0cbce541564eb415e43b440472d883ecd283058",
+          "0014c30afa58ae0673b00a45b5c17dff4633780f1400"
+        ]
+      }
+    ]
+  }
+}
diff --git a/test/payments.js b/test/payments.js
new file mode 100644
index 0000000..14382e5
--- /dev/null
+++ b/test/payments.js
@@ -0,0 +1,65 @@
+/* global describe, it */
+
+const assert = require('assert')
+const u = require('./payments.utils')
+
+;['p2ms', 'p2pk', 'p2pkh', 'p2sh', 'p2wpkh', 'p2wsh'].forEach(function (p) {
+  describe(p, function () {
+    const fn = require('../src/payments/' + p)
+    const fixtures = require('./fixtures/' + p)
+
+    fixtures.valid.forEach(function (f, i) {
+      const args = u.preform(f.arguments)
+
+      it(f.description + ' as expected', function () {
+        const actual = fn(args, f.options)
+        u.equate(actual, f.expected, f.arguments)
+      })
+
+      it(f.description + ' as expected (no validation)', function () {
+        const actual = fn(args, Object.assign({}, f.options, {
+          validate: false
+        }))
+
+        u.equate(actual, f.expected, f.arguments)
+      })
+    })
+
+    fixtures.invalid.forEach(function (f) {
+      it('throws ' + (f.description || f.exception), function () {
+        const args = u.preform(f.arguments)
+
+        assert.throws(function () {
+          fn(args, f.options)
+        }, new RegExp(f.exception))
+      })
+    })
+
+    // cross-verify dynamically too
+    if (!fixtures.dynamic) return
+    const { depends, details } = fixtures.dynamic
+
+    details.forEach(function (f) {
+      const detail = u.preform(f)
+      const disabled = {}
+      if (f.disabled) f.disabled.forEach(function (k) { disabled[k] = true })
+
+      for (let key in depends) {
+        if (key in disabled) continue
+        const dependencies = depends[key]
+
+        dependencies.forEach(function (dependency) {
+          if (!Array.isArray(dependency)) dependency = [dependency]
+
+          const args = {}
+          dependency.forEach(function (d) { u.from(d, detail, args) })
+          const expected = u.from(key, detail)
+
+          it(f.description + ', ' + key + ' derives from ' + JSON.stringify(dependency), function () {
+            u.equate(fn(args), expected)
+          })
+        })
+      }
+    })
+  })
+})
diff --git a/test/payments.utils.js b/test/payments.utils.js
new file mode 100644
index 0000000..22001e9
--- /dev/null
+++ b/test/payments.utils.js
@@ -0,0 +1,128 @@
+let t = require('assert')
+let bscript = require('../src/script')
+let bnetworks = require('../src/networks')
+
+function tryHex (x) {
+  if (Buffer.isBuffer(x)) return x.toString('hex')
+  if (Array.isArray(x)) return x.map(tryHex)
+  return x
+}
+function tryASM (x) {
+  if (Buffer.isBuffer(x)) return bscript.toASM(x)
+  return x
+}
+function asmToBuffer (x) {
+  if (x === '') return Buffer.alloc(0)
+  return bscript.fromASM(x)
+}
+function carryOver (a, b) {
+  for (let k in b) {
+    if (k in a && k === 'redeem') {
+      carryOver(a[k], b[k])
+      continue
+    }
+
+    // don't, the value was specified
+    if (k in a) continue
+
+    // otherwise, expect match
+    a[k] = b[k]
+  }
+}
+
+function equateBase (a, b, context) {
+  if ('output' in b) t.strictEqual(tryASM(a.output), tryASM(b.output), `Inequal ${context}output`)
+  if ('input' in b) t.strictEqual(tryASM(a.input), tryASM(b.input), `Inequal ${context}input`)
+  if ('witness' in b) t.deepEqual(tryHex(a.witness), tryHex(b.witness), `Inequal ${context}witness`)
+}
+
+function equate (a, b, args) {
+  b = Object.assign({}, b)
+  carryOver(b, args)
+
+  // by null, we mean 'undefined', but JSON
+  if (b.input === null) b.input = undefined
+  if (b.output === null) b.output = undefined
+  if (b.witness === null) b.witness = undefined
+  if (b.redeem) {
+    if (b.redeem.input === null) b.redeem.input = undefined
+    if (b.redeem.output === null) b.redeem.output = undefined
+    if (b.redeem.witness === null) b.redeem.witness = undefined
+  }
+
+  equateBase(a, b, '')
+  if (b.redeem) equateBase(a.redeem, b.redeem, 'redeem.')
+  if (b.network) t.deepEqual(a.network, b.network, 'Inequal *.network')
+
+  // contextual
+  if (b.signature === null) b.signature = undefined
+  if ('address' in b) t.strictEqual(a.address, b.address, 'Inequal *.address')
+  if ('hash' in b) t.strictEqual(tryHex(a.hash), tryHex(b.hash), 'Inequal *.hash')
+  if ('pubkey' in b) t.strictEqual(tryHex(a.pubkey), tryHex(b.pubkey), 'Inequal *.pubkey')
+  if ('signature' in b) t.strictEqual(tryHex(a.signature), tryHex(b.signature), 'Inequal signature')
+  if ('m' in b) t.strictEqual(a.m, b.m, 'Inequal *.m')
+  if ('n' in b) t.strictEqual(a.n, b.n, 'Inequal *.n')
+  if ('pubkeys' in b) t.deepEqual(tryHex(a.pubkeys), tryHex(b.pubkeys), 'Inequal *.pubkeys')
+  if ('signatures' in b) t.deepEqual(tryHex(a.signatures), tryHex(b.signatures), 'Inequal *.signatures')
+}
+
+function preform (x) {
+  x = Object.assign({}, x)
+
+  if (x.network) x.network = bnetworks[x.network]
+  if (typeof x.inputHex === 'string') {
+    x.input = Buffer.from(x.inputHex, 'hex')
+    delete x.inputHex
+  }
+  if (typeof x.outputHex === 'string') {
+    x.output = Buffer.from(x.outputHex, 'hex')
+    delete x.outputHex
+  }
+  if (typeof x.output === 'string') x.output = asmToBuffer(x.output)
+  if (typeof x.input === 'string') x.input = asmToBuffer(x.input)
+  if (Array.isArray(x.witness)) {
+    x.witness = x.witness.map(function (y) {
+      return Buffer.from(y, 'hex')
+    })
+  }
+
+  if (x.hash) x.hash = Buffer.from(x.hash, 'hex')
+  if (x.pubkey) x.pubkey = Buffer.from(x.pubkey, 'hex')
+  if (x.signature) x.signature = Buffer.from(x.signature, 'hex')
+  if (x.pubkeys) x.pubkeys = x.pubkeys.map(function (y) { return Buffer.from(y, 'hex') })
+  if (x.signatures) x.signatures = x.signatures.map(function (y) { return Number.isFinite(y) ? y : Buffer.from(y, 'hex') })
+  if (x.redeem) {
+    if (typeof x.redeem.input === 'string') x.redeem.input = asmToBuffer(x.redeem.input)
+    if (typeof x.redeem.output === 'string') x.redeem.output = asmToBuffer(x.redeem.output)
+    if (Array.isArray(x.redeem.witness)) x.redeem.witness = x.redeem.witness.map(function (y) { return Buffer.from(y, 'hex') })
+    x.redeem.network = bnetworks[x.redeem.network] || x.network || bnetworks.bitcoin
+  }
+
+  return x
+}
+
+function from (path, object, result) {
+  path = path.split('.')
+  result = result || {}
+
+  let r = result
+  path.forEach((k, i) => {
+    if (i < path.length - 1) {
+      r[k] = r[k] || {}
+
+      // recurse
+      r = r[k]
+      object = object[k]
+    } else {
+      r[k] = object[k]
+    }
+  })
+
+  return result
+}
+
+module.exports = {
+  from,
+  equate,
+  preform
+}