From 25b5806cf146ef5d5f5770c60f102a7b37bcf660 Mon Sep 17 00:00:00 2001
From: junderw <junderwood@bitcoinbank.co.jp>
Date: Thu, 21 May 2020 11:11:12 +0900
Subject: [PATCH] Throw errors when p2wsh or p2wpkh contain uncompressed
 pubkeys.

This will enforce BIP143 compressed pubkey rules on an address generation level.
---
 src/payments/p2wpkh.js                 |  4 ++-
 src/payments/p2wsh.js                  | 34 ++++++++++++++++++-----
 test/fixtures/p2wpkh.json              | 17 ++++++++++++
 test/fixtures/p2wsh.json               | 24 +++++++++++++++++
 test/fixtures/transaction_builder.json |  4 +--
 ts_src/payments/p2wpkh.ts              |  4 ++-
 ts_src/payments/p2wsh.ts               | 37 +++++++++++++++++++++-----
 7 files changed, 107 insertions(+), 17 deletions(-)

diff --git a/src/payments/p2wpkh.js b/src/payments/p2wpkh.js
index b32e808..0ba4a51 100644
--- a/src/payments/p2wpkh.js
+++ b/src/payments/p2wpkh.js
@@ -107,12 +107,14 @@ function p2wpkh(a, opts) {
       if (hash.length > 0 && !hash.equals(pkh))
         throw new TypeError('Hash mismatch');
       else hash = pkh;
+      if (!ecc.isPoint(a.pubkey) || a.pubkey.length !== 33)
+        throw new TypeError('Invalid pubkey for p2wpkh');
     }
     if (a.witness) {
       if (a.witness.length !== 2) throw new TypeError('Witness is invalid');
       if (!bscript.isCanonicalScriptSignature(a.witness[0]))
         throw new TypeError('Witness has invalid signature');
-      if (!ecc.isPoint(a.witness[1]))
+      if (!ecc.isPoint(a.witness[1]) || a.witness[1].length !== 33)
         throw new TypeError('Witness has invalid pubkey');
       if (a.signature && !a.signature.equals(a.witness[0]))
         throw new TypeError('Signature mismatch');
diff --git a/src/payments/p2wsh.js b/src/payments/p2wsh.js
index 6a4aa24..1177f3c 100644
--- a/src/payments/p2wsh.js
+++ b/src/payments/p2wsh.js
@@ -6,6 +6,7 @@ const bscript = require('../script');
 const lazy = require('./lazy');
 const typef = require('typeforce');
 const OPS = bscript.OPS;
+const ecc = require('tiny-secp256k1');
 const bech32 = require('bech32');
 const EMPTY_BUFFER = Buffer.alloc(0);
 function stacksEqual(a, b) {
@@ -14,6 +15,14 @@ function stacksEqual(a, b) {
     return x.equals(b[i]);
   });
 }
+function chunkHasUncompressedPubkey(chunk) {
+  if (Buffer.isBuffer(chunk) && chunk.length === 65) {
+    if (ecc.isPoint(chunk)) return true;
+    else return false;
+  } else {
+    return false;
+  }
+}
 // input: <>
 // witness: [redeemScriptSig ...] {redeemScript}
 // output: OP_0 {sha256(redeemScript)}
@@ -51,6 +60,9 @@ function p2wsh(a, opts) {
   const _rchunks = lazy.value(() => {
     return bscript.decompile(a.redeem.input);
   });
+  const _rochunks = lazy.value(() => {
+    return bscript.decompile(a.redeem.output);
+  });
   let network = a.network;
   if (!network) {
     network = (a.redeem && a.redeem.network) || networks_1.bitcoin;
@@ -166,14 +178,24 @@ function p2wsh(a, opts) {
         !stacksEqual(a.witness, a.redeem.witness)
       )
         throw new TypeError('Witness and redeem.witness mismatch');
-    }
-    if (a.witness) {
       if (
-        a.redeem &&
-        a.redeem.output &&
-        !a.redeem.output.equals(a.witness[a.witness.length - 1])
-      )
+        (a.redeem.input && _rchunks().some(chunkHasUncompressedPubkey)) ||
+        (a.redeem.output && _rochunks().some(chunkHasUncompressedPubkey))
+      ) {
+        throw new TypeError(
+          'redeem.input or redeem.output contains uncompressed pubkey',
+        );
+      }
+    }
+    if (a.witness && a.witness.length > 0) {
+      const wScript = a.witness[a.witness.length - 1];
+      if (a.redeem && a.redeem.output && !a.redeem.output.equals(wScript))
         throw new TypeError('Witness and redeem.output mismatch');
+      if (
+        a.witness.some(chunkHasUncompressedPubkey) ||
+        (bscript.decompile(wScript) || []).some(chunkHasUncompressedPubkey)
+      )
+        throw new TypeError('Witness contains uncompressed pubkey');
     }
   }
   return Object.assign(o, a);
diff --git a/test/fixtures/p2wpkh.json b/test/fixtures/p2wpkh.json
index 4d62b60..057ba81 100644
--- a/test/fixtures/p2wpkh.json
+++ b/test/fixtures/p2wpkh.json
@@ -113,6 +113,23 @@
         "output": "OP_RESERVED ea6d525c0c955d90d3dbd29a81ef8bfb79003727"
       }
     },
+    {
+      "exception": "Invalid pubkey for p2wpkh",
+      "description": "Uncompressed pubkey in pubkey",
+      "arguments": {
+        "pubkey": "049fa211b0fca342589ca381cc96520c3d0e3924832158d9f29891936cac091e80687acdca51868ee1f89a3bb36bb16f186262938e1f94c1e7692924935b9b1457"
+      }
+    },
+    {
+      "exception": "Witness has invalid pubkey",
+      "description": "Uncompressed pubkey in witness",
+      "arguments": {
+        "witness": [
+          "300602010002010001",
+          "049fa211b0fca342589ca381cc96520c3d0e3924832158d9f29891936cac091e80687acdca51868ee1f89a3bb36bb16f186262938e1f94c1e7692924935b9b1457"
+        ]
+      }
+    },
     {
       "exception": "Pubkey mismatch",
       "options": {},
diff --git a/test/fixtures/p2wsh.json b/test/fixtures/p2wsh.json
index 0350fe9..0f60112 100644
--- a/test/fixtures/p2wsh.json
+++ b/test/fixtures/p2wsh.json
@@ -344,6 +344,30 @@
         }
       }
     },
+    {
+      "exception": "redeem.input or redeem.output contains uncompressed pubkey",
+      "arguments": {
+        "redeem": {
+          "output": "049fa211b0fca342589ca381cc96520c3d0e3924832158d9f29891936cac091e80687acdca51868ee1f89a3bb36bb16f186262938e1f94c1e7692924935b9b1457 OP_CHECKSIG"
+        }
+      }
+    },
+    {
+      "exception": "redeem.input or redeem.output contains uncompressed pubkey",
+      "arguments": {
+        "redeem": {
+          "input": "049fa211b0fca342589ca381cc96520c3d0e3924832158d9f29891936cac091e80687acdca51868ee1f89a3bb36bb16f186262938e1f94c1e7692924935b9b1457"
+        }
+      }
+    },
+    {
+      "exception": "Witness contains uncompressed pubkey",
+      "arguments": {
+        "witness": [
+          "049fa211b0fca342589ca381cc96520c3d0e3924832158d9f29891936cac091e80687acdca51868ee1f89a3bb36bb16f186262938e1f94c1e7692924935b9b1457"
+        ]
+      }
+    },
     {
       "exception": "Invalid prefix or Network mismatch",
       "arguments": {
diff --git a/test/fixtures/transaction_builder.json b/test/fixtures/transaction_builder.json
index 07226b0..1564e6a 100644
--- a/test/fixtures/transaction_builder.json
+++ b/test/fixtures/transaction_builder.json
@@ -2122,7 +2122,7 @@
       },
       {
         "description": "Transaction w/ P2WSH(P2PK), signing with uncompressed public key",
-        "exception": "BIP143 rejects uncompressed public keys in P2WPKH or P2WSH",
+        "exception": "redeem.input or redeem.output contains uncompressed pubkey",
         "inputs": [
           {
             "txId": "2fddebc1a7e67e04fc6b77645ae9ae10eeaa35e168606587d79b031ebca33345",
@@ -2148,7 +2148,7 @@
       },
       {
         "description": "Transaction w/ P2SH(P2WSH(P2PK)), signing with uncompressed public key",
-        "exception": "BIP143 rejects uncompressed public keys in P2WPKH or P2WSH",
+        "exception": "redeem.input or redeem.output contains uncompressed pubkey",
         "inputs": [
           {
             "txId": "2fddebc1a7e67e04fc6b77645ae9ae10eeaa35e168606587d79b031ebca33345",
diff --git a/ts_src/payments/p2wpkh.ts b/ts_src/payments/p2wpkh.ts
index fc2a458..27c61c9 100644
--- a/ts_src/payments/p2wpkh.ts
+++ b/ts_src/payments/p2wpkh.ts
@@ -118,13 +118,15 @@ export function p2wpkh(a: Payment, opts?: PaymentOpts): Payment {
       if (hash.length > 0 && !hash.equals(pkh))
         throw new TypeError('Hash mismatch');
       else hash = pkh;
+      if (!ecc.isPoint(a.pubkey) || a.pubkey.length !== 33)
+        throw new TypeError('Invalid pubkey for p2wpkh');
     }
 
     if (a.witness) {
       if (a.witness.length !== 2) throw new TypeError('Witness is invalid');
       if (!bscript.isCanonicalScriptSignature(a.witness[0]))
         throw new TypeError('Witness has invalid signature');
-      if (!ecc.isPoint(a.witness[1]))
+      if (!ecc.isPoint(a.witness[1]) || a.witness[1].length !== 33)
         throw new TypeError('Witness has invalid pubkey');
 
       if (a.signature && !a.signature.equals(a.witness[0]))
diff --git a/ts_src/payments/p2wsh.ts b/ts_src/payments/p2wsh.ts
index 132dde4..173767d 100644
--- a/ts_src/payments/p2wsh.ts
+++ b/ts_src/payments/p2wsh.ts
@@ -1,10 +1,11 @@
 import * as bcrypto from '../crypto';
 import { bitcoin as BITCOIN_NETWORK } from '../networks';
 import * as bscript from '../script';
-import { Payment, PaymentOpts, StackFunction } from './index';
+import { Payment, PaymentOpts, StackElement, StackFunction } from './index';
 import * as lazy from './lazy';
 const typef = require('typeforce');
 const OPS = bscript.OPS;
+const ecc = require('tiny-secp256k1');
 
 const bech32 = require('bech32');
 
@@ -18,6 +19,15 @@ function stacksEqual(a: Buffer[], b: Buffer[]): boolean {
   });
 }
 
+function chunkHasUncompressedPubkey(chunk: StackElement): boolean {
+  if (Buffer.isBuffer(chunk) && chunk.length === 65) {
+    if (ecc.isPoint(chunk)) return true;
+    else return false;
+  } else {
+    return false;
+  }
+}
+
 // input: <>
 // witness: [redeemScriptSig ...] {redeemScript}
 // output: OP_0 {sha256(redeemScript)}
@@ -59,6 +69,9 @@ export function p2wsh(a: Payment, opts?: PaymentOpts): Payment {
   const _rchunks = lazy.value(() => {
     return bscript.decompile(a.redeem!.input!);
   }) as StackFunction;
+  const _rochunks = lazy.value(() => {
+    return bscript.decompile(a.redeem!.output!);
+  }) as StackFunction;
 
   let network = a.network;
   if (!network) {
@@ -187,15 +200,25 @@ export function p2wsh(a: Payment, opts?: PaymentOpts): Payment {
         !stacksEqual(a.witness, a.redeem.witness)
       )
         throw new TypeError('Witness and redeem.witness mismatch');
+      if (
+        (a.redeem.input && _rchunks().some(chunkHasUncompressedPubkey)) ||
+        (a.redeem.output && _rochunks().some(chunkHasUncompressedPubkey))
+      ) {
+        throw new TypeError(
+          'redeem.input or redeem.output contains uncompressed pubkey',
+        );
+      }
     }
 
-    if (a.witness) {
-      if (
-        a.redeem &&
-        a.redeem.output &&
-        !a.redeem.output.equals(a.witness[a.witness.length - 1])
-      )
+    if (a.witness && a.witness.length > 0) {
+      const wScript = a.witness[a.witness.length - 1];
+      if (a.redeem && a.redeem.output && !a.redeem.output.equals(wScript))
         throw new TypeError('Witness and redeem.output mismatch');
+      if (
+        a.witness.some(chunkHasUncompressedPubkey) ||
+        (bscript.decompile(wScript) || []).some(chunkHasUncompressedPubkey)
+      )
+        throw new TypeError('Witness contains uncompressed pubkey');
     }
   }