diff --git a/package-lock.json b/package-lock.json index 75ffeaacc..17e16965a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "bitcoinjs-lib", - "version": "6.0.2", + "version": "6.1.0-rc.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -24,15 +24,15 @@ } }, "@babel/compat-data": { - "version": "7.18.5", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.18.5.tgz", - "integrity": "sha512-BxhE40PVCBxVEJsSBhB6UWyAuqJRxGsAw8BdHMJ3AKGydcwuWW4kOO3HmqBQAdcq/OP+/DlTVxLvsCzRTnZuGg==", + "version": "7.17.10", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.17.10.tgz", + "integrity": "sha512-GZt/TCsG70Ms19gfZO1tM4CVnXsPgEPBCpJu+Qz3L0LUDsY5nZqFZglIoPC1kIYOtNBZlrnFT+klg12vFGZXrw==", "dev": true }, "@babel/core": { - "version": "7.18.5", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.18.5.tgz", - "integrity": "sha512-MGY8vg3DxMnctw0LdvSEojOsumc70g0t18gNyUdAZqB1Rpd1Bqo/svHGvt+UJ6JcGX+DIekGFDxxIWofBxLCnQ==", + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.18.2.tgz", + "integrity": "sha512-A8pri1YJiC5UnkdrWcmfZTJTV85b4UXTAfImGmCfYmax4TR9Cw8sDS0MOk++Gp2mE/BefVJ5nwy5yzqNJbP/DQ==", "dev": true, "requires": { "@ampproject/remapping": "^2.1.0", @@ -41,10 +41,10 @@ "@babel/helper-compilation-targets": "^7.18.2", "@babel/helper-module-transforms": "^7.18.0", "@babel/helpers": "^7.18.2", - "@babel/parser": "^7.18.5", + "@babel/parser": "^7.18.0", "@babel/template": "^7.16.7", - "@babel/traverse": "^7.18.5", - "@babel/types": "^7.18.4", + "@babel/traverse": "^7.18.2", + "@babel/types": "^7.18.2", "convert-source-map": "^1.7.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -235,9 +235,9 @@ } }, "@babel/parser": { - "version": "7.18.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.18.5.tgz", - "integrity": "sha512-YZWVaglMiplo7v8f1oMQ5ZPQr0vn7HPeZXxXWsxXJRjGVrzUFn9OxFQl1sb5wzfootjA/yChhW84BV+383FSOw==", + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.18.3.tgz", + "integrity": "sha512-rL50YcEuHbbauAFAysNsJA4/f89fGTOBRNs9P81sniKnKAr4xULe5AecolcsKbi88xu0ByWYDj/S1AJ3FSFuSQ==", "dev": true }, "@babel/template": { @@ -274,9 +274,9 @@ } }, "@babel/traverse": { - "version": "7.18.5", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.18.5.tgz", - "integrity": "sha512-aKXj1KT66sBj0vVzk6rEeAO6Z9aiiQ68wfDgge3nHhA/my6xMM/7HGQUNumKZaoa2qUPQ5whJG9aAifsxUKfLA==", + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.18.2.tgz", + "integrity": "sha512-9eNwoeovJ6KH9zcCNnENY7DMFwTU9JdGCFtqNLfUAqtUHRCOsTOqWoffosP8vKmNYeSBUv3yVJXjfd8ucwOjUA==", "dev": true, "requires": { "@babel/code-frame": "^7.16.7", @@ -285,8 +285,8 @@ "@babel/helper-function-name": "^7.17.9", "@babel/helper-hoist-variables": "^7.16.7", "@babel/helper-split-export-declaration": "^7.16.7", - "@babel/parser": "^7.18.5", - "@babel/types": "^7.18.4", + "@babel/parser": "^7.18.0", + "@babel/types": "^7.18.2", "debug": "^4.1.0", "globals": "^11.1.0" }, @@ -314,9 +314,9 @@ } }, "@babel/types": { - "version": "7.18.4", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.18.4.tgz", - "integrity": "sha512-ThN1mBcMq5pG/Vm2IcBmPPfyPXbd8S02rS+OBIDENdufvqC7Z/jHPCv9IcP01277aKtDI8g/2XysBN4hA8niiw==", + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.18.2.tgz", + "integrity": "sha512-0On6B8A4/+mFUto5WERt3EEuG1NznDirvwca1O8UwXQHVY8g3R7OzYgxXdOfMwLO08UrpUD/2+3Bclyq+/C94Q==", "dev": true, "requires": { "@babel/helper-validator-identifier": "^7.16.7", @@ -336,12 +336,6 @@ "resolve-from": "^5.0.0" }, "dependencies": { - "camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true - }, "find-up": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", @@ -361,15 +355,6 @@ "p-locate": "^4.1.0" } }, - "p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "requires": { - "p-try": "^2.0.0" - } - }, "p-locate": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", @@ -523,9 +508,9 @@ } }, "ajv": { - "version": "8.11.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz", - "integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==", + "version": "8.11.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.2.tgz", + "integrity": "sha512-E4bfmKAhGiSTvMfL1Myyycaub+cUEU2/IvpylXkUu7CHBkBj1f/ikdzbD7YQ6FKUbixDxeYvB/xY4fvyroDlQg==", "dev": true, "requires": { "fast-deep-equal": "^3.1.1", @@ -647,9 +632,9 @@ "dev": true }, "bip174": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/bip174/-/bip174-2.0.1.tgz", - "integrity": "sha512-i3X26uKJOkDTAalYAp0Er+qGMDhrbbh2o93/xiPyAN2s25KrClSpe3VXo/7mNJoqA5qfko8rLS2l3RWZgYmjKQ==" + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/bip174/-/bip174-2.1.0.tgz", + "integrity": "sha512-lkc0XyiX9E9KiVAS1ZiOqK1xfiwvf4FXDDdkDq5crcDzOq+xGytY+14qCsqz7kCiy8rpN1CRNfacRhf9G3JNSA==" }, "bip32": { "version": "3.0.1", @@ -711,12 +696,6 @@ "integrity": "sha512-pef6gxZFztEhaE9RY9HmWVmiIHqCb2OyS4HPKkpc6CIiiOa3Qmuoylxc5P2EkU3w+5eTSifI9SEZC88idAIGow==", "dev": true }, - "bn.js": { - "version": "4.11.8", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz", - "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==", - "dev": true - }, "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -743,15 +722,15 @@ "dev": true }, "browserslist": { - "version": "4.20.4", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.20.4.tgz", - "integrity": "sha512-ok1d+1WpnU24XYN7oC3QWgTyMhY/avPJ/r9T00xxvUOIparA/gc+UPUMaod3i+G6s+nI2nUb9xZ5k794uIwShw==", + "version": "4.20.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.20.3.tgz", + "integrity": "sha512-NBhymBQl1zM0Y5dQT/O+xiLP9/rzOIQdKM/eMJBAq7yBgaB6krIYLGejrwVYnSHZdqjscB1SPuAjHwxjvN6Wdg==", "dev": true, "requires": { - "caniuse-lite": "^1.0.30001349", - "electron-to-chromium": "^1.4.147", + "caniuse-lite": "^1.0.30001332", + "electron-to-chromium": "^1.4.118", "escalade": "^3.1.1", - "node-releases": "^2.0.5", + "node-releases": "^2.0.3", "picocolors": "^1.0.0" } }, @@ -798,15 +777,15 @@ } }, "camelcase": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", - "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", "dev": true }, "caniuse-lite": { - "version": "1.0.30001352", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001352.tgz", - "integrity": "sha512-GUgH8w6YergqPQDGWhJGt8GDRnY0L/iJVQcU3eJ46GYf52R8tk0Wxp0PymuFVZboJYXGiCqwozAYZNRjVj6IcA==", + "version": "1.0.30001343", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001343.tgz", + "integrity": "sha512-8KeCrAtPMabo/XW14B+R9sZYoClx1n0b+WYgwDKZPtWR3TcdvWzdSy7mPyFEmR5WU1St9v1PW6sdO5dkFOEzfA==", "dev": true }, "chalk": { @@ -953,9 +932,9 @@ } }, "dayjs": { - "version": "1.11.3", - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.3.tgz", - "integrity": "sha512-xxwlswWOlGhzgQ4TKzASQkUhqERI3egRNqgV4ScR8wlANA/A9tZ7miXa44vTTKEq5l7vWoL5G57bG3zA+Kow0A==", + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.6.tgz", + "integrity": "sha512-zZbY5giJAinCG+7AGaw0wIhNZ6J8AhWuSXKvuc1KAyMiRsvGQWqh4L+MomvhdAYjN+lqvVCMq1I41e3YHvXkyQ==", "dev": true }, "debug": { @@ -976,9 +955,9 @@ } }, "decamelize": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", - "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", "dev": true }, "default-require-extensions": { @@ -1017,9 +996,9 @@ } }, "electron-to-chromium": { - "version": "1.4.153", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.153.tgz", - "integrity": "sha512-57AV9DNW1R52HjOqnGOCCTLHMHItLTGu/WjB1KYIa4BQ7p0u8J0j8N78akPcOBStKE801xcMjTpmbAylflfIYQ==", + "version": "1.4.139", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.139.tgz", + "integrity": "sha512-lYxzcUCjWxxVug+A7UxBCUiVr13TCjfZFYJS9Lq1VpU/ErwV4a6zUQo9dfojuGpw/L/x9REGuBl6ICQPGgbs3g==", "dev": true }, "emoji-regex": { @@ -1232,7 +1211,7 @@ "imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", "dev": true }, "indent-string": { @@ -1268,7 +1247,7 @@ "is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", "dev": true }, "is-fullwidth-code-point": { @@ -1313,7 +1292,7 @@ "is-typedarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", "dev": true }, "is-unicode-supported": { @@ -1331,7 +1310,7 @@ "isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", "dev": true }, "istanbul-lib-coverage": { @@ -1370,17 +1349,18 @@ } }, "istanbul-lib-processinfo": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/istanbul-lib-processinfo/-/istanbul-lib-processinfo-2.0.3.tgz", - "integrity": "sha512-NkwHbo3E00oybX6NGJi6ar0B29vxyvNwoC7eJ4G4Yq28UfY758Hgn/heV8VRFhevPED4LXfFz0DQ8z/0kw9zMg==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-processinfo/-/istanbul-lib-processinfo-2.0.2.tgz", + "integrity": "sha512-kOwpa7z9hme+IBPZMzQ5vdQj8srYgAtaRqeI48NGmAQ+/5yKiHLV0QbYqQpxsdEF0+w14SoB8YbnHKcXE2KnYw==", "dev": true, "requires": { "archy": "^1.0.0", - "cross-spawn": "^7.0.3", - "istanbul-lib-coverage": "^3.2.0", + "cross-spawn": "^7.0.0", + "istanbul-lib-coverage": "^3.0.0-alpha.1", + "make-dir": "^3.0.0", "p-map": "^3.0.0", "rimraf": "^3.0.0", - "uuid": "^8.3.2" + "uuid": "^3.3.3" }, "dependencies": { "rimraf": { @@ -1489,7 +1469,7 @@ "lodash.flattendeep": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz", - "integrity": "sha512-uHaJFihxmJcEX3kT4I23ABqKKalJ/zDrDg0lsFtc1h+3uw49SIJ5beyhx5ExVRti3AvKoOJngIj7xz3oylPdWQ==", + "integrity": "sha1-+wMJF/hqMTTlvJvsDWngAT3f7bI=", "dev": true }, "lodash.get": { @@ -1615,9 +1595,9 @@ } }, "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, "requires": { "brace-expansion": "^1.1.7" @@ -1819,12 +1799,6 @@ "color-convert": "^2.0.1" } }, - "camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true - }, "cliui": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", @@ -1851,12 +1825,6 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, - "decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", - "dev": true - }, "find-up": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", @@ -1899,15 +1867,6 @@ "brace-expansion": "^1.1.7" } }, - "p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "requires": { - "p-try": "^2.0.0" - } - }, "p-locate": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", @@ -1937,12 +1896,6 @@ "strip-ansi": "^6.0.0" } }, - "y18n": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", - "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", - "dev": true - }, "yargs": { "version": "15.4.1", "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", @@ -1984,12 +1937,12 @@ } }, "p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", "dev": true, "requires": { - "yocto-queue": "^0.1.0" + "p-try": "^2.0.0" } }, "p-locate": { @@ -1999,6 +1952,17 @@ "dev": true, "requires": { "p-limit": "^3.0.2" + }, + "dependencies": { + "p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "requires": { + "yocto-queue": "^0.1.0" + } + } } }, "p-map": { @@ -2053,9 +2017,9 @@ "dev": true }, "pbkdf2": { - "version": "3.0.17", - "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.0.17.tgz", - "integrity": "sha512-U/il5MsrZp7mGg3mSQfn742na2T+1/vHDCG5/iTI3X9MKUuYUZVLQhyRsg06mCgDBTd57TxzgZt7P+fYfjRLtA==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.2.tgz", + "integrity": "sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA==", "dev": true, "requires": { "create-hash": "^1.1.2", @@ -2105,15 +2069,6 @@ "p-locate": "^4.1.0" } }, - "p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "requires": { - "p-try": "^2.0.0" - } - }, "p-locate": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", @@ -2198,7 +2153,7 @@ "release-zalgo": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/release-zalgo/-/release-zalgo-1.0.0.tgz", - "integrity": "sha512-gUAyHVHPPC5wdqX/LG4LWtRYtgjxyX78oanFNTMMyFEfOqdC54s3eE82imuWKbOeqYht2CrNf64Qb8vgmmtZGA==", + "integrity": "sha1-CXALflB0Mpc5Mw5TXFqQ+2eFFzA=", "dev": true, "requires": { "es6-error": "^4.0.1" @@ -2207,7 +2162,7 @@ "require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", "dev": true }, "require-from-string": { @@ -2294,7 +2249,7 @@ "set-blocking": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", "dev": true }, "sha.js": { @@ -2361,12 +2316,6 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true - }, - "is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true } } }, @@ -2481,9 +2430,9 @@ } }, "table": { - "version": "6.8.0", - "resolved": "https://registry.npmjs.org/table/-/table-6.8.0.tgz", - "integrity": "sha512-s/fitrbVeEyHKFa7mFdkuQMWlH1Wgw/yEXMt5xACT4ZpzWFluehAxRtUUQKPuWhaLAWhFcVx6w3oC8VKaUfPGA==", + "version": "6.8.1", + "resolved": "https://registry.npmjs.org/table/-/table-6.8.1.tgz", + "integrity": "sha512-Y4X9zqrCftUhMeH2EptSSERdVKt/nEdijTOacGD/97EKjhQ/Qs8RTlEGABSJNNN8lac9kheH+af7yAkEWlgneA==", "dev": true, "requires": { "ajv": "^8.0.1", @@ -2491,46 +2440,6 @@ "slice-ansi": "^4.0.0", "string-width": "^4.2.3", "strip-ansi": "^6.0.1" - }, - "dependencies": { - "ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true - }, - "emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true - }, - "string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - } - }, - "strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "requires": { - "ansi-regex": "^5.0.1" - } - } } }, "test-exclude": { @@ -2572,9 +2481,9 @@ } }, "tiny-secp256k1": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/tiny-secp256k1/-/tiny-secp256k1-2.1.2.tgz", - "integrity": "sha512-8qPw7zDK6Hco2tVGYGQeOmOPp/hZnREwy2iIkcq0ygAuqc9WHo29vKN94lNymh1QbB3nthtAMF6KTIrdbsIotA==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tiny-secp256k1/-/tiny-secp256k1-2.2.0.tgz", + "integrity": "sha512-2hPuUGCroLrxh6xxwoe+1RgPpOOK1w2uTnhgiHBpvoutBR+krNuT4hOXQyOaaYnZgoXBB6hBYkuAJHxyeBOPzQ==", "dev": true, "requires": { "uint8array-tools": "0.0.6" @@ -2583,7 +2492,7 @@ "to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", "dev": true }, "to-regex-range": { @@ -2702,9 +2611,9 @@ } }, "uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", "dev": true }, "varuint-bitcoin": { @@ -2727,7 +2636,7 @@ "which-module": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", - "integrity": "sha512-B+enWhmw6cjfVC7kS8Pj9pCrKSc5txArRyaYGe088shv/FGWH+0Rjx/xPgtsWfsUtS27FkP697E4DDhgrgoc0Q==", + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", "dev": true }, "wif": { @@ -2800,9 +2709,9 @@ } }, "y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", "dev": true }, "yargs": { @@ -2818,6 +2727,14 @@ "string-width": "^4.2.0", "y18n": "^5.0.5", "yargs-parser": "^20.2.2" + }, + "dependencies": { + "y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true + } } }, "yargs-parser": { @@ -2836,6 +2753,20 @@ "decamelize": "^4.0.0", "flat": "^5.0.2", "is-plain-obj": "^2.1.0" + }, + "dependencies": { + "camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true + }, + "decamelize": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", + "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", + "dev": true + } } }, "yn": { diff --git a/package.json b/package.json index dd644a20a..b2414f42f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "bitcoinjs-lib", - "version": "6.0.2", + "version": "6.1.0-rc.0", "description": "Client-side Bitcoin JavaScript library", "main": "./src/index.js", "types": "./src/index.d.ts", @@ -50,7 +50,7 @@ ], "dependencies": { "bech32": "^2.0.0", - "bip174": "^2.0.1", + "bip174": "^2.1.0", "bs58check": "^2.1.2", "create-hash": "^1.1.0", "ripemd160": "^2.0.2", @@ -73,7 +73,6 @@ "bip39": "^3.0.2", "bip65": "^1.0.1", "bip68": "^1.0.3", - "bn.js": "^4.11.8", "bs58": "^4.0.0", "dhttp": "^3.0.0", "ecpair": "^2.0.1", @@ -86,7 +85,7 @@ "randombytes": "^2.1.0", "regtest-client": "0.2.0", "rimraf": "^2.6.3", - "tiny-secp256k1": "^2.1.2", + "tiny-secp256k1": "^2.2.0", "ts-node": "^8.3.0", "tslint": "^6.1.3", "typescript": "^4.4.4" diff --git a/src/address.js b/src/address.js index 164bf7ef1..de0154a3a 100644 --- a/src/address.js +++ b/src/address.js @@ -4,14 +4,13 @@ exports.toOutputScript = exports.fromOutputScript = exports.toBech32 = exports.t const networks = require('./networks'); const payments = require('./payments'); const bscript = require('./script'); -const types = require('./types'); +const types_1 = require('./types'); const bech32_1 = require('bech32'); const bs58check = require('bs58check'); -const { typeforce } = types; const FUTURE_SEGWIT_MAX_SIZE = 40; const FUTURE_SEGWIT_MIN_SIZE = 2; const FUTURE_SEGWIT_MAX_VERSION = 16; -const FUTURE_SEGWIT_MIN_VERSION = 1; +const FUTURE_SEGWIT_MIN_VERSION = 2; const FUTURE_SEGWIT_VERSION_DIFF = 0x50; const FUTURE_SEGWIT_VERSION_WARNING = 'WARNING: Sending to a future segwit version address can lead to loss of funds. ' + @@ -69,7 +68,10 @@ function fromBech32(address) { } exports.fromBech32 = fromBech32; function toBase58Check(hash, version) { - typeforce(types.tuple(types.Hash160bit, types.UInt8), arguments); + (0, types_1.typeforce)( + (0, types_1.tuple)(types_1.Hash160bit, types_1.UInt8), + arguments, + ); const payload = Buffer.allocUnsafe(21); payload.writeUInt8(version, 0); hash.copy(payload, 1); @@ -99,6 +101,9 @@ function fromOutputScript(output, network) { try { return payments.p2wsh({ output, network }).address; } catch (e) {} + try { + return payments.p2tr({ output, network }).address; + } catch (e) {} try { return _toFutureSegwitAddress(output, network); } catch (e) {} @@ -129,6 +134,9 @@ function toOutputScript(address, network) { return payments.p2wpkh({ hash: decodeBech32.data }).output; if (decodeBech32.data.length === 32) return payments.p2wsh({ hash: decodeBech32.data }).output; + } else if (decodeBech32.version === 1) { + if (decodeBech32.data.length === 32) + return payments.p2tr({ pubkey: decodeBech32.data }).output; } else if ( decodeBech32.version >= FUTURE_SEGWIT_MIN_VERSION && decodeBech32.version <= FUTURE_SEGWIT_MAX_VERSION && diff --git a/src/ecc_lib.d.ts b/src/ecc_lib.d.ts new file mode 100644 index 000000000..201ebb5cf --- /dev/null +++ b/src/ecc_lib.d.ts @@ -0,0 +1,3 @@ +import { TinySecp256k1Interface } from './types'; +export declare function initEccLib(eccLib: TinySecp256k1Interface | undefined): void; +export declare function getEccLib(): TinySecp256k1Interface; diff --git a/src/ecc_lib.js b/src/ecc_lib.js new file mode 100644 index 000000000..eaa8a5327 --- /dev/null +++ b/src/ecc_lib.js @@ -0,0 +1,91 @@ +'use strict'; +Object.defineProperty(exports, '__esModule', { value: true }); +exports.getEccLib = exports.initEccLib = void 0; +const _ECCLIB_CACHE = {}; +function initEccLib(eccLib) { + if (!eccLib) { + // allow clearing the library + _ECCLIB_CACHE.eccLib = eccLib; + } else if (eccLib !== _ECCLIB_CACHE.eccLib) { + // new instance, verify it + verifyEcc(eccLib); + _ECCLIB_CACHE.eccLib = eccLib; + } +} +exports.initEccLib = initEccLib; +function getEccLib() { + if (!_ECCLIB_CACHE.eccLib) + throw new Error( + 'No ECC Library provided. You must call initEccLib() with a valid TinySecp256k1Interface instance', + ); + return _ECCLIB_CACHE.eccLib; +} +exports.getEccLib = getEccLib; +const h = hex => Buffer.from(hex, 'hex'); +function verifyEcc(ecc) { + assert(typeof ecc.isXOnlyPoint === 'function'); + assert( + ecc.isXOnlyPoint( + h('79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798'), + ), + ); + assert( + ecc.isXOnlyPoint( + h('fffffffffffffffffffffffffffffffffffffffffffffffffffffffeeffffc2e'), + ), + ); + assert( + ecc.isXOnlyPoint( + h('f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9'), + ), + ); + assert( + ecc.isXOnlyPoint( + h('0000000000000000000000000000000000000000000000000000000000000001'), + ), + ); + assert( + !ecc.isXOnlyPoint( + h('0000000000000000000000000000000000000000000000000000000000000000'), + ), + ); + assert( + !ecc.isXOnlyPoint( + h('fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f'), + ), + ); + assert(typeof ecc.xOnlyPointAddTweak === 'function'); + tweakAddVectors.forEach(t => { + const r = ecc.xOnlyPointAddTweak(h(t.pubkey), h(t.tweak)); + if (t.result === null) { + assert(r === null); + } else { + assert(r !== null); + assert(r.parity === t.parity); + assert(Buffer.from(r.xOnlyPubkey).equals(h(t.result))); + } + }); +} +function assert(bool) { + if (!bool) throw new Error('ecc library invalid'); +} +const tweakAddVectors = [ + { + pubkey: '79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798', + tweak: 'fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364140', + parity: -1, + result: null, + }, + { + pubkey: '1617d38ed8d8657da4d4761e8057bc396ea9e4b9d29776d4be096016dbd2509b', + tweak: 'a8397a935f0dfceba6ba9618f6451ef4d80637abf4e6af2669fbc9de6a8fd2ac', + parity: 1, + result: 'e478f99dab91052ab39a33ea35fd5e6e4933f4d28023cd597c9a1f6760346adf', + }, + { + pubkey: '2c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e668680991', + tweak: '823c3cd2142744b075a87eade7e1b8678ba308d566226a0056ca2b7a76f86b47', + parity: 0, + result: '9534f8dc8c6deda2dc007655981c78b49c5d96c778fbf363462a11ec9dfd948c', + }, +]; diff --git a/src/index.d.ts b/src/index.d.ts index b93c2aa40..420979ffe 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -12,3 +12,4 @@ export { Transaction } from './transaction'; export { Network } from './networks'; export { Payment, PaymentCreator, PaymentOpts, Stack, StackElement, } from './payments'; export { Input as TxInput, Output as TxOutput } from './transaction'; +export { initEccLib } from './ecc_lib'; diff --git a/src/index.js b/src/index.js index 983b0cc76..25d0b5a22 100644 --- a/src/index.js +++ b/src/index.js @@ -1,6 +1,6 @@ 'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); -exports.Transaction = exports.opcodes = exports.Psbt = exports.Block = exports.script = exports.payments = exports.networks = exports.crypto = exports.address = void 0; +exports.initEccLib = exports.Transaction = exports.opcodes = exports.Psbt = exports.Block = exports.script = exports.payments = exports.networks = exports.crypto = exports.address = void 0; const address = require('./address'); exports.address = address; const crypto = require('./crypto'); @@ -39,3 +39,10 @@ Object.defineProperty(exports, 'Transaction', { return transaction_1.Transaction; }, }); +var ecc_lib_1 = require('./ecc_lib'); +Object.defineProperty(exports, 'initEccLib', { + enumerable: true, + get: function() { + return ecc_lib_1.initEccLib; + }, +}); diff --git a/src/ops.js b/src/ops.js index 9d629cd00..7853ad0f0 100644 --- a/src/ops.js +++ b/src/ops.js @@ -117,6 +117,7 @@ const OPS = { OP_NOP8: 183, OP_NOP9: 184, OP_NOP10: 185, + OP_CHECKSIGADD: 186, OP_PUBKEYHASH: 253, OP_PUBKEY: 254, OP_INVALIDOPCODE: 255, diff --git a/src/payments/bip341.d.ts b/src/payments/bip341.d.ts new file mode 100644 index 000000000..8dd82409a --- /dev/null +++ b/src/payments/bip341.d.ts @@ -0,0 +1,42 @@ +/// +import { Tapleaf, Taptree } from '../types'; +export declare const LEAF_VERSION_TAPSCRIPT = 192; +export declare const MAX_TAPTREE_DEPTH = 128; +interface HashLeaf { + hash: Buffer; +} +interface HashBranch { + hash: Buffer; + left: HashTree; + right: HashTree; +} +interface TweakedPublicKey { + parity: number; + x: Buffer; +} +/** + * Binary tree representing leaf, branch, and root node hashes of a Taptree. + * Each node contains a hash, and potentially left and right branch hashes. + * This tree is used for 2 purposes: Providing the root hash for tweaking, + * and calculating merkle inclusion proofs when constructing a control block. + */ +export declare type HashTree = HashLeaf | HashBranch; +export declare function rootHashFromPath(controlBlock: Buffer, leafHash: Buffer): Buffer; +/** + * Build a hash tree of merkle nodes from the scripts binary tree. + * @param scriptTree - the tree of scripts to pairwise hash. + */ +export declare function toHashTree(scriptTree: Taptree): HashTree; +/** + * Given a HashTree, finds the path from a particular hash to the root. + * @param node - the root of the tree + * @param hash - the hash to search for + * @returns - array of sibling hashes, from leaf (inclusive) to root + * (exclusive) needed to prove inclusion of the specified hash. undefined if no + * path is found + */ +export declare function findScriptPath(node: HashTree, hash: Buffer): Buffer[] | undefined; +export declare function tapleafHash(leaf: Tapleaf): Buffer; +export declare function tapTweakHash(pubKey: Buffer, h: Buffer | undefined): Buffer; +export declare function tweakKey(pubKey: Buffer, h: Buffer | undefined): TweakedPublicKey | null; +export {}; diff --git a/src/payments/bip341.js b/src/payments/bip341.js new file mode 100644 index 000000000..02c976b5f --- /dev/null +++ b/src/payments/bip341.js @@ -0,0 +1,108 @@ +'use strict'; +Object.defineProperty(exports, '__esModule', { value: true }); +exports.tweakKey = exports.tapTweakHash = exports.tapleafHash = exports.findScriptPath = exports.toHashTree = exports.rootHashFromPath = exports.MAX_TAPTREE_DEPTH = exports.LEAF_VERSION_TAPSCRIPT = void 0; +const buffer_1 = require('buffer'); +const ecc_lib_1 = require('../ecc_lib'); +const bcrypto = require('../crypto'); +const bufferutils_1 = require('../bufferutils'); +const types_1 = require('../types'); +exports.LEAF_VERSION_TAPSCRIPT = 0xc0; +exports.MAX_TAPTREE_DEPTH = 128; +const isHashBranch = ht => 'left' in ht && 'right' in ht; +function rootHashFromPath(controlBlock, leafHash) { + if (controlBlock.length < 33) + throw new TypeError( + `The control-block length is too small. Got ${ + controlBlock.length + }, expected min 33.`, + ); + const m = (controlBlock.length - 33) / 32; + let kj = leafHash; + for (let j = 0; j < m; j++) { + const ej = controlBlock.slice(33 + 32 * j, 65 + 32 * j); + if (kj.compare(ej) < 0) { + kj = tapBranchHash(kj, ej); + } else { + kj = tapBranchHash(ej, kj); + } + } + return kj; +} +exports.rootHashFromPath = rootHashFromPath; +/** + * Build a hash tree of merkle nodes from the scripts binary tree. + * @param scriptTree - the tree of scripts to pairwise hash. + */ +function toHashTree(scriptTree) { + if ((0, types_1.isTapleaf)(scriptTree)) + return { hash: tapleafHash(scriptTree) }; + const hashes = [toHashTree(scriptTree[0]), toHashTree(scriptTree[1])]; + hashes.sort((a, b) => a.hash.compare(b.hash)); + const [left, right] = hashes; + return { + hash: tapBranchHash(left.hash, right.hash), + left, + right, + }; +} +exports.toHashTree = toHashTree; +/** + * Given a HashTree, finds the path from a particular hash to the root. + * @param node - the root of the tree + * @param hash - the hash to search for + * @returns - array of sibling hashes, from leaf (inclusive) to root + * (exclusive) needed to prove inclusion of the specified hash. undefined if no + * path is found + */ +function findScriptPath(node, hash) { + if (isHashBranch(node)) { + const leftPath = findScriptPath(node.left, hash); + if (leftPath !== undefined) return [...leftPath, node.right.hash]; + const rightPath = findScriptPath(node.right, hash); + if (rightPath !== undefined) return [...rightPath, node.left.hash]; + } else if (node.hash.equals(hash)) { + return []; + } + return undefined; +} +exports.findScriptPath = findScriptPath; +function tapleafHash(leaf) { + const version = leaf.version || exports.LEAF_VERSION_TAPSCRIPT; + return bcrypto.taggedHash( + 'TapLeaf', + buffer_1.Buffer.concat([ + buffer_1.Buffer.from([version]), + serializeScript(leaf.output), + ]), + ); +} +exports.tapleafHash = tapleafHash; +function tapTweakHash(pubKey, h) { + return bcrypto.taggedHash( + 'TapTweak', + buffer_1.Buffer.concat(h ? [pubKey, h] : [pubKey]), + ); +} +exports.tapTweakHash = tapTweakHash; +function tweakKey(pubKey, h) { + if (!buffer_1.Buffer.isBuffer(pubKey)) return null; + if (pubKey.length !== 32) return null; + if (h && h.length !== 32) return null; + const tweakHash = tapTweakHash(pubKey, h); + const res = (0, ecc_lib_1.getEccLib)().xOnlyPointAddTweak(pubKey, tweakHash); + if (!res || res.xOnlyPubkey === null) return null; + return { + parity: res.parity, + x: buffer_1.Buffer.from(res.xOnlyPubkey), + }; +} +exports.tweakKey = tweakKey; +function tapBranchHash(a, b) { + return bcrypto.taggedHash('TapBranch', buffer_1.Buffer.concat([a, b])); +} +function serializeScript(s) { + const varintLen = bufferutils_1.varuint.encodingLength(s.length); + const buffer = buffer_1.Buffer.allocUnsafe(varintLen); // better + bufferutils_1.varuint.encode(s.length, buffer); + return buffer_1.Buffer.concat([buffer, s]); +} diff --git a/src/payments/index.d.ts b/src/payments/index.d.ts index 1edf07167..07c12cc48 100644 --- a/src/payments/index.d.ts +++ b/src/payments/index.d.ts @@ -1,5 +1,6 @@ /// import { Network } from '../networks'; +import { Taptree } from '../types'; import { p2data as embed } from './embed'; import { p2ms } from './p2ms'; import { p2pk } from './p2pk'; @@ -7,6 +8,7 @@ import { p2pkh } from './p2pkh'; import { p2sh } from './p2sh'; import { p2wpkh } from './p2wpkh'; import { p2wsh } from './p2wsh'; +import { p2tr } from './p2tr'; export interface Payment { name?: string; network?: Network; @@ -17,11 +19,14 @@ export interface Payment { pubkeys?: Buffer[]; input?: Buffer; signatures?: Buffer[]; + internalPubkey?: Buffer; pubkey?: Buffer; signature?: Buffer; address?: string; hash?: Buffer; redeem?: Payment; + redeemVersion?: number; + scriptTree?: Taptree; witness?: Buffer[]; } export declare type PaymentCreator = (a: Payment, opts?: PaymentOpts) => Payment; @@ -33,4 +38,4 @@ export interface PaymentOpts { export declare type StackElement = Buffer | number; export declare type Stack = StackElement[]; export declare type StackFunction = () => Stack; -export { embed, p2ms, p2pk, p2pkh, p2sh, p2wpkh, p2wsh }; +export { embed, p2ms, p2pk, p2pkh, p2sh, p2wpkh, p2wsh, p2tr }; diff --git a/src/payments/index.js b/src/payments/index.js index c23c529c6..9ce55f859 100644 --- a/src/payments/index.js +++ b/src/payments/index.js @@ -1,6 +1,6 @@ 'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); -exports.p2wsh = exports.p2wpkh = exports.p2sh = exports.p2pkh = exports.p2pk = exports.p2ms = exports.embed = void 0; +exports.p2tr = exports.p2wsh = exports.p2wpkh = exports.p2sh = exports.p2pkh = exports.p2pk = exports.p2ms = exports.embed = void 0; const embed_1 = require('./embed'); Object.defineProperty(exports, 'embed', { enumerable: true, @@ -50,5 +50,12 @@ Object.defineProperty(exports, 'p2wsh', { return p2wsh_1.p2wsh; }, }); +const p2tr_1 = require('./p2tr'); +Object.defineProperty(exports, 'p2tr', { + enumerable: true, + get: function() { + return p2tr_1.p2tr; + }, +}); // TODO // witness commitment diff --git a/src/payments/p2tr.d.ts b/src/payments/p2tr.d.ts new file mode 100644 index 000000000..350ed0ffc --- /dev/null +++ b/src/payments/p2tr.d.ts @@ -0,0 +1,2 @@ +import { Payment, PaymentOpts } from './index'; +export declare function p2tr(a: Payment, opts?: PaymentOpts): Payment; diff --git a/src/payments/p2tr.js b/src/payments/p2tr.js new file mode 100644 index 000000000..182aa62c6 --- /dev/null +++ b/src/payments/p2tr.js @@ -0,0 +1,294 @@ +'use strict'; +Object.defineProperty(exports, '__esModule', { value: true }); +exports.p2tr = void 0; +const buffer_1 = require('buffer'); +const networks_1 = require('../networks'); +const bscript = require('../script'); +const types_1 = require('../types'); +const ecc_lib_1 = require('../ecc_lib'); +const bip341_1 = require('./bip341'); +const lazy = require('./lazy'); +const bech32_1 = require('bech32'); +const OPS = bscript.OPS; +const TAPROOT_WITNESS_VERSION = 0x01; +const ANNEX_PREFIX = 0x50; +function p2tr(a, opts) { + if ( + !a.address && + !a.output && + !a.pubkey && + !a.internalPubkey && + !(a.witness && a.witness.length > 1) + ) + throw new TypeError('Not enough data'); + opts = Object.assign({ validate: true }, opts || {}); + (0, types_1.typeforce)( + { + address: types_1.typeforce.maybe(types_1.typeforce.String), + input: types_1.typeforce.maybe(types_1.typeforce.BufferN(0)), + network: types_1.typeforce.maybe(types_1.typeforce.Object), + output: types_1.typeforce.maybe(types_1.typeforce.BufferN(34)), + internalPubkey: types_1.typeforce.maybe(types_1.typeforce.BufferN(32)), + hash: types_1.typeforce.maybe(types_1.typeforce.BufferN(32)), + pubkey: types_1.typeforce.maybe(types_1.typeforce.BufferN(32)), + signature: types_1.typeforce.maybe(types_1.typeforce.BufferN(64)), + witness: types_1.typeforce.maybe( + types_1.typeforce.arrayOf(types_1.typeforce.Buffer), + ), + scriptTree: types_1.typeforce.maybe(types_1.isTaptree), + redeem: types_1.typeforce.maybe({ + output: types_1.typeforce.maybe(types_1.typeforce.Buffer), + redeemVersion: types_1.typeforce.maybe(types_1.typeforce.Number), + witness: types_1.typeforce.maybe( + types_1.typeforce.arrayOf(types_1.typeforce.Buffer), + ), + }), + redeemVersion: types_1.typeforce.maybe(types_1.typeforce.Number), + }, + a, + ); + const _address = lazy.value(() => { + const result = bech32_1.bech32m.decode(a.address); + const version = result.words.shift(); + const data = bech32_1.bech32m.fromWords(result.words); + return { + version, + prefix: result.prefix, + data: buffer_1.Buffer.from(data), + }; + }); + // remove annex if present, ignored by taproot + const _witness = lazy.value(() => { + if (!a.witness || !a.witness.length) return; + if ( + a.witness.length >= 2 && + a.witness[a.witness.length - 1][0] === ANNEX_PREFIX + ) { + return a.witness.slice(0, -1); + } + return a.witness.slice(); + }); + const _hashTree = lazy.value(() => { + if (a.scriptTree) return (0, bip341_1.toHashTree)(a.scriptTree); + if (a.hash) return { hash: a.hash }; + return; + }); + const network = a.network || networks_1.bitcoin; + const o = { name: 'p2tr', network }; + lazy.prop(o, 'address', () => { + if (!o.pubkey) return; + const words = bech32_1.bech32m.toWords(o.pubkey); + words.unshift(TAPROOT_WITNESS_VERSION); + return bech32_1.bech32m.encode(network.bech32, words); + }); + lazy.prop(o, 'hash', () => { + const hashTree = _hashTree(); + if (hashTree) return hashTree.hash; + const w = _witness(); + if (w && w.length > 1) { + const controlBlock = w[w.length - 1]; + const leafVersion = controlBlock[0] & types_1.TAPLEAF_VERSION_MASK; + const script = w[w.length - 2]; + const leafHash = (0, bip341_1.tapleafHash)({ + output: script, + version: leafVersion, + }); + return (0, bip341_1.rootHashFromPath)(controlBlock, leafHash); + } + return null; + }); + lazy.prop(o, 'output', () => { + if (!o.pubkey) return; + return bscript.compile([OPS.OP_1, o.pubkey]); + }); + lazy.prop(o, 'redeemVersion', () => { + if (a.redeemVersion) return a.redeemVersion; + if ( + a.redeem && + a.redeem.redeemVersion !== undefined && + a.redeem.redeemVersion !== null + ) { + return a.redeem.redeemVersion; + } + return bip341_1.LEAF_VERSION_TAPSCRIPT; + }); + lazy.prop(o, 'redeem', () => { + const witness = _witness(); // witness without annex + if (!witness || witness.length < 2) return; + return { + output: witness[witness.length - 2], + witness: witness.slice(0, -2), + redeemVersion: + witness[witness.length - 1][0] & types_1.TAPLEAF_VERSION_MASK, + }; + }); + lazy.prop(o, 'pubkey', () => { + if (a.pubkey) return a.pubkey; + if (a.output) return a.output.slice(2); + if (a.address) return _address().data; + if (o.internalPubkey) { + const tweakedKey = (0, bip341_1.tweakKey)(o.internalPubkey, o.hash); + if (tweakedKey) return tweakedKey.x; + } + }); + lazy.prop(o, 'internalPubkey', () => { + if (a.internalPubkey) return a.internalPubkey; + const witness = _witness(); + if (witness && witness.length > 1) + return witness[witness.length - 1].slice(1, 33); + }); + lazy.prop(o, 'signature', () => { + if (a.signature) return a.signature; + const witness = _witness(); // witness without annex + if (!witness || witness.length !== 1) return; + return witness[0]; + }); + lazy.prop(o, 'witness', () => { + if (a.witness) return a.witness; + const hashTree = _hashTree(); + if (hashTree && a.redeem && a.redeem.output && a.internalPubkey) { + const leafHash = (0, bip341_1.tapleafHash)({ + output: a.redeem.output, + version: o.redeemVersion, + }); + const path = (0, bip341_1.findScriptPath)(hashTree, leafHash); + if (!path) return; + const outputKey = (0, bip341_1.tweakKey)(a.internalPubkey, hashTree.hash); + if (!outputKey) return; + const controlBock = buffer_1.Buffer.concat( + [ + buffer_1.Buffer.from([o.redeemVersion | outputKey.parity]), + a.internalPubkey, + ].concat(path), + ); + return [a.redeem.output, controlBock]; + } + if (a.signature) return [a.signature]; + }); + // extended validation + if (opts.validate) { + let pubkey = buffer_1.Buffer.from([]); + if (a.address) { + if (network && network.bech32 !== _address().prefix) + throw new TypeError('Invalid prefix or Network mismatch'); + if (_address().version !== TAPROOT_WITNESS_VERSION) + throw new TypeError('Invalid address version'); + if (_address().data.length !== 32) + throw new TypeError('Invalid address data'); + pubkey = _address().data; + } + if (a.pubkey) { + if (pubkey.length > 0 && !pubkey.equals(a.pubkey)) + throw new TypeError('Pubkey mismatch'); + else pubkey = a.pubkey; + } + if (a.output) { + if ( + a.output.length !== 34 || + a.output[0] !== OPS.OP_1 || + a.output[1] !== 0x20 + ) + throw new TypeError('Output is invalid'); + if (pubkey.length > 0 && !pubkey.equals(a.output.slice(2))) + throw new TypeError('Pubkey mismatch'); + else pubkey = a.output.slice(2); + } + if (a.internalPubkey) { + const tweakedKey = (0, bip341_1.tweakKey)(a.internalPubkey, o.hash); + if (pubkey.length > 0 && !pubkey.equals(tweakedKey.x)) + throw new TypeError('Pubkey mismatch'); + else pubkey = tweakedKey.x; + } + if (pubkey && pubkey.length) { + if (!(0, ecc_lib_1.getEccLib)().isXOnlyPoint(pubkey)) + throw new TypeError('Invalid pubkey for p2tr'); + } + const hashTree = _hashTree(); + if (a.hash && hashTree) { + if (!a.hash.equals(hashTree.hash)) throw new TypeError('Hash mismatch'); + } + if (a.redeem && a.redeem.output && hashTree) { + const leafHash = (0, bip341_1.tapleafHash)({ + output: a.redeem.output, + version: o.redeemVersion, + }); + if (!(0, bip341_1.findScriptPath)(hashTree, leafHash)) + throw new TypeError('Redeem script not in tree'); + } + const witness = _witness(); + // compare the provided redeem data with the one computed from witness + if (a.redeem && o.redeem) { + if (a.redeem.redeemVersion) { + if (a.redeem.redeemVersion !== o.redeem.redeemVersion) + throw new TypeError('Redeem.redeemVersion and witness mismatch'); + } + if (a.redeem.output) { + if (bscript.decompile(a.redeem.output).length === 0) + throw new TypeError('Redeem.output is invalid'); + // output redeem is constructed from the witness + if (o.redeem.output && !a.redeem.output.equals(o.redeem.output)) + throw new TypeError('Redeem.output and witness mismatch'); + } + if (a.redeem.witness) { + if ( + o.redeem.witness && + !stacksEqual(a.redeem.witness, o.redeem.witness) + ) + throw new TypeError('Redeem.witness and witness mismatch'); + } + } + if (witness && witness.length) { + if (witness.length === 1) { + // key spending + if (a.signature && !a.signature.equals(witness[0])) + throw new TypeError('Signature mismatch'); + } else { + // script path spending + const controlBlock = witness[witness.length - 1]; + if (controlBlock.length < 33) + throw new TypeError( + `The control-block length is too small. Got ${ + controlBlock.length + }, expected min 33.`, + ); + if ((controlBlock.length - 33) % 32 !== 0) + throw new TypeError( + `The control-block length of ${controlBlock.length} is incorrect!`, + ); + const m = (controlBlock.length - 33) / 32; + if (m > 128) + throw new TypeError( + `The script path is too long. Got ${m}, expected max 128.`, + ); + const internalPubkey = controlBlock.slice(1, 33); + if (a.internalPubkey && !a.internalPubkey.equals(internalPubkey)) + throw new TypeError('Internal pubkey mismatch'); + if (!(0, ecc_lib_1.getEccLib)().isXOnlyPoint(internalPubkey)) + throw new TypeError('Invalid internalPubkey for p2tr witness'); + const leafVersion = controlBlock[0] & types_1.TAPLEAF_VERSION_MASK; + const script = witness[witness.length - 2]; + const leafHash = (0, bip341_1.tapleafHash)({ + output: script, + version: leafVersion, + }); + const hash = (0, bip341_1.rootHashFromPath)(controlBlock, leafHash); + const outputKey = (0, bip341_1.tweakKey)(internalPubkey, hash); + if (!outputKey) + // todo: needs test data + throw new TypeError('Invalid outputKey for p2tr witness'); + if (pubkey.length && !pubkey.equals(outputKey.x)) + throw new TypeError('Pubkey mismatch for p2tr witness'); + if (outputKey.parity !== (controlBlock[0] & 1)) + throw new Error('Incorrect parity'); + } + } + } + return Object.assign(o, a); +} +exports.p2tr = p2tr; +function stacksEqual(a, b) { + if (a.length !== b.length) return false; + return a.every((x, i) => { + return x.equals(b[i]); + }); +} diff --git a/src/psbt.d.ts b/src/psbt.d.ts index 8603a6955..4c622b31f 100644 --- a/src/psbt.d.ts +++ b/src/psbt.d.ts @@ -80,7 +80,10 @@ export declare class Psbt { getFeeRate(): number; getFee(): number; finalizeAllInputs(): this; - finalizeInput(inputIndex: number, finalScriptsFunc?: FinalScriptsFunc): this; + finalizeInput(inputIndex: number, finalScriptsFunc?: FinalScriptsFunc | FinalTaprootScriptsFunc): this; + finalizeTaprootInput(inputIndex: number, tapLeafHashToFinalize?: Buffer, finalScriptsFunc?: FinalTaprootScriptsFunc): this; + private _finalizeInput; + private _finalizeTaprootInput; getInputType(inputIndex: number): AllScriptType; inputHasPubkey(inputIndex: number, pubkey: Buffer): boolean; inputHasHDKey(inputIndex: number, root: HDSigner): boolean; @@ -88,6 +91,8 @@ export declare class Psbt { outputHasHDKey(outputIndex: number, root: HDSigner): boolean; validateSignaturesOfAllInputs(validator: ValidateSigFunction): boolean; validateSignaturesOfInput(inputIndex: number, validator: ValidateSigFunction, pubkey?: Buffer): boolean; + private _validateSignaturesOfInput; + private validateSignaturesOfTaprootInput; signAllInputsHD(hdKeyPair: HDSigner, sighashTypes?: number[]): this; signAllInputsHDAsync(hdKeyPair: HDSigner | HDSignerAsync, sighashTypes?: number[]): Promise; signInputHD(inputIndex: number, hdKeyPair: HDSigner, sighashTypes?: number[]): this; @@ -95,7 +100,14 @@ export declare class Psbt { signAllInputs(keyPair: Signer, sighashTypes?: number[]): this; signAllInputsAsync(keyPair: Signer | SignerAsync, sighashTypes?: number[]): Promise; signInput(inputIndex: number, keyPair: Signer, sighashTypes?: number[]): this; + signTaprootInput(inputIndex: number, keyPair: Signer, tapLeafHashToSign?: Buffer, sighashTypes?: number[]): this; + private _signInput; + private _signTaprootInput; signInputAsync(inputIndex: number, keyPair: Signer | SignerAsync, sighashTypes?: number[]): Promise; + signTaprootInputAsync(inputIndex: number, keyPair: Signer | SignerAsync, tapLeafHash?: Buffer, sighashTypes?: number[]): Promise; + private _signInputAsync; + private _signTaprootInputAsync; + private checkTaprootHashesForSig; toBuffer(): Buffer; toHex(): string; toBase64(): string; @@ -155,12 +167,14 @@ export interface Signer { publicKey: Buffer; network?: any; sign(hash: Buffer, lowR?: boolean): Buffer; + signSchnorr?(hash: Buffer): Buffer; getPublicKey?(): Buffer; } export interface SignerAsync { publicKey: Buffer; network?: any; sign(hash: Buffer, lowR?: boolean): Promise; + signSchnorr?(hash: Buffer): Promise; getPublicKey?(): Buffer; } /** @@ -178,5 +192,10 @@ isP2WSH: boolean) => { finalScriptSig: Buffer | undefined; finalScriptWitness: Buffer | undefined; }; +declare type FinalTaprootScriptsFunc = (inputIndex: number, // Which input is it? +input: PsbtInput, // The PSBT input contents +tapLeafHashToFinalize?: Buffer) => { + finalScriptWitness: Buffer | undefined; +}; declare type AllScriptType = 'witnesspubkeyhash' | 'pubkeyhash' | 'multisig' | 'pubkey' | 'nonstandard' | 'p2sh-witnesspubkeyhash' | 'p2sh-pubkeyhash' | 'p2sh-multisig' | 'p2sh-pubkey' | 'p2sh-nonstandard' | 'p2wsh-pubkeyhash' | 'p2wsh-multisig' | 'p2wsh-pubkey' | 'p2wsh-nonstandard' | 'p2sh-p2wsh-pubkeyhash' | 'p2sh-p2wsh-multisig' | 'p2sh-p2wsh-pubkey' | 'p2sh-p2wsh-nonstandard'; export {}; diff --git a/src/psbt.js b/src/psbt.js index 616219580..2f2e94a96 100644 --- a/src/psbt.js +++ b/src/psbt.js @@ -6,11 +6,13 @@ const varuint = require('bip174/src/lib/converter/varint'); const utils_1 = require('bip174/src/lib/utils'); const address_1 = require('./address'); const bufferutils_1 = require('./bufferutils'); -const crypto_1 = require('./crypto'); const networks_1 = require('./networks'); const payments = require('./payments'); +const bip341_1 = require('./payments/bip341'); const bscript = require('./script'); const transaction_1 = require('./transaction'); +const bip371_1 = require('./psbt/bip371'); +const psbtutils_1 = require('./psbt/psbtutils'); /** * These are the default arguments for a Psbt instance. */ @@ -198,6 +200,7 @@ class Psbt { `Requires single object with at least [hash] and [index]`, ); } + (0, bip371_1.checkTaprootInputFields)(inputData, inputData, 'addInput'); checkInputsForPartialSig(this.data.inputs, 'addInput'); if (inputData.witnessScript) checkInvalidP2WSH(inputData.witnessScript); const c = this.__CACHE; @@ -237,6 +240,7 @@ class Psbt { const script = (0, address_1.toOutputScript)(address, network); outputData = Object.assign(outputData, { script }); } + (0, bip371_1.checkTaprootOutputFields)(outputData, outputData, 'addOutput'); const c = this.__CACHE; this.data.addOutput(outputData); c.__FEE = undefined; @@ -271,8 +275,33 @@ class Psbt { range(this.data.inputs.length).forEach(idx => this.finalizeInput(idx)); return this; } - finalizeInput(inputIndex, finalScriptsFunc = getFinalScripts) { + finalizeInput(inputIndex, finalScriptsFunc) { const input = (0, utils_1.checkForInput)(this.data.inputs, inputIndex); + if ((0, bip371_1.isTaprootInput)(input)) + return this._finalizeTaprootInput( + inputIndex, + input, + undefined, + finalScriptsFunc, + ); + return this._finalizeInput(inputIndex, input, finalScriptsFunc); + } + finalizeTaprootInput( + inputIndex, + tapLeafHashToFinalize, + finalScriptsFunc = bip371_1.tapScriptFinalizer, + ) { + const input = (0, utils_1.checkForInput)(this.data.inputs, inputIndex); + if ((0, bip371_1.isTaprootInput)(input)) + return this._finalizeTaprootInput( + inputIndex, + input, + tapLeafHashToFinalize, + finalScriptsFunc, + ); + throw new Error(`Cannot finalize input #${inputIndex}. Not Taproot.`); + } + _finalizeInput(inputIndex, input, finalScriptsFunc = getFinalScripts) { const { script, isP2SH, isP2WSH, isSegwit } = getScriptFromInput( inputIndex, input, @@ -296,6 +325,37 @@ class Psbt { this.data.clearFinalizedInput(inputIndex); return this; } + _finalizeTaprootInput( + inputIndex, + input, + tapLeafHashToFinalize, + finalScriptsFunc = bip371_1.tapScriptFinalizer, + ) { + if (!input.witnessUtxo) + throw new Error( + `Cannot finalize input #${inputIndex}. Missing withness utxo.`, + ); + // Check key spend first. Increased privacy and reduced block space. + if (input.tapKeySig) { + const payment = payments.p2tr({ + output: input.witnessUtxo.script, + signature: input.tapKeySig, + }); + const finalScriptWitness = (0, psbtutils_1.witnessStackToScriptWitness)( + payment.witness, + ); + this.data.updateInput(inputIndex, { finalScriptWitness }); + } else { + const { finalScriptWitness } = finalScriptsFunc( + inputIndex, + input, + tapLeafHashToFinalize, + ); + this.data.updateInput(inputIndex, { finalScriptWitness }); + } + this.data.clearFinalizedInput(inputIndex); + return this; + } getInputType(inputIndex) { const input = (0, utils_1.checkForInput)(this.data.inputs, inputIndex); const script = getScriptFromUtxo(inputIndex, input, this.__CACHE); @@ -341,6 +401,16 @@ class Psbt { return results.reduce((final, res) => res === true && final, true); } validateSignaturesOfInput(inputIndex, validator, pubkey) { + const input = this.data.inputs[inputIndex]; + if ((0, bip371_1.isTaprootInput)(input)) + return this.validateSignaturesOfTaprootInput( + inputIndex, + validator, + pubkey, + ); + return this._validateSignaturesOfInput(inputIndex, validator, pubkey); + } + _validateSignaturesOfInput(inputIndex, validator, pubkey) { const input = this.data.inputs[inputIndex]; const partialSig = (input || {}).partialSig; if (!input || !partialSig || partialSig.length < 1) @@ -374,6 +444,54 @@ class Psbt { } return results.every(res => res === true); } + validateSignaturesOfTaprootInput(inputIndex, validator, pubkey) { + const input = this.data.inputs[inputIndex]; + const tapKeySig = (input || {}).tapKeySig; + const tapScriptSig = (input || {}).tapScriptSig; + if (!input && !tapKeySig && !(tapScriptSig && !tapScriptSig.length)) + throw new Error('No signatures to validate'); + if (typeof validator !== 'function') + throw new Error('Need validator function to validate signatures'); + pubkey = pubkey && (0, bip371_1.toXOnly)(pubkey); + const allHashses = pubkey + ? getTaprootHashesForSig( + inputIndex, + input, + this.data.inputs, + pubkey, + this.__CACHE, + ) + : getAllTaprootHashesForSig( + inputIndex, + input, + this.data.inputs, + this.__CACHE, + ); + if (!allHashses.length) throw new Error('No signatures for this pubkey'); + const tapKeyHash = allHashses.find(h => !!h.leafHash); + if (tapKeySig && tapKeyHash) { + const isValidTapkeySig = validator( + tapKeyHash.pubkey, + tapKeyHash.hash, + tapKeySig, + ); + if (!isValidTapkeySig) return false; + } + if (tapScriptSig) { + for (const tapSig of tapScriptSig) { + const tapSigHash = allHashses.find(h => tapSig.pubkey.equals(h.pubkey)); + if (tapSigHash) { + const isValidTapScriptSig = validator( + tapSig.pubkey, + tapSigHash.hash, + tapSig.signature, + ); + if (!isValidTapScriptSig) return false; + } + } + } + return true; + } signAllInputsHD( hdKeyPair, sighashTypes = [transaction_1.Transaction.SIGHASH_ALL], @@ -457,10 +575,7 @@ class Psbt { .catch(reject); }); } - signAllInputs( - keyPair, - sighashTypes = [transaction_1.Transaction.SIGHASH_ALL], - ) { + signAllInputs(keyPair, sighashTypes) { if (!keyPair || !keyPair.publicKey) throw new Error('Need Signer to sign input'); // TODO: Add a pubkey/pubkeyhash cache to each input @@ -480,10 +595,7 @@ class Psbt { } return this; } - signAllInputsAsync( - keyPair, - sighashTypes = [transaction_1.Transaction.SIGHASH_ALL], - ) { + signAllInputsAsync(keyPair, sighashTypes) { return new Promise((resolve, reject) => { if (!keyPair || !keyPair.publicKey) return reject(new Error('Need Signer to sign input')); @@ -512,13 +624,40 @@ class Psbt { }); }); } - signInput( + signInput(inputIndex, keyPair, sighashTypes) { + if (!keyPair || !keyPair.publicKey) + throw new Error('Need Signer to sign input'); + const input = (0, utils_1.checkForInput)(this.data.inputs, inputIndex); + if ((0, bip371_1.isTaprootInput)(input)) { + return this._signTaprootInput( + inputIndex, + input, + keyPair, + undefined, + sighashTypes, + ); + } + return this._signInput(inputIndex, keyPair, sighashTypes); + } + signTaprootInput(inputIndex, keyPair, tapLeafHashToSign, sighashTypes) { + if (!keyPair || !keyPair.publicKey) + throw new Error('Need Signer to sign input'); + const input = (0, utils_1.checkForInput)(this.data.inputs, inputIndex); + if ((0, bip371_1.isTaprootInput)(input)) + return this._signTaprootInput( + inputIndex, + input, + keyPair, + tapLeafHashToSign, + sighashTypes, + ); + throw new Error(`Input #${inputIndex} is not of type Taproot.`); + } + _signInput( inputIndex, keyPair, sighashTypes = [transaction_1.Transaction.SIGHASH_ALL], ) { - if (!keyPair || !keyPair.publicKey) - throw new Error('Need Signer to sign input'); const { hash, sighashType } = getHashAndSighashType( this.data.inputs, inputIndex, @@ -535,32 +674,182 @@ class Psbt { this.data.updateInput(inputIndex, { partialSig }); return this; } - signInputAsync( + _signTaprootInput( inputIndex, + input, keyPair, - sighashTypes = [transaction_1.Transaction.SIGHASH_ALL], + tapLeafHashToSign, + allowedSighashTypes = [transaction_1.Transaction.SIGHASH_DEFAULT], ) { + const hashesForSig = this.checkTaprootHashesForSig( + inputIndex, + input, + keyPair, + tapLeafHashToSign, + allowedSighashTypes, + ); + const tapKeySig = hashesForSig + .filter(h => !h.leafHash) + .map(h => + (0, bip371_1.serializeTaprootSignature)( + keyPair.signSchnorr(h.hash), + input.sighashType, + ), + )[0]; + const tapScriptSig = hashesForSig + .filter(h => !!h.leafHash) + .map(h => ({ + pubkey: (0, bip371_1.toXOnly)(keyPair.publicKey), + signature: (0, bip371_1.serializeTaprootSignature)( + keyPair.signSchnorr(h.hash), + input.sighashType, + ), + leafHash: h.leafHash, + })); + if (tapKeySig) { + this.data.updateInput(inputIndex, { tapKeySig }); + } + if (tapScriptSig.length) { + this.data.updateInput(inputIndex, { tapScriptSig }); + } + return this; + } + signInputAsync(inputIndex, keyPair, sighashTypes) { return Promise.resolve().then(() => { if (!keyPair || !keyPair.publicKey) throw new Error('Need Signer to sign input'); - const { hash, sighashType } = getHashAndSighashType( - this.data.inputs, - inputIndex, - keyPair.publicKey, - this.__CACHE, - sighashTypes, - ); - return Promise.resolve(keyPair.sign(hash)).then(signature => { - const partialSig = [ - { - pubkey: keyPair.publicKey, - signature: bscript.signature.encode(signature, sighashType), + const input = (0, utils_1.checkForInput)(this.data.inputs, inputIndex); + if ((0, bip371_1.isTaprootInput)(input)) + return this._signTaprootInputAsync( + inputIndex, + input, + keyPair, + undefined, + sighashTypes, + ); + return this._signInputAsync(inputIndex, keyPair, sighashTypes); + }); + } + signTaprootInputAsync(inputIndex, keyPair, tapLeafHash, sighashTypes) { + return Promise.resolve().then(() => { + if (!keyPair || !keyPair.publicKey) + throw new Error('Need Signer to sign input'); + const input = (0, utils_1.checkForInput)(this.data.inputs, inputIndex); + if ((0, bip371_1.isTaprootInput)(input)) + return this._signTaprootInputAsync( + inputIndex, + input, + keyPair, + tapLeafHash, + sighashTypes, + ); + throw new Error(`Input #${inputIndex} is not of type Taproot.`); + }); + } + _signInputAsync( + inputIndex, + keyPair, + sighashTypes = [transaction_1.Transaction.SIGHASH_ALL], + ) { + const { hash, sighashType } = getHashAndSighashType( + this.data.inputs, + inputIndex, + keyPair.publicKey, + this.__CACHE, + sighashTypes, + ); + return Promise.resolve(keyPair.sign(hash)).then(signature => { + const partialSig = [ + { + pubkey: keyPair.publicKey, + signature: bscript.signature.encode(signature, sighashType), + }, + ]; + this.data.updateInput(inputIndex, { partialSig }); + }); + } + async _signTaprootInputAsync( + inputIndex, + input, + keyPair, + tapLeafHash, + sighashTypes = [transaction_1.Transaction.SIGHASH_DEFAULT], + ) { + const hashesForSig = this.checkTaprootHashesForSig( + inputIndex, + input, + keyPair, + tapLeafHash, + sighashTypes, + ); + const signaturePromises = []; + const tapKeyHash = hashesForSig.filter(h => !h.leafHash)[0]; + if (tapKeyHash) { + const tapKeySigPromise = Promise.resolve( + keyPair.signSchnorr(tapKeyHash.hash), + ).then(sig => { + return { + tapKeySig: (0, bip371_1.serializeTaprootSignature)( + sig, + input.sighashType, + ), + }; + }); + signaturePromises.push(tapKeySigPromise); + } + const tapScriptHashes = hashesForSig.filter(h => !!h.leafHash); + if (tapScriptHashes.length) { + const tapScriptSigPromises = tapScriptHashes.map(tsh => { + return Promise.resolve(keyPair.signSchnorr(tsh.hash)).then( + signature => { + const tapScriptSig = [ + { + pubkey: (0, bip371_1.toXOnly)(keyPair.publicKey), + signature: (0, bip371_1.serializeTaprootSignature)( + signature, + input.sighashType, + ), + leafHash: tsh.leafHash, + }, + ]; + return { tapScriptSig }; }, - ]; - this.data.updateInput(inputIndex, { partialSig }); + ); }); + signaturePromises.push(...tapScriptSigPromises); + } + return Promise.all(signaturePromises).then(results => { + results.forEach(v => this.data.updateInput(inputIndex, v)); }); } + checkTaprootHashesForSig( + inputIndex, + input, + keyPair, + tapLeafHashToSign, + allowedSighashTypes, + ) { + if (typeof keyPair.signSchnorr !== 'function') + throw new Error( + `Need Schnorr Signer to sign taproot input #${inputIndex}.`, + ); + const hashesForSig = getTaprootHashesForSig( + inputIndex, + input, + this.data.inputs, + keyPair.publicKey, + this.__CACHE, + tapLeafHashToSign, + allowedSighashTypes, + ); + if (!hashesForSig || !hashesForSig.length) + throw new Error( + `Can not sign for input #${inputIndex} with the key ${keyPair.publicKey.toString( + 'hex', + )}`, + ); + return hashesForSig; + } toBuffer() { checkCache(this.__CACHE); return this.data.toBuffer(); @@ -579,6 +868,11 @@ class Psbt { } updateInput(inputIndex, updateData) { if (updateData.witnessScript) checkInvalidP2WSH(updateData.witnessScript); + (0, bip371_1.checkTaprootInputFields)( + this.data.inputs[inputIndex], + updateData, + 'updateInput', + ); this.data.updateInput(inputIndex, updateData); if (updateData.nonWitnessUtxo) { addNonWitnessTxCache( @@ -590,6 +884,12 @@ class Psbt { return this; } updateOutput(outputIndex, updateData) { + const outputData = this.data.outputs[outputIndex]; + (0, bip371_1.checkTaprootOutputFields)( + outputData, + updateData, + 'updateOutput', + ); this.data.updateOutput(outputIndex, updateData); return this; } @@ -703,22 +1003,6 @@ function hasSigs(neededSigs, partialSig, pubkeys) { function isFinalized(input) { return !!input.finalScriptSig || !!input.finalScriptWitness; } -function isPaymentFactory(payment) { - return script => { - try { - payment({ output: script }); - return true; - } catch (err) { - return false; - } - }; -} -const isP2MS = isPaymentFactory(payments.p2ms); -const isP2PK = isPaymentFactory(payments.p2pk); -const isP2PKH = isPaymentFactory(payments.p2pkh); -const isP2WPKH = isPaymentFactory(payments.p2wpkh); -const isP2WSHScript = isPaymentFactory(payments.p2wsh); -const isP2SHScript = isPaymentFactory(payments.p2sh); function bip32DerivationIsMine(root) { return d => { if (!d.masterFingerprint.equals(root.fingerprint)) return false; @@ -752,37 +1036,11 @@ function checkFees(psbt, cache, opts) { } function checkInputsForPartialSig(inputs, action) { inputs.forEach(input => { - let throws = false; - let pSigs = []; - if ((input.partialSig || []).length === 0) { - if (!input.finalScriptSig && !input.finalScriptWitness) return; - pSigs = getPsigsFromInputFinalScripts(input); - } else { - pSigs = input.partialSig; - } - pSigs.forEach(pSig => { - const { hashType } = bscript.signature.decode(pSig.signature); - const whitelist = []; - const isAnyoneCanPay = - hashType & transaction_1.Transaction.SIGHASH_ANYONECANPAY; - if (isAnyoneCanPay) whitelist.push('addInput'); - const hashMod = hashType & 0x1f; - switch (hashMod) { - case transaction_1.Transaction.SIGHASH_ALL: - break; - case transaction_1.Transaction.SIGHASH_SINGLE: - case transaction_1.Transaction.SIGHASH_NONE: - whitelist.push('addOutput'); - whitelist.push('setInputSequence'); - break; - } - if (whitelist.indexOf(action) === -1) { - throws = true; - } - }); - if (throws) { + const throws = (0, bip371_1.isTaprootInput)(input) + ? (0, bip371_1.checkTaprootInputForSigs)(input, action) + : (0, psbtutils_1.checkInputForSig)(input, action); + if (throws) throw new Error('Can not modify transaction, signatures exist.'); - } }); } function checkPartialSigSighashes(input) { @@ -796,7 +1054,7 @@ function checkPartialSigSighashes(input) { }); } function checkScriptForPubkey(pubkey, script, action) { - if (!pubkeyInScript(pubkey, script)) { + if (!(0, psbtutils_1.pubkeyInScript)(pubkey, script)) { throw new Error( `Can not ${action} for this input with the key ${pubkey.toString('hex')}`, ); @@ -890,9 +1148,13 @@ function prepareFinalScripts( const p2sh = !isP2SH ? null : payments.p2sh({ redeem: p2wsh || payment }); if (isSegwit) { if (p2wsh) { - finalScriptWitness = witnessStackToScriptWitness(p2wsh.witness); + finalScriptWitness = (0, psbtutils_1.witnessStackToScriptWitness)( + p2wsh.witness, + ); } else { - finalScriptWitness = witnessStackToScriptWitness(payment.witness); + finalScriptWitness = (0, psbtutils_1.witnessStackToScriptWitness)( + payment.witness, + ); } if (p2sh) { finalScriptSig = p2sh.input; @@ -934,13 +1196,7 @@ function getHashForSig(inputIndex, input, cache, forValidate, sighashTypes) { const unsignedTx = cache.__TX; const sighashType = input.sighashType || transaction_1.Transaction.SIGHASH_ALL; - if (sighashTypes && sighashTypes.indexOf(sighashType) < 0) { - const str = sighashTypeToString(sighashType); - throw new Error( - `Sighash type is not allowed. Retry the sign method passing the ` + - `sighashTypes array of whitelisted types. Sighash type: ${str}`, - ); - } + checkSighashTypeAllowed(sighashType, sighashTypes); let hash; let prevout; if (input.nonWitnessUtxo) { @@ -978,7 +1234,7 @@ function getHashForSig(inputIndex, input, cache, forValidate, sighashTypes) { prevout.value, sighashType, ); - } else if (isP2WPKH(meaningfulScript)) { + } else if ((0, psbtutils_1.isP2WPKH)(meaningfulScript)) { // P2WPKH uses the P2PKH template for prevoutScript when signing const signingScript = payments.p2pkh({ hash: meaningfulScript.slice(2) }) .output; @@ -1020,6 +1276,89 @@ function getHashForSig(inputIndex, input, cache, forValidate, sighashTypes) { hash, }; } +function getAllTaprootHashesForSig(inputIndex, input, inputs, cache) { + const allPublicKeys = []; + if (input.tapInternalKey) { + const outputKey = (0, bip371_1.tweakInternalPubKey)(inputIndex, input); + allPublicKeys.push(outputKey); + } + if (input.tapScriptSig) { + const tapScriptPubkeys = input.tapScriptSig.map(tss => tss.pubkey); + allPublicKeys.push(...tapScriptPubkeys); + } + const allHashes = allPublicKeys.map(pubicKey => + getTaprootHashesForSig(inputIndex, input, inputs, pubicKey, cache), + ); + return allHashes.flat(); +} +function getTaprootHashesForSig( + inputIndex, + input, + inputs, + pubkey, + cache, + tapLeafHashToSign, + allowedSighashTypes, +) { + const unsignedTx = cache.__TX; + const sighashType = + input.sighashType || transaction_1.Transaction.SIGHASH_DEFAULT; + checkSighashTypeAllowed(sighashType, allowedSighashTypes); + const prevOuts = inputs.map((i, index) => + getScriptAndAmountFromUtxo(index, i, cache), + ); + const signingScripts = prevOuts.map(o => o.script); + const values = prevOuts.map(o => o.value); + const hashes = []; + if (input.tapInternalKey && !tapLeafHashToSign) { + const outputKey = (0, bip371_1.tweakInternalPubKey)(inputIndex, input); + if ((0, bip371_1.toXOnly)(pubkey).equals(outputKey)) { + const tapKeyHash = unsignedTx.hashForWitnessV1( + inputIndex, + signingScripts, + values, + sighashType, + ); + hashes.push({ pubkey, hash: tapKeyHash }); + } + } + const tapLeafHashes = (input.tapLeafScript || []) + .filter(tapLeaf => (0, psbtutils_1.pubkeyInScript)(pubkey, tapLeaf.script)) + .map(tapLeaf => { + const hash = (0, bip341_1.tapleafHash)({ + output: tapLeaf.script, + version: tapLeaf.leafVersion, + }); + return Object.assign({ hash }, tapLeaf); + }) + .filter( + tapLeaf => !tapLeafHashToSign || tapLeafHashToSign.equals(tapLeaf.hash), + ) + .map(tapLeaf => { + const tapScriptHash = unsignedTx.hashForWitnessV1( + inputIndex, + signingScripts, + values, + transaction_1.Transaction.SIGHASH_DEFAULT, + tapLeaf.hash, + ); + return { + pubkey, + hash: tapScriptHash, + leafHash: tapLeaf.hash, + }; + }); + return hashes.concat(tapLeafHashes); +} +function checkSighashTypeAllowed(sighashType, sighashTypes) { + if (sighashTypes && sighashTypes.indexOf(sighashType) < 0) { + const str = sighashTypeToString(sighashType); + throw new Error( + `Sighash type is not allowed. Retry the sign method passing the ` + + `sighashTypes array of whitelisted types. Sighash type: ${str}`, + ); + } +} function getPayment(script, scriptType, partialSig) { let payment; switch (scriptType) { @@ -1053,20 +1392,6 @@ function getPayment(script, scriptType, partialSig) { } return payment; } -function getPsigsFromInputFinalScripts(input) { - const scriptItems = !input.finalScriptSig - ? [] - : bscript.decompile(input.finalScriptSig) || []; - const witnessItems = !input.finalScriptWitness - ? [] - : bscript.decompile(input.finalScriptWitness) || []; - return scriptItems - .concat(witnessItems) - .filter(item => { - return Buffer.isBuffer(item) && bscript.isCanonicalScriptSignature(item); - }) - .map(sig => ({ signature: sig })); -} function getScriptFromInput(inputIndex, input, cache) { const unsignedTx = cache.__TX; const res = { @@ -1094,7 +1419,7 @@ function getScriptFromInput(inputIndex, input, cache) { res.script = input.witnessUtxo.script; } } - if (input.witnessScript || isP2WPKH(res.script)) { + if (input.witnessScript || (0, psbtutils_1.isP2WPKH)(res.script)) { res.isSegwit = true; } return res; @@ -1184,28 +1509,6 @@ function sighashTypeToString(sighashType) { } return text; } -function witnessStackToScriptWitness(witness) { - let buffer = Buffer.allocUnsafe(0); - function writeSlice(slice) { - buffer = Buffer.concat([buffer, Buffer.from(slice)]); - } - function writeVarInt(i) { - const currentLen = buffer.length; - const varintLen = varuint.encodingLength(i); - buffer = Buffer.concat([buffer, Buffer.allocUnsafe(varintLen)]); - varuint.encode(i, buffer, currentLen); - } - function writeVarSlice(slice) { - writeVarInt(slice.length); - writeSlice(slice); - } - function writeVector(vector) { - writeVarInt(vector.length); - vector.forEach(writeVarSlice); - } - writeVector(witness); - return buffer; -} function addNonWitnessTxCache(cache, input, inputIndex) { cache.__NON_WITNESS_UTXO_BUF_CACHE[inputIndex] = input.nonWitnessUtxo; const tx = transaction_1.Transaction.fromBuffer(input.nonWitnessUtxo); @@ -1268,15 +1571,23 @@ function nonWitnessUtxoTxFromCache(cache, input, inputIndex) { return c[inputIndex]; } function getScriptFromUtxo(inputIndex, input, cache) { + const { script } = getScriptAndAmountFromUtxo(inputIndex, input, cache); + return script; +} +function getScriptAndAmountFromUtxo(inputIndex, input, cache) { if (input.witnessUtxo !== undefined) { - return input.witnessUtxo.script; + return { + script: input.witnessUtxo.script, + value: input.witnessUtxo.value, + }; } else if (input.nonWitnessUtxo !== undefined) { const nonWitnessUtxoTx = nonWitnessUtxoTxFromCache( cache, input, inputIndex, ); - return nonWitnessUtxoTx.outs[cache.__TX.ins[inputIndex].index].script; + const o = nonWitnessUtxoTx.outs[cache.__TX.ins[inputIndex].index]; + return { script: o.script, value: o.value }; } else { throw new Error("Can't find pubkey in input without Utxo data"); } @@ -1290,7 +1601,7 @@ function pubkeyInInput(pubkey, input, inputIndex, cache) { input.redeemScript, input.witnessScript, ); - return pubkeyInScript(pubkey, meaningfulScript); + return (0, psbtutils_1.pubkeyInScript)(pubkey, meaningfulScript); } function pubkeyInOutput(pubkey, output, outputIndex, cache) { const script = cache.__TX.outs[outputIndex].script; @@ -1301,7 +1612,7 @@ function pubkeyInOutput(pubkey, output, outputIndex, cache) { output.redeemScript, output.witnessScript, ); - return pubkeyInScript(pubkey, meaningfulScript); + return (0, psbtutils_1.pubkeyInScript)(pubkey, meaningfulScript); } function redeemFromFinalScriptSig(finalScript) { if (!finalScript) return; @@ -1349,9 +1660,10 @@ function getMeaningfulScript( redeemScript, witnessScript, ) { - const isP2SH = isP2SHScript(script); - const isP2SHP2WSH = isP2SH && redeemScript && isP2WSHScript(redeemScript); - const isP2WSH = isP2WSHScript(script); + const isP2SH = (0, psbtutils_1.isP2SHScript)(script); + const isP2SHP2WSH = + isP2SH && redeemScript && (0, psbtutils_1.isP2WSHScript)(redeemScript); + const isP2WSH = (0, psbtutils_1.isP2WSHScript)(script); if (isP2SH && redeemScript === undefined) throw new Error('scriptPubkey is P2SH but redeemScript missing'); if ((isP2WSH || isP2SHP2WSH) && witnessScript === undefined) @@ -1386,24 +1698,18 @@ function getMeaningfulScript( }; } function checkInvalidP2WSH(script) { - if (isP2WPKH(script) || isP2SHScript(script)) { + if ( + (0, psbtutils_1.isP2WPKH)(script) || + (0, psbtutils_1.isP2SHScript)(script) + ) { throw new Error('P2WPKH or P2SH can not be contained within P2WSH'); } } -function pubkeyInScript(pubkey, script) { - const pubkeyHash = (0, crypto_1.hash160)(pubkey); - const decompiled = bscript.decompile(script); - if (decompiled === null) throw new Error('Unknown script error'); - return decompiled.some(element => { - if (typeof element === 'number') return false; - return element.equals(pubkey) || element.equals(pubkeyHash); - }); -} function classifyScript(script) { - if (isP2WPKH(script)) return 'witnesspubkeyhash'; - if (isP2PKH(script)) return 'pubkeyhash'; - if (isP2MS(script)) return 'multisig'; - if (isP2PK(script)) return 'pubkey'; + if ((0, psbtutils_1.isP2WPKH)(script)) return 'witnesspubkeyhash'; + if ((0, psbtutils_1.isP2PKH)(script)) return 'pubkeyhash'; + if ((0, psbtutils_1.isP2MS)(script)) return 'multisig'; + if ((0, psbtutils_1.isP2PK)(script)) return 'pubkey'; return 'nonstandard'; } function range(n) { diff --git a/src/psbt/bip371.d.ts b/src/psbt/bip371.d.ts new file mode 100644 index 000000000..fae6e756b --- /dev/null +++ b/src/psbt/bip371.d.ts @@ -0,0 +1,41 @@ +/// +import { Taptree } from '../types'; +import { PsbtInput, PsbtOutput, TapLeaf } from 'bip174/src/lib/interfaces'; +export declare const toXOnly: (pubKey: Buffer) => Buffer; +/** + * Default tapscript finalizer. It searches for the `tapLeafHashToFinalize` if provided. + * Otherwise it will search for the tapleaf that has at least one signature and has the shortest path. + * @param inputIndex the position of the PSBT input. + * @param input the PSBT input. + * @param tapLeafHashToFinalize optional, if provided the finalizer will search for a tapleaf that has this hash + * and will try to build the finalScriptWitness. + * @returns the finalScriptWitness or throws an exception if no tapleaf found. + */ +export declare function tapScriptFinalizer(inputIndex: number, input: PsbtInput, tapLeafHashToFinalize?: Buffer): { + finalScriptWitness: Buffer | undefined; +}; +export declare function serializeTaprootSignature(sig: Buffer, sighashType?: number): Buffer; +export declare function isTaprootInput(input: PsbtInput): boolean; +export declare function isTaprootOutput(output: PsbtOutput, script?: Buffer): boolean; +export declare function checkTaprootInputFields(inputData: PsbtInput, newInputData: PsbtInput, action: string): void; +export declare function checkTaprootOutputFields(outputData: PsbtOutput, newOutputData: PsbtOutput, action: string): void; +export declare function tweakInternalPubKey(inputIndex: number, input: PsbtInput): Buffer; +/** + * Convert a binary tree to a BIP371 type list. Each element of the list is (according to BIP371): + * One or more tuples representing the depth, leaf version, and script for a leaf in the Taproot tree, + * allowing the entire tree to be reconstructed. The tuples must be in depth first search order so that + * the tree is correctly reconstructed. + * @param tree the binary tap tree + * @returns a list of BIP 371 tapleaves + */ +export declare function tapTreeToList(tree: Taptree): TapLeaf[]; +/** + * Convert a BIP371 TapLeaf list to a TapTree (binary). + * @param leaves a list of tapleaves where each element of the list is (according to BIP371): + * One or more tuples representing the depth, leaf version, and script for a leaf in the Taproot tree, + * allowing the entire tree to be reconstructed. The tuples must be in depth first search order so that + * the tree is correctly reconstructed. + * @returns the corresponding taptree, or throws an exception if the tree cannot be reconstructed + */ +export declare function tapTreeFromList(leaves?: TapLeaf[]): Taptree; +export declare function checkTaprootInputForSigs(input: PsbtInput, action: string): boolean; diff --git a/src/psbt/bip371.js b/src/psbt/bip371.js new file mode 100644 index 000000000..8e76ecda7 --- /dev/null +++ b/src/psbt/bip371.js @@ -0,0 +1,349 @@ +'use strict'; +Object.defineProperty(exports, '__esModule', { value: true }); +exports.checkTaprootInputForSigs = exports.tapTreeFromList = exports.tapTreeToList = exports.tweakInternalPubKey = exports.checkTaprootOutputFields = exports.checkTaprootInputFields = exports.isTaprootOutput = exports.isTaprootInput = exports.serializeTaprootSignature = exports.tapScriptFinalizer = exports.toXOnly = void 0; +const types_1 = require('../types'); +const transaction_1 = require('../transaction'); +const psbtutils_1 = require('./psbtutils'); +const bip341_1 = require('../payments/bip341'); +const payments_1 = require('../payments'); +const psbtutils_2 = require('./psbtutils'); +const toXOnly = pubKey => (pubKey.length === 32 ? pubKey : pubKey.slice(1, 33)); +exports.toXOnly = toXOnly; +/** + * Default tapscript finalizer. It searches for the `tapLeafHashToFinalize` if provided. + * Otherwise it will search for the tapleaf that has at least one signature and has the shortest path. + * @param inputIndex the position of the PSBT input. + * @param input the PSBT input. + * @param tapLeafHashToFinalize optional, if provided the finalizer will search for a tapleaf that has this hash + * and will try to build the finalScriptWitness. + * @returns the finalScriptWitness or throws an exception if no tapleaf found. + */ +function tapScriptFinalizer(inputIndex, input, tapLeafHashToFinalize) { + const tapLeaf = findTapLeafToFinalize( + input, + inputIndex, + tapLeafHashToFinalize, + ); + try { + const sigs = sortSignatures(input, tapLeaf); + const witness = sigs.concat(tapLeaf.script).concat(tapLeaf.controlBlock); + return { + finalScriptWitness: (0, psbtutils_1.witnessStackToScriptWitness)(witness), + }; + } catch (err) { + throw new Error(`Can not finalize taproot input #${inputIndex}: ${err}`); + } +} +exports.tapScriptFinalizer = tapScriptFinalizer; +function serializeTaprootSignature(sig, sighashType) { + const sighashTypeByte = sighashType + ? Buffer.from([sighashType]) + : Buffer.from([]); + return Buffer.concat([sig, sighashTypeByte]); +} +exports.serializeTaprootSignature = serializeTaprootSignature; +function isTaprootInput(input) { + return ( + input && + !!( + input.tapInternalKey || + input.tapMerkleRoot || + (input.tapLeafScript && input.tapLeafScript.length) || + (input.tapBip32Derivation && input.tapBip32Derivation.length) || + (input.witnessUtxo && (0, psbtutils_1.isP2TR)(input.witnessUtxo.script)) + ) + ); +} +exports.isTaprootInput = isTaprootInput; +function isTaprootOutput(output, script) { + return ( + output && + !!( + output.tapInternalKey || + output.tapTree || + (output.tapBip32Derivation && output.tapBip32Derivation.length) || + (script && (0, psbtutils_1.isP2TR)(script)) + ) + ); +} +exports.isTaprootOutput = isTaprootOutput; +function checkTaprootInputFields(inputData, newInputData, action) { + checkMixedTaprootAndNonTaprootInputFields(inputData, newInputData, action); + checkIfTapLeafInTree(inputData, newInputData, action); +} +exports.checkTaprootInputFields = checkTaprootInputFields; +function checkTaprootOutputFields(outputData, newOutputData, action) { + checkMixedTaprootAndNonTaprootOutputFields(outputData, newOutputData, action); + checkTaprootScriptPubkey(outputData, newOutputData); +} +exports.checkTaprootOutputFields = checkTaprootOutputFields; +function checkTaprootScriptPubkey(outputData, newOutputData) { + if (!newOutputData.tapTree && !newOutputData.tapInternalKey) return; + const tapInternalKey = + newOutputData.tapInternalKey || outputData.tapInternalKey; + const tapTree = newOutputData.tapTree || outputData.tapTree; + if (tapInternalKey) { + const { script: scriptPubkey } = outputData; + const script = getTaprootScripPubkey(tapInternalKey, tapTree); + if (scriptPubkey && !scriptPubkey.equals(script)) + throw new Error('Error adding output. Script or address missmatch.'); + } +} +function getTaprootScripPubkey(tapInternalKey, tapTree) { + const scriptTree = tapTree && tapTreeFromList(tapTree.leaves); + const { output } = (0, payments_1.p2tr)({ + internalPubkey: tapInternalKey, + scriptTree, + }); + return output; +} +function tweakInternalPubKey(inputIndex, input) { + const tapInternalKey = input.tapInternalKey; + const outputKey = + tapInternalKey && + (0, bip341_1.tweakKey)(tapInternalKey, input.tapMerkleRoot); + if (!outputKey) + throw new Error( + `Cannot tweak tap internal key for input #${inputIndex}. Public key: ${tapInternalKey && + tapInternalKey.toString('hex')}`, + ); + return outputKey.x; +} +exports.tweakInternalPubKey = tweakInternalPubKey; +/** + * Convert a binary tree to a BIP371 type list. Each element of the list is (according to BIP371): + * One or more tuples representing the depth, leaf version, and script for a leaf in the Taproot tree, + * allowing the entire tree to be reconstructed. The tuples must be in depth first search order so that + * the tree is correctly reconstructed. + * @param tree the binary tap tree + * @returns a list of BIP 371 tapleaves + */ +function tapTreeToList(tree) { + if (!(0, types_1.isTaptree)(tree)) + throw new Error( + 'Cannot convert taptree to tapleaf list. Expecting a tapree structure.', + ); + return _tapTreeToList(tree); +} +exports.tapTreeToList = tapTreeToList; +/** + * Convert a BIP371 TapLeaf list to a TapTree (binary). + * @param leaves a list of tapleaves where each element of the list is (according to BIP371): + * One or more tuples representing the depth, leaf version, and script for a leaf in the Taproot tree, + * allowing the entire tree to be reconstructed. The tuples must be in depth first search order so that + * the tree is correctly reconstructed. + * @returns the corresponding taptree, or throws an exception if the tree cannot be reconstructed + */ +function tapTreeFromList(leaves = []) { + if (leaves.length === 1 && leaves[0].depth === 0) + return { + output: leaves[0].script, + version: leaves[0].leafVersion, + }; + return instertLeavesInTree(leaves); +} +exports.tapTreeFromList = tapTreeFromList; +function checkTaprootInputForSigs(input, action) { + const sigs = extractTaprootSigs(input); + return sigs.some(sig => + (0, psbtutils_2.signatureBlocksAction)(sig, decodeSchnorrSignature, action), + ); +} +exports.checkTaprootInputForSigs = checkTaprootInputForSigs; +function decodeSchnorrSignature(signature) { + return { + signature: signature.slice(0, 64), + hashType: + signature.slice(64)[0] || transaction_1.Transaction.SIGHASH_DEFAULT, + }; +} +function extractTaprootSigs(input) { + const sigs = []; + if (input.tapKeySig) sigs.push(input.tapKeySig); + if (input.tapScriptSig) + sigs.push(...input.tapScriptSig.map(s => s.signature)); + if (!sigs.length) { + const finalTapKeySig = getTapKeySigFromWithness(input.finalScriptWitness); + if (finalTapKeySig) sigs.push(finalTapKeySig); + } + return sigs; +} +function getTapKeySigFromWithness(finalScriptWitness) { + if (!finalScriptWitness) return; + const witness = finalScriptWitness.slice(2); + // todo: add schnorr signature validation + if (witness.length === 64 || witness.length === 65) return witness; +} +function _tapTreeToList(tree, leaves = [], depth = 0) { + if (depth > bip341_1.MAX_TAPTREE_DEPTH) + throw new Error('Max taptree depth exceeded.'); + if (!tree) return []; + if ((0, types_1.isTapleaf)(tree)) { + leaves.push({ + depth, + leafVersion: tree.version || bip341_1.LEAF_VERSION_TAPSCRIPT, + script: tree.output, + }); + return leaves; + } + if (tree[0]) _tapTreeToList(tree[0], leaves, depth + 1); + if (tree[1]) _tapTreeToList(tree[1], leaves, depth + 1); + return leaves; +} +function instertLeavesInTree(leaves) { + let tree; + for (const leaf of leaves) { + tree = instertLeafInTree(leaf, tree); + if (!tree) throw new Error(`No room left to insert tapleaf in tree`); + } + return tree; +} +function instertLeafInTree(leaf, tree, depth = 0) { + if (depth > bip341_1.MAX_TAPTREE_DEPTH) + throw new Error('Max taptree depth exceeded.'); + if (leaf.depth === depth) { + if (!tree) + return { + output: leaf.script, + version: leaf.leafVersion, + }; + return; + } + if ((0, types_1.isTapleaf)(tree)) return; + const leftSide = instertLeafInTree(leaf, tree && tree[0], depth + 1); + if (leftSide) return [leftSide, tree && tree[1]]; + const rightSide = instertLeafInTree(leaf, tree && tree[1], depth + 1); + if (rightSide) return [tree && tree[0], rightSide]; +} +function checkMixedTaprootAndNonTaprootInputFields( + inputData, + newInputData, + action, +) { + const isBadTaprootUpdate = + isTaprootInput(inputData) && hasNonTaprootFields(newInputData); + const isBadNonTaprootUpdate = + hasNonTaprootFields(inputData) && isTaprootInput(newInputData); + const hasMixedFields = + inputData === newInputData && + (isTaprootInput(newInputData) && hasNonTaprootFields(newInputData)); // todo: bad? use !=== + if (isBadTaprootUpdate || isBadNonTaprootUpdate || hasMixedFields) + throw new Error( + `Invalid arguments for Psbt.${action}. ` + + `Cannot use both taproot and non-taproot fields.`, + ); +} +function checkMixedTaprootAndNonTaprootOutputFields( + inputData, + newInputData, + action, +) { + const isBadTaprootUpdate = + isTaprootOutput(inputData) && hasNonTaprootFields(newInputData); + const isBadNonTaprootUpdate = + hasNonTaprootFields(inputData) && isTaprootOutput(newInputData); + const hasMixedFields = + inputData === newInputData && + (isTaprootOutput(newInputData) && hasNonTaprootFields(newInputData)); + if (isBadTaprootUpdate || isBadNonTaprootUpdate || hasMixedFields) + throw new Error( + `Invalid arguments for Psbt.${action}. ` + + `Cannot use both taproot and non-taproot fields.`, + ); +} +function checkIfTapLeafInTree(inputData, newInputData, action) { + if (newInputData.tapMerkleRoot) { + const newLeafsInTree = (newInputData.tapLeafScript || []).every(l => + isTapLeafInTree(l, newInputData.tapMerkleRoot), + ); + const oldLeafsInTree = (inputData.tapLeafScript || []).every(l => + isTapLeafInTree(l, newInputData.tapMerkleRoot), + ); + if (!newLeafsInTree || !oldLeafsInTree) + throw new Error( + `Invalid arguments for Psbt.${action}. Tapleaf not part of taptree.`, + ); + } else if (inputData.tapMerkleRoot) { + const newLeafsInTree = (newInputData.tapLeafScript || []).every(l => + isTapLeafInTree(l, inputData.tapMerkleRoot), + ); + if (!newLeafsInTree) + throw new Error( + `Invalid arguments for Psbt.${action}. Tapleaf not part of taptree.`, + ); + } +} +function isTapLeafInTree(tapLeaf, merkleRoot) { + if (!merkleRoot) return true; + const leafHash = (0, bip341_1.tapleafHash)({ + output: tapLeaf.script, + version: tapLeaf.leafVersion, + }); + const rootHash = (0, bip341_1.rootHashFromPath)( + tapLeaf.controlBlock, + leafHash, + ); + return rootHash.equals(merkleRoot); +} +function sortSignatures(input, tapLeaf) { + const leafHash = (0, bip341_1.tapleafHash)({ + output: tapLeaf.script, + version: tapLeaf.leafVersion, + }); + return (input.tapScriptSig || []) + .filter(tss => tss.leafHash.equals(leafHash)) + .map(tss => addPubkeyPositionInScript(tapLeaf.script, tss)) + .sort((t1, t2) => t2.positionInScript - t1.positionInScript) + .map(t => t.signature); +} +function addPubkeyPositionInScript(script, tss) { + return Object.assign( + { + positionInScript: (0, psbtutils_1.pubkeyPositionInScript)( + tss.pubkey, + script, + ), + }, + tss, + ); +} +/** + * Find tapleaf by hash, or get the signed tapleaf with the shortest path. + */ +function findTapLeafToFinalize(input, inputIndex, leafHashToFinalize) { + if (!input.tapScriptSig || !input.tapScriptSig.length) + throw new Error( + `Can not finalize taproot input #${inputIndex}. No tapleaf script signature provided.`, + ); + const tapLeaf = (input.tapLeafScript || []) + .sort((a, b) => a.controlBlock.length - b.controlBlock.length) + .find(leaf => + canFinalizeLeaf(leaf, input.tapScriptSig, leafHashToFinalize), + ); + if (!tapLeaf) + throw new Error( + `Can not finalize taproot input #${inputIndex}. Signature for tapleaf script not found.`, + ); + return tapLeaf; +} +function canFinalizeLeaf(leaf, tapScriptSig, hash) { + const leafHash = (0, bip341_1.tapleafHash)({ + output: leaf.script, + version: leaf.leafVersion, + }); + const whiteListedHash = !hash || hash.equals(leafHash); + return ( + whiteListedHash && + tapScriptSig.find(tss => tss.leafHash.equals(leafHash)) !== undefined + ); +} +function hasNonTaprootFields(io) { + return ( + io && + !!( + io.redeemScript || + io.witnessScript || + (io.bip32Derivation && io.bip32Derivation.length) + ) + ); +} diff --git a/src/psbt/psbtutils.d.ts b/src/psbt/psbtutils.d.ts new file mode 100644 index 000000000..6491132a8 --- /dev/null +++ b/src/psbt/psbtutils.d.ts @@ -0,0 +1,19 @@ +/// +import { PsbtInput } from 'bip174/src/lib/interfaces'; +export declare const isP2MS: (script: Buffer) => boolean; +export declare const isP2PK: (script: Buffer) => boolean; +export declare const isP2PKH: (script: Buffer) => boolean; +export declare const isP2WPKH: (script: Buffer) => boolean; +export declare const isP2WSHScript: (script: Buffer) => boolean; +export declare const isP2SHScript: (script: Buffer) => boolean; +export declare const isP2TR: (script: Buffer) => boolean; +export declare function witnessStackToScriptWitness(witness: Buffer[]): Buffer; +export declare function pubkeyPositionInScript(pubkey: Buffer, script: Buffer): number; +export declare function pubkeyInScript(pubkey: Buffer, script: Buffer): boolean; +export declare function checkInputForSig(input: PsbtInput, action: string): boolean; +declare type SignatureDecodeFunc = (buffer: Buffer) => { + signature: Buffer; + hashType: number; +}; +export declare function signatureBlocksAction(signature: Buffer, signatureDecodeFn: SignatureDecodeFunc, action: string): boolean; +export {}; diff --git a/src/psbt/psbtutils.js b/src/psbt/psbtutils.js new file mode 100644 index 000000000..086faa4f0 --- /dev/null +++ b/src/psbt/psbtutils.js @@ -0,0 +1,120 @@ +'use strict'; +Object.defineProperty(exports, '__esModule', { value: true }); +exports.signatureBlocksAction = exports.checkInputForSig = exports.pubkeyInScript = exports.pubkeyPositionInScript = exports.witnessStackToScriptWitness = exports.isP2TR = exports.isP2SHScript = exports.isP2WSHScript = exports.isP2WPKH = exports.isP2PKH = exports.isP2PK = exports.isP2MS = void 0; +const varuint = require('bip174/src/lib/converter/varint'); +const bscript = require('../script'); +const transaction_1 = require('../transaction'); +const crypto_1 = require('../crypto'); +const payments = require('../payments'); +function isPaymentFactory(payment) { + return script => { + try { + payment({ output: script }); + return true; + } catch (err) { + return false; + } + }; +} +exports.isP2MS = isPaymentFactory(payments.p2ms); +exports.isP2PK = isPaymentFactory(payments.p2pk); +exports.isP2PKH = isPaymentFactory(payments.p2pkh); +exports.isP2WPKH = isPaymentFactory(payments.p2wpkh); +exports.isP2WSHScript = isPaymentFactory(payments.p2wsh); +exports.isP2SHScript = isPaymentFactory(payments.p2sh); +exports.isP2TR = isPaymentFactory(payments.p2tr); +function witnessStackToScriptWitness(witness) { + let buffer = Buffer.allocUnsafe(0); + function writeSlice(slice) { + buffer = Buffer.concat([buffer, Buffer.from(slice)]); + } + function writeVarInt(i) { + const currentLen = buffer.length; + const varintLen = varuint.encodingLength(i); + buffer = Buffer.concat([buffer, Buffer.allocUnsafe(varintLen)]); + varuint.encode(i, buffer, currentLen); + } + function writeVarSlice(slice) { + writeVarInt(slice.length); + writeSlice(slice); + } + function writeVector(vector) { + writeVarInt(vector.length); + vector.forEach(writeVarSlice); + } + writeVector(witness); + return buffer; +} +exports.witnessStackToScriptWitness = witnessStackToScriptWitness; +function pubkeyPositionInScript(pubkey, script) { + const pubkeyHash = (0, crypto_1.hash160)(pubkey); + const pubkeyXOnly = pubkey.slice(1, 33); // slice before calling? + const decompiled = bscript.decompile(script); + if (decompiled === null) throw new Error('Unknown script error'); + return decompiled.findIndex(element => { + if (typeof element === 'number') return false; + return ( + element.equals(pubkey) || + element.equals(pubkeyHash) || + element.equals(pubkeyXOnly) + ); + }); +} +exports.pubkeyPositionInScript = pubkeyPositionInScript; +function pubkeyInScript(pubkey, script) { + return pubkeyPositionInScript(pubkey, script) !== -1; +} +exports.pubkeyInScript = pubkeyInScript; +function checkInputForSig(input, action) { + const pSigs = extractPartialSigs(input); + return pSigs.some(pSig => + signatureBlocksAction(pSig, bscript.signature.decode, action), + ); +} +exports.checkInputForSig = checkInputForSig; +function signatureBlocksAction(signature, signatureDecodeFn, action) { + const { hashType } = signatureDecodeFn(signature); + const whitelist = []; + const isAnyoneCanPay = + hashType & transaction_1.Transaction.SIGHASH_ANYONECANPAY; + if (isAnyoneCanPay) whitelist.push('addInput'); + const hashMod = hashType & 0x1f; + switch (hashMod) { + case transaction_1.Transaction.SIGHASH_ALL: + break; + case transaction_1.Transaction.SIGHASH_SINGLE: + case transaction_1.Transaction.SIGHASH_NONE: + whitelist.push('addOutput'); + whitelist.push('setInputSequence'); + break; + } + if (whitelist.indexOf(action) === -1) { + return true; + } + return false; +} +exports.signatureBlocksAction = signatureBlocksAction; +function extractPartialSigs(input) { + let pSigs = []; + if ((input.partialSig || []).length === 0) { + if (!input.finalScriptSig && !input.finalScriptWitness) return []; + pSigs = getPsigsFromInputFinalScripts(input); + } else { + pSigs = input.partialSig; + } + return pSigs.map(p => p.signature); +} +function getPsigsFromInputFinalScripts(input) { + const scriptItems = !input.finalScriptSig + ? [] + : bscript.decompile(input.finalScriptSig) || []; + const witnessItems = !input.finalScriptWitness + ? [] + : bscript.decompile(input.finalScriptWitness) || []; + return scriptItems + .concat(witnessItems) + .filter(item => { + return Buffer.isBuffer(item) && bscript.isCanonicalScriptSignature(item); + }) + .map(sig => ({ signature: sig })); +} diff --git a/src/types.d.ts b/src/types.d.ts index 5a8505d34..b3d93589d 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -10,6 +10,29 @@ export declare function Signer(obj: any): boolean; export declare function Satoshi(value: number): boolean; export declare const ECPoint: any; export declare const Network: any; +export interface XOnlyPointAddTweakResult { + parity: 1 | 0; + xOnlyPubkey: Uint8Array; +} +export interface Tapleaf { + output: Buffer; + version?: number; +} +export declare const TAPLEAF_VERSION_MASK = 254; +export declare function isTapleaf(o: any): o is Tapleaf; +/** + * Binary tree repsenting script path spends for a Taproot input. + * Each node is either a single Tapleaf, or a pair of Tapleaf | Taptree. + * The tree has no balancing requirements. + */ +export declare type Taptree = [Taptree | Tapleaf, Taptree | Tapleaf] | Tapleaf; +export declare function isTaptree(scriptTree: any): scriptTree is Taptree; +export interface TinySecp256k1Interface { + isXOnlyPoint(p: Uint8Array): boolean; + xOnlyPointAddTweak(p: Uint8Array, tweak: Uint8Array): XOnlyPointAddTweakResult | null; + privateAdd(d: Uint8Array, tweak: Uint8Array): Uint8Array | null; + privateNegate(d: Uint8Array): Uint8Array; +} export declare const Buffer256bit: any; export declare const Hash160bit: any; export declare const Hash256bit: any; diff --git a/src/types.js b/src/types.js index a6d1efa16..3a2dc51ea 100644 --- a/src/types.js +++ b/src/types.js @@ -1,6 +1,6 @@ 'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); -exports.oneOf = exports.Null = exports.BufferN = exports.Function = exports.UInt32 = exports.UInt8 = exports.tuple = exports.maybe = exports.Hex = exports.Buffer = exports.String = exports.Boolean = exports.Array = exports.Number = exports.Hash256bit = exports.Hash160bit = exports.Buffer256bit = exports.Network = exports.ECPoint = exports.Satoshi = exports.Signer = exports.BIP32Path = exports.UInt31 = exports.isPoint = exports.typeforce = void 0; +exports.oneOf = exports.Null = exports.BufferN = exports.Function = exports.UInt32 = exports.UInt8 = exports.tuple = exports.maybe = exports.Hex = exports.Buffer = exports.String = exports.Boolean = exports.Array = exports.Number = exports.Hash256bit = exports.Hash160bit = exports.Buffer256bit = exports.isTaptree = exports.isTapleaf = exports.TAPLEAF_VERSION_MASK = exports.Network = exports.ECPoint = exports.Satoshi = exports.Signer = exports.BIP32Path = exports.UInt31 = exports.isPoint = exports.typeforce = void 0; const buffer_1 = require('buffer'); exports.typeforce = require('typeforce'); const ZERO32 = buffer_1.Buffer.alloc(32, 0); @@ -68,6 +68,21 @@ exports.Network = exports.typeforce.compile({ scriptHash: exports.typeforce.UInt8, wif: exports.typeforce.UInt8, }); +exports.TAPLEAF_VERSION_MASK = 0xfe; +function isTapleaf(o) { + if (!o || !('output' in o)) return false; + if (!buffer_1.Buffer.isBuffer(o.output)) return false; + if (o.version !== undefined) + return (o.version & exports.TAPLEAF_VERSION_MASK) === o.version; + return true; +} +exports.isTapleaf = isTapleaf; +function isTaptree(scriptTree) { + if (!(0, exports.Array)(scriptTree)) return isTapleaf(scriptTree); + if (scriptTree.length !== 2) return false; + return scriptTree.every(t => isTaptree(t)); +} +exports.isTaptree = isTaptree; exports.Buffer256bit = exports.typeforce.BufferN(32); exports.Hash160bit = exports.typeforce.BufferN(20); exports.Hash256bit = exports.typeforce.BufferN(32); diff --git a/test/address.spec.ts b/test/address.spec.ts index b1304c323..23c18b9f6 100644 --- a/test/address.spec.ts +++ b/test/address.spec.ts @@ -1,9 +1,12 @@ import * as assert from 'assert'; import { describe, it } from 'mocha'; +import * as ecc from 'tiny-secp256k1'; import * as baddress from '../src/address'; import * as bscript from '../src/script'; import * as fixtures from './fixtures/address.json'; +import { initEccLib } from '../src'; + const NETWORKS = Object.assign( { litecoin: { @@ -65,6 +68,7 @@ describe('address', () => { }); describe('fromOutputScript', () => { + initEccLib(ecc); fixtures.standard.forEach(f => { it('encodes ' + f.script.slice(0, 30) + '... (' + f.network + ')', () => { const script = bscript.fromASM(f.script); @@ -79,7 +83,7 @@ describe('address', () => { const script = bscript.fromASM(f.script); assert.throws(() => { - baddress.fromOutputScript(script); + baddress.fromOutputScript(script, undefined); }, new RegExp(f.exception)); }); }); @@ -138,10 +142,11 @@ describe('address', () => { }); fixtures.invalid.toOutputScript.forEach(f => { - it('throws when ' + f.exception, () => { + it('throws when ' + (f.exception || f.paymentException), () => { + const exception = f.paymentException || `${f.address} ${f.exception}`; assert.throws(() => { baddress.toOutputScript(f.address, f.network as any); - }, new RegExp(f.address + ' ' + f.exception)); + }, new RegExp(exception)); }); }); }); diff --git a/test/fixtures/address.json b/test/fixtures/address.json index 765ea8aca..0430b7887 100644 --- a/test/fixtures/address.json +++ b/test/fixtures/address.json @@ -80,10 +80,10 @@ }, { "network": "bitcoin", - "bech32": "bc1pw508d6qejxtdg4y5r3zarvary0c5xw7kw508d6qejxtdg4y5r3zarvary0c5xw7kt5nd6y", + "bech32": "bc1p3efq8ujsj0qr5xvms7mv89p8cz0crqdtuxe9ms6grqgxc9sgsntslthf6w", "version": 1, - "data": "751e76e8199196d454941c45d1b3a323f1433bd6751e76e8199196d454941c45d1b3a323f1433bd6", - "script": "OP_1 751e76e8199196d454941c45d1b3a323f1433bd6751e76e8199196d454941c45d1b3a323f1433bd6" + "data": "8e5203f25093c03a199b87b6c39427c09f8181abe1b25dc34818106c160884d7", + "script": "OP_1 8e5203f25093c03a199b87b6c39427c09f8181abe1b25dc34818106c160884d7" }, { "network": "bitcoin", @@ -116,10 +116,16 @@ ], "bech32": [ { - "address": "bc1pw508d6qejxtdg4y5r3zarvary0c5xw7kw508d6qejxtdg4y5r3zarvary0c5xw7kt5nd6y", + "address": "tb1qqqqqp399et2xygdj5xreqhjjvcmzhxw4aywxecjdzew6hylgvsesrxh6hy", + "version": 0, + "prefix": "tb", + "data": "000000c4a5cad46221b2a187905e5266362b99d5e91c6ce24d165dab93e86433" + }, + { + "address": "bc1p3efq8ujsj0qr5xvms7mv89p8cz0crqdtuxe9ms6grqgxc9sgsntslthf6w", "version": 1, "prefix": "bc", - "data": "751e76e8199196d454941c45d1b3a323f1433bd6751e76e8199196d454941c45d1b3a323f1433bd6" + "data": "8e5203f25093c03a199b87b6c39427c09f8181abe1b25dc34818106c160884d7" }, { "address": "bc1zw508d6qejxtdg4y5r3zarvaryvaxxpcs", @@ -195,6 +201,19 @@ { "exception": "has no matching Address", "script": "OP_0 751e76e8199196d454941c45d1b3a323f1433bd6751e76e8199196d454941c45d1b3a323f1433bd675" + }, + { + "exception": "has no matching Address", + "script": "OP_1 75" + }, + { + "exception": "has no matching Address", + "script": "OP_1 751e76e8199196d454941c45d1b3a323f1433bd6751e76e8199196d454941c45d1b3a323f1433bd675" + }, + { + "description": "pubkey is not valid x coordinate", + "exception": "has no matching Address", + "script": "OP_1 fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f" } ], "toOutputScript": [ @@ -297,6 +316,14 @@ { "address": "bc1gmk9yu", "exception": "has no matching Script" + }, + { + "address": "bc1pw508d6qejxtdg4y5r3zarvary0c5xw7kw508d6qejxtdg4y5r3zarvary0c5xw7kw55h884v", + "exception": "has no matching Script" + }, + { + "address": "bc1pllllllllllllllllllllllllllllllllllllllllllllallllshsdfvw2y", + "paymentException": "TypeError: Invalid pubkey for p2tr" } ] } diff --git a/test/fixtures/p2tr.json b/test/fixtures/p2tr.json new file mode 100644 index 000000000..aaa82fbb4 --- /dev/null +++ b/test/fixtures/p2tr.json @@ -0,0 +1,1198 @@ +{ + "valid": [ + { + "description": "output and pubkey from address", + "arguments": { + "address": "bc1p4dss6gkgq8003g0qyd5drwfqrztsadf2w2v3juz73gdz7cx82r6sj7lcqx" + }, + "options": {}, + "expected": { + "name": "p2tr", + "output": "OP_1 ab610d22c801def8a1e02368d1b92018970eb52a729919705e8a1a2f60c750f5", + "pubkey": "ab610d22c801def8a1e02368d1b92018970eb52a729919705e8a1a2f60c750f5", + "signature": null, + "input": null, + "witness": null + } + }, + { + "description": "address and pubkey from output", + "arguments": { + "output": "OP_1 ab610d22c801def8a1e02368d1b92018970eb52a729919705e8a1a2f60c750f5" + }, + "expected": { + "name": "p2tr", + "address": "bc1p4dss6gkgq8003g0qyd5drwfqrztsadf2w2v3juz73gdz7cx82r6sj7lcqx", + "pubkey": "ab610d22c801def8a1e02368d1b92018970eb52a729919705e8a1a2f60c750f5", + "signature": null, + "input": null, + "witness": null + } + }, + { + "description": "address and output from pubkey", + "arguments": { + "pubkey": "ab610d22c801def8a1e02368d1b92018970eb52a729919705e8a1a2f60c750f5" + }, + "expected": { + "name": "p2tr", + "address": "bc1p4dss6gkgq8003g0qyd5drwfqrztsadf2w2v3juz73gdz7cx82r6sj7lcqx", + "output": "OP_1 ab610d22c801def8a1e02368d1b92018970eb52a729919705e8a1a2f60c750f5", + "signature": null, + "input": null, + "witness": null + } + }, + { + "description": "address, output and witness from pubkey and signature", + "arguments": { + "pubkey": "ab610d22c801def8a1e02368d1b92018970eb52a729919705e8a1a2f60c750f5", + "signature": "a251221c339a7129dd0b769698aca40d8d9da9570ab796a1820b91ab7dbf5acbea21c88ba8f1e9308a21729baf080734beaf97023882d972f75e380d480fd704" + }, + "expected": { + "name": "p2tr", + "address": "bc1p4dss6gkgq8003g0qyd5drwfqrztsadf2w2v3juz73gdz7cx82r6sj7lcqx", + "output": "OP_1 ab610d22c801def8a1e02368d1b92018970eb52a729919705e8a1a2f60c750f5", + "input": null, + "witness": [ + "a251221c339a7129dd0b769698aca40d8d9da9570ab796a1820b91ab7dbf5acbea21c88ba8f1e9308a21729baf080734beaf97023882d972f75e380d480fd704" + ] + } + }, + { + "description": "address, output and signature from pubkey and witness", + "arguments": { + "pubkey": "ab610d22c801def8a1e02368d1b92018970eb52a729919705e8a1a2f60c750f5", + "witness": [ + "300602010002010001" + ] + }, + "expected": { + "name": "p2tr", + "address": "bc1p4dss6gkgq8003g0qyd5drwfqrztsadf2w2v3juz73gdz7cx82r6sj7lcqx", + "output": "OP_1 ab610d22c801def8a1e02368d1b92018970eb52a729919705e8a1a2f60c750f5", + "input": null, + "signature": "300602010002010001", + "witness": [ + "300602010002010001" + ] + } + }, + { + "description": "address, pubkey and output from internalPubkey", + "arguments": { + "internalPubkey": "9fa5ffb68821cf559001caa0577eeea4978b29416def328a707b15e91701a2f7" + }, + "expected": { + "name": "p2tr", + "address": "bc1prs7pxymu7jhsptzjlwlqnk8jyg5qmq4sdlc3rwcy7pd3ydz92xjq5ap2sg", + "pubkey": "1c3c13137cf4af00ac52fbbe09d8f222280d82b06ff111bb04f05b12344551a4", + "output": "OP_1 1c3c13137cf4af00ac52fbbe09d8f222280d82b06ff111bb04f05b12344551a4", + "signature": null, + "input": null, + "witness": null + } + }, + { + "description": "address, pubkey, internalPubkey, redeeem and output from witness", + "arguments": { + "witness": [ + "9675a9982c6398ea9d441cb7a943bcd6ff033cc3a2e01a0178a7d3be4575be863871c6bf3eef5ecd34721c784259385ca9101c3a313e010ac942c99de05aaaa602", + "5799cf4b193b730fb99580b186f7477c2cca4d28957326f6f1a5d14116438530e7ec0ce1cd465ad96968ae8a6a09d4d37a060a115919f56fcfebe7b2277cc2df5cc08fb6cda9105ee2512b2e22635aba", + "7520c7b5db9562078049719228db2ac80cb9643ec96c8055aa3b29c2c03d4d99edb0ac", + "c1a7957acbaaf7b444c53d9e0c9436e8a8a3247fd515095d66ddf6201918b40a3668f9a4ccdffcf778da624dca2dda0b08e763ec52fd4ad403ec7563a3504d0cc168b9a77a410029e01dac89567c9b2e6cd726e840351df3f2f58fefe976200a19244150d04153909f660184d656ee95fa7bf8e1d4ec83da1fca34f64bc279b76d257ec623e08baba2cfa4ea9e99646e88f1eb1668c00c0f15b7443c8ab83481611cc3ae85eb89a7bfc40067eb1d2e6354a32426d0ce710e88bc4cc0718b99c325509c9d02a6a980d675a8969be10ee9bef82cafee2fc913475667ccda37b1bc7f13f64e56c449c532658ba8481631c02ead979754c809584a875951619cec8fb040c33f06468ae0266cd8693d6a64cea5912be32d8de95a6da6300b0c50fdcd6001ea41126e7b7e5280d455054a816560028f5ca53c9a50ee52f10e15c5337315bad1f5277acb109a1418649dc6ead2fe14699742fee7182f2f15e54279c7d932ed2799d01d73c97e68bbc94d6f7f56ee0a80efd7c76e3169e10d1a1ba3b5f1eb02369dc43af687461c7a2a3344d13eb5485dca29a67f16b4cb988923060fd3b65d0f0352bb634bcc44f2fe668836dcd0f604150049835135dc4b4fbf90fb334b3938a1f137eb32f047c65b85e6c1173b890b6d0162b48b186d1f1af8521945924ac8ac8efec321bf34f1d4b3d4a304a10313052c652d53f6ecb8a55586614e8950cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c" + ] + }, + "expected": { + "name": "p2tr", + "internalPubkey": "a7957acbaaf7b444c53d9e0c9436e8a8a3247fd515095d66ddf6201918b40a36", + "pubkey": "1ebe8b90363bd097aa9f352c8b21914e1886bc09fe9e70c09f33ef2d2abdf4bc", + "hash": "c5c62d7fc595ba5fbe61602eb1a29e2e4763408fe1e2b161beb7cb3c71ebcad9", + "address": "bc1pr6lghypk80gf025lx5kgkgv3fcvgd0qfl608psylx0hj624a7j7qay80rv", + "output": "OP_1 1ebe8b90363bd097aa9f352c8b21914e1886bc09fe9e70c09f33ef2d2abdf4bc", + "signature": null, + "input": null, + "redeem" : { + "output": "OP_DROP c7b5db9562078049719228db2ac80cb9643ec96c8055aa3b29c2c03d4d99edb0 OP_CHECKSIG", + "redeemVersion": 192, + "witness": [ + "9675a9982c6398ea9d441cb7a943bcd6ff033cc3a2e01a0178a7d3be4575be863871c6bf3eef5ecd34721c784259385ca9101c3a313e010ac942c99de05aaaa602", + "5799cf4b193b730fb99580b186f7477c2cca4d28957326f6f1a5d14116438530e7ec0ce1cd465ad96968ae8a6a09d4d37a060a115919f56fcfebe7b2277cc2df5cc08fb6cda9105ee2512b2e22635aba" + ] + }, + "witness": [ + "9675a9982c6398ea9d441cb7a943bcd6ff033cc3a2e01a0178a7d3be4575be863871c6bf3eef5ecd34721c784259385ca9101c3a313e010ac942c99de05aaaa602", + "5799cf4b193b730fb99580b186f7477c2cca4d28957326f6f1a5d14116438530e7ec0ce1cd465ad96968ae8a6a09d4d37a060a115919f56fcfebe7b2277cc2df5cc08fb6cda9105ee2512b2e22635aba", + "7520c7b5db9562078049719228db2ac80cb9643ec96c8055aa3b29c2c03d4d99edb0ac", + "c1a7957acbaaf7b444c53d9e0c9436e8a8a3247fd515095d66ddf6201918b40a3668f9a4ccdffcf778da624dca2dda0b08e763ec52fd4ad403ec7563a3504d0cc168b9a77a410029e01dac89567c9b2e6cd726e840351df3f2f58fefe976200a19244150d04153909f660184d656ee95fa7bf8e1d4ec83da1fca34f64bc279b76d257ec623e08baba2cfa4ea9e99646e88f1eb1668c00c0f15b7443c8ab83481611cc3ae85eb89a7bfc40067eb1d2e6354a32426d0ce710e88bc4cc0718b99c325509c9d02a6a980d675a8969be10ee9bef82cafee2fc913475667ccda37b1bc7f13f64e56c449c532658ba8481631c02ead979754c809584a875951619cec8fb040c33f06468ae0266cd8693d6a64cea5912be32d8de95a6da6300b0c50fdcd6001ea41126e7b7e5280d455054a816560028f5ca53c9a50ee52f10e15c5337315bad1f5277acb109a1418649dc6ead2fe14699742fee7182f2f15e54279c7d932ed2799d01d73c97e68bbc94d6f7f56ee0a80efd7c76e3169e10d1a1ba3b5f1eb02369dc43af687461c7a2a3344d13eb5485dca29a67f16b4cb988923060fd3b65d0f0352bb634bcc44f2fe668836dcd0f604150049835135dc4b4fbf90fb334b3938a1f137eb32f047c65b85e6c1173b890b6d0162b48b186d1f1af8521945924ac8ac8efec321bf34f1d4b3d4a304a10313052c652d53f6ecb8a55586614e8950cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c" + ] + } + }, + { + "description": "address, pubkey, internalPubkey and output from witness with annex", + "arguments": { + "witness": [ + "9675a9982c6398ea9d441cb7a943bcd6ff033cc3a2e01a0178a7d3be4575be863871c6bf3eef5ecd34721c784259385ca9101c3a313e010ac942c99de05aaaa602", + "5799cf4b193b730fb99580b186f7477c2cca4d28957326f6f1a5d14116438530e7ec0ce1cd465ad96968ae8a6a09d4d37a060a115919f56fcfebe7b2277cc2df5cc08fb6cda9105ee2512b2e22635aba", + "7520c7b5db9562078049719228db2ac80cb9643ec96c8055aa3b29c2c03d4d99edb0ac", + "c1a7957acbaaf7b444c53d9e0c9436e8a8a3247fd515095d66ddf6201918b40a3668f9a4ccdffcf778da624dca2dda0b08e763ec52fd4ad403ec7563a3504d0cc168b9a77a410029e01dac89567c9b2e6cd726e840351df3f2f58fefe976200a19244150d04153909f660184d656ee95fa7bf8e1d4ec83da1fca34f64bc279b76d257ec623e08baba2cfa4ea9e99646e88f1eb1668c00c0f15b7443c8ab83481611cc3ae85eb89a7bfc40067eb1d2e6354a32426d0ce710e88bc4cc0718b99c325509c9d02a6a980d675a8969be10ee9bef82cafee2fc913475667ccda37b1bc7f13f64e56c449c532658ba8481631c02ead979754c809584a875951619cec8fb040c33f06468ae0266cd8693d6a64cea5912be32d8de95a6da6300b0c50fdcd6001ea41126e7b7e5280d455054a816560028f5ca53c9a50ee52f10e15c5337315bad1f5277acb109a1418649dc6ead2fe14699742fee7182f2f15e54279c7d932ed2799d01d73c97e68bbc94d6f7f56ee0a80efd7c76e3169e10d1a1ba3b5f1eb02369dc43af687461c7a2a3344d13eb5485dca29a67f16b4cb988923060fd3b65d0f0352bb634bcc44f2fe668836dcd0f604150049835135dc4b4fbf90fb334b3938a1f137eb32f047c65b85e6c1173b890b6d0162b48b186d1f1af8521945924ac8ac8efec321bf34f1d4b3d4a304a10313052c652d53f6ecb8a55586614e8950cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c", + "5000000000000000001111111111111111" + ] + }, + "expected": { + "name": "p2tr", + "internalPubkey": "a7957acbaaf7b444c53d9e0c9436e8a8a3247fd515095d66ddf6201918b40a36", + "pubkey": "1ebe8b90363bd097aa9f352c8b21914e1886bc09fe9e70c09f33ef2d2abdf4bc", + "hash": "c5c62d7fc595ba5fbe61602eb1a29e2e4763408fe1e2b161beb7cb3c71ebcad9", + "address": "bc1pr6lghypk80gf025lx5kgkgv3fcvgd0qfl608psylx0hj624a7j7qay80rv", + "output": "OP_1 1ebe8b90363bd097aa9f352c8b21914e1886bc09fe9e70c09f33ef2d2abdf4bc", + "signature": null, + "input": null, + "witness": [ + "9675a9982c6398ea9d441cb7a943bcd6ff033cc3a2e01a0178a7d3be4575be863871c6bf3eef5ecd34721c784259385ca9101c3a313e010ac942c99de05aaaa602", + "5799cf4b193b730fb99580b186f7477c2cca4d28957326f6f1a5d14116438530e7ec0ce1cd465ad96968ae8a6a09d4d37a060a115919f56fcfebe7b2277cc2df5cc08fb6cda9105ee2512b2e22635aba", + "7520c7b5db9562078049719228db2ac80cb9643ec96c8055aa3b29c2c03d4d99edb0ac", + "c1a7957acbaaf7b444c53d9e0c9436e8a8a3247fd515095d66ddf6201918b40a3668f9a4ccdffcf778da624dca2dda0b08e763ec52fd4ad403ec7563a3504d0cc168b9a77a410029e01dac89567c9b2e6cd726e840351df3f2f58fefe976200a19244150d04153909f660184d656ee95fa7bf8e1d4ec83da1fca34f64bc279b76d257ec623e08baba2cfa4ea9e99646e88f1eb1668c00c0f15b7443c8ab83481611cc3ae85eb89a7bfc40067eb1d2e6354a32426d0ce710e88bc4cc0718b99c325509c9d02a6a980d675a8969be10ee9bef82cafee2fc913475667ccda37b1bc7f13f64e56c449c532658ba8481631c02ead979754c809584a875951619cec8fb040c33f06468ae0266cd8693d6a64cea5912be32d8de95a6da6300b0c50fdcd6001ea41126e7b7e5280d455054a816560028f5ca53c9a50ee52f10e15c5337315bad1f5277acb109a1418649dc6ead2fe14699742fee7182f2f15e54279c7d932ed2799d01d73c97e68bbc94d6f7f56ee0a80efd7c76e3169e10d1a1ba3b5f1eb02369dc43af687461c7a2a3344d13eb5485dca29a67f16b4cb988923060fd3b65d0f0352bb634bcc44f2fe668836dcd0f604150049835135dc4b4fbf90fb334b3938a1f137eb32f047c65b85e6c1173b890b6d0162b48b186d1f1af8521945924ac8ac8efec321bf34f1d4b3d4a304a10313052c652d53f6ecb8a55586614e8950cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c", + "5000000000000000001111111111111111" + ] + } + }, + { + "description": "address, pubkey, output and hash from internalPubkey and a script tree with one leaf", + "arguments": { + "internalPubkey": "50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0", + "scriptTree": { + "output": "83d8ee77a0f3a32a5cea96fd1624d623b836c1e5d1ac2dcde46814b619320c18 OP_CHECKSIG" + } + }, + "expected": { + "name": "p2tr", + "address": "bc1pjegs09vkeder9m4sw3ycjf2rnpa8nljdqmuleunk9eshu8cq3xysvhgp2u", + "pubkey": "9651079596cb7232eeb07449892543987a79fe4d06f9fcf2762e617e1f008989", + "output": "OP_1 9651079596cb7232eeb07449892543987a79fe4d06f9fcf2762e617e1f008989", + "hash": "16e3f3b8b9c1e453c56b547785cdd25259d65823a2064f30783acc58ef012633", + "signature": null, + "input": null, + "witness": null + } + }, + { + "description": "address, pubkey, output and hash from internalPubkey and a script tree with two leafs", + "arguments": { + "internalPubkey": "2258b1c3160be0864a541854eec9164a572f094f7562628281a8073bb89173a7", + "scriptTree": [ + { + "output": "d826a0a53abb6ffc60df25b9c152870578faef4b2eb5a09bdd672bbe32cdd79b OP_CHECKSIG" + }, + { + "output": "d826a0a53abb6ffc60df25b9c152870578faef4b2eb5a09bdd672bbe32cdd79b OP_CHECKSIG" + } + ] + }, + "expected": { + "name": "p2tr", + "address": "bc1ptj0v8rwcj6s36p4r26ws6htx0fct43n0mxdvdeh9043whlxlq3kq9965ke", + "pubkey": "5c9ec38dd896a11d06a3569d0d5d667a70bac66fd99ac6e6e57d62ebfcdf046c", + "output": "OP_1 5c9ec38dd896a11d06a3569d0d5d667a70bac66fd99ac6e6e57d62ebfcdf046c", + "hash": "ce00198cd4667abae1f94aa5862d089e2967af5aec20715c692db74e3d66bb73", + "signature": null, + "input": null, + "witness": null + } + }, + { + "description": "address, pubkey, output and hash from internalPubkey and a script tree with three leafs", + "arguments": { + "internalPubkey": "7631cacec3343052d87ef4d0065f61dde82d7d2db0c1cc02ef61ef3c982ea763", + "scriptTree": [ + { + "output": "50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0 OP_CHECKSIG" + }, + [ + { + "output": "50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0 OP_CHECKSIG" + }, + { + "output": "9b4d495b74887815a1ff623c055c6eac6b6b2e07d2a016d6526ebac71dd99744 OP_CHECKSIG" + } + ] + ] + }, + "expected": { + "name": "p2tr", + "address": "bc1pkq0t8nkmqswn3qjg9uy6ux2hsyyz4as25v8unfjc9s8q2e4c00sqku9lxh", + "pubkey": "b01eb3cedb041d3882482f09ae195781082af60aa30fc9a6582c0e0566b87be0", + "output": "OP_1 b01eb3cedb041d3882482f09ae195781082af60aa30fc9a6582c0e0566b87be0", + "hash": "7ae0cc2057b1a7bf0e09c787e1d7b6b2355ac112a7b80380a5c1e942155b0c0f", + "signature": null, + "input": null, + "witness": null + } + }, + { + "description": "address, pubkey, output and hash from internalPubkey and a script tree with four leafs", + "arguments": { + "internalPubkey": "d0c19def28bb1b39451c1a814737615983967780d223b79969ba692182c6006b", + "scriptTree": [ + [ + { + "output": "9b4d495b74887815a1ff623c055c6eac6b6b2e07d2a016d6526ebac71dd99744 OP_CHECKSIG" + }, + { + "output": "50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0 OP_CHECKSIG" + } + ], + [ + { + "output": "50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0 OP_CHECKSIG" + }, + { + "output": "9b4d495b74887815a1ff623c055c6eac6b6b2e07d2a016d6526ebac71dd99744 OP_CHECKSIG" + } + ] + ] + }, + "expected": { + "name": "p2tr", + "address": "bc1pstdzevc40j059s0473rghhv9e05l9f5xv7l6dtlavvq22rzfna3syjvjut", + "pubkey": "82da2cb3157c9f42c1f5f4468bdd85cbe9f2a68667bfa6affd6300a50c499f63", + "output": "OP_1 82da2cb3157c9f42c1f5f4468bdd85cbe9f2a68667bfa6affd6300a50c499f63", + "hash": "d673e784eac9b70289130a0bd359023a0fbdde51dc069b9efb4157c2cdce3ea5", + "signature": null, + "input": null, + "witness": null + } + }, + { + "description": "address, pubkey, output and hash from internalPubkey and a script tree with seven leafs", + "arguments": { + "internalPubkey": "f95886b02a84928c5c15bdca32784993105f73de27fa6ad8c1a60389b999267c", + "scriptTree": [ + [ + { + "output": "50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0 OP_CHECKSIG" + }, + [ + { + "output": "50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0 OP_CHECKSIG" + }, + { + "output": "2258b1c3160be0864a541854eec9164a572f094f7562628281a8073bb89173a7 OP_CHECKSIG" + } + ] + ], + [ + { + "output": "50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0 OP_CHECKSIG" + }, + [ + { + "output": "50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0 OP_CHECKSIG" + }, + [ + { + "output": "50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0 OP_CHECKSIG" + }, + { + "output": "03a669ea926f381582ec4a000b9472ba8a17347f5fb159eddd4a07036a6718eb OP_CHECKSIG" + } + ] + ] + ] + ] + }, + "expected": { + "name": "p2tr", + "address": "bc1pfas4r5s5208puwzj20hvwg2dw2kanc06yxczzdd66729z63pk43q7zwlu6", + "pubkey": "4f6151d21453ce1e385253eec7214d72add9e1fa21b02135bad794516a21b562", + "output": "OP_1 4f6151d21453ce1e385253eec7214d72add9e1fa21b02135bad794516a21b562", + "hash": "16fb2e99bdf86f67ee6980d0418658f15df7e19476053b58f45a89df2e219b1b", + "signature": null, + "input": null, + "witness": null + } + }, + { + "description": "address, pubkey, and output from internalPubkey redeem, and hash (one leaf, no tree)", + "arguments": { + "internalPubkey": "aba457d16a8d59151c387f24d1eb887efbe24644c1ee64b261282e7baebdb247", + "redeem": { + "output": "50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac4 OP_CHECKSIG" + }, + "hash": "b424dea09f840b932a00373cdcdbd25650b8c3acfe54a9f4a641a286721b8d26" + }, + "expected": { + "name": "p2tr", + "address": "bc1pnxyp0ahcg53jzgrzj57hnlgdtqtzn7qqhmgjgczk8hzhcltq974qazepzf", + "pubkey": "998817f6f84523212062953d79fd0d581629f800bed12460563dc57c7d602faa", + "output": "OP_1 998817f6f84523212062953d79fd0d581629f800bed12460563dc57c7d602faa", + "signature": null, + "input": null, + "witness": [ + "2050929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac4ac", + "c0aba457d16a8d59151c387f24d1eb887efbe24644c1ee64b261282e7baebdb247" + ] + } + }, + { + "description": "address, pubkey, output and hash from internalPubkey and a script tree with seven leafs (2)", + "arguments": { + "internalPubkey": "aba457d16a8d59151c387f24d1eb887efbe24644c1ee64b261282e7baebdb247", + "redeem": { + "output": "50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac4 OP_CHECKSIG" + }, + "scriptTree": [ + { + "output": "00a9da96087a72258f83b338ef7f0ea8cbbe05da5f18f091eb397d1ecbf7c3d3 OP_CHECKSIG" + }, + [ + [ + { + "output": "00a9da96087a72258f83b338ef7f0ea8cbbe05da5f18f091eb397d1ecbf7c3d4 OP_CHECKSIG" + }, + [ + { + "output": "50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac1 OP_CHECKSIG" + }, + { + "output": "50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac2 OP_CHECKSIG" + } + ] + ], + [ + [ + { + "output": "50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac3 OP_CHECKSIG" + }, + { + "output": "50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac4 OP_CHECKSIG" + } + ], + { + "output": "00a9da96087a72258f83b338ef7f0ea8cbbe05da5f18f091eb397d1ecbf7c3d5 OP_CHECKSIG" + } + ] + ] + ] + }, + "expected": { + "name": "p2tr", + "address": "bc1pd2llmtym6c5hyecf5zqsyjz9q0jlxaaksw9j0atx8lc8a0e0vrmsw9ewly", + "pubkey": "6abffdac9bd629726709a08102484503e5f377b6838b27f5663ff07ebf2f60f7", + "output": "OP_1 6abffdac9bd629726709a08102484503e5f377b6838b27f5663ff07ebf2f60f7", + "hash": "88b7e3b495a84aa2bc12780b1773f130ce5eb747b0c28dc4840b7c9280f7326d", + "signature": null, + "input": null, + "witness": [ + "2050929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac4ac", + "c0aba457d16a8d59151c387f24d1eb887efbe24644c1ee64b261282e7baebdb247dac795766bbda1eaeaa45e5bfa0a950fdd5f4c4aada5b1f3082edc9689b9fd0a315fb34a7a93dcaed5e26cf7468be5bd377dda7a4d29128f7dd98db6da9bf04325fff3aa86365bac7534dcb6495867109941ec444dd35294e0706e29e051066d73e0d427bd3249bb921fa78c04fb76511f583ff48c97210d17c2d9dcfbb95023" + ] + } + }, + { + "description": "BIP341 Test case 1", + "arguments": { + "internalPubkey": "d6889cb081036e0faefa3a35157ad71086b123b2b144b649798b494c300a961d" + }, + "options": {}, + "expected": { + "name": "p2tr", + "output": "OP_1 53a1f6e454df1aa2776a2814a721372d6258050de330b3c6d10ee8f4e0dda343", + "pubkey": "53a1f6e454df1aa2776a2814a721372d6258050de330b3c6d10ee8f4e0dda343", + "address": "bc1p2wsldez5mud2yam29q22wgfh9439spgduvct83k3pm50fcxa5dps59h4z5", + "signature": null, + "input": null, + "witness": null + } + }, + { + "description": "BIP341 Test case 2", + "arguments": { + "internalPubkey": "187791b6f712a8ea41c8ecdd0ee77fab3e85263b37e1ec18a3651926b3a6cf27", + "redeem": { + "output": "d85a959b0290bf19bb89ed43c916be835475d013da4b362117393e25a48229b8 OP_CHECKSIG", + "redeemVersion": 192 + }, + "scriptTree": { + "output": "d85a959b0290bf19bb89ed43c916be835475d013da4b362117393e25a48229b8 OP_CHECKSIG", + "version": 192 + } + }, + "options": {}, + "expected": { + "name": "p2tr", + "output": "OP_1 147c9c57132f6e7ecddba9800bb0c4449251c92a1e60371ee77557b6620f3ea3", + "pubkey": "147c9c57132f6e7ecddba9800bb0c4449251c92a1e60371ee77557b6620f3ea3", + "address": "bc1pz37fc4cn9ah8anwm4xqqhvxygjf9rjf2resrw8h8w4tmvcs0863sa2e586", + "hash": "5b75adecf53548f3ec6ad7d78383bf84cc57b55a3127c72b9a2481752dd88b21", + "witness": [ + "20d85a959b0290bf19bb89ed43c916be835475d013da4b362117393e25a48229b8ac", + "c1187791b6f712a8ea41c8ecdd0ee77fab3e85263b37e1ec18a3651926b3a6cf27" + ], + "redeem": { + "output": "d85a959b0290bf19bb89ed43c916be835475d013da4b362117393e25a48229b8 OP_CHECKSIG", + "redeemVersion": 192 + }, + "signature": null, + "input": null + } + }, + { + "description": "BIP341 Test case 3", + "arguments": { + "internalPubkey": "93478e9488f956df2396be2ce6c5cced75f900dfa18e7dabd2428aae78451820", + "redeem": { + "output": "b617298552a72ade070667e86ca63b8f5789a9fe8731ef91202a91c9f3459007 OP_CHECKSIG", + "redeemVersion": 192 + }, + "scriptTree": { + "output": "b617298552a72ade070667e86ca63b8f5789a9fe8731ef91202a91c9f3459007 OP_CHECKSIG", + "version": 192 + } + }, + "options": {}, + "expected": { + "name": "p2tr", + "output": "OP_1 e4d810fd50586274face62b8a807eb9719cef49c04177cc6b76a9a4251d5450e", + "pubkey": "e4d810fd50586274face62b8a807eb9719cef49c04177cc6b76a9a4251d5450e", + "address": "bc1punvppl2stp38f7kwv2u2spltjuvuaayuqsthe34hd2dyy5w4g58qqfuag5", + "hash": "c525714a7f49c28aedbbba78c005931a81c234b2f6c99a73e4d06082adc8bf2b", + "witness": [ + "20b617298552a72ade070667e86ca63b8f5789a9fe8731ef91202a91c9f3459007ac", + "c093478e9488f956df2396be2ce6c5cced75f900dfa18e7dabd2428aae78451820" + ], + "signature": null, + "input": null + } + }, + { + "description": "BIP341 Test case 4 - spend leaf 0", + "arguments": { + "internalPubkey": "ee4fe085983462a184015d1f782d6a5f8b9c2b60130aff050ce221ecf3786592", + "redeem": { + "output": "387671353e273264c495656e27e39ba899ea8fee3bb69fb2a680e22093447d48 OP_CHECKSIG", + "redeemVersion": 192 + }, + "scriptTree": [ + { + "output": "387671353e273264c495656e27e39ba899ea8fee3bb69fb2a680e22093447d48 OP_CHECKSIG", + "version": 192 + }, + { + "output": "424950333431", + "version": 152 + } + ] + }, + "options": {}, + "expected": { + "name": "p2tr", + "output": "OP_1 0f63ca2c7639b9bb4be0465cc0aa3ee78a0761ba5f5f7d6ff8eab340f09da561", + "pubkey": "0f63ca2c7639b9bb4be0465cc0aa3ee78a0761ba5f5f7d6ff8eab340f09da561", + "address": "bc1ppa3u5trk8xumkjlqgewvp237u79qwcd6ta0h6mlca2e5puya54ssw9zq0y", + "hash": "f3004d6c183e038105d436db1424f321613366cbb7b05939bf05d763a9ebb962", + "witness": [ + "20387671353e273264c495656e27e39ba899ea8fee3bb69fb2a680e22093447d48ac", + "c0ee4fe085983462a184015d1f782d6a5f8b9c2b60130aff050ce221ecf37865927b2c2af8aa3e8b7bfe2f62a155f91427489c5c3b32be47e0b3fac755fc780e0e" + ], + "signature": null, + "input": null + } + }, + { + "description": "BIP341 Test case 4 - spend leaf 1", + "arguments": { + "internalPubkey": "ee4fe085983462a184015d1f782d6a5f8b9c2b60130aff050ce221ecf3786592", + "redeem": { + "output": "424950333431", + "redeemVersion": 152 + }, + "scriptTree": [ + { + "output": "387671353e273264c495656e27e39ba899ea8fee3bb69fb2a680e22093447d48 OP_CHECKSIG", + "version": 192 + }, + { + "output": "424950333431", + "version": 152 + } + ] + }, + "options": {}, + "expected": { + "name": "p2tr", + "output": "OP_1 0f63ca2c7639b9bb4be0465cc0aa3ee78a0761ba5f5f7d6ff8eab340f09da561", + "pubkey": "0f63ca2c7639b9bb4be0465cc0aa3ee78a0761ba5f5f7d6ff8eab340f09da561", + "address": "bc1ppa3u5trk8xumkjlqgewvp237u79qwcd6ta0h6mlca2e5puya54ssw9zq0y", + "hash": "f3004d6c183e038105d436db1424f321613366cbb7b05939bf05d763a9ebb962", + "witness": [ + "06424950333431", + "98ee4fe085983462a184015d1f782d6a5f8b9c2b60130aff050ce221ecf37865928ad69ec7cf41c2a4001fd1f738bf1e505ce2277acdcaa63fe4765192497f47a7" + ], + "signature": null, + "input": null + } + }, + { + "description": "BIP341 Test case 5 - spend leaf 0", + "arguments": { + "internalPubkey": "f9f400803e683727b14f463836e1e78e1c64417638aa066919291a225f0e8dd8", + "redeem": { + "output": "44b178d64c32c4a05cc4f4d1407268f764c940d20ce97abfd44db5c3592b72fd OP_CHECKSIG", + "redeemVersion": 192 + }, + "scriptTree": [ + { + "output": "44b178d64c32c4a05cc4f4d1407268f764c940d20ce97abfd44db5c3592b72fd OP_CHECKSIG", + "version": 192 + }, + { + "output": "546170726f6f74", + "version": 82 + } + ] + }, + "options": {}, + "expected": { + "name": "p2tr", + "output": "OP_1 053690babeabbb7850c32eead0acf8df990ced79f7a31e358fabf2658b4bc587", + "pubkey": "053690babeabbb7850c32eead0acf8df990ced79f7a31e358fabf2658b4bc587", + "address": "bc1pq5mfpw474wahs5xr9m4dpt8cm7vsemte7733udv040extz6tckrs29g04c", + "hash": "d9c2c32808b41c0301d876d49c0af72e1d98e84b99ca9b4bb67fea1a7424b755", + "witness": [ + "2044b178d64c32c4a05cc4f4d1407268f764c940d20ce97abfd44db5c3592b72fdac", + "c1f9f400803e683727b14f463836e1e78e1c64417638aa066919291a225f0e8dd8e44d5f8fa5892c8b6d4d09a08d36edd0b08636e30311302e2448ad8172fb3433" + ], + "signature": null, + "input": null + } + }, + { + "description": "BIP341 Test case 5 - spend leaf 1", + "arguments": { + "internalPubkey": "f9f400803e683727b14f463836e1e78e1c64417638aa066919291a225f0e8dd8", + "redeem": { + "output": "546170726f6f74", + "redeemVersion": 82 + }, + "scriptTree": [ + { + "output": "44b178d64c32c4a05cc4f4d1407268f764c940d20ce97abfd44db5c3592b72fd OP_CHECKSIG", + "version": 192 + }, + { + "output": "546170726f6f74", + "version": 82 + } + ] + }, + "options": {}, + "expected": { + "name": "p2tr", + "output": "OP_1 053690babeabbb7850c32eead0acf8df990ced79f7a31e358fabf2658b4bc587", + "pubkey": "053690babeabbb7850c32eead0acf8df990ced79f7a31e358fabf2658b4bc587", + "address": "bc1pq5mfpw474wahs5xr9m4dpt8cm7vsemte7733udv040extz6tckrs29g04c", + "hash": "d9c2c32808b41c0301d876d49c0af72e1d98e84b99ca9b4bb67fea1a7424b755", + "witness": [ + "07546170726f6f74", + "53f9f400803e683727b14f463836e1e78e1c64417638aa066919291a225f0e8dd864512fecdb5afa04f98839b50e6f0cb7b1e539bf6f205f67934083cdcc3c8d89" + ], + "signature": null, + "input": null + } + }, + { + "description": "BIP341 Test case 6 - spend leaf 0", + "arguments": { + "internalPubkey": "e0dfe2300b0dd746a3f8674dfd4525623639042569d829c7f0eed9602d263e6f", + "redeem": { + "output": "72ea6adcf1d371dea8fba1035a09f3d24ed5a059799bae114084130ee5898e69 OP_CHECKSIG", + "redeemVersion": 192 + }, + "scriptTree": [ + { + "output": "72ea6adcf1d371dea8fba1035a09f3d24ed5a059799bae114084130ee5898e69 OP_CHECKSIG", + "version": 192 + }, + [ + { + "output": "2352d137f2f3ab38d1eaa976758873377fa5ebb817372c71e2c542313d4abda8 OP_CHECKSIG", + "version": 192 + }, + { + "output": "7337c0dd4253cb86f2c43a2351aadd82cccb12a172cd120452b9bb8324f2186a OP_CHECKSIG", + "version": 192 + } + ] + ] + }, + "options": {}, + "expected": { + "name": "p2tr", + "output": "OP_1 91b64d5324723a985170e4dc5a0f84c041804f2cd12660fa5dec09fc21783605", + "pubkey": "91b64d5324723a985170e4dc5a0f84c041804f2cd12660fa5dec09fc21783605", + "address": "bc1pjxmy65eywgafs5tsunw95ruycpqcqnev6ynxp7jaasylcgtcxczs6n332e", + "hash": "ccbd66c6f7e8fdab47b3a486f59d28262be857f30d4773f2d5ea47f7761ce0e2", + "witness": [ + "2072ea6adcf1d371dea8fba1035a09f3d24ed5a059799bae114084130ee5898e69ac", + "c0e0dfe2300b0dd746a3f8674dfd4525623639042569d829c7f0eed9602d263e6fffe578e9ea769027e4f5a3de40732f75a88a6353a09d767ddeb66accef85e553" + ], + "signature": null, + "input": null + } + }, + { + "description": "BIP341 Test case 6 - spend leaf 1", + "arguments": { + "internalPubkey": "e0dfe2300b0dd746a3f8674dfd4525623639042569d829c7f0eed9602d263e6f", + "redeem": { + "output": "2352d137f2f3ab38d1eaa976758873377fa5ebb817372c71e2c542313d4abda8 OP_CHECKSIG", + "redeemVersion": 192 + }, + "scriptTree": [ + { + "output": "72ea6adcf1d371dea8fba1035a09f3d24ed5a059799bae114084130ee5898e69 OP_CHECKSIG", + "version": 192 + }, + [ + { + "output": "2352d137f2f3ab38d1eaa976758873377fa5ebb817372c71e2c542313d4abda8 OP_CHECKSIG", + "version": 192 + }, + { + "output": "7337c0dd4253cb86f2c43a2351aadd82cccb12a172cd120452b9bb8324f2186a OP_CHECKSIG", + "version": 192 + } + ] + ] + }, + "options": {}, + "expected": { + "name": "p2tr", + "output": "OP_1 91b64d5324723a985170e4dc5a0f84c041804f2cd12660fa5dec09fc21783605", + "pubkey": "91b64d5324723a985170e4dc5a0f84c041804f2cd12660fa5dec09fc21783605", + "address": "bc1pjxmy65eywgafs5tsunw95ruycpqcqnev6ynxp7jaasylcgtcxczs6n332e", + "hash": "ccbd66c6f7e8fdab47b3a486f59d28262be857f30d4773f2d5ea47f7761ce0e2", + "witness": [ + "202352d137f2f3ab38d1eaa976758873377fa5ebb817372c71e2c542313d4abda8ac", + "c0e0dfe2300b0dd746a3f8674dfd4525623639042569d829c7f0eed9602d263e6f9e31407bffa15fefbf5090b149d53959ecdf3f62b1246780238c24501d5ceaf62645a02e0aac1fe69d69755733a9b7621b694bb5b5cde2bbfc94066ed62b9817" + ], + "signature": null, + "input": null + } + }, + { + "description": "BIP341 Test case 6 - spend leaf 2", + "arguments": { + "internalPubkey": "e0dfe2300b0dd746a3f8674dfd4525623639042569d829c7f0eed9602d263e6f", + "redeem": { + "output": "7337c0dd4253cb86f2c43a2351aadd82cccb12a172cd120452b9bb8324f2186a OP_CHECKSIG", + "redeemVersion": 192 + }, + "scriptTree": [ + { + "output": "72ea6adcf1d371dea8fba1035a09f3d24ed5a059799bae114084130ee5898e69 OP_CHECKSIG", + "version": 192 + }, + [ + { + "output": "2352d137f2f3ab38d1eaa976758873377fa5ebb817372c71e2c542313d4abda8 OP_CHECKSIG", + "version": 192 + }, + { + "output": "7337c0dd4253cb86f2c43a2351aadd82cccb12a172cd120452b9bb8324f2186a OP_CHECKSIG", + "version": 192 + } + ] + ] + }, + "options": {}, + "expected": { + "name": "p2tr", + "output": "OP_1 91b64d5324723a985170e4dc5a0f84c041804f2cd12660fa5dec09fc21783605", + "pubkey": "91b64d5324723a985170e4dc5a0f84c041804f2cd12660fa5dec09fc21783605", + "address": "bc1pjxmy65eywgafs5tsunw95ruycpqcqnev6ynxp7jaasylcgtcxczs6n332e", + "hash": "ccbd66c6f7e8fdab47b3a486f59d28262be857f30d4773f2d5ea47f7761ce0e2", + "witness": [ + "207337c0dd4253cb86f2c43a2351aadd82cccb12a172cd120452b9bb8324f2186aac", + "c0e0dfe2300b0dd746a3f8674dfd4525623639042569d829c7f0eed9602d263e6fba982a91d4fc552163cb1c0da03676102d5b7a014304c01f0c77b2b8e888de1c2645a02e0aac1fe69d69755733a9b7621b694bb5b5cde2bbfc94066ed62b9817" + ], + "signature": null, + "input": null + } + }, + { + "description": "BIP341 Test case 7 - spend leaf 0", + "arguments": { + "internalPubkey": "55adf4e8967fbd2e29f20ac896e60c3b0f1d5b0efa9d34941b5958c7b0a0312d", + "redeem": { + "output": "71981521ad9fc9036687364118fb6ccd2035b96a423c59c5430e98310a11abe2 OP_CHECKSIG" + }, + "scriptTree": [ + { + "output": "71981521ad9fc9036687364118fb6ccd2035b96a423c59c5430e98310a11abe2 OP_CHECKSIG", + "version": 192 + }, + [ + { + "output": "d5094d2dbe9b76e2c245a2b89b6006888952e2faa6a149ae318d69e520617748 OP_CHECKSIG", + "version": 192 + }, + { + "output": "c440b462ad48c7a77f94cd4532d8f2119dcebbd7c9764557e62726419b08ad4c OP_CHECKSIG", + "version": 192 + } + ] + ] + }, + "options": {}, + "expected": { + "name": "p2tr", + "output": "OP_1 75169f4001aa68f15bbed28b218df1d0a62cbbcf1188c6665110c293c907b831", + "pubkey": "75169f4001aa68f15bbed28b218df1d0a62cbbcf1188c6665110c293c907b831", + "address": "bc1pw5tf7sqp4f50zka7629jrr036znzew70zxyvvej3zrpf8jg8hqcssyuewe", + "hash": "2f6b2c5397b6d68ca18e09a3f05161668ffe93a988582d55c6f07bd5b3329def", + "witness": [ + "2071981521ad9fc9036687364118fb6ccd2035b96a423c59c5430e98310a11abe2ac", + "c155adf4e8967fbd2e29f20ac896e60c3b0f1d5b0efa9d34941b5958c7b0a0312d3cd369a528b326bc9d2133cbd2ac21451acb31681a410434672c8e34fe757e91" + ], + "signature": null, + "input": null + } + }, + { + "description": "BIP341 Test case 7 - spend leaf 1", + "arguments": { + "internalPubkey": "55adf4e8967fbd2e29f20ac896e60c3b0f1d5b0efa9d34941b5958c7b0a0312d", + "redeem": { + "output": "d5094d2dbe9b76e2c245a2b89b6006888952e2faa6a149ae318d69e520617748 OP_CHECKSIG", + "redeemVersion": 192 + }, + "scriptTree": [ + { + "output": "71981521ad9fc9036687364118fb6ccd2035b96a423c59c5430e98310a11abe2 OP_CHECKSIG", + "version": 192 + }, + [ + { + "output": "d5094d2dbe9b76e2c245a2b89b6006888952e2faa6a149ae318d69e520617748 OP_CHECKSIG", + "version": 192 + }, + { + "output": "c440b462ad48c7a77f94cd4532d8f2119dcebbd7c9764557e62726419b08ad4c OP_CHECKSIG", + "version": 192 + } + ] + ] + }, + "options": {}, + "expected": { + "name": "p2tr", + "output": "OP_1 75169f4001aa68f15bbed28b218df1d0a62cbbcf1188c6665110c293c907b831", + "pubkey": "75169f4001aa68f15bbed28b218df1d0a62cbbcf1188c6665110c293c907b831", + "address": "bc1pw5tf7sqp4f50zka7629jrr036znzew70zxyvvej3zrpf8jg8hqcssyuewe", + "hash": "2f6b2c5397b6d68ca18e09a3f05161668ffe93a988582d55c6f07bd5b3329def", + "witness": [ + "20d5094d2dbe9b76e2c245a2b89b6006888952e2faa6a149ae318d69e520617748ac", + "c155adf4e8967fbd2e29f20ac896e60c3b0f1d5b0efa9d34941b5958c7b0a0312dd7485025fceb78b9ed667db36ed8b8dc7b1f0b307ac167fa516fe4352b9f4ef7f154e8e8e17c31d3462d7132589ed29353c6fafdb884c5a6e04ea938834f0d9d" + ], + "signature": null, + "input": null + } + }, + { + "description": "BIP341 Test case 7 - spend leaf 2", + "arguments": { + "internalPubkey": "55adf4e8967fbd2e29f20ac896e60c3b0f1d5b0efa9d34941b5958c7b0a0312d", + "redeem": { + "output": "c440b462ad48c7a77f94cd4532d8f2119dcebbd7c9764557e62726419b08ad4c OP_CHECKSIG", + "redeemVersion": 192 + }, + "scriptTree": [ + { + "output": "71981521ad9fc9036687364118fb6ccd2035b96a423c59c5430e98310a11abe2 OP_CHECKSIG", + "version": 192 + }, + [ + { + "output": "d5094d2dbe9b76e2c245a2b89b6006888952e2faa6a149ae318d69e520617748 OP_CHECKSIG", + "version": 192 + }, + { + "output": "c440b462ad48c7a77f94cd4532d8f2119dcebbd7c9764557e62726419b08ad4c OP_CHECKSIG", + "version": 192 + } + ] + ] + }, + "options": {}, + "expected": { + "name": "p2tr", + "output": "OP_1 75169f4001aa68f15bbed28b218df1d0a62cbbcf1188c6665110c293c907b831", + "pubkey": "75169f4001aa68f15bbed28b218df1d0a62cbbcf1188c6665110c293c907b831", + "address": "bc1pw5tf7sqp4f50zka7629jrr036znzew70zxyvvej3zrpf8jg8hqcssyuewe", + "hash": "2f6b2c5397b6d68ca18e09a3f05161668ffe93a988582d55c6f07bd5b3329def", + "witness": [ + "20c440b462ad48c7a77f94cd4532d8f2119dcebbd7c9764557e62726419b08ad4cac", + "c155adf4e8967fbd2e29f20ac896e60c3b0f1d5b0efa9d34941b5958c7b0a0312d737ed1fe30bc42b8022d717b44f0d93516617af64a64753b7a06bf16b26cd711f154e8e8e17c31d3462d7132589ed29353c6fafdb884c5a6e04ea938834f0d9d" + ], + "signature": null, + "input": null + } + } + ], + "invalid": [ + { + "exception": "Not enough data", + "arguments": {} + }, + { + "exception": "Not enough data", + "arguments": { + "signature": "300602010002010001" + } + }, + { + "description": "Incorrect Witness Version", + "exception": "Output is invalid", + "arguments": { + "output": "OP_0 ab610d22c801def8a1e02368d1b92018970eb52a729919705e8a1a2f60c750f5" + } + }, + { + "description": "Invalid x coordinate for pubkey in pubkey", + "exception": "Invalid pubkey for p2tr", + "arguments": { + "pubkey": "f136e956540197c21ff3c075d32a6e3c82f1ee1e646cc0f08f51b0b5edafa762" + } + }, + { + "description": "Invalid x coordinate for pubkey in output", + "exception": "Invalid pubkey for p2tr", + "arguments": { + "output": "OP_1 f136e956540197c21ff3c075d32a6e3c82f1ee1e646cc0f08f51b0b5edafa762" + } + }, + { + "description": "Invalid x coordinate for pubkey in address", + "exception": "Invalid pubkey for p2tr", + "arguments": { + "address": "bc1p7ymwj4j5qxtuy8lncp6ax2nw8jp0rms7v3kvpuy02xcttmd05a3qmwlnez" + } + }, + { + "description": "Pubkey mismatch between pubkey and output", + "exception": "Pubkey mismatch", + "options": {}, + "arguments": { + "pubkey": "ab610d22c801def8a1e02368d1b92018970eb52a729919705e8a1a2f60c750f5", + "output": "OP_1 12d7dac98d69a086a50b30959a3537950f356ffc6f50a263ab75c8a3ec9d44c1" + } + }, + { + "description": "Pubkey mismatch between pubkey and address", + "exception": "Pubkey mismatch", + "options": {}, + "arguments": { + "pubkey": "ab610d22c801def8a1e02368d1b92018970eb52a729919705e8a1a2f60c750f5", + "address": "bc1pztta4jvddxsgdfgtxz2e5dfhj58n2mludag2ycatwhy28myagnqsnl7mv7" + } + }, + { + "description": "Pubkey mismatch between output and address", + "exception": "Pubkey mismatch", + "options": {}, + "arguments": { + "output": "OP_1 ab610d22c801def8a1e02368d1b92018970eb52a729919705e8a1a2f60c750f5", + "address": "bc1pztta4jvddxsgdfgtxz2e5dfhj58n2mludag2ycatwhy28myagnqsnl7mv7" + } + }, + { + "description": "Pubkey mismatch between internalPubkey and pubkey", + "exception": "Pubkey mismatch", + "options": {}, + "arguments": { + "internalPubkey": "9fa5ffb68821cf559001caa0577eeea4978b29416def328a707b15e91701a2f7", + "pubkey": "ab610d22c801def8a1e02368d1b92018970eb52a729919705e8a1a2f60c750f5" + } + }, + { + "description": "Hash mismatch between scriptTree and hash", + "exception": "Hash mismatch", + "options": {}, + "arguments": { + "internalPubkey": "9fa5ffb68821cf559001caa0577eeea4978b29416def328a707b15e91701a2f7", + "scriptTree": { + "output": "83d8ee77a0f3a32a5cea96fd1624d623b836c1e5d1ac2dcde46814b619320c18 OP_CHECKSIG" + }, + "hash": "b76077013c8e303085e300000000000000000000000000000000000000000000" + } + }, + { + "exception": "Expected Point", + "options": {}, + "arguments": { + "internalPubkey": "9fa5ffb68821cf559001caa0577eeea4978b29416def328a707b15e91701a2f8" + } + }, + { + "exception": "Signature mismatch", + "arguments": { + "pubkey": "ab610d22c801def8a1e02368d1b92018970eb52a729919705e8a1a2f60c750f5", + "signature": "a251221c339a7129dd0b769698aca40d8d9da9570ab796a1820b91ab7dbf5acbea21c88ba8f1e9308a21729baf080734beaf97023882d972f75e380d480fd704", + "witness": [ + "607b8b5b5c8614757736e3d5811790636d2a8e2ea14418f8cff66b2e898b3b7536a49b7c4bc8b3227953194bf5d0548e13e3526fdb36beeefadda1ec834a0db2" + ] + } + }, + { + "exception": "Invalid prefix or Network mismatch", + "arguments": { + "address": "bcrt1prhepe49mpmhclwcqmkzpaz43revunykc7fc0f9az6pq08sn4qe7sxtrd8y" + } + }, + { + "exception": "Invalid address version", + "arguments": { + "address": "bc1z4dss6gkgq8003g0qyd5drwfqrztsadf2w2v3juz73gdz7cx82r6s6rxhwd" + } + }, + { + "exception": "Invalid address data", + "arguments": { + "address": "bc1p4dss6gkgq8003g0qyd5drwfqrztsadf2w2v3juz73gdz7cx82qh3d2w3" + } + }, + { + "description": "Control block length too small", + "exception": "The control-block length is too small. Got 16, expected min 33.", + "arguments": { + "witness": [ + "9675a9982c6398ea9d441cb7a943bcd6ff033cc3a2e01a0178a7d3be4575be863871c6bf3eef5ecd34721c784259385ca9101c3a313e010ac942c99de05aaaa602", + "5799cf4b193b730fb99580b186f7477c2cca4d28957326f6f1a5d14116438530e7ec0ce1cd465ad96968ae8a6a09d4d37a060a115919f56fcfebe7b2277cc2df5cc08fb6cda9105ee2512b2e22635aba", + "7520c7b5db9562078049719228db2ac80cb9643ec96c8055aa3b29c2c03d4d99edb0ac", + "c1a7957acbaaf7b444c53d9e0c9436e8" + ] + } + }, + { + "description": "Control block must have a length of 33 + 32m (0 <= m <= 128)", + "exception": "The control-block length of 40 is incorrect!", + "arguments": { + "witness": [ + "9675a9982c6398ea9d441cb7a943bcd6ff033cc3a2e01a0178a7d3be4575be863871c6bf3eef5ecd34721c784259385ca9101c3a313e010ac942c99de05aaaa602", + "5799cf4b193b730fb99580b186f7477c2cca4d28957326f6f1a5d14116438530e7ec0ce1cd465ad96968ae8a6a09d4d37a060a115919f56fcfebe7b2277cc2df5cc08fb6cda9105ee2512b2e22635aba", + "7520c7b5db9562078049719228db2ac80cb9643ec96c8055aa3b29c2c03d4d99edb0ac", + "c1a7957acbaaf7b444c53d9e0c9436e8a8a3247fd515095d66ddf6201918b40a3668f9a4ccdffcf77" + ] + } + }, + { + "description": "Control block length too large", + "exception": "The script path is too long. Got 129, expected max 128.", + "arguments": { + "witness": [ + "9675a9982c6398ea9d441cb7a943bcd6ff033cc3a2e01a0178a7d3be4575be863871c6bf3eef5ecd34721c784259385ca9101c3a313e010ac942c99de05aaaa602", + "5799cf4b193b730fb99580b186f7477c2cca4d28957326f6f1a5d14116438530e7ec0ce1cd465ad96968ae8a6a09d4d37a060a115919f56fcfebe7b2277cc2df5cc08fb6cda9105ee2512b2e22635aba", + "7520c7b5db9562078049719228db2ac80cb9643ec96c8055aa3b29c2c03d4d99edb0ac", + "c1a7957acbaaf7b444c53d9e0c9436e8a8a3247fd515095d66ddf6201918b40a3668f9a4ccdffcf778da624dca2dda0b08e763ec52fd4ad403ec7563a3504d0cc168b9a77a410029e01dac89567c9b2e6cd726e840351df3f2f58fefe976200a19244150d04153909f660184d656ee95fa7bf8e1d4ec83da1fca34f64bc279b76d257ec623e08baba2cfa4ea9e99646e88f1eb1668c00c0f15b7443c8ab83481611cc3ae85eb89a7bfc40067eb1d2e6354a32426d0ce710e88bc4cc0718b99c325509c9d02a6a980d675a8969be10ee9bef82cafee2fc913475667ccda37b1bc7f13f64e56c449c532658ba8481631c02ead979754c809584a875951619cec8fb040c33f06468ae0266cd8693d6a64cea5912be32d8de95a6da6300b0c50fdcd6001ea41126e7b7e5280d455054a816560028f5ca53c9a50ee52f10e15c5337315bad1f5277acb109a1418649dc6ead2fe14699742fee7182f2f15e54279c7d932ed2799d01d73c97e68bbc94d6f7f56ee0a80efd7c76e3169e10d1a1ba3b5f1eb02369dc43af687461c7a2a3344d13eb5485dca29a67f16b4cb988923060fd3b65d0f0352bb634bcc44f2fe668836dcd0f604150049835135dc4b4fbf90fb334b3938a1f137eb32f047c65b85e6c1173b890b6d0162b48b186d1f1af8521945924ac8ac8efec321bf34f1d4b3d4a304a10313052c652d53f6ecb8a55586614e8950cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c50cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c50cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c50cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c50cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c50cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c50cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c50cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c50cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c50cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c50cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c50cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c50cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c50cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c50cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c50cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c50cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c50cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c50cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c50cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c50cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c50cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c50cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c50cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c50cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c50cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c50cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c50cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c50cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c50cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c50cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c50cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c50cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c50cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c50cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c50cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c50cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c50cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c50cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c50cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c50cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c50cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c50cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c50cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c50cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c50cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c50cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c50cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c50cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c50cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c50cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c50cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c50cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c50cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c50cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c50cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c50cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c" + ] + } + }, + { + "description": "Invalid internalPubkey in control block", + "exception": "Invalid internalPubkey for p2tr witness", + "arguments": { + "witness": [ + "9675a9982c6398ea9d441cb7a943bcd6ff033cc3a2e01a0178a7d3be4575be863871c6bf3eef5ecd34721c784259385ca9101c3a313e010ac942c99de05aaaa602", + "5799cf4b193b730fb99580b186f7477c2cca4d28957326f6f1a5d14116438530e7ec0ce1cd465ad96968ae8a6a09d4d37a060a115919f56fcfebe7b2277cc2df5cc08fb6cda9105ee2512b2e22635aba", + "7520c7b5db9562078049719228db2ac80cb9643ec96c8055aa3b29c2c03d4d99edb0ac", + "c14444444444444444453d9e0c9436e8a8a3247fd515095d66ddf6201918b40a3668f9a4ccdffcf778da624dca2dda0b08e763ec52fd4ad403ec7563a3504d0cc168b9a77a410029e01dac89567c9b2e6cd726e840351df3f2f58fefe976200a19244150d04153909f660184d656ee95fa7bf8e1d4ec83da1fca34f64bc279b76d257ec623e08baba2cfa4ea9e99646e88f1eb1668c00c0f15b7443c8ab83481611cc3ae85eb89a7bfc40067eb1d2e6354a32426d0ce710e88bc4cc0718b99c325509c9d02a6a980d675a8969be10ee9bef82cafee2fc913475667ccda37b1bc7f13f64e56c449c532658ba8481631c02ead979754c809584a875951619cec8fb040c33f06468ae0266cd8693d6a64cea5912be32d8de95a6da6300b0c50fdcd6001ea41126e7b7e5280d455054a816560028f5ca53c9a50ee52f10e15c5337315bad1f5277acb109a1418649dc6ead2fe14699742fee7182f2f15e54279c7d932ed2799d01d73c97e68bbc94d6f7f56ee0a80efd7c76e3169e10d1a1ba3b5f1eb02369dc43af687461c7a2a3344d13eb5485dca29a67f16b4cb988923060fd3b65d0f0352bb634bcc44f2fe668836dcd0f604150049835135dc4b4fbf90fb334b3938a1f137eb32f047c65b85e6c1173b890b6d0162b48b186d1f1af8521945924ac8ac8efec321bf34f1d4b3d4a304a10313052c652d53f6ecb8a55586614e8950cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c" + ] + } + }, + { + "description": "internalPubkey mismatch between control block and internalKey", + "exception": "Internal pubkey mismatch", + "arguments": { + "internalPubkey": "9fa5ffb68821cf559001caa0577eeea4978b29416def328a707b15e91701a2f7", + "witness": [ + "9675a9982c6398ea9d441cb7a943bcd6ff033cc3a2e01a0178a7d3be4575be863871c6bf3eef5ecd34721c784259385ca9101c3a313e010ac942c99de05aaaa602", + "5799cf4b193b730fb99580b186f7477c2cca4d28957326f6f1a5d14116438530e7ec0ce1cd465ad96968ae8a6a09d4d37a060a115919f56fcfebe7b2277cc2df5cc08fb6cda9105ee2512b2e22635aba", + "7520c7b5db9562078049719228db2ac80cb9643ec96c8055aa3b29c2c03d4d99edb0ac", + "c1a7957acbaaf7b444c53d9e0c9436e8a8a3247fd515095d66ddf6201918b40a3668f9a4ccdffcf778da624dca2dda0b08e763ec52fd4ad403ec7563a3504d0cc168b9a77a410029e01dac89567c9b2e6cd726e840351df3f2f58fefe976200a19244150d04153909f660184d656ee95fa7bf8e1d4ec83da1fca34f64bc279b76d257ec623e08baba2cfa4ea9e99646e88f1eb1668c00c0f15b7443c8ab83481611cc3ae85eb89a7bfc40067eb1d2e6354a32426d0ce710e88bc4cc0718b99c325509c9d02a6a980d675a8969be10ee9bef82cafee2fc913475667ccda37b1bc7f13f64e56c449c532658ba8481631c02ead979754c809584a875951619cec8fb040c33f06468ae0266cd8693d6a64cea5912be32d8de95a6da6300b0c50fdcd6001ea41126e7b7e5280d455054a816560028f5ca53c9a50ee52f10e15c5337315bad1f5277acb109a1418649dc6ead2fe14699742fee7182f2f15e54279c7d932ed2799d01d73c97e68bbc94d6f7f56ee0a80efd7c76e3169e10d1a1ba3b5f1eb02369dc43af687461c7a2a3344d13eb5485dca29a67f16b4cb988923060fd3b65d0f0352bb634bcc44f2fe668836dcd0f604150049835135dc4b4fbf90fb334b3938a1f137eb32f047c65b85e6c1173b890b6d0162b48b186d1f1af8521945924ac8ac8efec321bf34f1d4b3d4a304a10313052c652d53f6ecb8a55586614e8950cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c" + ] + } + }, + { + "description": "pubkey mismatch between outputKey and pubkey", + "exception": "Pubkey mismatch for p2tr witness", + "arguments": { + "pubkey": "df0e070ca2fca05ecd191bdba047841d62414b2bdcb6249c258fd64c0dd251ff", + "witness": [ + "9675a9982c6398ea9d441cb7a943bcd6ff033cc3a2e01a0178a7d3be4575be863871c6bf3eef5ecd34721c784259385ca9101c3a313e010ac942c99de05aaaa602", + "5799cf4b193b730fb99580b186f7477c2cca4d28957326f6f1a5d14116438530e7ec0ce1cd465ad96968ae8a6a09d4d37a060a115919f56fcfebe7b2277cc2df5cc08fb6cda9105ee2512b2e22635aba", + "7520c7b5db9562078049719228db2ac80cb9643ec96c8055aa3b29c2c03d4d99edb0ac", + "c1a7957acbaaf7b444c53d9e0c9436e8a8a3247fd515095d66ddf6201918b40a3668f9a4ccdffcf778da624dca2dda0b08e763ec52fd4ad403ec7563a3504d0cc168b9a77a410029e01dac89567c9b2e6cd726e840351df3f2f58fefe976200a19244150d04153909f660184d656ee95fa7bf8e1d4ec83da1fca34f64bc279b76d257ec623e08baba2cfa4ea9e99646e88f1eb1668c00c0f15b7443c8ab83481611cc3ae85eb89a7bfc40067eb1d2e6354a32426d0ce710e88bc4cc0718b99c325509c9d02a6a980d675a8969be10ee9bef82cafee2fc913475667ccda37b1bc7f13f64e56c449c532658ba8481631c02ead979754c809584a875951619cec8fb040c33f06468ae0266cd8693d6a64cea5912be32d8de95a6da6300b0c50fdcd6001ea41126e7b7e5280d455054a816560028f5ca53c9a50ee52f10e15c5337315bad1f5277acb109a1418649dc6ead2fe14699742fee7182f2f15e54279c7d932ed2799d01d73c97e68bbc94d6f7f56ee0a80efd7c76e3169e10d1a1ba3b5f1eb02369dc43af687461c7a2a3344d13eb5485dca29a67f16b4cb988923060fd3b65d0f0352bb634bcc44f2fe668836dcd0f604150049835135dc4b4fbf90fb334b3938a1f137eb32f047c65b85e6c1173b890b6d0162b48b186d1f1af8521945924ac8ac8efec321bf34f1d4b3d4a304a10313052c652d53f6ecb8a55586614e8950cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c" + ] + } + }, + { + "description": "parity", + "exception": "Incorrect parity", + "arguments": { + "witness": [ + "9675a9982c6398ea9d441cb7a943bcd6ff033cc3a2e01a0178a7d3be4575be863871c6bf3eef5ecd34721c784259385ca9101c3a313e010ac942c99de05aaaa602", + "5799cf4b193b730fb99580b186f7477c2cca4d28957326f6f1a5d14116438530e7ec0ce1cd465ad96968ae8a6a09d4d37a060a115919f56fcfebe7b2277cc2df5cc08fb6cda9105ee2512b2e22635aba", + "7520c7b5db9562078049719228db2ac80cb9643ec96c8055aa3b29c2c03d4d99edb0ac", + "c0a7957acbaaf7b444c53d9e0c9436e8a8a3247fd515095d66ddf6201918b40a3668f9a4ccdffcf778da624dca2dda0b08e763ec52fd4ad403ec7563a3504d0cc168b9a77a410029e01dac89567c9b2e6cd726e840351df3f2f58fefe976200a19244150d04153909f660184d656ee95fa7bf8e1d4ec83da1fca34f64bc279b76d257ec623e08baba2cfa4ea9e99646e88f1eb1668c00c0f15b7443c8ab83481611cc3ae85eb89a7bfc40067eb1d2e6354a32426d0ce710e88bc4cc0718b99c325509c9d02a6a980d675a8969be10ee9bef82cafee2fc913475667ccda37b1bc7f13f64e56c449c532658ba8481631c02ead979754c809584a875951619cec8fb040c33f06468ae0266cd8693d6a64cea5912be32d8de95a6da6300b0c50fdcd6001ea41126e7b7e5280d455054a816560028f5ca53c9a50ee52f10e15c5337315bad1f5277acb109a1418649dc6ead2fe14699742fee7182f2f15e54279c7d932ed2799d01d73c97e68bbc94d6f7f56ee0a80efd7c76e3169e10d1a1ba3b5f1eb02369dc43af687461c7a2a3344d13eb5485dca29a67f16b4cb988923060fd3b65d0f0352bb634bcc44f2fe668836dcd0f604150049835135dc4b4fbf90fb334b3938a1f137eb32f047c65b85e6c1173b890b6d0162b48b186d1f1af8521945924ac8ac8efec321bf34f1d4b3d4a304a10313052c652d53f6ecb8a55586614e8950cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c" + ] + } + }, + { + "description": "Script Tree is not a binary tree (has tree leafs)", + "exception": "property \"scriptTree\" of type \\?isTaptree, got Array", + "options": {}, + "arguments": { + "internalPubkey": "9fa5ffb68821cf559001caa0577eeea4978b29416def328a707b15e91701a2f7", + "scriptTree": [ + { + "output": "71981521ad9fc9036687364118fb6ccd2035b96a423c59c5430e98310a11abe2 OP_CHECKSIG", + "version": 192 + }, + [ + { + "output": "d5094d2dbe9b76e2c245a2b89b6006888952e2faa6a149ae318d69e520617748 OP_CHECKSIG", + "version": 192 + }, + { + "output": "c440b462ad48c7a77f94cd4532d8f2119dcebbd7c9764557e62726419b08ad4c OP_CHECKSIG", + "version": 192 + }, + { + "output": "c440b462ad48c7a77f94cd4532d8f2119dcebbd7c9764557e62726419b08ad4c OP_CHECKSIG", + "version": 192 + } + ] + ], + "hash": "b76077013c8e303085e300000000000000000000000000000000000000000000" + } + }, + { + "description": "Script Tree is not a TapTree tree (leaf has no script)", + "exception": "property \"scriptTree\" of type \\?isTaptree, got Array", + "options": {}, + "arguments": { + "internalPubkey": "9fa5ffb68821cf559001caa0577eeea4978b29416def328a707b15e91701a2f7", + "scriptTree": [ + { + "output": "71981521ad9fc9036687364118fb6ccd2035b96a423c59c5430e98310a11abe2 OP_CHECKSIG", + "version": 192 + }, + [ + [ + [ + [ + { + "output": "d5094d2dbe9b76e2c245a2b89b6006888952e2faa6a149ae318d69e520617748 OP_CHECKSIG", + "version": 192 + }, + { + "version": 192 + } + ] + ] + ] + ] + ], + "hash": "b76077013c8e303085e300000000000000000000000000000000000000000000" + } + }, + { + "description": "Incorrect redeem version", + "exception": "Redeem.redeemVersion and witness mismatch", + "arguments": { + "witness": [ + "20d85a959b0290bf19bb89ed43c916be835475d013da4b362117393e25a48229b8ac", + "c1187791b6f712a8ea41c8ecdd0ee77fab3e85263b37e1ec18a3651926b3a6cf27" + ], + "redeem": { + "output": "d85a959b0290bf19bb89ed43c916be835475d013da4b362117393e25a48229b8 OP_CHECKSIG", + "redeemVersion": 111 + } + } + }, + { + "description": "Incorrect redeem output", + "exception": "Redeem.output and witness mismatch", + "arguments": { + "witness": [ + "20d85a959b0290bf19bb89ed43c916be835475d013da4b362117393e25a48229b8ac", + "c1187791b6f712a8ea41c8ecdd0ee77fab3e85263b37e1ec18a3651926b3a6cf27" + ], + "redeem": { + "output": "d85a959b0290bf19bb89ed43c916be835475d013da4b362117393e0000000000 OP_CHECKSIG", + "redeemVersion": 192 + } + } + }, + { + "description": "Incorrect redeem witness", + "exception": "Redeem.witness and witness mismatch", + "arguments": { + "witness": [ + "9675a9982c6398ea9d441cb7a943bcd6ff033cc3a2e01a0178a7d3be4575be863871c6bf3eef5ecd34721c784259385ca9101c3a313e010ac942c99de05aaaa602", + "5799cf4b193b730fb99580b186f7477c2cca4d28957326f6f1a5d14116438530e7ec0ce1cd465ad96968ae8a6a09d4d37a060a115919f56fcfebe7b2277cc2df5cc08fb6cda9105ee2512b2e22635aba", + "7520c7b5db9562078049719228db2ac80cb9643ec96c8055aa3b29c2c03d4d99edb0ac", + "c1a7957acbaaf7b444c53d9e0c9436e8a8a3247fd515095d66ddf6201918b40a3668f9a4ccdffcf778da624dca2dda0b08e763ec52fd4ad403ec7563a3504d0cc168b9a77a410029e01dac89567c9b2e6cd726e840351df3f2f58fefe976200a19244150d04153909f660184d656ee95fa7bf8e1d4ec83da1fca34f64bc279b76d257ec623e08baba2cfa4ea9e99646e88f1eb1668c00c0f15b7443c8ab83481611cc3ae85eb89a7bfc40067eb1d2e6354a32426d0ce710e88bc4cc0718b99c325509c9d02a6a980d675a8969be10ee9bef82cafee2fc913475667ccda37b1bc7f13f64e56c449c532658ba8481631c02ead979754c809584a875951619cec8fb040c33f06468ae0266cd8693d6a64cea5912be32d8de95a6da6300b0c50fdcd6001ea41126e7b7e5280d455054a816560028f5ca53c9a50ee52f10e15c5337315bad1f5277acb109a1418649dc6ead2fe14699742fee7182f2f15e54279c7d932ed2799d01d73c97e68bbc94d6f7f56ee0a80efd7c76e3169e10d1a1ba3b5f1eb02369dc43af687461c7a2a3344d13eb5485dca29a67f16b4cb988923060fd3b65d0f0352bb634bcc44f2fe668836dcd0f604150049835135dc4b4fbf90fb334b3938a1f137eb32f047c65b85e6c1173b890b6d0162b48b186d1f1af8521945924ac8ac8efec321bf34f1d4b3d4a304a10313052c652d53f6ecb8a55586614e8950cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c" + ], + "redeem" : { + "output": "OP_DROP c7b5db9562078049719228db2ac80cb9643ec96c8055aa3b29c2c03d4d99edb0 OP_CHECKSIG", + "redeemVersion": 192, + "witness": [ + "9675a9982c6398ea9d441cb7a943bcd6ff033cc3a2e01a0178a7d3be4575be863871c6bf3eef5ecd34721c784259385ca9101c3a313e0100000000000000000000", + "5799cf4b193b730fb99580b186f7477c2cca4d28957326f6f1a5d14116438530e7ec0ce1cd465ad96968ae8a6a09d4d37a060a115919f56fcfebe7b2277cc2df5cc08fb6cda9105ee2512b2e22635aba" + ] + } + } + }, + { + "description": "Incorrect redeem output ASM", + "exception": "Redeem.output is invalid", + "arguments": { + "witness": [ + "9675a9982c6398ea9d441cb7a943bcd6ff033cc3a2e01a0178a7d3be4575be863871c6bf3eef5ecd34721c784259385ca9101c3a313e010ac942c99de05aaaa602", + "5799cf4b193b730fb99580b186f7477c2cca4d28957326f6f1a5d14116438530e7ec0ce1cd465ad96968ae8a6a09d4d37a060a115919f56fcfebe7b2277cc2df5cc08fb6cda9105ee2512b2e22635aba", + "7520c7b5db9562078049719228db2ac80cb9643ec96c8055aa3b29c2c03d4d99edb0ac", + "c1a7957acbaaf7b444c53d9e0c9436e8a8a3247fd515095d66ddf6201918b40a3668f9a4ccdffcf778da624dca2dda0b08e763ec52fd4ad403ec7563a3504d0cc168b9a77a410029e01dac89567c9b2e6cd726e840351df3f2f58fefe976200a19244150d04153909f660184d656ee95fa7bf8e1d4ec83da1fca34f64bc279b76d257ec623e08baba2cfa4ea9e99646e88f1eb1668c00c0f15b7443c8ab83481611cc3ae85eb89a7bfc40067eb1d2e6354a32426d0ce710e88bc4cc0718b99c325509c9d02a6a980d675a8969be10ee9bef82cafee2fc913475667ccda37b1bc7f13f64e56c449c532658ba8481631c02ead979754c809584a875951619cec8fb040c33f06468ae0266cd8693d6a64cea5912be32d8de95a6da6300b0c50fdcd6001ea41126e7b7e5280d455054a816560028f5ca53c9a50ee52f10e15c5337315bad1f5277acb109a1418649dc6ead2fe14699742fee7182f2f15e54279c7d932ed2799d01d73c97e68bbc94d6f7f56ee0a80efd7c76e3169e10d1a1ba3b5f1eb02369dc43af687461c7a2a3344d13eb5485dca29a67f16b4cb988923060fd3b65d0f0352bb634bcc44f2fe668836dcd0f604150049835135dc4b4fbf90fb334b3938a1f137eb32f047c65b85e6c1173b890b6d0162b48b186d1f1af8521945924ac8ac8efec321bf34f1d4b3d4a304a10313052c652d53f6ecb8a55586614e8950cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c" + ], + "redeem" : { + "output": "", + "redeemVersion": 192, + "witness": [ + "9675a9982c6398ea9d441cb7a943bcd6ff033cc3a2e01a0178a7d3be4575be863871c6bf3eef5ecd34721c784259385ca9101c3a313e0100000000000000000000", + "5799cf4b193b730fb99580b186f7477c2cca4d28957326f6f1a5d14116438530e7ec0ce1cd465ad96968ae8a6a09d4d37a060a115919f56fcfebe7b2277cc2df5cc08fb6cda9105ee2512b2e22635aba" + ] + } + } + }, + { + "description": "Redeem script not in tree", + "exception": "Redeem script not in tree", + "options": {}, + "arguments": { + "internalPubkey": "9fa5ffb68821cf559001caa0577eeea4978b29416def328a707b15e91701a2f7", + "scriptTree": { + "output": "83d8ee77a0f3a32a5cea96fd1624d623b836c1e5d1ac2dcde46814b619320c18 OP_CHECKSIG" + }, + "redeem": { + "output": "83d8ee77a0f3a32a5cea96fd1624d623b836c1e5d1ac2dcde46814b619320c19 OP_CHECKSIG" + } + } + } + ], + "dynamic": { + "depends": {}, + "details": [] + } +} diff --git a/test/fixtures/psbt.json b/test/fixtures/psbt.json index 0e51d57cf..ec6eda328 100644 --- a/test/fixtures/psbt.json +++ b/test/fixtures/psbt.json @@ -116,6 +116,10 @@ { "description": "PSBT with unknown types in the inputs.", "psbt": "cHNidP8BAD8CAAAAAf//////////////////////////////////////////AAAAAAD/////AQAAAAAAAAAAA2oBAAAAAAAACg8BAgMEBQYHCAkPAQIDBAUGBwgJCgsMDQ4PAAA=" + }, + { + "description": "PSBT with one P2TR input and one P2TR output.", + "psbt": "cHNidP8BAF4CAAAAAWbQAKi9hNXynJhqPu8bqkvp0kHihVShkWGh3yLy15+LAAAAAAD/////Aej9AAAAAAAAIlEgRvZJfLLxnVDD6emCqVDcyGIUsB/M5DekIGHHvbEjDTMAAAAAAAEBK7gFAQAAAAAAIlEglCHnNLD50sRn6n3Rl8Yay0RnzcvJ9MsMVx+LY6XEDK4AAA==" } ], "failSignChecks": [ @@ -243,6 +247,36 @@ } ], "result": "cHNidP8BAJoCAAAAAljoeiG1ba8MI76OcHBFbDNvfLqlyHV5JPVFiHuyq911AAAAAAD/////g40EJ9DsZQpoqka7CwmK6kQiwHGyyng1Kgd5WdB86h0BAAAAAP////8CcKrwCAAAAAAWABTYXCtx0AYLCcmIauuBXlCZHdoSTQDh9QUAAAAAFgAUAK6pouXw+HaliN9VRuh0LR2HAI8AAAAAAAEAuwIAAAABqtc5MQGL0l+ErkALaISL4J23BurCrBgpi6vucatlb4sAAAAASEcwRAIgWPb8fGoz4bMVSNSByCbAFb0wE1qtQs1neQ2rZtKtJDsCIEoc7SYExnNbY5PltBaR3XiwDwxZQvufdRhW+qk4FX26Af7///8CgPD6AgAAAAAXqRQPuUY0IWlrgsgzryQceMF9295JNIfQ8gonAQAAABepFCnKdPigj4GZlCgYXJe12FLkBj9hh2UAAAABAwQBAAAAAQRHUiEClYO/Oa4KYJdHrRma3dY0+mEIVZ1sXNObTCGD8auW4H8hAtq2H/SaFNtqfQKwzR+7ePxLGDErW05U2uTbovv+9TbXUq4iBgKVg785rgpgl0etGZrd1jT6YQhVnWxc05tMIYPxq5bgfxDZDGpPAAAAgAAAAIAAAACAIgYC2rYf9JoU22p9ArDNH7t4/EsYMStbTlTa5Nui+/71NtcQ2QxqTwAAAIAAAACAAQAAgAABASAAwusLAAAAABepFLf1+vQOPUClpFmx2zU18rcvqSHohwEDBAEAAAABBCIAIIwjUxc3Q7WV37Sge3K6jkLjeX2nTof+fZ10l+OyAokDAQVHUiEDCJ3BDHrG21T5EymvYXMz2ziM6tDCMfcjN50bmQMLAtwhAjrdkE89bc9Z3bkGsN7iNSm3/7ntUOXoYVGSaGAiHw5zUq4iBgI63ZBPPW3PWd25BrDe4jUpt/+57VDl6GFRkmhgIh8OcxDZDGpPAAAAgAAAAIADAACAIgYDCJ3BDHrG21T5EymvYXMz2ziM6tDCMfcjN50bmQMLAtwQ2QxqTwAAAIAAAACAAgAAgAAiAgOppMN/WZbTqiXbrGtXCvBlA5RJKUJGCzVHU+2e7KWHcRDZDGpPAAAAgAAAAIAEAACAACICAn9jmXV9Lv9VoTatAsaEsYOLZVbl8bazQoKpS2tQBRCWENkMak8AAACAAAAAgAUAAIAA" + }, + { + "description": "Update taproot output", + "isTaproot": true, + "psbt": "cHNidP8BAF4CAAAAAbc48X2AGPRzFoMvXHqldFViBB89ZrPfTH6yxr42pJsWAAAAAAAKAAAAAZBBBgAAAAAAIlEgwjn5jxVDmCN6B+wTPt4zCjlF+5KOEDapZ4S9Y3HZxdwAAAAAAAEBK6BoBgAAAAAAIlEgOJumKbXWO2VGchS5k1l1NzOsAyg8aBVQbDNJ88BPtH5iFcF6ESg3Xx4VxbYhRfEEyJc6dumg3zlxfNO8piELt1JMShpSnJ+zzX53bWG2IltsYQ6JBvuPqmxZrFw+lbX4LSnWGlKcn7PNfndtYbYiW2xhDokG+4+qbFmsXD6VtfgtKdYmWrJ1IAOmFFKDvSwulomGXZP6LYOYOGBHzSRnrh2w9cb0vzIGrMAAAA==", + "outputData": [ + { + "tapInternalKey": "Buffer.from('b3310106b13af5def8fc341fe7120e844ab73d4437649aefcadd306d974c9958', 'hex')", + "tapTree": { + "leaves": [ + { + "depth": 1, + "leafVersion": 192, + "script": "Buffer.from('2050929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0ac', 'hex')" + }, + { + "depth": 2, + "leafVersion": 192, + "script": "Buffer.from('2050929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0ac', 'hex')" + }, + { + "depth": 2, + "leafVersion": 192, + "script": "Buffer.from('5ab2752003a6145283bd2c2e9689865d93fa2d8398386047cd2467ae1db0f5c6f4bf3206ac', 'hex')" + } + ] + } + } + ], + "result": "cHNidP8BAF4CAAAAAbc48X2AGPRzFoMvXHqldFViBB89ZrPfTH6yxr42pJsWAAAAAAAKAAAAAZBBBgAAAAAAIlEgwjn5jxVDmCN6B+wTPt4zCjlF+5KOEDapZ4S9Y3HZxdwAAAAAAAEBK6BoBgAAAAAAIlEgOJumKbXWO2VGchS5k1l1NzOsAyg8aBVQbDNJ88BPtH5iFcF6ESg3Xx4VxbYhRfEEyJc6dumg3zlxfNO8piELt1JMShpSnJ+zzX53bWG2IltsYQ6JBvuPqmxZrFw+lbX4LSnWGlKcn7PNfndtYbYiW2xhDokG+4+qbFmsXD6VtfgtKdYmWrJ1IAOmFFKDvSwulomGXZP6LYOYOGBHzSRnrh2w9cb0vzIGrMAAAQUgszEBBrE69d74/DQf5xIOhEq3PUQ3ZJrvyt0wbZdMmVgBBnIBwCIgUJKbdMGgSVS3i0tgNel6XgeKWg8o7JbVR7/ums6AOsCsAsAiIFCSm3TBoElUt4tLYDXpel4HiloPKOyW1Ue/7prOgDrArALAJVqydSADphRSg70sLpaJhl2T+i2DmDhgR80kZ64dsPXG9L8yBqwA" } ], "signer": [ @@ -273,6 +307,71 @@ } ], "result": "cHNidP8BAJoCAAAAAljoeiG1ba8MI76OcHBFbDNvfLqlyHV5JPVFiHuyq911AAAAAAD/////g40EJ9DsZQpoqka7CwmK6kQiwHGyyng1Kgd5WdB86h0BAAAAAP////8CcKrwCAAAAAAWABTYXCtx0AYLCcmIauuBXlCZHdoSTQDh9QUAAAAAFgAUAK6pouXw+HaliN9VRuh0LR2HAI8AAAAAAAEAuwIAAAABqtc5MQGL0l+ErkALaISL4J23BurCrBgpi6vucatlb4sAAAAASEcwRAIgWPb8fGoz4bMVSNSByCbAFb0wE1qtQs1neQ2rZtKtJDsCIEoc7SYExnNbY5PltBaR3XiwDwxZQvufdRhW+qk4FX26Af7///8CgPD6AgAAAAAXqRQPuUY0IWlrgsgzryQceMF9295JNIfQ8gonAQAAABepFCnKdPigj4GZlCgYXJe12FLkBj9hh2UAAAAiAgLath/0mhTban0CsM0fu3j8SxgxK1tOVNrk26L7/vU210gwRQIhAPYQOLMI3B2oZaNIUnRvAVdyk0IIxtJEVDk82ZvfIhd3AiAFbmdaZ1ptCgK4WxTl4pB02KJam1dgvqKBb2YZEKAG6gEBAwQBAAAAAQRHUiEClYO/Oa4KYJdHrRma3dY0+mEIVZ1sXNObTCGD8auW4H8hAtq2H/SaFNtqfQKwzR+7ePxLGDErW05U2uTbovv+9TbXUq4iBgKVg785rgpgl0etGZrd1jT6YQhVnWxc05tMIYPxq5bgfxDZDGpPAAAAgAAAAIAAAACAIgYC2rYf9JoU22p9ArDNH7t4/EsYMStbTlTa5Nui+/71NtcQ2QxqTwAAAIAAAACAAQAAgAABASAAwusLAAAAABepFLf1+vQOPUClpFmx2zU18rcvqSHohyICAjrdkE89bc9Z3bkGsN7iNSm3/7ntUOXoYVGSaGAiHw5zRzBEAiBl9FulmYtZon/+GnvtAWrx8fkNVLOqj3RQql9WolEDvQIgf3JHA60e25ZoCyhLVtT/y4j3+3Weq74IqjDym4UTg9IBAQMEAQAAAAEEIgAgjCNTFzdDtZXftKB7crqOQuN5fadOh/59nXSX47ICiQMBBUdSIQMIncEMesbbVPkTKa9hczPbOIzq0MIx9yM3nRuZAwsC3CECOt2QTz1tz1nduQaw3uI1Kbf/ue1Q5ehhUZJoYCIfDnNSriIGAjrdkE89bc9Z3bkGsN7iNSm3/7ntUOXoYVGSaGAiHw5zENkMak8AAACAAAAAgAMAAIAiBgMIncEMesbbVPkTKa9hczPbOIzq0MIx9yM3nRuZAwsC3BDZDGpPAAAAgAAAAIACAACAACICA6mkw39ZltOqJdusa1cK8GUDlEkpQkYLNUdT7Z7spYdxENkMak8AAACAAAAAgAQAAIAAIgICf2OZdX0u/1WhNq0CxoSxg4tlVuXxtrNCgqlLa1AFEJYQ2QxqTwAAAIAAAACABQAAgAA=" + }, + { + "description": "Sign PSBT with 3 inputs [P2PKH, P2TR (key-path), P2WPKH] and two outputs [P2TR, P2WPKH]", + "psbt": "cHNidP8BAM8CAAAAAwPzd9k+uLSN1rgF01xY1TIA/8N+YytNZ4VP9gKFP4MyAAAAAAD/////ZtAAqL2E1fKcmGo+7xuqS+nSQeKFVKGRYaHfIvLXn4sAAAAAAP////9+h+SlCwIx1MUDT7Bek0NrWXS7xnSPi5LbYbDc9sxYIgAAAAAA/////wIgKRsAAAAAACJRIEb2SXyy8Z1Qw+npgqlQ3MhiFLAfzOQ3pCBhx72xIw0zuAUBAAAAAAAWABTJijE0v48z5ZmmfEAADXdCBcG0FAAAAAAAAQDiAgAAAAABAUfY2D1t0dyMeEH39C1yOdIxigpqm7XJNqHVT3Lc+FkiAAAAAAD+////AhIsGwAAAAAAGXapFJ5+8XZ3ZP80oFldvEwrcNsBftBmiKyYdK6xAAAAABepFLDBn59UffGbX7u/olyFDG0eG1UJhwJHMEQCIDAd3s05C61flXVFqOtov0NoHRGr8KFcOpH6R/81F46EAiBt+j9hHyvT2hYEyf8fdYsM9IgbnybtPV+kRTHDa6Rj0AEhAmmZfwmoHsmCkEOn9AfRTh+863mURelmE8hSqL4MG1EydJwgAAABASu4BQEAAAAAACJRIJQh5zSw+dLEZ+p90ZfGGstEZ83LyfTLDFcfi2OlxAyuARcglCHnNLD50sRn6n3Rl8Yay0RnzcvJ9MsMVx+LY6XEDK4AAQEfECcAAAAAAAAWABRPoqyhKLb53uUwnE9wBR5Jxl/XMgAAAA==", + "isTaproot": true, + "keys": [ + { + "inputToSign": 0, + "WIF": "cRyKzLXVgTReWe7wgfEiXktTa9tf4e5DK1STha274d7BBbnucTaR" + }, + { + "inputToSign": 1, + "WIF": "cR62L1G154fjHFrBCJMxJxbk1rcxhT2xcTh7WstvFdFDsZ9uFiVj" + }, + { + "inputToSign": 2, + "WIF": "cPPRdCmAMZMjPdHfRmTCmzYVruZHJ8GbM1FqN2W6DnmEPWDg29aL" + } + ], + "result": "cHNidP8BAM8CAAAAAwPzd9k+uLSN1rgF01xY1TIA/8N+YytNZ4VP9gKFP4MyAAAAAAD/////ZtAAqL2E1fKcmGo+7xuqS+nSQeKFVKGRYaHfIvLXn4sAAAAAAP////9+h+SlCwIx1MUDT7Bek0NrWXS7xnSPi5LbYbDc9sxYIgAAAAAA/////wIgKRsAAAAAACJRIEb2SXyy8Z1Qw+npgqlQ3MhiFLAfzOQ3pCBhx72xIw0zuAUBAAAAAAAWABTJijE0v48z5ZmmfEAADXdCBcG0FAAAAAAAAQDiAgAAAAABAUfY2D1t0dyMeEH39C1yOdIxigpqm7XJNqHVT3Lc+FkiAAAAAAD+////AhIsGwAAAAAAGXapFJ5+8XZ3ZP80oFldvEwrcNsBftBmiKyYdK6xAAAAABepFLDBn59UffGbX7u/olyFDG0eG1UJhwJHMEQCIDAd3s05C61flXVFqOtov0NoHRGr8KFcOpH6R/81F46EAiBt+j9hHyvT2hYEyf8fdYsM9IgbnybtPV+kRTHDa6Rj0AEhAmmZfwmoHsmCkEOn9AfRTh+863mURelmE8hSqL4MG1EydJwgACICAi5ovBH1xLoGxPqtFh48wUEpnM+St1SbPzRwO7kBNKOQRzBEAiBpWClBybtHveXkhAgTiE8QSczMJs8MGuH4LOSNRA6s/AIgWlbB3xJOtJIsszj1qZ/whA5jK9wnTzeZzDlVs/ivq2cBAAEBK7gFAQAAAAAAIlEglCHnNLD50sRn6n3Rl8Yay0RnzcvJ9MsMVx+LY6XEDK4BE0Cd7/ny+QreV7urBWKNroQWCvnZczwkU0kLZiKsJQjtftKHWXMknftjt1d4K6aPYH7cBXzhlrUF+2GovjYLccZeARcglCHnNLD50sRn6n3Rl8Yay0RnzcvJ9MsMVx+LY6XEDK4AAQEfECcAAAAAAAAWABRPoqyhKLb53uUwnE9wBR5Jxl/XMiICA6VOpEBbPJM/xYsqO2euttYFpgec9vcxggyTyoklK660SDBFAiEAoCIktghL55iuMAmkzwYJzb+h+qmNewZXxAx/06ObxIQCIELCsBz/wd2wPlnJb27OluxMkTPnCyHA2C+SxHiX/FvPAQAAAA==" + }, + { + "description": "Sign PSBT with 1 input [P2TR] (script-path, 3-of-3) and one output [P2TR]", + "psbt": "cHNidP8BAF4CAAAAAQU2GLj/HTOEN804Hc6JRNTLM7EmDlIBdjG2G1aUw266AAAAAAD/////AYAaBgAAAAAAIlEg35sLGepBXUbR93XfxDJcYBCHqNVjw/jogOgPVdic1UgAAAAAAAEBK6BoBgAAAAAAIlEgVZx4+tHeORcb0jDJnOytrOnNOGL1uS0MdMUg1GQeS15iFcFmuQP4s1ds7KJtMOh4fTw1QCgxkWUA3FUAuUzKHzjDvhpSnJ+zzX53bWG2IltsYQ6JBvuPqmxZrFw+lbX4LSnWGlKcn7PNfndtYbYiW2xhDokG+4+qbFmsXD6VtfgtKdZpII+JGq8uLXsUYXjFSofirt3Z0daGaSJXko5nzwgXT/N1rCA5X4Ep3WO0pcLxISTqoFt6ftMPcOUfuTMF3uzFQuf567ogqKs3vBYJ2DTDkTs1ONrRyE1/m2qDX/wXV3eRWjeuNXK6U5zAAAA=", + "isTaproot": true, + "keys": [ + { + "inputToSign": 0, + "WIF": "cRXSy63fXDve59e8cvqozVFfqXJB6YL6cPzoRewmEsux81SgPrfj" + }, + { + "inputToSign": 0, + "WIF": "cQQXUJocNBS6oZCCtyhCsdN5ammK6WoJWpx44ANKxZSN2A3WDDEN" + }, + { + "inputToSign": 0, + "WIF": "cTrPNrN2EQo4ppBHcFNxyLBFq2WLjZoNKY5nQbPwAGdhQqqsRKSu" + } + ], + "result": "cHNidP8BAF4CAAAAAQU2GLj/HTOEN804Hc6JRNTLM7EmDlIBdjG2G1aUw266AAAAAAD/////AYAaBgAAAAAAIlEg35sLGepBXUbR93XfxDJcYBCHqNVjw/jogOgPVdic1UgAAAAAAAEBK6BoBgAAAAAAIlEgVZx4+tHeORcb0jDJnOytrOnNOGL1uS0MdMUg1GQeS15BFDlfgSndY7SlwvEhJOqgW3p+0w9w5R+5MwXe7MVC5/nrrqI0FdZdKILLLZgRVK8L9Bn2ijU6IcoqqyImKIWt3MtAA3alBoU7IBCkBk9OHD1wE8fJI4y+lbnTRj48e8AAwRM77q3Rml679qCzGvEAKAs99UNMaXHQIhgGfRP11AMlJkEUj4kary4texRheMVKh+Ku3dnR1oZpIleSjmfPCBdP83WuojQV1l0ogsstmBFUrwv0GfaKNTohyiqrIiYoha3cy0DYJZ6Lv7FZPIBRZFfVgF5v3gcRiQnT8aM82Q5IPkwkzZrGo4ThZblvunG/+hu8ZPuJrUU+uXb+s9rcwSH+BihIQRSoqze8FgnYNMOROzU42tHITX+baoNf/BdXd5FaN641cq6iNBXWXSiCyy2YEVSvC/QZ9oo1OiHKKqsiJiiFrdzLQEfQ5UkAg4lTbhJxjMzB7hu6ad1fywYxHCXjFXHHrm5PJTOFJLg2oTnwuQToz/Z2AW/UET7Op+WSoHZvW4tzzLhiFcFmuQP4s1ds7KJtMOh4fTw1QCgxkWUA3FUAuUzKHzjDvhpSnJ+zzX53bWG2IltsYQ6JBvuPqmxZrFw+lbX4LSnWGlKcn7PNfndtYbYiW2xhDokG+4+qbFmsXD6VtfgtKdZpII+JGq8uLXsUYXjFSofirt3Z0daGaSJXko5nzwgXT/N1rCA5X4Ep3WO0pcLxISTqoFt6ftMPcOUfuTMF3uzFQuf567ogqKs3vBYJ2DTDkTs1ONrRyE1/m2qDX/wXV3eRWjeuNXK6U5zAAAA=" + }, + { + "description": "Sign PSBT with 1 input [P2TR]. Signer pubkey found in two tapleaf scripts (both get signed)", + "psbt": "cHNidP8BAF4CAAAAATNOP+fCPDCsZrgGIptDVbwY/yrkM2xaFfLe05BSLulZAAAAAAD/////AZBBBgAAAAAAIlEgo6glLro4LvJCjIXCj+2xYC8NQ5BB/tvaXLukmWNHDaQAAAAAAAEBK6BoBgAAAAAAIlEgo6glLro4LvJCjIXCj+2xYC8NQ5BB/tvaXLukmWNHDaRiFcDXgjtX0c88beKd6Y8zRYbANZXEc8dDhlup8v3SCiX0bBpSnJ+zzX53bWG2IltsYQ6JBvuPqmxZrFw+lbX4LSnWWeEQK6JF8Ev4E8Yg25gvDzbizKAO6m6JD082gFjsWNVpIP1IuUG/pcYMyG7KOHraqMXMKKOJb5eIpnjGt7LrvcsWrCDxKlbBaB54iRPj/r6t5N9DtBFItV8uoHfEwGboXpVsHLogt01J+NwSsj7PM7m6QbLDiXLdoYzn9pHNA4kAGF1pL3+6U5zAQhXA14I7V9HPPG3inemPM0WGwDWVxHPHQ4ZbqfL90gol9Gx4Lk64Mk5QMzz2Xo97sRZFcGH0TduAOb6H0hHOB/0CrCMg/Ui5Qb+lxgzIbso4etqoxcwoo4lvl4imeMa3suu9yxaswAAA", + "isTaproot": true, + "keys": [ + { + "inputToSign": 0, + "WIF": "cSu5bjn9TsAEeqZxEFKy5of3FKfHha6FT56KvfAGmu6rMmPNJTrV" + } + ], + "result": "cHNidP8BAF4CAAAAATNOP+fCPDCsZrgGIptDVbwY/yrkM2xaFfLe05BSLulZAAAAAAD/////AZBBBgAAAAAAIlEgo6glLro4LvJCjIXCj+2xYC8NQ5BB/tvaXLukmWNHDaQAAAAAAAEBK6BoBgAAAAAAIlEgo6glLro4LvJCjIXCj+2xYC8NQ5BB/tvaXLukmWNHDaRBFP1IuUG/pcYMyG7KOHraqMXMKKOJb5eIpnjGt7LrvcsWIjJncH2zqZGpUloPoJZipi/NkKmjWthVrvnedXPOOOdABNUCXyGRAy4nthiGpE8dXioP2P50J+fU5gojlqunTwPKRPvqWcCzOoaJGvUEF+oxpMXZ1+FZWHRppoeoZFParUEU/Ui5Qb+lxgzIbso4etqoxcwoo4lvl4imeMa3suu9yxZZ4RArokXwS/gTxiDbmC8PNuLMoA7qbokPTzaAWOxY1UAF3RzlWz+5cWYG1EqfZTT/CO7O3hGYvMMjlJV5rluR916WCgGYO2hHj9fiEH0rxD3BRbzR78PShCen+beqDncSYhXA14I7V9HPPG3inemPM0WGwDWVxHPHQ4ZbqfL90gol9GwaUpyfs81+d21htiJbbGEOiQb7j6psWaxcPpW1+C0p1lnhECuiRfBL+BPGINuYLw824sygDupuiQ9PNoBY7FjVaSD9SLlBv6XGDMhuyjh62qjFzCijiW+XiKZ4xrey673LFqwg8SpWwWgeeIkT4/6+reTfQ7QRSLVfLqB3xMBm6F6VbBy6ILdNSfjcErI+zzO5ukGyw4ly3aGM5/aRzQOJABhdaS9/ulOcwEIVwNeCO1fRzzxt4p3pjzNFhsA1lcRzx0OGW6ny/dIKJfRseC5OuDJOUDM89l6Pe7EWRXBh9E3bgDm+h9IRzgf9AqwjIP1IuUG/pcYMyG7KOHraqMXMKKOJb5eIpnjGt7LrvcsWrMAAAA==" + }, + { + "description": "Sign PSBT with 1 input [P2TR]. Signer pubkey found in two tapleaf scripts (sign only the matching tapleaf hash)", + "psbt": "cHNidP8BAF4CAAAAAdXMkUOLeYvgm981k3T7Pmdf5Dr31jOxvpFHiXiU9D2gAAAAAAD/////AZBBBgAAAAAAIlEgD31MIAvwCaanOsAzyBoQ4o/WozgFiqVQTEqGJE18XqwAAAAAAAEBK6BoBgAAAAAAIlEgD31MIAvwCaanOsAzyBoQ4o/WozgFiqVQTEqGJE18XqxiFcAI1JOUM3VATBJC28HI1B5zKQNd5N4/c4g7M1CYhZWnWhpSnJ+zzX53bWG2IltsYQ6JBvuPqmxZrFw+lbX4LSnWk4qTbS/aPOMicn4cednnrKiVhdggWbZxsQhaXXg5/EtpICo/Wsfk0hSOCy21lUyn+vXnc1SG1lNVWHlnXCh3xegqrCCcumXmHlTmveMSBLOPSfb6H3laX97tZtF4gqmxzV+gbLogXBeUvuJ0b/mMOa3539xiz0/Nf9RQCWGZXXoCNcFUYde6U5zAQhXACNSTlDN1QEwSQtvByNQecykDXeTeP3OIOzNQmIWVp1pfFb/yNVlibfG8oN35ON4kJ+kBZQ4LphUz16+7jituPiMgKj9ax+TSFI4LLbWVTKf69edzVIbWU1VYeWdcKHfF6CqswAAA", + "isTaproot": true, + "keys": [ + { + "inputToSign": 0, + "tapLeafHashToSign": "f2d9fd9a2f80e0e7abeac881398fc37198f46e5c802ec00c95152aa6f703e71e", + "WIF": "cP76Rzf6bVcmFbnJ3DigWvyNvki2bZeXxoq3B5pcZ8zVRnT4fKdx" + } + ], + "result": "cHNidP8BAF4CAAAAAdXMkUOLeYvgm981k3T7Pmdf5Dr31jOxvpFHiXiU9D2gAAAAAAD/////AZBBBgAAAAAAIlEgD31MIAvwCaanOsAzyBoQ4o/WozgFiqVQTEqGJE18XqwAAAAAAAEBK6BoBgAAAAAAIlEgD31MIAvwCaanOsAzyBoQ4o/WozgFiqVQTEqGJE18XqxBFCo/Wsfk0hSOCy21lUyn+vXnc1SG1lNVWHlnXCh3xegq8tn9mi+A4Oer6siBOY/DcZj0blyALsAMlRUqpvcD5x5Aa7X0m4UCLaHA/Vnjkl+if6rVeiBbIlbHXHLh7RqJyB8Wgs66p6/ZnwSW/HD6o7rMHffIva+jgJgYWf6MvzrfTWIVwAjUk5QzdUBMEkLbwcjUHnMpA13k3j9ziDszUJiFladaGlKcn7PNfndtYbYiW2xhDokG+4+qbFmsXD6VtfgtKdaTipNtL9o84yJyfhx52eesqJWF2CBZtnGxCFpdeDn8S2kgKj9ax+TSFI4LLbWVTKf69edzVIbWU1VYeWdcKHfF6CqsIJy6ZeYeVOa94xIEs49J9vofeVpf3u1m0XiCqbHNX6BsuiBcF5S+4nRv+Yw5rfnf3GLPT81/1FAJYZldegI1wVRh17pTnMBCFcAI1JOUM3VATBJC28HI1B5zKQNd5N4/c4g7M1CYhZWnWl8Vv/I1WWJt8byg3fk43iQn6QFlDgumFTPXr7uOK24+IyAqP1rH5NIUjgsttZVMp/r153NUhtZTVVh5Z1wod8XoKqzAAAA=" } ], "combiner": [ @@ -295,6 +394,42 @@ { "psbt": "cHNidP8BAJoCAAAAAljoeiG1ba8MI76OcHBFbDNvfLqlyHV5JPVFiHuyq911AAAAAAD/////g40EJ9DsZQpoqka7CwmK6kQiwHGyyng1Kgd5WdB86h0BAAAAAP////8CcKrwCAAAAAAWABTYXCtx0AYLCcmIauuBXlCZHdoSTQDh9QUAAAAAFgAUAK6pouXw+HaliN9VRuh0LR2HAI8AAAAAAAEAuwIAAAABqtc5MQGL0l+ErkALaISL4J23BurCrBgpi6vucatlb4sAAAAASEcwRAIgWPb8fGoz4bMVSNSByCbAFb0wE1qtQs1neQ2rZtKtJDsCIEoc7SYExnNbY5PltBaR3XiwDwxZQvufdRhW+qk4FX26Af7///8CgPD6AgAAAAAXqRQPuUY0IWlrgsgzryQceMF9295JNIfQ8gonAQAAABepFCnKdPigj4GZlCgYXJe12FLkBj9hh2UAAAAiAgKVg785rgpgl0etGZrd1jT6YQhVnWxc05tMIYPxq5bgf0cwRAIgdAGK1BgAl7hzMjwAFXILNoTMgSOJEEjn282bVa1nnJkCIHPTabdA4+tT3O+jOCPIBwUUylWn3ZVE8VfBZ5EyYRGMASICAtq2H/SaFNtqfQKwzR+7ePxLGDErW05U2uTbovv+9TbXSDBFAiEA9hA4swjcHahlo0hSdG8BV3KTQgjG0kRUOTzZm98iF3cCIAVuZ1pnWm0KArhbFOXikHTYolqbV2C+ooFvZhkQoAbqAQEDBAEAAAABBEdSIQKVg785rgpgl0etGZrd1jT6YQhVnWxc05tMIYPxq5bgfyEC2rYf9JoU22p9ArDNH7t4/EsYMStbTlTa5Nui+/71NtdSriIGApWDvzmuCmCXR60Zmt3WNPphCFWdbFzTm0whg/GrluB/ENkMak8AAACAAAAAgAAAAIAiBgLath/0mhTban0CsM0fu3j8SxgxK1tOVNrk26L7/vU21xDZDGpPAAAAgAAAAIABAACAAAEBIADC6wsAAAAAF6kUt/X69A49QKWkWbHbNTXyty+pIeiHIgIDCJ3BDHrG21T5EymvYXMz2ziM6tDCMfcjN50bmQMLAtxHMEQCIGLrelVhB6fHP0WsSrWh3d9vcHX7EnWWmn84Pv/3hLyyAiAMBdu3Rw2/LwhVfdNWxzJcHtMJE+mWzThAlF2xIijaXwEiAgI63ZBPPW3PWd25BrDe4jUpt/+57VDl6GFRkmhgIh8Oc0cwRAIgZfRbpZmLWaJ//hp77QFq8fH5DVSzqo90UKpfVqJRA70CIH9yRwOtHtuWaAsoS1bU/8uI9/t1nqu+CKow8puFE4PSAQEDBAEAAAABBCIAIIwjUxc3Q7WV37Sge3K6jkLjeX2nTof+fZ10l+OyAokDAQVHUiEDCJ3BDHrG21T5EymvYXMz2ziM6tDCMfcjN50bmQMLAtwhAjrdkE89bc9Z3bkGsN7iNSm3/7ntUOXoYVGSaGAiHw5zUq4iBgI63ZBPPW3PWd25BrDe4jUpt/+57VDl6GFRkmhgIh8OcxDZDGpPAAAAgAAAAIADAACAIgYDCJ3BDHrG21T5EymvYXMz2ziM6tDCMfcjN50bmQMLAtwQ2QxqTwAAAIAAAACAAgAAgAAiAgOppMN/WZbTqiXbrGtXCvBlA5RJKUJGCzVHU+2e7KWHcRDZDGpPAAAAgAAAAIAEAACAACICAn9jmXV9Lv9VoTatAsaEsYOLZVbl8bazQoKpS2tQBRCWENkMak8AAACAAAAAgAUAAIAA", "result": "cHNidP8BAJoCAAAAAljoeiG1ba8MI76OcHBFbDNvfLqlyHV5JPVFiHuyq911AAAAAAD/////g40EJ9DsZQpoqka7CwmK6kQiwHGyyng1Kgd5WdB86h0BAAAAAP////8CcKrwCAAAAAAWABTYXCtx0AYLCcmIauuBXlCZHdoSTQDh9QUAAAAAFgAUAK6pouXw+HaliN9VRuh0LR2HAI8AAAAAAAEAuwIAAAABqtc5MQGL0l+ErkALaISL4J23BurCrBgpi6vucatlb4sAAAAASEcwRAIgWPb8fGoz4bMVSNSByCbAFb0wE1qtQs1neQ2rZtKtJDsCIEoc7SYExnNbY5PltBaR3XiwDwxZQvufdRhW+qk4FX26Af7///8CgPD6AgAAAAAXqRQPuUY0IWlrgsgzryQceMF9295JNIfQ8gonAQAAABepFCnKdPigj4GZlCgYXJe12FLkBj9hh2UAAAABB9oARzBEAiB0AYrUGACXuHMyPAAVcgs2hMyBI4kQSOfbzZtVrWecmQIgc9Npt0Dj61Pc76M4I8gHBRTKVafdlUTxV8FnkTJhEYwBSDBFAiEA9hA4swjcHahlo0hSdG8BV3KTQgjG0kRUOTzZm98iF3cCIAVuZ1pnWm0KArhbFOXikHTYolqbV2C+ooFvZhkQoAbqAUdSIQKVg785rgpgl0etGZrd1jT6YQhVnWxc05tMIYPxq5bgfyEC2rYf9JoU22p9ArDNH7t4/EsYMStbTlTa5Nui+/71NtdSrgABASAAwusLAAAAABepFLf1+vQOPUClpFmx2zU18rcvqSHohwEHIyIAIIwjUxc3Q7WV37Sge3K6jkLjeX2nTof+fZ10l+OyAokDAQjaBABHMEQCIGLrelVhB6fHP0WsSrWh3d9vcHX7EnWWmn84Pv/3hLyyAiAMBdu3Rw2/LwhVfdNWxzJcHtMJE+mWzThAlF2xIijaXwFHMEQCIGX0W6WZi1mif/4ae+0BavHx+Q1Us6qPdFCqX1aiUQO9AiB/ckcDrR7blmgLKEtW1P/LiPf7dZ6rvgiqMPKbhROD0gFHUiEDCJ3BDHrG21T5EymvYXMz2ziM6tDCMfcjN50bmQMLAtwhAjrdkE89bc9Z3bkGsN7iNSm3/7ntUOXoYVGSaGAiHw5zUq4AIgIDqaTDf1mW06ol26xrVwrwZQOUSSlCRgs1R1Ptnuylh3EQ2QxqTwAAAIAAAACABAAAgAAiAgJ/Y5l1fS7/VaE2rQLGhLGDi2VW5fG2s0KCqUtrUAUQlhDZDGpPAAAAgAAAAIAFAACAAA==" + }, + { + "description": "Finalze taproot key-path", + "psbt": "cHNidP8BAM8CAAAAAwPzd9k+uLSN1rgF01xY1TIA/8N+YytNZ4VP9gKFP4MyAAAAAAD/////ZtAAqL2E1fKcmGo+7xuqS+nSQeKFVKGRYaHfIvLXn4sAAAAAAP////9+h+SlCwIx1MUDT7Bek0NrWXS7xnSPi5LbYbDc9sxYIgAAAAAA/////wIgKRsAAAAAACJRIEb2SXyy8Z1Qw+npgqlQ3MhiFLAfzOQ3pCBhx72xIw0zuAUBAAAAAAAWABTJijE0v48z5ZmmfEAADXdCBcG0FAAAAAAAAQDiAgAAAAABAUfY2D1t0dyMeEH39C1yOdIxigpqm7XJNqHVT3Lc+FkiAAAAAAD+////AhIsGwAAAAAAGXapFJ5+8XZ3ZP80oFldvEwrcNsBftBmiKyYdK6xAAAAABepFLDBn59UffGbX7u/olyFDG0eG1UJhwJHMEQCIDAd3s05C61flXVFqOtov0NoHRGr8KFcOpH6R/81F46EAiBt+j9hHyvT2hYEyf8fdYsM9IgbnybtPV+kRTHDa6Rj0AEhAmmZfwmoHsmCkEOn9AfRTh+863mURelmE8hSqL4MG1EydJwgACICAi5ovBH1xLoGxPqtFh48wUEpnM+St1SbPzRwO7kBNKOQRzBEAiBpWClBybtHveXkhAgTiE8QSczMJs8MGuH4LOSNRA6s/AIgWlbB3xJOtJIsszj1qZ/whA5jK9wnTzeZzDlVs/ivq2cBAAEBK7gFAQAAAAAAIlEglCHnNLD50sRn6n3Rl8Yay0RnzcvJ9MsMVx+LY6XEDK4BE0Cd7/ny+QreV7urBWKNroQWCvnZczwkU0kLZiKsJQjtftKHWXMknftjt1d4K6aPYH7cBXzhlrUF+2GovjYLccZeARcglCHnNLD50sRn6n3Rl8Yay0RnzcvJ9MsMVx+LY6XEDK4AAQEfECcAAAAAAAAWABRPoqyhKLb53uUwnE9wBR5Jxl/XMiICA6VOpEBbPJM/xYsqO2euttYFpgec9vcxggyTyoklK660SDBFAiEAoCIktghL55iuMAmkzwYJzb+h+qmNewZXxAx/06ObxIQCIELCsBz/wd2wPlnJb27OluxMkTPnCyHA2C+SxHiX/FvPAQAAAA==", + "result": "cHNidP8BAM8CAAAAAwPzd9k+uLSN1rgF01xY1TIA/8N+YytNZ4VP9gKFP4MyAAAAAAD/////ZtAAqL2E1fKcmGo+7xuqS+nSQeKFVKGRYaHfIvLXn4sAAAAAAP////9+h+SlCwIx1MUDT7Bek0NrWXS7xnSPi5LbYbDc9sxYIgAAAAAA/////wIgKRsAAAAAACJRIEb2SXyy8Z1Qw+npgqlQ3MhiFLAfzOQ3pCBhx72xIw0zuAUBAAAAAAAWABTJijE0v48z5ZmmfEAADXdCBcG0FAAAAAAAAQDiAgAAAAABAUfY2D1t0dyMeEH39C1yOdIxigpqm7XJNqHVT3Lc+FkiAAAAAAD+////AhIsGwAAAAAAGXapFJ5+8XZ3ZP80oFldvEwrcNsBftBmiKyYdK6xAAAAABepFLDBn59UffGbX7u/olyFDG0eG1UJhwJHMEQCIDAd3s05C61flXVFqOtov0NoHRGr8KFcOpH6R/81F46EAiBt+j9hHyvT2hYEyf8fdYsM9IgbnybtPV+kRTHDa6Rj0AEhAmmZfwmoHsmCkEOn9AfRTh+863mURelmE8hSqL4MG1EydJwgAAEHakcwRAIgaVgpQcm7R73l5IQIE4hPEEnMzCbPDBrh+CzkjUQOrPwCIFpWwd8STrSSLLM49amf8IQOYyvcJ083mcw5VbP4r6tnASECLmi8EfXEugbE+q0WHjzBQSmcz5K3VJs/NHA7uQE0o5AAAQEruAUBAAAAAAAiUSCUIec0sPnSxGfqfdGXxhrLRGfNy8n0ywxXH4tjpcQMrgEIQgFAne/58vkK3le7qwVija6EFgr52XM8JFNJC2YirCUI7X7Sh1lzJJ37Y7dXeCumj2B+3AV84Za1BfthqL42C3HGXgABAR8QJwAAAAAAABYAFE+irKEotvne5TCcT3AFHknGX9cyAQhsAkgwRQIhAKAiJLYIS+eYrjAJpM8GCc2/ofqpjXsGV8QMf9Ojm8SEAiBCwrAc/8HdsD5ZyW9uzpbsTJEz5wshwNgvksR4l/xbzwEhA6VOpEBbPJM/xYsqO2euttYFpgec9vcxggyTyoklK660AAAA", + "isTaproot": true + }, + { + "description": "Finalze taproot script-path", + "psbt": "cHNidP8BAF4CAAAAAQU2GLj/HTOEN804Hc6JRNTLM7EmDlIBdjG2G1aUw266AAAAAAD/////AYAaBgAAAAAAIlEg35sLGepBXUbR93XfxDJcYBCHqNVjw/jogOgPVdic1UgAAAAAAAEBK6BoBgAAAAAAIlEgVZx4+tHeORcb0jDJnOytrOnNOGL1uS0MdMUg1GQeS15BFDlfgSndY7SlwvEhJOqgW3p+0w9w5R+5MwXe7MVC5/nrrqI0FdZdKILLLZgRVK8L9Bn2ijU6IcoqqyImKIWt3MtAA3alBoU7IBCkBk9OHD1wE8fJI4y+lbnTRj48e8AAwRM77q3Rml679qCzGvEAKAs99UNMaXHQIhgGfRP11AMlJkEUj4kary4texRheMVKh+Ku3dnR1oZpIleSjmfPCBdP83WuojQV1l0ogsstmBFUrwv0GfaKNTohyiqrIiYoha3cy0DYJZ6Lv7FZPIBRZFfVgF5v3gcRiQnT8aM82Q5IPkwkzZrGo4ThZblvunG/+hu8ZPuJrUU+uXb+s9rcwSH+BihIQRSoqze8FgnYNMOROzU42tHITX+baoNf/BdXd5FaN641cq6iNBXWXSiCyy2YEVSvC/QZ9oo1OiHKKqsiJiiFrdzLQEfQ5UkAg4lTbhJxjMzB7hu6ad1fywYxHCXjFXHHrm5PJTOFJLg2oTnwuQToz/Z2AW/UET7Op+WSoHZvW4tzzLhiFcFmuQP4s1ds7KJtMOh4fTw1QCgxkWUA3FUAuUzKHzjDvhpSnJ+zzX53bWG2IltsYQ6JBvuPqmxZrFw+lbX4LSnWGlKcn7PNfndtYbYiW2xhDokG+4+qbFmsXD6VtfgtKdZpII+JGq8uLXsUYXjFSofirt3Z0daGaSJXko5nzwgXT/N1rCA5X4Ep3WO0pcLxISTqoFt6ftMPcOUfuTMF3uzFQuf567ogqKs3vBYJ2DTDkTs1ONrRyE1/m2qDX/wXV3eRWjeuNXK6U5zAAAA=", + "result": "cHNidP8BAF4CAAAAAQU2GLj/HTOEN804Hc6JRNTLM7EmDlIBdjG2G1aUw266AAAAAAD/////AYAaBgAAAAAAIlEg35sLGepBXUbR93XfxDJcYBCHqNVjw/jogOgPVdic1UgAAAAAAAEBK6BoBgAAAAAAIlEgVZx4+tHeORcb0jDJnOytrOnNOGL1uS0MdMUg1GQeS14BCP2PAQVAR9DlSQCDiVNuEnGMzMHuG7pp3V/LBjEcJeMVcceubk8lM4UkuDahOfC5BOjP9nYBb9QRPs6n5ZKgdm9bi3PMuEADdqUGhTsgEKQGT04cPXATx8kjjL6VudNGPjx7wADBEzvurdGaXrv2oLMa8QAoCz31Q0xpcdAiGAZ9E/XUAyUmQNglnou/sVk8gFFkV9WAXm/eBxGJCdPxozzZDkg+TCTNmsajhOFluW+6cb/6G7xk+4mtRT65dv6z2tzBIf4GKEhoII+JGq8uLXsUYXjFSofirt3Z0daGaSJXko5nzwgXT/N1rCA5X4Ep3WO0pcLxISTqoFt6ftMPcOUfuTMF3uzFQuf567ogqKs3vBYJ2DTDkTs1ONrRyE1/m2qDX/wXV3eRWjeuNXK6U5xhwWa5A/izV2zsom0w6Hh9PDVAKDGRZQDcVQC5TMofOMO+GlKcn7PNfndtYbYiW2xhDokG+4+qbFmsXD6VtfgtKdYaUpyfs81+d21htiJbbGEOiQb7j6psWaxcPpW1+C0p1gAA", + "isTaproot": true + }, + { + "description": "Finalze taproot script-path (3-of-3)", + "psbt": "cHNidP8BAM8CAAAAAwPzd9k+uLSN1rgF01xY1TIA/8N+YytNZ4VP9gKFP4MyAAAAAAD/////ZtAAqL2E1fKcmGo+7xuqS+nSQeKFVKGRYaHfIvLXn4sAAAAAAP////9+h+SlCwIx1MUDT7Bek0NrWXS7xnSPi5LbYbDc9sxYIgAAAAAA/////wIgKRsAAAAAACJRIEb2SXyy8Z1Qw+npgqlQ3MhiFLAfzOQ3pCBhx72xIw0zuAUBAAAAAAAWABTJijE0v48z5ZmmfEAADXdCBcG0FAAAAAAAAQDiAgAAAAABAUfY2D1t0dyMeEH39C1yOdIxigpqm7XJNqHVT3Lc+FkiAAAAAAD+////AhIsGwAAAAAAGXapFJ5+8XZ3ZP80oFldvEwrcNsBftBmiKyYdK6xAAAAABepFLDBn59UffGbX7u/olyFDG0eG1UJhwJHMEQCIDAd3s05C61flXVFqOtov0NoHRGr8KFcOpH6R/81F46EAiBt+j9hHyvT2hYEyf8fdYsM9IgbnybtPV+kRTHDa6Rj0AEhAmmZfwmoHsmCkEOn9AfRTh+863mURelmE8hSqL4MG1EydJwgACICAi5ovBH1xLoGxPqtFh48wUEpnM+St1SbPzRwO7kBNKOQRzBEAiBpWClBybtHveXkhAgTiE8QSczMJs8MGuH4LOSNRA6s/AIgWlbB3xJOtJIsszj1qZ/whA5jK9wnTzeZzDlVs/ivq2cBAAEBK7gFAQAAAAAAIlEglCHnNLD50sRn6n3Rl8Yay0RnzcvJ9MsMVx+LY6XEDK4BE0ADaUubfpFFrzbU+vL8qCzZE/FO+9unzylfpIgQZ4HTy2qPUtLvbyH59GApdz0SiUZGl8K6Crvt9YIfI/5FxbOLARcglCHnNLD50sRn6n3Rl8Yay0RnzcvJ9MsMVx+LY6XEDK4AAQEfECcAAAAAAAAWABRPoqyhKLb53uUwnE9wBR5Jxl/XMiICA6VOpEBbPJM/xYsqO2euttYFpgec9vcxggyTyoklK660SDBFAiEAoCIktghL55iuMAmkzwYJzb+h+qmNewZXxAx/06ObxIQCIELCsBz/wd2wPlnJb27OluxMkTPnCyHA2C+SxHiX/FvPAQAAAA==", + "result": "cHNidP8BAM8CAAAAAwPzd9k+uLSN1rgF01xY1TIA/8N+YytNZ4VP9gKFP4MyAAAAAAD/////ZtAAqL2E1fKcmGo+7xuqS+nSQeKFVKGRYaHfIvLXn4sAAAAAAP////9+h+SlCwIx1MUDT7Bek0NrWXS7xnSPi5LbYbDc9sxYIgAAAAAA/////wIgKRsAAAAAACJRIEb2SXyy8Z1Qw+npgqlQ3MhiFLAfzOQ3pCBhx72xIw0zuAUBAAAAAAAWABTJijE0v48z5ZmmfEAADXdCBcG0FAAAAAAAAQDiAgAAAAABAUfY2D1t0dyMeEH39C1yOdIxigpqm7XJNqHVT3Lc+FkiAAAAAAD+////AhIsGwAAAAAAGXapFJ5+8XZ3ZP80oFldvEwrcNsBftBmiKyYdK6xAAAAABepFLDBn59UffGbX7u/olyFDG0eG1UJhwJHMEQCIDAd3s05C61flXVFqOtov0NoHRGr8KFcOpH6R/81F46EAiBt+j9hHyvT2hYEyf8fdYsM9IgbnybtPV+kRTHDa6Rj0AEhAmmZfwmoHsmCkEOn9AfRTh+863mURelmE8hSqL4MG1EydJwgAAEHakcwRAIgaVgpQcm7R73l5IQIE4hPEEnMzCbPDBrh+CzkjUQOrPwCIFpWwd8STrSSLLM49amf8IQOYyvcJ083mcw5VbP4r6tnASECLmi8EfXEugbE+q0WHjzBQSmcz5K3VJs/NHA7uQE0o5AAAQEruAUBAAAAAAAiUSCUIec0sPnSxGfqfdGXxhrLRGfNy8n0ywxXH4tjpcQMrgEIQgFAA2lLm36RRa821Pry/Kgs2RPxTvvbp88pX6SIEGeB08tqj1LS728h+fRgKXc9EolGRpfCugq77fWCHyP+RcWziwABAR8QJwAAAAAAABYAFE+irKEotvne5TCcT3AFHknGX9cyAQhsAkgwRQIhAKAiJLYIS+eYrjAJpM8GCc2/ofqpjXsGV8QMf9Ojm8SEAiBCwrAc/8HdsD5ZyW9uzpbsTJEz5wshwNgvksR4l/xbzwEhA6VOpEBbPJM/xYsqO2euttYFpgec9vcxggyTyoklK660AAAA", + "isTaproot": true + }, + { + "description": "Finalze taproot with tapkey sig and tapscript sigs (choose tapkey)", + "psbt": "cHNidP8BAF4CAAAAAQrtX/VtEfTwY2iXi+s8lzx2JZbV7w9a8q6lONJ4SBm1AAAAAAD/////AZBBBgAAAAAAIlEgp+9JIlFbn3ZwqTz8p4UU73qJfQl4FvNDuyBa51FMm/kAAAAAAAEBK6BoBgAAAAAAIlEgp+9JIlFbn3ZwqTz8p4UU73qJfQl4FvNDuyBa51FMm/kBE0DqpLv0n1NC7N2okYWhHwP+mCqaDGOmNGCobgPw54CAtJdHPKY2i/ioNzq/m2Muh0hUYcLlOcU5U4xF7W+6gvRuQRQGAr5sFRVLM9rtDJmH/nCNJu0u72GXSA2wf9k4GEnXrossGVn91s+ghIb0v5fVWfWjsqU6wRJBundeShgEvDIVQNduanOw6c03gjyLWLim7b/7yet7XRp1uNrtwrMklOMSujYI/h2o+TSqS7eNwCkNddW3R6v03L6S9hWHTUYprclBFDhs6fMvKmERoTq2yrBafaRMmYoKkSuw9D4o3xjiC403iywZWf3Wz6CEhvS/l9VZ9aOypTrBEkG6d15KGAS8MhVAMKZJmsPn+pjmlWzj5oPGOj8XSs0uCMnDL1jjnosEuO7G0y9Y46GlGnfOM54w/0n2qKkdDWHpXX14F9gNesiP5EEUfKybjQL7rXdJwSP1Fb15LaJyYFrW3yJnkN+aP9IdhTOGRF1EGo0fkg5hbe3o7iwS+jTgqmGOQDsHExHbJgmL90A8hHY8WzI+xJsFXMzi5ztkZS8pXf07iNlfDnLKZYyk9ZRBkAsJ9oYsvC19Irrq27l918aqfQc6sZBiskZyMs5wQRR8rJuNAvutd0nBI/UVvXktonJgWtbfImeQ35o/0h2FM4ssGVn91s+ghIb0v5fVWfWjsqU6wRJBundeShgEvDIVQFeVntO1MSRANCT3fpEWhijVJAbF6SVBdPlNAaRKc1sn3Gz+/HTfD+f9sqRlSIsF4D+ouhv6URBtAZUCuKHW6vxiFcFB6oQ4lfHrNv47TKgSz/MUn2hgozxlIgmi2demetlGgxpSnJ+zzX53bWG2IltsYQ6JBvuPqmxZrFw+lbX4LSnWhkRdRBqNH5IOYW3t6O4sEvo04KphjkA7BxMR2yYJi/dpIHysm40C+613ScEj9RW9eS2icmBa1t8iZ5Dfmj/SHYUzrCAGAr5sFRVLM9rtDJmH/nCNJu0u72GXSA2wf9k4GEnXrrogOGzp8y8qYRGhOrbKsFp9pEyZigqRK7D0PijfGOILjTe6U5zAQhXBQeqEOJXx6zb+O0yoEs/zFJ9oYKM8ZSIJotnXpnrZRoMvbQYwKuRcSf7lDrRMU3H56Gh6NeGkogAw4hpvAn/mgCMgfKybjQL7rXdJwSP1Fb15LaJyYFrW3yJnkN+aP9IdhTOswAEXIEHqhDiV8es2/jtMqBLP8xSfaGCjPGUiCaLZ16Z62UaDARggNhmyJFW2/EeankNIJjRP9QFlju4/j/nEEcMAnlXe+XcAAA==", + "result": "cHNidP8BAF4CAAAAAQrtX/VtEfTwY2iXi+s8lzx2JZbV7w9a8q6lONJ4SBm1AAAAAAD/////AZBBBgAAAAAAIlEgp+9JIlFbn3ZwqTz8p4UU73qJfQl4FvNDuyBa51FMm/kAAAAAAAEBK6BoBgAAAAAAIlEgp+9JIlFbn3ZwqTz8p4UU73qJfQl4FvNDuyBa51FMm/kBCEIBQOqku/SfU0Ls3aiRhaEfA/6YKpoMY6Y0YKhuA/DngIC0l0c8pjaL+Kg3Or+bYy6HSFRhwuU5xTlTjEXtb7qC9G4AAA==", + "isTaproot": true + }, + { + "description": "Finalze taproot script-path with two tapleafs signed (chooses the one with the shortest path)", + "psbt": "cHNidP8BAF4CAAAAAZsypn8thOjCXwg39Z1pAQCp7ndI8Jrw/8SGSmhgRiJpAAAAAAD/////AZBBBgAAAAAAIlEgFa11cf11Y+PGzl0UxS5788pld5zElAl9YY0OtTHhHzQAAAAAAAEBK6BoBgAAAAAAIlEgFa11cf11Y+PGzl0UxS5788pld5zElAl9YY0OtTHhHzRBFAUf68vx5rAfWwxMqZNce2OVVqlduXONBcm2JEPAT2Fc2gpbUnze59ZSHmflF7erkp8eUGBfJBmAcUZG5WBbC/pAJihlaGGDKsSd04fSyIk5WDACUedUvDE1C4zD80ubbX5qFzgxizY7pGG5zrrGAoW0KOEJ8TW4WEmh/WsE0erDxEEUIKvXw9clC9dJYjC2Woo/TrIhnRP9powDMmB/GF4rBRsRCNvHrz3TPp4Id2O6W4qhmerk87mqanTdLIapgqEamkDnpOB240Ce+yZlOPO3sxZDmpXhmsxtIl6a/R6TS2ekf2VCdZXg4Dqy3OpjzxQ/jDMbXbzETL2mdHlsgQnBnUcoYhXBr6rSntatZCCpyXS0zh+WAkLdMI4XHA0rVYAbFyHRzrqYKqOqTjCjec9FzSPuoLS+tXB4VvMgzUTs3kZS/bDfqlRY6TCnm2dtB3ED1fA6rlsUAqbWuurDp2W9ONMZj7NiIyAFH+vL8eawH1sMTKmTXHtjlVapXblzjQXJtiRDwE9hXKzAohXBr6rSntatZCCpyXS0zh+WAkLdMI4XHA0rVYAbFyHRzrq0JN6gn4QLkyoANzzc29JWULjDrP5UqfSmQaKGchuNJtrHlXZrvaHq6qReW/oKlQ/dX0xKraWx8wgu3JaJuf0K7iijuCxK7L69N4En3IjdGeHqk+26eie3VOHIgdMIh0q9q7FEmRK2sbmE7NhWpbGiU/lLBm6XT+D9HvPO+T63rSMgIKvXw9clC9dJYjC2Woo/TrIhnRP9powDMmB/GF4rBRuswAAA", + "result": "cHNidP8BAF4CAAAAAZsypn8thOjCXwg39Z1pAQCp7ndI8Jrw/8SGSmhgRiJpAAAAAAD/////AZBBBgAAAAAAIlEgFa11cf11Y+PGzl0UxS5788pld5zElAl9YY0OtTHhHzQAAAAAAAEBK6BoBgAAAAAAIlEgFa11cf11Y+PGzl0UxS5788pld5zElAl9YY0OtTHhHzQBCMcDQCYoZWhhgyrEndOH0siJOVgwAlHnVLwxNQuMw/NLm21+ahc4MYs2O6Rhuc66xgKFtCjhCfE1uFhJof1rBNHqw8QiIAUf68vx5rAfWwxMqZNce2OVVqlduXONBcm2JEPAT2FcrGHBr6rSntatZCCpyXS0zh+WAkLdMI4XHA0rVYAbFyHRzrqYKqOqTjCjec9FzSPuoLS+tXB4VvMgzUTs3kZS/bDfqlRY6TCnm2dtB3ED1fA6rlsUAqbWuurDp2W9ONMZj7NiAAA=", + "isTaproot": true + }, + { + "description": "Finalze taproot script-path with two tapleafs signed (explicitly choose leaf)", + "psbt": "cHNidP8BAF4CAAAAAZsypn8thOjCXwg39Z1pAQCp7ndI8Jrw/8SGSmhgRiJpAAAAAAD/////AZBBBgAAAAAAIlEgFa11cf11Y+PGzl0UxS5788pld5zElAl9YY0OtTHhHzQAAAAAAAEBK6BoBgAAAAAAIlEgFa11cf11Y+PGzl0UxS5788pld5zElAl9YY0OtTHhHzRBFAUf68vx5rAfWwxMqZNce2OVVqlduXONBcm2JEPAT2Fc2gpbUnze59ZSHmflF7erkp8eUGBfJBmAcUZG5WBbC/pAJihlaGGDKsSd04fSyIk5WDACUedUvDE1C4zD80ubbX5qFzgxizY7pGG5zrrGAoW0KOEJ8TW4WEmh/WsE0erDxEEUIKvXw9clC9dJYjC2Woo/TrIhnRP9powDMmB/GF4rBRsRCNvHrz3TPp4Id2O6W4qhmerk87mqanTdLIapgqEamkDnpOB240Ce+yZlOPO3sxZDmpXhmsxtIl6a/R6TS2ekf2VCdZXg4Dqy3OpjzxQ/jDMbXbzETL2mdHlsgQnBnUcoYhXBr6rSntatZCCpyXS0zh+WAkLdMI4XHA0rVYAbFyHRzrqYKqOqTjCjec9FzSPuoLS+tXB4VvMgzUTs3kZS/bDfqlRY6TCnm2dtB3ED1fA6rlsUAqbWuurDp2W9ONMZj7NiIyAFH+vL8eawH1sMTKmTXHtjlVapXblzjQXJtiRDwE9hXKzAohXBr6rSntatZCCpyXS0zh+WAkLdMI4XHA0rVYAbFyHRzrq0JN6gn4QLkyoANzzc29JWULjDrP5UqfSmQaKGchuNJtrHlXZrvaHq6qReW/oKlQ/dX0xKraWx8wgu3JaJuf0K7iijuCxK7L69N4En3IjdGeHqk+26eie3VOHIgdMIh0q9q7FEmRK2sbmE7NhWpbGiU/lLBm6XT+D9HvPO+T63rSMgIKvXw9clC9dJYjC2Woo/TrIhnRP9powDMmB/GF4rBRuswAAA", + "result": "cHNidP8BAF4CAAAAAZsypn8thOjCXwg39Z1pAQCp7ndI8Jrw/8SGSmhgRiJpAAAAAAD/////AZBBBgAAAAAAIlEgFa11cf11Y+PGzl0UxS5788pld5zElAl9YY0OtTHhHzQAAAAAAAEBK6BoBgAAAAAAIlEgFa11cf11Y+PGzl0UxS5788pld5zElAl9YY0OtTHhHzQBCMcDQCYoZWhhgyrEndOH0siJOVgwAlHnVLwxNQuMw/NLm21+ahc4MYs2O6Rhuc66xgKFtCjhCfE1uFhJof1rBNHqw8QiIAUf68vx5rAfWwxMqZNce2OVVqlduXONBcm2JEPAT2FcrGHBr6rSntatZCCpyXS0zh+WAkLdMI4XHA0rVYAbFyHRzrqYKqOqTjCjec9FzSPuoLS+tXB4VvMgzUTs3kZS/bDfqlRY6TCnm2dtB3ED1fA6rlsUAqbWuurDp2W9ONMZj7NiAAA=", + "isTaproot": true } ], "extractor": [ @@ -338,6 +473,51 @@ "index": 2 }, "equals": "cHNidP8BADMCAAAAAQABAgMEBQYHCAkKCwwNDg8AAQIDBAUGBwgJCgsMDQ4PAgAAAAD/////AAAAAAAAAAA=" + }, + { + "description": "checks for mixed taproot and non-taproot fields", + "inputData": { + "hash": "Buffer.from('000102030405060708090a0b0c0d0e0f000102030405060708090a0b0c0d0e0f', 'hex')", + "index": 0, + "redeemScript": "Buffer.from('0014000102030405060708090a0b0c0d0e0f00010000', 'hex')", + "tapInternalKey": "Buffer.from('000102030405060708090a0b0c0d0e0f00010203040506070000000000000000', 'hex')" + }, + "exception": "Invalid arguments for Psbt.addInput. Cannot use both taproot and non-taproot fields." + }, + { + "description": "checks for tapleaf in taptree", + "inputData": { + "hash": "Buffer.from('000102030405060708090a0b0c0d0e0f000102030405060708090a0b0c0d0e0f', 'hex')", + "index": 0, + "tapMerkleRoot": "Buffer.from('5cf5873456b400364b18bdc9a1a233870fa622a6010deef1d4e08474f3a103b4', 'hex')", + "tapLeafScript": [ + { + "leafVersion": 192, + "script": "Buffer.from('20d5e347235eba74ae0cec686b668d5a9432f45a555d6ab22cebf5974bde9dc4f3ac', 'hex')", + "controlBlock": "Buffer.from('c0720768b9946ac22371653d92cc343bf109b8a3f819e231159fd4f2b1328944ecb424dea09f840b932a00373cdcdbd25650b8c3acfe54a9f4a641a286721b8d26dac795766bbda1eaeaa45e5bfa0a950fdd5f4c4aada5b1f3082edc9689b9fd0aee28a3b82c4aecbebd378127dc88dd19e1ea93edba7a27b754e1c881d308874a7ac4489544b7b840d045599ec9415ae71ccb090c2c465c05c9fa05f41c7a06d5', 'hex')" + } + ] + }, + "exception": "Invalid arguments for Psbt.addInput. Tapleaf not part of taptree." + } + ] + }, + "updateInput": { + "checks": [ + { + "description": "checks for new tapleaf in taptree", + "psbt": "cHNidP8BADMCAAAAAQ1YzlMrOEvTUad+B8zippgoNUwrXhwvsHMMGwZb6S7LAAAAAAD/////AAAAAAAAAQEroGgGAAAAAAAiUSD3Vv2gTbCrHTNlF18uY/YKLdATh0ph4pwrCyUHkhtZngEYIHsGySMEjYsxl3fCSP6Ap7tG06AUPLXA8nGnwzSEH70VAAA=", + "index": 0, + "inputData": { + "tapLeafScript": [ + { + "leafVersion": 192, + "script": "Buffer.from('20d5e347235eba74ae0cec686b668d5a9432f45a555d6ab22cebf5974bde9dc4f3ac', 'hex')", + "controlBlock": "Buffer.from('c0720768b9946ac22371653d92cc343bf109b8a3f819e231159fd4f2b1328944ecb424dea09f840b932a00373cdcdbd25650b8c3acfe54a9f4a641a286721b8d26dac795766bbda1eaeaa45e5bfa0a950fdd5f4c4aada5b1f3082edc9689b9fd0aee28a3b82c4aecbebd378127dc88dd19e1ea93edba7a27b754e1c881d308874a7ac4489544b7b840d045599ec9415ae71ccb090c2c465c05c9fa05f41c7a06d5', 'hex')" + } + ] + }, + "exception": "Invalid arguments for Psbt.updateInput. Tapleaf not part of taptree." } ] }, @@ -351,12 +531,133 @@ }, "exception": "Error adding output." }, + { + "description": "Checks the mandatory values for adding an output", + "outputData": { + "value": 1000 + }, + "exception": "Invalid arguments for Psbt\\.addOutput. Requires single object with at least \\[script or address\\] and \\[value\\]" + }, { "description": "Adds output normally", "outputData": { "address": "1P2NFEBp32V2arRwZNww6tgXEV58FG94mr", "value": 42 } + }, + { + "description": "Adds taproot output with internal tap key and tap tree", + "isTaproot": true, + "psbt": "cHNidP8BADMCAAAAAcbgSGx76du9GXsr4c6Yk7DFglfHi7M2jdCNUXwc+Q+EAAAAAAD/////AAAAAAAAAQEroGgGAAAAAAAiUSClLxmVQ6aZXLEwkYA/WGZuIE91BHT7xP7DIEAvz/NITaIVwd2NnolThxX+QCpgFksj8u93bzuZy6olaHxJNHaArwK1tCTeoJ+EC5MqADc83NvSVlC4w6z+VKn0pkGihnIbjSbax5V2a72h6uqkXlv6CpUP3V9MSq2lsfMILtyWibn9Cu4oo7gsSuy+vTeBJ9yI3Rnh6pPtunont1ThyIHTCIdKesRIlUS3uEDQRVmeyUFa5xzLCQwsRlwFyfoF9Bx6BtUjIFX368Cp2d6uXSbyrn23vC4KMjRRlEssuTTh7ThM6bXQrMAAAA==", + "outputData": { + "address": "bc1px4ssshedlz4jc56ses3lftz462a06jwy8my4pwpx6twx30vvv6nsgwcpu3", + "value": 410000, + "tapInternalKey": "Buffer.from('f6d4ce132444de7f0e3a1d2be9b38ceec798cf9a76eeeac585869445830eb167', 'hex')", + "tapTree": { + "leaves": [ + { + "depth": 2, + "leafVersion": 192, + "script": "Buffer.from('2050929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0ac', 'hex')" + }, + { + "depth": 3, + "leafVersion": 192, + "script": "Buffer.from('2050929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac1ac', 'hex')" + }, + { + "depth": 3, + "leafVersion": 192, + "script": "Buffer.from('202258b1c3160be0864a541854eec9164a572f094f7562628281a8073bb89173a7ac', 'hex')" + }, + { + "depth": 2, + "leafVersion": 192, + "script": "Buffer.from('2050929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac2ac', 'hex')" + }, + { + "depth": 3, + "leafVersion": 192, + "script": "Buffer.from('2050929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac3ac', 'hex')" + }, + { + "depth": 4, + "leafVersion": 192, + "script": "Buffer.from('2050929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac4ac', 'hex')" + }, + { + "depth": 4, + "leafVersion": 192, + "script": "Buffer.from('2055f7ebc0a9d9deae5d26f2ae7db7bc2e0a323451944b2cb934e1ed384ce9b5d0ac', 'hex')" + } + ] + } + }, + "result": "cHNidP8BAF4CAAAAAcbgSGx76du9GXsr4c6Yk7DFglfHi7M2jdCNUXwc+Q+EAAAAAAD/////AZBBBgAAAAAAIlEgNWEIXy34qyxTUMwj9KxV0rr9ScQ+yVC4JtLcaL2MZqcAAAAAAAEBK6BoBgAAAAAAIlEgpS8ZlUOmmVyxMJGAP1hmbiBPdQR0+8T+wyBAL8/zSE2iFcHdjZ6JU4cV/kAqYBZLI/Lvd287mcuqJWh8STR2gK8CtbQk3qCfhAuTKgA3PNzb0lZQuMOs/lSp9KZBooZyG40m2seVdmu9oerqpF5b+gqVD91fTEqtpbHzCC7clom5/QruKKO4LErsvr03gSfciN0Z4eqT7bp6J7dU4ciB0wiHSnrESJVEt7hA0EVZnslBWuccywkMLEZcBcn6BfQcegbVIyBV9+vAqdnerl0m8q59t7wuCjI0UZRLLLk04e04TOm10KzAAAEFIPbUzhMkRN5/DjodK+mzjO7HmM+adu7qxYWGlEWDDrFnAQb9AwECwCIgUJKbdMGgSVS3i0tgNel6XgeKWg8o7JbVR7/ums6AOsCsA8AiIFCSm3TBoElUt4tLYDXpel4HiloPKOyW1Ue/7prOgDrBrAPAIiAiWLHDFgvghkpUGFTuyRZKVy8JT3ViYoKBqAc7uJFzp6wCwCIgUJKbdMGgSVS3i0tgNel6XgeKWg8o7JbVR7/ums6AOsKsA8AiIFCSm3TBoElUt4tLYDXpel4HiloPKOyW1Ue/7prOgDrDrATAIiBQkpt0waBJVLeLS2A16XpeB4paDyjsltVHv+6azoA6xKwEwCIgVffrwKnZ3q5dJvKufbe8LgoyNFGUSyy5NOHtOEzptdCsAA==" + }, + { + "description": "Adds taproot output with internal tap key and correct address", + "isTaproot": true, + "psbt": "cHNidP8BAFwCAAAAAk9Ve2845C8v//JR71uKzf70FjeNfx7SvB4l3A+Q44UKAAAAAAD/////FIp10hu+RPgTSFGPmjwb01Tf/a5UkcPFUpOw/X6UEPYAAAAAAP////8AAAAAAAABASugaAYAAAAAACJRINqadqaDhePTT29qdKQScUmyJxEeDw12HLjkMYMVSlnkARcgDN4bJzix3HdE8+FUM6UgA1lEKpgpIImVUePmAGYh7yYAAQDAAgAAAAFbIoaAJqDd/f4mZppgYV4UR9bgU8lT9pTH4/aDJdFUWgAAAABrSDBFAiEA6JS0xMdSQhG+9gAPSxGs6HazyauyNUBMwrmF386IAxwCIGiyH9QHCzKOBTtg+VsISF4nUi9NfAtJAtC02J03+I07ASEDu1tkEI8W1bd6qrPZ3uLaHvE90BDUvRCwvTTzPYPXzwf/////AaBoBgAAAAAAGXapFL5QEf51k4TA7Mp5d18IM2ddq79oiKwAAAAAAAA=", + "outputData": { + "tapInternalKey": "Buffer.from('884d969439deced21d1ab71ecd9ef9a6a8795215588ce7eff4ad5efc903e40ec', 'hex')", + "address": "bc1p74zfvfd7rndcn8lvzuec4hj4kzp9nnjvapvx5fgtqsegzz656hhsee8kwu", + "value": 410000 + }, + "result": "cHNidP8BAIcCAAAAAk9Ve2845C8v//JR71uKzf70FjeNfx7SvB4l3A+Q44UKAAAAAAD/////FIp10hu+RPgTSFGPmjwb01Tf/a5UkcPFUpOw/X6UEPYAAAAAAP////8BkEEGAAAAAAAiUSD1RJYlvhzbiZ/sFzOK3lWwglnOTOhYaiULBDKBC1TV7wAAAAAAAQEroGgGAAAAAAAiUSDamnamg4Xj009vanSkEnFJsicRHg8Ndhy45DGDFUpZ5AEXIAzeGyc4sdx3RPPhVDOlIANZRCqYKSCJlVHj5gBmIe8mAAEAwAIAAAABWyKGgCag3f3+JmaaYGFeFEfW4FPJU/aUx+P2gyXRVFoAAAAAa0gwRQIhAOiUtMTHUkIRvvYAD0sRrOh2s8mrsjVATMK5hd/OiAMcAiBosh/UBwsyjgU7YPlbCEheJ1IvTXwLSQLQtNidN/iNOwEhA7tbZBCPFtW3eqqz2d7i2h7xPdAQ1L0QsL008z2D188H/////wGgaAYAAAAAABl2qRS+UBH+dZOEwOzKeXdfCDNnXau/aIisAAAAAAABBSCITZaUOd7O0h0atx7NnvmmqHlSFViM5+/0rV78kD5A7AA=" + }, + { + "description": "Adds taproot output with internal tap key and bad address", + "isTaproot": true, + "psbt": "cHNidP8BAFwCAAAAAk9Ve2845C8v//JR71uKzf70FjeNfx7SvB4l3A+Q44UKAAAAAAD/////FIp10hu+RPgTSFGPmjwb01Tf/a5UkcPFUpOw/X6UEPYAAAAAAP////8AAAAAAAABASugaAYAAAAAACJRINqadqaDhePTT29qdKQScUmyJxEeDw12HLjkMYMVSlnkARcgDN4bJzix3HdE8+FUM6UgA1lEKpgpIImVUePmAGYh7yYAAQDAAgAAAAFbIoaAJqDd/f4mZppgYV4UR9bgU8lT9pTH4/aDJdFUWgAAAABrSDBFAiEA6JS0xMdSQhG+9gAPSxGs6HazyauyNUBMwrmF386IAxwCIGiyH9QHCzKOBTtg+VsISF4nUi9NfAtJAtC02J03+I07ASEDu1tkEI8W1bd6qrPZ3uLaHvE90BDUvRCwvTTzPYPXzwf/////AaBoBgAAAAAAGXapFL5QEf51k4TA7Mp5d18IM2ddq79oiKwAAAAAAAA=", + "outputData": { + "tapInternalKey": "Buffer.from('884d969439deced21d1ab71ecd9ef9a6a8795215588ce7eff4ad5efc903e40ec', 'hex')", + "address": "bc1p3efq8ujsj0qr5xvms7mv89p8cz0crqdtuxe9ms6grqgxc9sgsntslthf6w", + "value": 410000 + }, + "exception": "Error adding output. Script or address missmatch." + }, + { + "description": "Adds taproot output with both taproot and non-taproot fields", + "isTaproot": true, + "psbt": "cHNidP8BAFwCAAAAAk9Ve2845C8v//JR71uKzf70FjeNfx7SvB4l3A+Q44UKAAAAAAD/////FIp10hu+RPgTSFGPmjwb01Tf/a5UkcPFUpOw/X6UEPYAAAAAAP////8AAAAAAAABASugaAYAAAAAACJRINqadqaDhePTT29qdKQScUmyJxEeDw12HLjkMYMVSlnkARcgDN4bJzix3HdE8+FUM6UgA1lEKpgpIImVUePmAGYh7yYAAQDAAgAAAAFbIoaAJqDd/f4mZppgYV4UR9bgU8lT9pTH4/aDJdFUWgAAAABrSDBFAiEA6JS0xMdSQhG+9gAPSxGs6HazyauyNUBMwrmF386IAxwCIGiyH9QHCzKOBTtg+VsISF4nUi9NfAtJAtC02J03+I07ASEDu1tkEI8W1bd6qrPZ3uLaHvE90BDUvRCwvTTzPYPXzwf/////AaBoBgAAAAAAGXapFL5QEf51k4TA7Mp5d18IM2ddq79oiKwAAAAAAAA=", + "outputData": { + "address": "bc1px4ssshedlz4jc56ses3lftz462a06jwy8my4pwpx6twx30vvv6nsgwcpu3", + "value": 410000, + "tapInternalKey": "Buffer.from('884d969439deced21d1ab71ecd9ef9a6a8795215588ce7eff4ad5efc903e40ec', 'hex')", + "redeemScript": "Buffer.from('5221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752ae', 'hex')" + }, + "exception": "Invalid arguments for Psbt.addOutput. Cannot use both taproot and non-taproot fields." + }, + { + "description": "Adds output after tapkey signature has been added", + "isTaproot": true, + "psbt": "cHNidP8BAIcCAAAAAvz6EyMK2VQ0DFKWZyIyGZLZCnaIDtG5RI55OrGVs6u7AAAAAAD/////oB9A2l2NN5WPNjBDaRsz9Nmla1WdM3jl96ElSV5WWHMAAAAAAP////8BkEEGAAAAAAAiUSCnucwjGG2x9zGqP+E0PwxTkptd/9AYMW12ch7ffaogBAAAAAAAAQEroGgGAAAAAAAiUSDbRhL5phuWYLyqKbSqVb/Pic2mpPL46idVKQur9i06ugETQOPidQWL9dHDxd819Pn5QNNGyH0fmDv58pexSJVDalZ0W/HPVliPFZy7s//0TfTbvFce87/nKXcrwz5YuItIY6cBFyAOgJIoTQyYcpAqNfsK9CutgWckFE3mBMgpL50lKXb7oQABAL8CAAAAAcdkwPNWdYy1O8QM1xRkrJjas72QGQPEanYEpv6iT3PXAAAAAGpHMEQCIFpl6Nx8hoZA1o5Huo958RS/Hndy+DloLhp0nvRyBzN8AiB9jEbitThSs6TZ1XVBHZebj4nZbLMg3CCtss8WSZpv7QEhA+VxIxwCWeqoeDMYcEw3EC0ZPxIkOqt0YSK9Y3hAKTd1/////wGgaAYAAAAAABl2qRS9gA7n636cw49cUBJWm7IDJPKuuoisAAAAACICAknGKVC7w5Ek4l3IK5XCUwLNCeU4DD+76cdMAYDB72g3RzBEAiAjekFbBddaHY/LZidwKd3sBbifjmbEpka2Ps0B2Crg+wIgUoN4BhGCwMRTDaHbAX+0MXSGYA9nSlqeQJXpWQRCBfQBAAEFIE6DvlLK0f4BrtpMgez01WQYKLUQZ8HtbfuDPD52hmSvAA==", + "outputData": { + "address": "bc1px4ssshedlz4jc56ses3lftz462a06jwy8my4pwpx6twx30vvv6nsgwcpu3", + "value": 410000 + }, + "exception": "Can not modify transaction, signatures exist." + }, + { + "description": "Adds output after tap key input has been finalized", + "isTaproot": true, + "psbt": "cHNidP8BAF4CAAAAAfyhtBTm+3OPYBuMHvdPMf9jqniZDY925hbmnt8hxbA6AAAAAAD/////AZBBBgAAAAAAIlEgV6CyzSs5a6Tc8A+TYOiozWnh6FRH9E/VWFanhMAEbLEAAAAAAAEBK6BoBgAAAAAAIlEgV6CyzSs5a6Tc8A+TYOiozWnh6FRH9E/VWFanhMAEbLEBCEIBQOAqWBD/2jhPWzQvesT8sjkN2Cowphp3QvmlWbHiLx753ChcUovvWyBlWiCq77Kk+lZGEhC4vjClSjc26br+dc8AAA==", + "outputData": { + "address": "bc1px4ssshedlz4jc56ses3lftz462a06jwy8my4pwpx6twx30vvv6nsgwcpu3", + "value": 410000 + }, + "exception": "Can not modify transaction, signatures exist." + }, + { + "description": "Adds output after tapscript signatures have been added", + "isTaproot": true, + "psbt": "cHNidP8BAF4CAAAAAa/0mhnSBXdEBKbbMC+2hm6AZZtCLBxBeubd1sjtau5dAAAAAAD/////AZBBBgAAAAAAIlEgISRIfamb9rCYzad52ikfoUUuFlvyTcImZMavR0jEaUQAAAAAAAEBK6BoBgAAAAAAIlEgISRIfamb9rCYzad52ikfoUUuFlvyTcImZMavR0jEaURBFBnEcOpwiHjNYPtWJOrQ8Pgc9bxBKyZh/i2D837Z1rC8BibL1C4Z/5e6dKzWfkzpsIbE5WEVn1bYpAAjrqIKMHlAKkl3w3Gfpkl9b0yDVbTlZd4yCEL9V2DJs6zpPrEmn3wiohBy8wwE6EZ0FxQdrCupnHKXhHBjpcHVwfJRQfcy9EEULx1ijisiHgGb/9/hBNhsIOv1ZyWsfmi/Ql+oz7AOuqAGJsvULhn/l7p0rNZ+TOmwhsTlYRWfVtikACOuogoweUBKNkxBf6vT8m7ISt1WikLWW9udCP7OQLXztr1IPalJT5z+esAWmgeLS7QoLgzTu8AnYp/rHxsgZ6CgiV8tlkciQRRXE7VxCk67h7Ee6CbSgNyotChx7CgwNTfxdJkyvCS0DgYmy9QuGf+XunSs1n5M6bCGxOVhFZ9W2KQAI66iCjB5QFN6DGtLlSIFBjZbdh3rbKBtBcEDSiEcuVxnSPpdM1RnQRmw5Ujo+/76wZfmGBMFzV0IA7vnHzzXN73jT6O8/wJiFcDBdB6IhNxYUBYgZT1K7FG5SblQ3S6nQMKRLc2vPcA0BhpSnJ+zzX53bWG2IltsYQ6JBvuPqmxZrFw+lbX4LSnWGlKcn7PNfndtYbYiW2xhDokG+4+qbFmsXD6VtfgtKdZpIFcTtXEKTruHsR7oJtKA3Ki0KHHsKDA1N/F0mTK8JLQOrCAvHWKOKyIeAZv/3+EE2Gwg6/VnJax+aL9CX6jPsA66oLogGcRw6nCIeM1g+1Yk6tDw+Bz1vEErJmH+LYPzftnWsLy6U5zAAAA=", + "outputData": { + "address": "bc1px4ssshedlz4jc56ses3lftz462a06jwy8my4pwpx6twx30vvv6nsgwcpu3", + "value": 410000 + }, + "exception": "Can not modify transaction, signatures exist." } ] }, @@ -459,10 +760,32 @@ "description": "allows signing non-whitelisted sighashtype when explicitly passed in", "shouldSign": { "psbt": "cHNidP8BADMBAAAAAYaq+PdOUY2PnV9kZKa82XlqrPByOqwH2TRg2+LQdqs2AAAAAAD/////AAAAAAAAAQEgAOH1BQAAAAAXqRSTNeWHqa9INvSnQ120SZeJc+6JSocBAwSBAAAAAQQWABQvLKRyDqYsPYImhD3eURpDGL10RwAA", - "sighashTypes": [129], + "sighashTypes": [ + 129 + ], "inputToCheck": 0, "WIF": "KxnAnQh6UJBxLF8Weup77yn8tWhLHhDhnXeyJuzmmcZA5aRdMJni" } + }, + { + "description": "checks taproot key-path signer (tweaked key) matches internal tap key", + "isTaproot": true, + "shouldSign": { + "psbt": "cHNidP8BAF4CAAAAAf17fGksrz9eKGx1nSU3RX4vcwr7bfNdQzPZ9dSEkWBcAAAAAAD/////AZBBBgAAAAAAIlEgPLBe/d3922lmXjTIt52b9NG1HFDC9jzPCTn111AG8TQAAAAAAAEBK6BoBgAAAAAAIlEgPLBe/d3922lmXjTIt52b9NG1HFDC9jzPCTn111AG8TQBFyBbMLiqcPfsndZoMRtk9GKEFd0jSA3rwEa60nKQNwdHrAEYIOUOtfVyfPBm+ZzIsEfwzo2JL2WTslXAznzBllU7Oa4zAAA=", + "inputToCheck": 0, + "WIF": "KypUz2y1jdgzM8HGDUx9DYLmyzd8EWhruvLX2J5iSL7MiAcc7dBG", + "result": "cHNidP8BAF4CAAAAAf17fGksrz9eKGx1nSU3RX4vcwr7bfNdQzPZ9dSEkWBcAAAAAAD/////AZBBBgAAAAAAIlEgPLBe/d3922lmXjTIt52b9NG1HFDC9jzPCTn111AG8TQAAAAAAAEBK6BoBgAAAAAAIlEgPLBe/d3922lmXjTIt52b9NG1HFDC9jzPCTn111AG8TQBE0AAqkKg+dq3eThMoqzjh214urhgUoGTgwHlNyyMQ2RwhfeRIhAp+m9mZwQoXOxK7p2ILjf2G5j28F9KMhMzH7bXARcgWzC4qnD37J3WaDEbZPRihBXdI0gN68BGutJykDcHR6wBGCDlDrX1cnzwZvmcyLBH8M6NiS9lk7JVwM58wZZVOzmuMwAA" + } + }, + { + "description": "check taproot script-path signer found for the input", + "isTaproot": true, + "shouldSign": { + "psbt": "cHNidP8BAF4CAAAAAUYAJ/rjwsScVTFXg1in8cNdaBDtwLAsQ6l1O8sWijmlAAAAAAD/////AZBBBgAAAAAAIlEgFlrANpbJgyRLzVMhozrVHKr6rGrz7e2bQ+3TyITRhB4AAAAAAAEBK6BoBgAAAAAAIlEgFlrANpbJgyRLzVMhozrVHKr6rGrz7e2bQ+3TyITRhB5BFNs7WfGuA1DZ7ZorvaC77E4rn/I2hbVtogxHpEnxKZ3DGlKcn7PNfndtYbYiW2xhDokG+4+qbFmsXD6VtfgtKdZAbjBxWKodWLnyqPP18sLAjhTc/OXtHVtk+Abc/e8SoTIxaOORlmqOegbCKAUyL4+NFdAlgtcUyHUCxWaxJ3ykxGIVwOpmBmLM44rn+zwtvl1cyWYheMCVYuX2agqeiHOvsrXIGlKcn7PNfndtYbYiW2xhDokG+4+qbFmsXD6VtfgtKdYaUpyfs81+d21htiJbbGEOiQb7j6psWaxcPpW1+C0p1mkgq1snxZRfWXi8CMjFSaobG3elbWWeL9QqflyaQtTYEG2sINs7WfGuA1DZ7ZorvaC77E4rn/I2hbVtogxHpEnxKZ3DuiB8Z8sVUSbQp0q8mQiD5n5coMrRzQAASnO9NspgIk9pZ7pTnMAAAA==", + "inputToCheck": 0, + "WIF": "L2wCzcNaJwG1W9djnumJnPQZTCpfeCkR2wgwfupphmThSrwTMCR6", + "result": "cHNidP8BAF4CAAAAAUYAJ/rjwsScVTFXg1in8cNdaBDtwLAsQ6l1O8sWijmlAAAAAAD/////AZBBBgAAAAAAIlEgFlrANpbJgyRLzVMhozrVHKr6rGrz7e2bQ+3TyITRhB4AAAAAAAEBK6BoBgAAAAAAIlEgFlrANpbJgyRLzVMhozrVHKr6rGrz7e2bQ+3TyITRhB5BFKtbJ8WUX1l4vAjIxUmqGxt3pW1lni/UKn5cmkLU2BBtT3q/MbhvStFL2jaKZM6N8TeYvCUfSVytPrzfjkXWQbdAoS5lV4a+yP0Y4nTr7ltbsrwMo+U3TNcC/ebE6U6xk/4er9bXfdvZNvgR+TgqrO/iZjNNI6QcGaudPObgsgYKTEEU2ztZ8a4DUNntmiu9oLvsTiuf8jaFtW2iDEekSfEpncMaUpyfs81+d21htiJbbGEOiQb7j6psWaxcPpW1+C0p1kBuMHFYqh1YufKo8/XywsCOFNz85e0dW2T4Btz97xKhMjFo45GWao56BsIoBTIvj40V0CWC1xTIdQLFZrEnfKTEYhXA6mYGYszjiuf7PC2+XVzJZiF4wJVi5fZqCp6Ic6+ytcgaUpyfs81+d21htiJbbGEOiQb7j6psWaxcPpW1+C0p1hpSnJ+zzX53bWG2IltsYQ6JBvuPqmxZrFw+lbX4LSnWaSCrWyfFlF9ZeLwIyMVJqhsbd6VtZZ4v1Cp+XJpC1NgQbawg2ztZ8a4DUNntmiu9oLvsTiuf8jaFtW2iDEekSfEpncO6IHxnyxVRJtCnSryZCIPmflygytHNAABKc702ymAiT2lnulOcwAAA" + } } ] }, @@ -512,6 +835,14 @@ } ] }, + "finalizeInput": { + "finalizeTapleafByHash": { + "psbt": "cHNidP8BAF4CAAAAAXbYuDMSrbXuy1CXHZaiA8jjI+wcNkkCOL2dJD2dNf3kAAAAAAD/////AZBBBgAAAAAAIlEgcJi/YJDTvc6x6mRTIYgQG/+fEmLs2RjsO/FHW5okOEsAAAAAAAEBK6BoBgAAAAAAIlEgcJi/YJDTvc6x6mRTIYgQG/+fEmLs2RjsO/FHW5okOEtBFG78QHu9pQ+jR/B1+dz9XQtU0jFgoC5AFXcvibFi1jFAEkntiZ6MdFzn0seJJapRqACzEKXd0ZlMa34/iq0HnrVAfYaCk2E35rtXsMfbr/Hrbk/BtLF2VLepE0vIIB3UOWZEUF8akmlnL4Kh0xwMn4Cp99QivnI0B3XOsYfEznfDHUEUesul6blPS+qc/rm41L9nGaAck9KDAZAutbDsNLSZuXXMWh7785JCWLEpzsZRkM/9RJ+EaLYT/0O2sg4cHNHo1EBHFn/xRZno2fX/vLc1l2qHqZGB2vbLkZDj5KsVfqeJd78uIc5b6uAG0K+BBtq2H6HUvl+8RWd2GWmVoMTYppVNYhXAjGyV+gMPwhuPgQ05yTUt2dmE2xJLdkeHBaiap2ej2SuYKqOqTjCjec9FzSPuoLS+tXB4VvMgzUTs3kZS/bDfqvuLvWTUb+e6sUIP9JrqDAHAjkYMd3K3PTfk+B6aMTv4IyB6y6XpuU9L6pz+ubjUv2cZoByT0oMBkC61sOw0tJm5dazAohXAjGyV+gMPwhuPgQ05yTUt2dmE2xJLdkeHBaiap2ej2Su0JN6gn4QLkyoANzzc29JWULjDrP5UqfSmQaKGchuNJtrHlXZrvaHq6qReW/oKlQ/dX0xKraWx8wgu3JaJuf0K7iijuCxK7L69N4En3IjdGeHqk+26eie3VOHIgdMIh0q48S7pX3ESdKuEJ4Jo9s+PwRFNvGdokOHBSefrgpE1JCMgbvxAe72lD6NH8HX53P1dC1TSMWCgLkAVdy+JsWLWMUCswAAA", + "index": 0, + "leafHash": "1249ed899e8c745ce7d2c78925aa51a800b310a5ddd1994c6b7e3f8aad079eb5", + "result": "cHNidP8BAF4CAAAAAXbYuDMSrbXuy1CXHZaiA8jjI+wcNkkCOL2dJD2dNf3kAAAAAAD/////AZBBBgAAAAAAIlEgcJi/YJDTvc6x6mRTIYgQG/+fEmLs2RjsO/FHW5okOEsAAAAAAAEBK6BoBgAAAAAAIlEgcJi/YJDTvc6x6mRTIYgQG/+fEmLs2RjsO/FHW5okOEsBCP0HAQNAfYaCk2E35rtXsMfbr/Hrbk/BtLF2VLepE0vIIB3UOWZEUF8akmlnL4Kh0xwMn4Cp99QivnI0B3XOsYfEznfDHSIgbvxAe72lD6NH8HX53P1dC1TSMWCgLkAVdy+JsWLWMUCsocCMbJX6Aw/CG4+BDTnJNS3Z2YTbEkt2R4cFqJqnZ6PZK7Qk3qCfhAuTKgA3PNzb0lZQuMOs/lSp9KZBooZyG40m2seVdmu9oerqpF5b+gqVD91fTEqtpbHzCC7clom5/QruKKO4LErsvr03gSfciN0Z4eqT7bp6J7dU4ciB0wiHSrjxLulfcRJ0q4Qngmj2z4/BEU28Z2iQ4cFJ5+uCkTUkAAA=" + } + }, "finalizeAllInputs": [ { "type": "P2PK", @@ -551,6 +882,18 @@ "incorrectPubkey": "Buffer.from('029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e02a', 'hex')", "nonExistantIndex": 42 }, + "validateSignaturesOfTapKeyInput": { + "psbt": "cHNidP8BAM8CAAAAAwPzd9k+uLSN1rgF01xY1TIA/8N+YytNZ4VP9gKFP4MyAAAAAAD/////ZtAAqL2E1fKcmGo+7xuqS+nSQeKFVKGRYaHfIvLXn4sAAAAAAP////9+h+SlCwIx1MUDT7Bek0NrWXS7xnSPi5LbYbDc9sxYIgAAAAAA/////wIgKRsAAAAAACJRIEb2SXyy8Z1Qw+npgqlQ3MhiFLAfzOQ3pCBhx72xIw0zuAUBAAAAAAAWABTJijE0v48z5ZmmfEAADXdCBcG0FAAAAAAAAQDiAgAAAAABAUfY2D1t0dyMeEH39C1yOdIxigpqm7XJNqHVT3Lc+FkiAAAAAAD+////AhIsGwAAAAAAGXapFJ5+8XZ3ZP80oFldvEwrcNsBftBmiKyYdK6xAAAAABepFLDBn59UffGbX7u/olyFDG0eG1UJhwJHMEQCIDAd3s05C61flXVFqOtov0NoHRGr8KFcOpH6R/81F46EAiBt+j9hHyvT2hYEyf8fdYsM9IgbnybtPV+kRTHDa6Rj0AEhAmmZfwmoHsmCkEOn9AfRTh+863mURelmE8hSqL4MG1EydJwgACICAi5ovBH1xLoGxPqtFh48wUEpnM+St1SbPzRwO7kBNKOQRzBEAiBpWClBybtHveXkhAgTiE8QSczMJs8MGuH4LOSNRA6s/AIgWlbB3xJOtJIsszj1qZ/whA5jK9wnTzeZzDlVs/ivq2cBAAEBK7gFAQAAAAAAIlEglCHnNLD50sRn6n3Rl8Yay0RnzcvJ9MsMVx+LY6XEDK4BE0Cd7/ny+QreV7urBWKNroQWCvnZczwkU0kLZiKsJQjtftKHWXMknftjt1d4K6aPYH7cBXzhlrUF+2GovjYLccZeARcglCHnNLD50sRn6n3Rl8Yay0RnzcvJ9MsMVx+LY6XEDK4AAQEfECcAAAAAAAAWABRPoqyhKLb53uUwnE9wBR5Jxl/XMiICA6VOpEBbPJM/xYsqO2euttYFpgec9vcxggyTyoklK660SDBFAiEAoCIktghL55iuMAmkzwYJzb+h+qmNewZXxAx/06ObxIQCIELCsBz/wd2wPlnJb27OluxMkTPnCyHA2C+SxHiX/FvPAQAAAA==", + "index": 1, + "pubkey": "Buffer.from('024fef5c5163bea69a93e74a59672bbeb081837077cb94cfa6e481a5cf00d8ab18', 'hex')", + "incorrectPubkey": "Buffer.from('029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e02a', 'hex')" + }, + "validateSignaturesOfTapScriptInput": { + "psbt": "cHNidP8BAF4CAAAAAQU2GLj/HTOEN804Hc6JRNTLM7EmDlIBdjG2G1aUw266AAAAAAD/////AYAaBgAAAAAAIlEg35sLGepBXUbR93XfxDJcYBCHqNVjw/jogOgPVdic1UgAAAAAAAEBK6BoBgAAAAAAIlEgVZx4+tHeORcb0jDJnOytrOnNOGL1uS0MdMUg1GQeS15BFDlfgSndY7SlwvEhJOqgW3p+0w9w5R+5MwXe7MVC5/nrrqI0FdZdKILLLZgRVK8L9Bn2ijU6IcoqqyImKIWt3MtAA3alBoU7IBCkBk9OHD1wE8fJI4y+lbnTRj48e8AAwRM77q3Rml679qCzGvEAKAs99UNMaXHQIhgGfRP11AMlJkEUj4kary4texRheMVKh+Ku3dnR1oZpIleSjmfPCBdP83WuojQV1l0ogsstmBFUrwv0GfaKNTohyiqrIiYoha3cy0DYJZ6Lv7FZPIBRZFfVgF5v3gcRiQnT8aM82Q5IPkwkzZrGo4ThZblvunG/+hu8ZPuJrUU+uXb+s9rcwSH+BihIQRSoqze8FgnYNMOROzU42tHITX+baoNf/BdXd5FaN641cq6iNBXWXSiCyy2YEVSvC/QZ9oo1OiHKKqsiJiiFrdzLQEfQ5UkAg4lTbhJxjMzB7hu6ad1fywYxHCXjFXHHrm5PJTOFJLg2oTnwuQToz/Z2AW/UET7Op+WSoHZvW4tzzLhiFcFmuQP4s1ds7KJtMOh4fTw1QCgxkWUA3FUAuUzKHzjDvhpSnJ+zzX53bWG2IltsYQ6JBvuPqmxZrFw+lbX4LSnWGlKcn7PNfndtYbYiW2xhDokG+4+qbFmsXD6VtfgtKdZpII+JGq8uLXsUYXjFSofirt3Z0daGaSJXko5nzwgXT/N1rCA5X4Ep3WO0pcLxISTqoFt6ftMPcOUfuTMF3uzFQuf567ogqKs3vBYJ2DTDkTs1ONrRyE1/m2qDX/wXV3eRWjeuNXK6U5zAAAA=", + "index": 0, + "pubkey": "Buffer.from('02395f8129dd63b4a5c2f12124eaa05b7a7ed30f70e51fb93305deecc542e7f9eb', 'hex')", + "incorrectPubkey": "Buffer.from('029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e02a', 'hex')" + }, "getFeeRate": { "psbt": "cHNidP8BAJoCAAAAAljoeiG1ba8MI76OcHBFbDNvfLqlyHV5JPVFiHuyq911AAAAAAD/////g40EJ9DsZQpoqka7CwmK6kQiwHGyyng1Kgd5WdB86h0BAAAAAP////8CcKrwCAAAAAAWABTYXCtx0AYLCcmIauuBXlCZHdoSTQDh9QUAAAAAFgAUAK6pouXw+HaliN9VRuh0LR2HAI8AAAAAAAEAuwIAAAABqtc5MQGL0l+ErkALaISL4J23BurCrBgpi6vucatlb4sAAAAASEcwRAIgWPb8fGoz4bMVSNSByCbAFb0wE1qtQs1neQ2rZtKtJDsCIEoc7SYExnNbY5PltBaR3XiwDwxZQvufdRhW+qk4FX26Af7///8CgPD6AgAAAAAXqRQPuUY0IWlrgsgzryQceMF9295JNIfQ8gonAQAAABepFCnKdPigj4GZlCgYXJe12FLkBj9hh2UAAAAiAgKVg785rgpgl0etGZrd1jT6YQhVnWxc05tMIYPxq5bgf0cwRAIgdAGK1BgAl7hzMjwAFXILNoTMgSOJEEjn282bVa1nnJkCIHPTabdA4+tT3O+jOCPIBwUUylWn3ZVE8VfBZ5EyYRGMASICAtq2H/SaFNtqfQKwzR+7ePxLGDErW05U2uTbovv+9TbXSDBFAiEA9hA4swjcHahlo0hSdG8BV3KTQgjG0kRUOTzZm98iF3cCIAVuZ1pnWm0KArhbFOXikHTYolqbV2C+ooFvZhkQoAbqAQEDBAEAAAABBEdSIQKVg785rgpgl0etGZrd1jT6YQhVnWxc05tMIYPxq5bgfyEC2rYf9JoU22p9ArDNH7t4/EsYMStbTlTa5Nui+/71NtdSriIGApWDvzmuCmCXR60Zmt3WNPphCFWdbFzTm0whg/GrluB/ENkMak8AAACAAAAAgAAAAIAiBgLath/0mhTban0CsM0fu3j8SxgxK1tOVNrk26L7/vU21xDZDGpPAAAAgAAAAIABAACAAAEBIADC6wsAAAAAF6kUt/X69A49QKWkWbHbNTXyty+pIeiHIgIDCJ3BDHrG21T5EymvYXMz2ziM6tDCMfcjN50bmQMLAtxHMEQCIGLrelVhB6fHP0WsSrWh3d9vcHX7EnWWmn84Pv/3hLyyAiAMBdu3Rw2/LwhVfdNWxzJcHtMJE+mWzThAlF2xIijaXwEiAgI63ZBPPW3PWd25BrDe4jUpt/+57VDl6GFRkmhgIh8Oc0cwRAIgZfRbpZmLWaJ//hp77QFq8fH5DVSzqo90UKpfVqJRA70CIH9yRwOtHtuWaAsoS1bU/8uI9/t1nqu+CKow8puFE4PSAQEDBAEAAAABBCIAIIwjUxc3Q7WV37Sge3K6jkLjeX2nTof+fZ10l+OyAokDAQVHUiEDCJ3BDHrG21T5EymvYXMz2ziM6tDCMfcjN50bmQMLAtwhAjrdkE89bc9Z3bkGsN7iNSm3/7ntUOXoYVGSaGAiHw5zUq4iBgI63ZBPPW3PWd25BrDe4jUpt/+57VDl6GFRkmhgIh8OcxDZDGpPAAAAgAAAAIADAACAIgYDCJ3BDHrG21T5EymvYXMz2ziM6tDCMfcjN50bmQMLAtwQ2QxqTwAAAIAAAACAAgAAgAAiAgOppMN/WZbTqiXbrGtXCvBlA5RJKUJGCzVHU+2e7KWHcRDZDGpPAAAAgAAAAIAEAACAACICAn9jmXV9Lv9VoTatAsaEsYOLZVbl8bazQoKpS2tQBRCWENkMak8AAACAAAAAgAUAAIAA", "fee": 21 @@ -565,4 +908,4 @@ "clone": { "psbt": "cHNidP8BAKYCAAAAAlwKQ3suPWwEJ/zQ9sZsIioOcHKU1KoLMxlMNSXVIkEWAAAAAAD/////YYDJMap+mYgbTrCNAdpWHN+EkKvl+XYao/6co/EQfwMAAAAAAP////8CkF8BAAAAAAAWABRnPBAmVHz2HL+8/1U+QG5L2thjmjhKAAAAAAAAIgAg700yfFRyhWzQnPHIUb/XQqsjlpf4A0uw682pCVWuQ8IAAAAAAAEBKzB1AAAAAAAAIgAgth9oE4cDfC5aV58VgkW5CptHsIxppYzJV8C5kT6aTo8iAgNfNgnFnEPS9s4PY/I5bezpWiAEzQ4DRTASgdSdeMM06UgwRQIhALO0xRpuqP3aVkm+DPykrtqe6fPNSgNblp9K9MAwmtHJAiAHV5ZvZN8Vi49n/o9ISFyvtHsPXXPKqBxC9m2m2HlpYgEBBSMhA182CcWcQ9L2zg9j8jlt7OlaIATNDgNFMBKB1J14wzTprAABASuAOAEAAAAAACIAILYfaBOHA3wuWlefFYJFuQqbR7CMaaWMyVfAuZE+mk6PIgIDXzYJxZxD0vbOD2PyOW3s6VogBM0OA0UwEoHUnXjDNOlIMEUCIQC6XN6tpo9mYlZej4BXSSh5D1K6aILBfQ4WBWXUrISx6wIgVaxFUsz8y59xJ1V4sZ1qarHX9pZ+MJmLKbl2ZSadisoBAQUjIQNfNgnFnEPS9s4PY/I5bezpWiAEzQ4DRTASgdSdeMM06awAAAA=" } -} +} \ No newline at end of file diff --git a/test/integration/taproot.md b/test/integration/taproot.md index 401034061..7627450e2 100644 --- a/test/integration/taproot.md +++ b/test/integration/taproot.md @@ -9,8 +9,9 @@ A simple keyspend example that is possible with the current API is below. ## TODO -- [ ] p2tr payment API to make script spends easier +- [x] p2tr payment API to make script spends easier - [ ] Support within the Psbt class + - partial support added ## Example diff --git a/test/integration/taproot.spec.ts b/test/integration/taproot.spec.ts index f7b3733fa..b12c46f31 100644 --- a/test/integration/taproot.spec.ts +++ b/test/integration/taproot.spec.ts @@ -1,11 +1,20 @@ import BIP32Factory from 'bip32'; +import ECPairFactory from 'ecpair'; import * as ecc from 'tiny-secp256k1'; import { describe, it } from 'mocha'; -import * as bitcoin from '../..'; +import { PsbtInput, TapLeafScript } from 'bip174/src/lib/interfaces'; import { regtestUtils } from './_regtest'; +import * as bitcoin from '../..'; +import { Taptree } from '../../src/types'; +import { toXOnly, tapTreeToList, tapTreeFromList } from '../../src/psbt/bip371'; +import { witnessStackToScriptWitness } from '../../src/psbt/psbtutils'; +import { TapLeaf } from 'bip174/src/lib/interfaces'; + const rng = require('randombytes'); const regtest = regtestUtils.network; +bitcoin.initEccLib(ecc); const bip32 = BIP32Factory(ecc); +const ECPair = ECPairFactory(ecc); describe('bitcoinjs-lib (transaction with taproot)', () => { it('can create (and broadcast via 3PBP) a taproot keyspend Transaction', async () => { @@ -42,8 +51,550 @@ describe('bitcoinjs-lib (transaction with taproot)', () => { value: sendAmount, }); }); + + it('can create (and broadcast via 3PBP) a taproot key-path spend Transaction', async () => { + const internalKey = bip32.fromSeed(rng(64), regtest); + const p2pkhKey = bip32.fromSeed(rng(64), regtest); + + const { output } = bitcoin.payments.p2tr({ + internalPubkey: toXOnly(internalKey.publicKey), + network: regtest, + }); + + const { output: p2pkhOutput } = bitcoin.payments.p2pkh({ + pubkey: p2pkhKey.publicKey, + network: regtest, + }); + + // amount from faucet + const amount = 42e4; + // amount to send + const sendAmount = amount - 1e4; + // get faucet + const unspent = await regtestUtils.faucetComplex(output!, amount); + + // non segwit utxo + const p2pkhUnspent = await regtestUtils.faucetComplex(p2pkhOutput!, amount); + const utx = await regtestUtils.fetch(p2pkhUnspent.txId); + const nonWitnessUtxo = Buffer.from(utx.txHex, 'hex'); + + const psbt = new bitcoin.Psbt({ network: regtest }); + psbt.addInput({ + hash: unspent.txId, + index: 0, + witnessUtxo: { value: amount, script: output! }, + tapInternalKey: toXOnly(internalKey.publicKey), + }); + psbt.addInput({ index: 0, hash: p2pkhUnspent.txId, nonWitnessUtxo }); + + const sendInternalKey = bip32.fromSeed(rng(64), regtest); + const sendPubKey = toXOnly(sendInternalKey.publicKey); + const { address: sendAddress } = bitcoin.payments.p2tr({ + internalPubkey: sendPubKey, + network: regtest, + }); + + psbt.addOutput({ + value: sendAmount, + address: sendAddress!, + tapInternalKey: sendPubKey, + }); + + const tweakedSigner = tweakSigner(internalKey!, { network: regtest }); + await psbt.signInputAsync(0, tweakedSigner); + await psbt.signInputAsync(1, p2pkhKey); + + psbt.finalizeAllInputs(); + const tx = psbt.extractTransaction(); + const rawTx = tx.toBuffer(); + + const hex = rawTx.toString('hex'); + + await regtestUtils.broadcast(hex); + await regtestUtils.verify({ + txId: tx.getId(), + address: sendAddress!, + vout: 0, + value: sendAmount, + }); + }); + + it('can create (and broadcast via 3PBP) a taproot key-path spend Transaction (with unused scriptTree)', async () => { + const internalKey = bip32.fromSeed(rng(64), regtest); + const leafKey = bip32.fromSeed(rng(64), regtest); + + const leafScriptAsm = `${toXOnly(leafKey.publicKey).toString( + 'hex', + )} OP_CHECKSIG`; + const leafScript = bitcoin.script.fromASM(leafScriptAsm); + + const scriptTree = { + output: leafScript, + }; + + const { output, address, hash } = bitcoin.payments.p2tr({ + internalPubkey: toXOnly(internalKey.publicKey), + scriptTree, + network: regtest, + }); + + // amount from faucet + const amount = 42e4; + // amount to send + const sendAmount = amount - 1e4; + // get faucet + const unspent = await regtestUtils.faucetComplex(output!, amount); + + const psbt = new bitcoin.Psbt({ network: regtest }); + psbt.addInput({ + hash: unspent.txId, + index: 0, + witnessUtxo: { value: amount, script: output! }, + tapInternalKey: toXOnly(internalKey.publicKey), + tapMerkleRoot: hash, + }); + psbt.addOutput({ value: sendAmount, address: address! }); + + const tweakedSigner = tweakSigner(internalKey!, { + tweakHash: hash, + network: regtest, + }); + psbt.signInput(0, tweakedSigner); + + psbt.finalizeAllInputs(); + const tx = psbt.extractTransaction(); + const rawTx = tx.toBuffer(); + + const hex = rawTx.toString('hex'); + + await regtestUtils.broadcast(hex); + await regtestUtils.verify({ + txId: tx.getId(), + address: address!, + vout: 0, + value: sendAmount, + }); + }); + + it('can create (and broadcast via 3PBP) a taproot script-path spend Transaction - OP_CHECKSIG', async () => { + const internalKey = bip32.fromSeed(rng(64), regtest); + const leafKey = bip32.fromSeed(rng(64), regtest); + + const leafScriptAsm = `${toXOnly(leafKey.publicKey).toString( + 'hex', + )} OP_CHECKSIG`; + const leafScript = bitcoin.script.fromASM(leafScriptAsm); + + const scriptTree: Taptree = [ + [ + { + output: bitcoin.script.fromASM( + '50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0 OP_CHECKSIG', + ), + }, + [ + { + output: bitcoin.script.fromASM( + '50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac1 OP_CHECKSIG', + ), + }, + { + output: bitcoin.script.fromASM( + '2258b1c3160be0864a541854eec9164a572f094f7562628281a8073bb89173a7 OP_CHECKSIG', + ), + }, + ], + ], + [ + { + output: bitcoin.script.fromASM( + '50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac2 OP_CHECKSIG', + ), + }, + [ + { + output: bitcoin.script.fromASM( + '50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac3 OP_CHECKSIG', + ), + }, + [ + { + output: bitcoin.script.fromASM( + '50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac4 OP_CHECKSIG', + ), + }, + { + output: leafScript, + }, + ], + ], + ], + ]; + const redeem = { + output: leafScript, + redeemVersion: 192, + }; + + const { output, witness } = bitcoin.payments.p2tr({ + internalPubkey: toXOnly(internalKey.publicKey), + scriptTree, + redeem, + network: regtest, + }); + + // amount from faucet + const amount = 42e4; + // amount to send + const sendAmount = amount - 1e4; + // get faucet + const unspent = await regtestUtils.faucetComplex(output!, amount); + + const psbt = new bitcoin.Psbt({ network: regtest }); + psbt.addInput({ + hash: unspent.txId, + index: 0, + witnessUtxo: { value: amount, script: output! }, + }); + psbt.updateInput(0, { + tapLeafScript: [ + { + leafVersion: redeem.redeemVersion, + script: redeem.output, + controlBlock: witness![witness!.length - 1], + }, + ], + }); + + const sendInternalKey = bip32.fromSeed(rng(64), regtest); + const sendPubKey = toXOnly(sendInternalKey.publicKey); + const { address: sendAddress } = bitcoin.payments.p2tr({ + internalPubkey: sendPubKey, + scriptTree, + network: regtest, + }); + + psbt.addOutput({ + value: sendAmount, + address: sendAddress!, + tapInternalKey: sendPubKey, + tapTree: { leaves: tapTreeToList(scriptTree) }, + }); + + psbt.signInput(0, leafKey); + psbt.finalizeInput(0); + const tx = psbt.extractTransaction(); + const rawTx = tx.toBuffer(); + const hex = rawTx.toString('hex'); + + await regtestUtils.broadcast(hex); + await regtestUtils.verify({ + txId: tx.getId(), + address: sendAddress!, + vout: 0, + value: sendAmount, + }); + }); + + it('can create (and broadcast via 3PBP) a taproot script-path spend Transaction - OP_CHECKSEQUENCEVERIFY', async () => { + const internalKey = bip32.fromSeed(rng(64), regtest); + const leafKey = bip32.fromSeed(rng(64), regtest); + const leafPubkey = toXOnly(leafKey.publicKey).toString('hex'); + + const leafScriptAsm = `OP_10 OP_CHECKSEQUENCEVERIFY OP_DROP ${leafPubkey} OP_CHECKSIG`; + const leafScript = bitcoin.script.fromASM(leafScriptAsm); + + const scriptTree: Taptree = [ + { + output: bitcoin.script.fromASM( + '50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0 OP_CHECKSIG', + ), + }, + [ + { + output: bitcoin.script.fromASM( + '50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0 OP_CHECKSIG', + ), + }, + { + output: leafScript, + }, + ], + ]; + const redeem = { + output: leafScript, + redeemVersion: 192, + }; + + const { output, witness } = bitcoin.payments.p2tr({ + internalPubkey: toXOnly(internalKey.publicKey), + scriptTree, + redeem, + network: regtest, + }); + + // amount from faucet + const amount = 42e4; + // amount to send + const sendAmount = amount - 1e4; + // get faucet + const unspent = await regtestUtils.faucetComplex(output!, amount); + + const psbt = new bitcoin.Psbt({ network: regtest }); + psbt.addInput({ + hash: unspent.txId, + index: 0, + sequence: 10, + witnessUtxo: { value: amount, script: output! }, + }); + psbt.updateInput(0, { + tapLeafScript: [ + { + leafVersion: redeem.redeemVersion, + script: redeem.output, + controlBlock: witness![witness!.length - 1], + }, + ], + }); + + const sendInternalKey = bip32.fromSeed(rng(64), regtest); + const sendPubKey = toXOnly(sendInternalKey.publicKey); + const { address: sendAddress } = bitcoin.payments.p2tr({ + internalPubkey: sendPubKey, + scriptTree, + network: regtest, + }); + + psbt.addOutput({ value: sendAmount, address: sendAddress! }); + // just to test that updateOutput works as expected + psbt.updateOutput(0, { + tapInternalKey: sendPubKey, + tapTree: { leaves: tapTreeToList(scriptTree) }, + }); + + await psbt.signInputAsync(0, leafKey); + + psbt.finalizeInput(0); + const tx = psbt.extractTransaction(); + const rawTx = tx.toBuffer(); + const hex = rawTx.toString('hex'); + + try { + // broadcast before the confirmation period has expired + await regtestUtils.broadcast(hex); + throw new Error('Broadcast should fail.'); + } catch (err) { + if ((err as any).message !== 'non-BIP68-final') + throw new Error( + 'Expected OP_CHECKSEQUENCEVERIFY validation to fail. But it faild with: ' + + err, + ); + } + await regtestUtils.mine(10); + await regtestUtils.broadcast(hex); + await regtestUtils.verify({ + txId: tx.getId(), + address: sendAddress!, + vout: 0, + value: sendAmount, + }); + }); + + it('can create (and broadcast via 3PBP) a taproot script-path spend Transaction - OP_CHECKSIGADD (3-of-3)', async () => { + const internalKey = bip32.fromSeed(rng(64), regtest); + + const leafKeys = []; + const leafPubkeys = []; + for (let i = 0; i < 3; i++) { + const leafKey = bip32.fromSeed(rng(64), regtest); + leafKeys.push(leafKey); + leafPubkeys.push(toXOnly(leafKey.publicKey).toString('hex')); + } + + const leafScriptAsm = `${leafPubkeys[2]} OP_CHECKSIG ${ + leafPubkeys[1] + } OP_CHECKSIGADD ${leafPubkeys[0]} OP_CHECKSIGADD OP_3 OP_NUMEQUAL`; + + const leafScript = bitcoin.script.fromASM(leafScriptAsm); + + const scriptTree: Taptree = [ + { + output: bitcoin.script.fromASM( + '50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0 OP_CHECKSIG', + ), + }, + [ + { + output: bitcoin.script.fromASM( + '50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0 OP_CHECKSIG', + ), + }, + { + output: leafScript, + }, + ], + ]; + const redeem = { + output: leafScript, + redeemVersion: 192, + }; + + const { output, address, witness } = bitcoin.payments.p2tr({ + internalPubkey: toXOnly(internalKey.publicKey), + scriptTree, + redeem, + network: regtest, + }); + + // amount from faucet + const amount = 42e4; + // amount to send + const sendAmount = amount - 1e4; + // get faucet + const unspent = await regtestUtils.faucetComplex(output!, amount); + + const psbt = new bitcoin.Psbt({ network: regtest }); + psbt.addInput({ + hash: unspent.txId, + index: 0, + witnessUtxo: { value: amount, script: output! }, + }); + psbt.updateInput(0, { + tapLeafScript: [ + { + leafVersion: redeem.redeemVersion, + script: redeem.output, + controlBlock: witness![witness!.length - 1], + }, + ], + }); + + psbt.addOutput({ value: sendAmount, address: address! }); + + // random order for signers + psbt.signInput(0, leafKeys[1]); + psbt.signInput(0, leafKeys[2]); + psbt.signInput(0, leafKeys[0]); + + psbt.finalizeInput(0); + const tx = psbt.extractTransaction(); + const rawTx = tx.toBuffer(); + const hex = rawTx.toString('hex'); + + await regtestUtils.broadcast(hex); + await regtestUtils.verify({ + txId: tx.getId(), + address: address!, + vout: 0, + value: sendAmount, + }); + }); + + it('can create (and broadcast via 3PBP) a taproot script-path spend Transaction - custom finalizer', async () => { + const leafCount = 8; + const leaves = Array.from({ length: leafCount }).map( + (_, index) => + ({ + depth: 3, + leafVersion: 192, + script: bitcoin.script.fromASM(`OP_ADD OP_${index * 2} OP_EQUAL`), + } as TapLeaf), + ); + const scriptTree = tapTreeFromList(leaves); + + for (let leafIndex = 1; leafIndex < leafCount; leafIndex++) { + const redeem = { + output: bitcoin.script.fromASM(`OP_ADD OP_${leafIndex * 2} OP_EQUAL`), + redeemVersion: 192, + }; + + const internalKey = bip32.fromSeed(rng(64), regtest); + const { output, witness } = bitcoin.payments.p2tr({ + internalPubkey: toXOnly(internalKey.publicKey), + scriptTree, + redeem, + network: regtest, + }); + + // amount from faucet + const amount = 42e4; + // amount to send + const sendAmount = amount - 1e4; + // get faucet + const unspent = await regtestUtils.faucetComplex(output!, amount); + + const psbt = new bitcoin.Psbt({ network: regtest }); + psbt.addInput({ + hash: unspent.txId, + index: 0, + witnessUtxo: { value: amount, script: output! }, + }); + + const tapLeafScript: TapLeafScript = { + leafVersion: redeem.redeemVersion, + script: redeem.output, + controlBlock: witness![witness!.length - 1], + }; + psbt.updateInput(0, { tapLeafScript: [tapLeafScript] }); + + const sendAddress = + 'bcrt1pqknex3jwpsaatu5e5dcjw70nac3fr5k5y3hcxr4hgg6rljzp59nqs6a0vh'; + psbt.addOutput({ + value: sendAmount, + address: sendAddress, + }); + + const leafIndexFinalizerFn = buildLeafIndexFinalizer( + tapLeafScript, + leafIndex, + ); + psbt.finalizeInput(0, leafIndexFinalizerFn); + const tx = psbt.extractTransaction(); + const rawTx = tx.toBuffer(); + const hex = rawTx.toString('hex'); + + await regtestUtils.broadcast(hex); + await regtestUtils.verify({ + txId: tx.getId(), + address: sendAddress!, + vout: 0, + value: sendAmount, + }); + } + }); }); +function buildLeafIndexFinalizer( + tapLeafScript: TapLeafScript, + leafIndex: number, +): ( + inputIndex: number, + _input: PsbtInput, + _tapLeafHashToFinalize?: Buffer, +) => { + finalScriptWitness: Buffer | undefined; +} { + return ( + inputIndex: number, + _input: PsbtInput, + _tapLeafHashToFinalize?: Buffer, + ): { + finalScriptWitness: Buffer | undefined; + } => { + try { + const scriptSolution = [ + Buffer.from([leafIndex]), + Buffer.from([leafIndex]), + ]; + const witness = scriptSolution + .concat(tapLeafScript.script) + .concat(tapLeafScript.controlBlock); + return { finalScriptWitness: witnessStackToScriptWitness(witness) }; + } catch (err) { + throw new Error(`Can not finalize taproot input #${inputIndex}: ${err}`); + } + }; +} + // Order of the curve (N) - 1 const N_LESS_1 = Buffer.from( 'fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364140', @@ -59,7 +610,7 @@ const ONE = Buffer.from( // (This is recommended by BIP341) function createKeySpendOutput(publicKey: Buffer): Buffer { // x-only pubkey (remove 1 byte y parity) - const myXOnlyPubkey = publicKey.slice(1, 33); + const myXOnlyPubkey = toXOnly(publicKey); const commitHash = bitcoin.crypto.taggedHash('TapTweak', myXOnlyPubkey); const tweakResult = ecc.xOnlyPointAddTweak(myXOnlyPubkey, commitHash); if (tweakResult === null) throw new Error('Invalid Tweak'); @@ -86,7 +637,7 @@ function signTweaked(messageHash: Buffer, key: KeyPair): Uint8Array { : ecc.privateAdd(ecc.privateSub(N_LESS_1, key.privateKey!)!, ONE)!; const tweakHash = bitcoin.crypto.taggedHash( 'TapTweak', - key.publicKey.slice(1, 33), + toXOnly(key.publicKey), ); const newPrivateKey = ecc.privateAdd(privateKey!, tweakHash); if (newPrivateKey === null) throw new Error('Invalid Tweak'); @@ -120,3 +671,34 @@ function createSigned( tx.ins[0].witness = [signature]; return tx; } + +// This logic will be extracted to ecpair +function tweakSigner(signer: bitcoin.Signer, opts: any = {}): bitcoin.Signer { + // @ts-ignore + let privateKey: Uint8Array | undefined = signer.privateKey!; + if (!privateKey) { + throw new Error('Private key is required for tweaking signer!'); + } + if (signer.publicKey[0] === 3) { + privateKey = ecc.privateNegate(privateKey); + } + + const tweakedPrivateKey = ecc.privateAdd( + privateKey, + tapTweakHash(toXOnly(signer.publicKey), opts.tweakHash), + ); + if (!tweakedPrivateKey) { + throw new Error('Invalid tweaked private key!'); + } + + return ECPair.fromPrivateKey(Buffer.from(tweakedPrivateKey), { + network: opts.network, + }); +} + +function tapTweakHash(pubKey: Buffer, h: Buffer | undefined): Buffer { + return bitcoin.crypto.taggedHash( + 'TapTweak', + Buffer.concat(h ? [pubKey, h] : [pubKey]), + ); +} diff --git a/test/payments.spec.ts b/test/payments.spec.ts index bc123cba3..07b1442f9 100644 --- a/test/payments.spec.ts +++ b/test/payments.spec.ts @@ -1,113 +1,124 @@ import * as assert from 'assert'; +import * as ecc from 'tiny-secp256k1'; import { describe, it } from 'mocha'; import { PaymentCreator } from '../src/payments'; import * as u from './payments.utils'; -['embed', 'p2ms', 'p2pk', 'p2pkh', 'p2sh', 'p2wpkh', 'p2wsh'].forEach(p => { - describe(p, () => { - let fn: PaymentCreator; - const payment = require('../src/payments/' + p); - if (p === 'embed') { - fn = payment.p2data; - } else { - fn = payment[p]; - } - const fixtures = require('./fixtures/' + p); +import { initEccLib } from '../src'; - fixtures.valid.forEach((f: any) => { - it(f.description + ' as expected', () => { - const args = u.preform(f.arguments); - const actual = fn(args, f.options); - - u.equate(actual, f.expected, f.arguments); +['embed', 'p2ms', 'p2pk', 'p2pkh', 'p2sh', 'p2wpkh', 'p2wsh', 'p2tr'].forEach( + p => { + describe(p, () => { + beforeEach(() => { + initEccLib(p === 'p2tr' ? ecc : undefined); }); + let fn: PaymentCreator; + const payment = require('../src/payments/' + p); + if (p === 'embed') { + fn = payment.p2data; + } else { + fn = payment[p]; + } - it(f.description + ' as expected (no validation)', () => { - const args = u.preform(f.arguments); - const actual = fn( - args, - Object.assign({}, f.options, { - validate: false, - }), - ); + const fixtures = require('./fixtures/' + p); - u.equate(actual, f.expected, f.arguments); - }); - }); - - fixtures.invalid.forEach((f: any) => { - it( - 'throws ' + f.exception + (f.description ? 'for ' + f.description : ''), - () => { + fixtures.valid.forEach((f: any) => { + it(f.description + ' as expected', () => { const args = u.preform(f.arguments); + const actual = fn(args, f.options); - assert.throws(() => { - fn(args, f.options); - }, new RegExp(f.exception)); - }, - ); - }); - - if (p === 'p2sh') { - const p2wsh = require('../src/payments/p2wsh').p2wsh; - const p2pk = require('../src/payments/p2pk').p2pk; - it('properly assembles nested p2wsh with names', () => { - const actual = fn({ - redeem: p2wsh({ - redeem: p2pk({ - pubkey: Buffer.from( - '03e15819590382a9dd878f01e2f0cbce541564eb415e43b440472d883ecd283058', - 'hex', - ), - }), - }), + u.equate(actual, f.expected, f.arguments); }); - assert.strictEqual( - actual.address, - '3MGbrbye4ttNUXM8WAvBFRKry4fkS9fjuw', - ); - assert.strictEqual(actual.name, 'p2sh-p2wsh-p2pk'); - assert.strictEqual(actual.redeem!.name, 'p2wsh-p2pk'); - assert.strictEqual(actual.redeem!.redeem!.name, 'p2pk'); - }); - } - // cross-verify dynamically too - if (!fixtures.dynamic) return; - const { depends, details } = fixtures.dynamic; + it(f.description + ' as expected (no validation)', () => { + const args = u.preform(f.arguments); + const actual = fn( + args, + Object.assign({}, f.options, { + validate: false, + }), + ); - details.forEach((f: any) => { - const detail = u.preform(f); - const disabled: any = {}; - if (f.disabled) - f.disabled.forEach((k: string) => { - disabled[k] = true; + u.equate(actual, f.expected, f.arguments); }); + }); - for (const key in depends) { - if (key in disabled) continue; - const dependencies = depends[key]; + fixtures.invalid.forEach((f: any) => { + it( + 'throws ' + + f.exception + + (f.description ? 'for ' + f.description : ''), + () => { + const args = u.preform(f.arguments); - dependencies.forEach((dependency: any) => { - if (!Array.isArray(dependency)) dependency = [dependency]; + assert.throws(() => { + fn(args, f.options); + }, new RegExp(f.exception)); + }, + ); + }); - const args = {}; - dependency.forEach((d: any) => { - u.from(d, detail, args); + if (p === 'p2sh') { + const p2wsh = require('../src/payments/p2wsh').p2wsh; + const p2pk = require('../src/payments/p2pk').p2pk; + it('properly assembles nested p2wsh with names', () => { + const actual = fn({ + redeem: p2wsh({ + redeem: p2pk({ + pubkey: Buffer.from( + '03e15819590382a9dd878f01e2f0cbce541564eb415e43b440472d883ecd283058', + 'hex', + ), + }), + }), }); - const expected = u.from(key, detail); - - it( - f.description + - ', ' + - key + - ' derives from ' + - JSON.stringify(dependency), - () => { - u.equate(fn(args), expected); - }, + assert.strictEqual( + actual.address, + '3MGbrbye4ttNUXM8WAvBFRKry4fkS9fjuw', ); + assert.strictEqual(actual.name, 'p2sh-p2wsh-p2pk'); + assert.strictEqual(actual.redeem!.name, 'p2wsh-p2pk'); + assert.strictEqual(actual.redeem!.redeem!.name, 'p2pk'); }); } + + // cross-verify dynamically too + if (!fixtures.dynamic) return; + const { depends, details } = fixtures.dynamic; + + details.forEach((f: any) => { + const detail = u.preform(f); + const disabled: any = {}; + if (f.disabled) + f.disabled.forEach((k: string) => { + disabled[k] = true; + }); + + for (const key in depends) { + if (key in disabled) continue; + const dependencies = depends[key]; + + dependencies.forEach((dependency: any) => { + if (!Array.isArray(dependency)) dependency = [dependency]; + + const args = {}; + dependency.forEach((d: any) => { + u.from(d, detail, args); + }); + const expected = u.from(key, detail); + + it( + f.description + + ', ' + + key + + ' derives from ' + + JSON.stringify(dependency), + () => { + u.equate(fn(args), expected); + }, + ); + }); + } + }); }); - }); -}); + }, +); diff --git a/test/payments.utils.ts b/test/payments.utils.ts index c0635f3cf..e2599204c 100644 --- a/test/payments.utils.ts +++ b/test/payments.utils.ts @@ -52,6 +52,12 @@ function equateBase(a: any, b: any, context: string): void { tryHex(b.witness), `Inequal ${context}witness`, ); + if ('redeemVersion' in b) + t.strictEqual( + a.redeemVersion, + b.redeemVersion, + `Inequal ${context}redeemVersion`, + ); } export function equate(a: any, b: any, args?: any): void { @@ -62,10 +68,12 @@ export function equate(a: any, b: any, args?: any): void { if (b.input === null) b.input = undefined; if (b.output === null) b.output = undefined; if (b.witness === null) b.witness = undefined; + if (b.redeemVersion === null) b.redeemVersion = 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; + if (b.redeem.redeemVersion === null) b.redeem.redeemVersion = undefined; } equateBase(a, b, ''); @@ -86,6 +94,12 @@ export function equate(a: any, b: any, args?: any): void { 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 ('internalPubkey' in b) + t.strictEqual( + tryHex(a.internalPubkey), + tryHex(b.internalPubkey), + 'Inequal *.internalPubkey', + ); if ('signature' in b) t.strictEqual( tryHex(a.signature), @@ -129,6 +143,7 @@ export function preform(x: any): any { if (x.data) x.data = x.data.map(fromHex); if (x.hash) x.hash = Buffer.from(x.hash, 'hex'); if (x.pubkey) x.pubkey = Buffer.from(x.pubkey, 'hex'); + if (x.internalPubkey) x.internalPubkey = Buffer.from(x.internalPubkey, 'hex'); if (x.signature) x.signature = Buffer.from(x.signature, 'hex'); if (x.pubkeys) x.pubkeys = x.pubkeys.map(fromHex); if (x.signatures) @@ -147,6 +162,7 @@ export function preform(x: any): any { x.redeem.network = (BNETWORKS as any)[x.redeem.network]; } + if (x.scriptTree) x.scriptTree = convertScriptTree(x.scriptTree); return x; } @@ -169,3 +185,15 @@ export function from(path: string, object: any, result?: any): any { return result; } + +export function convertScriptTree(scriptTree: any, leafVersion?: number): any { + if (Array.isArray(scriptTree)) + return scriptTree.map(tr => convertScriptTree(tr, leafVersion)); + + const script = Object.assign({}, scriptTree); + if (typeof script.output === 'string') { + script.output = asmToBuffer(scriptTree.output); + script.version = script.version || leafVersion; + } + return script; +} diff --git a/test/psbt.spec.ts b/test/psbt.spec.ts index f583e8068..6d295a80f 100644 --- a/test/psbt.spec.ts +++ b/test/psbt.spec.ts @@ -5,12 +5,19 @@ import * as crypto from 'crypto'; import ECPairFactory from 'ecpair'; import { describe, it } from 'mocha'; +import { convertScriptTree } from './payments.utils'; +import { LEAF_VERSION_TAPSCRIPT } from '../src/payments/bip341'; +import { tapTreeToList, tapTreeFromList } from '../src/psbt/bip371'; +import { Taptree } from '../src/types'; +import { initEccLib } from '../src'; + const bip32 = BIP32Factory(ecc); const ECPair = ECPairFactory(ecc); import { networks as NETWORKS, payments, Psbt, Signer, SignerAsync } from '..'; import * as preFixtures from './fixtures/psbt.json'; +import * as taprootFixtures from './fixtures/p2tr.json'; const validator = ( pubkey: Buffer, @@ -18,6 +25,12 @@ const validator = ( signature: Buffer, ): boolean => ECPair.fromPublicKey(pubkey).verify(msghash, signature); +const schnorrValidator = ( + pubkey: Buffer, + msghash: Buffer, + signature: Buffer, +): boolean => ecc.verifySchnorr(msghash, pubkey, signature); + const initBuffers = (object: any): typeof preFixtures => JSON.parse(JSON.stringify(object), (_, value) => { const regex = new RegExp(/^Buffer.from\(['"](.*)['"], ['"](.*)['"]\)$/); @@ -72,6 +85,10 @@ const failedAsyncSigner = (publicKey: Buffer): SignerAsync => { // const b = (hex: string) => Buffer.from(hex, 'hex'); describe(`Psbt`, () => { + beforeEach(() => { + // provide the ECC lib only when required + initEccLib(undefined); + }); describe('BIP174 Test Vectors', () => { fixtures.bip174.invalid.forEach(f => { it(`Invalid: ${f.description}`, () => { @@ -115,6 +132,7 @@ describe(`Psbt`, () => { fixtures.bip174.updater.forEach(f => { it('Updates PSBT to the expected result', () => { + if (f.isTaproot) initEccLib(ecc); const psbt = Psbt.fromBase64(f.psbt); for (const inputOrOutput of ['input', 'output']) { @@ -133,11 +151,19 @@ describe(`Psbt`, () => { fixtures.bip174.signer.forEach(f => { it('Signs PSBT to the expected result', () => { + if (f.isTaproot) initEccLib(ecc); const psbt = Psbt.fromBase64(f.psbt); - f.keys.forEach(({ inputToSign, WIF }) => { + // @ts-ignore // cannot find tapLeafHashToSign + f.keys.forEach(({ inputToSign, tapLeafHashToSign, WIF }) => { const keyPair = ECPair.fromWIF(WIF, NETWORKS.testnet); - psbt.signInput(inputToSign, keyPair); + if (tapLeafHashToSign) + psbt.signTaprootInput( + inputToSign, + keyPair, + Buffer.from(tapLeafHashToSign, 'hex'), + ); + else psbt.signInput(inputToSign, keyPair); }); assert.strictEqual(psbt.toBase64(), f.result); @@ -160,6 +186,7 @@ describe(`Psbt`, () => { fixtures.bip174.finalizer.forEach(f => { it('Finalizes inputs and gives the expected PSBT', () => { + if (f.isTaproot) initEccLib(ecc); const psbt = Psbt.fromBase64(f.psbt); psbt.finalizeAllInputs(); @@ -209,6 +236,7 @@ describe(`Psbt`, () => { describe('signInputAsync', () => { fixtures.signInput.checks.forEach(f => { it(f.description, async () => { + if (f.isTaproot) initEccLib(ecc); if (f.shouldSign) { const psbtThatShouldsign = Psbt.fromBase64(f.shouldSign.psbt); await assert.doesNotReject(async () => { @@ -217,14 +245,22 @@ describe(`Psbt`, () => { ECPair.fromWIF(f.shouldSign.WIF), f.shouldSign.sighashTypes || undefined, ); + if (f.shouldSign.result) + assert.strictEqual( + psbtThatShouldsign.toBase64(), + f.shouldSign.result, + ); }); + const failMessage = f.isTaproot + ? /Need Schnorr Signer to sign taproot input #0./ + : /sign failed/; await assert.rejects(async () => { await psbtThatShouldsign.signInputAsync( f.shouldSign.inputToCheck, failedAsyncSigner(ECPair.fromWIF(f.shouldSign.WIF).publicKey), f.shouldSign.sighashTypes || undefined, ); - }, /sign failed/); + }, failMessage); } if (f.shouldThrow) { @@ -256,6 +292,7 @@ describe(`Psbt`, () => { describe('signInput', () => { fixtures.signInput.checks.forEach(f => { it(f.description, () => { + if (f.isTaproot) initEccLib(ecc); if (f.shouldSign) { const psbtThatShouldsign = Psbt.fromBase64(f.shouldSign.psbt); assert.doesNotThrow(() => { @@ -288,6 +325,7 @@ describe(`Psbt`, () => { fixtures.signInput.checks.forEach(f => { if (f.description === 'checks the input exists') return; it(f.description, async () => { + if (f.isTaproot) initEccLib(ecc); if (f.shouldSign) { const psbtThatShouldsign = Psbt.fromBase64(f.shouldSign.psbt); await assert.doesNotReject(async () => { @@ -318,6 +356,7 @@ describe(`Psbt`, () => { fixtures.signInput.checks.forEach(f => { if (f.description === 'checks the input exists') return; it(f.description, () => { + if (f.isTaproot) initEccLib(ecc); if (f.shouldSign) { const psbtThatShouldsign = Psbt.fromBase64(f.shouldSign.psbt); assert.doesNotThrow(() => { @@ -468,6 +507,42 @@ describe(`Psbt`, () => { }); }); + describe('finalizeInput', () => { + it(`Finalizes tapleaf by hash`, () => { + const f = fixtures.finalizeInput.finalizeTapleafByHash; + const psbt = Psbt.fromBase64(f.psbt); + + psbt.finalizeTaprootInput(f.index, Buffer.from(f.leafHash, 'hex')); + + assert.strictEqual(psbt.toBase64(), f.result); + }); + + it(`fails if tapleaf hash not found`, () => { + const f = fixtures.finalizeInput.finalizeTapleafByHash; + const psbt = Psbt.fromBase64(f.psbt); + + assert.throws(() => { + psbt.finalizeTaprootInput( + f.index, + Buffer.from(f.leafHash, 'hex').reverse(), + ); + }, new RegExp('Can not finalize taproot input #0. Signature for tapleaf script not found.')); + }); + + it(`fails if trying to finalzie non-taproot input`, () => { + const psbt = new Psbt(); + psbt.addInput({ + hash: + '0000000000000000000000000000000000000000000000000000000000000000', + index: 0, + }); + + assert.throws(() => { + psbt.finalizeTaprootInput(0); + }, new RegExp('Cannot finalize input #0. Not Taproot.')); + }); + }); + describe('finalizeAllInputs', () => { fixtures.finalizeAllInputs.forEach(f => { it(`Finalizes inputs of type "${f.type}"`, () => { @@ -530,10 +605,25 @@ describe(`Psbt`, () => { }); }); + describe('updateInput', () => { + fixtures.updateInput.checks.forEach(f => { + it(f.description, () => { + const psbt = Psbt.fromBase64(f.psbt); + + if (f.exception) { + assert.throws(() => { + psbt.updateInput(f.index, f.inputData as any); + }, new RegExp(f.exception)); + } + }); + }); + }); + describe('addOutput', () => { fixtures.addOutput.checks.forEach(f => { it(f.description, () => { - const psbt = new Psbt(); + if (f.isTaproot) initEccLib(ecc); + const psbt = f.psbt ? Psbt.fromBase64(f.psbt) : new Psbt(); if (f.exception) { assert.throws(() => { @@ -546,6 +636,9 @@ describe(`Psbt`, () => { assert.doesNotThrow(() => { psbt.addOutput(f.outputData as any); }); + if (f.result) { + assert.strictEqual(psbt.toBase64(), f.result); + } assert.doesNotThrow(() => { psbt.addOutputs([f.outputData as any]); }); @@ -952,6 +1045,135 @@ describe(`Psbt`, () => { }); }); + describe('validateSignaturesOfTapKeyInput', () => { + const f = fixtures.validateSignaturesOfTapKeyInput; + it('Correctly validates all signatures', () => { + initEccLib(ecc); + const psbt = Psbt.fromBase64(f.psbt); + assert.strictEqual( + psbt.validateSignaturesOfInput(f.index, schnorrValidator), + true, + ); + }); + + it('Correctly validates a signature against a pubkey', () => { + initEccLib(ecc); + const psbt = Psbt.fromBase64(f.psbt); + assert.strictEqual( + psbt.validateSignaturesOfInput( + f.index, + schnorrValidator, + f.pubkey as any, + ), + true, + ); + assert.throws(() => { + psbt.validateSignaturesOfInput( + f.index, + schnorrValidator, + f.incorrectPubkey as any, + ); + }, new RegExp('No signatures for this pubkey')); + }); + }); + + describe('validateSignaturesOfTapScriptInput', () => { + const f = fixtures.validateSignaturesOfTapScriptInput; + it('Correctly validates all signatures', () => { + initEccLib(ecc); + const psbt = Psbt.fromBase64(f.psbt); + assert.strictEqual( + psbt.validateSignaturesOfInput(f.index, schnorrValidator), + true, + ); + }); + + it('Correctly validates a signature against a pubkey', () => { + initEccLib(ecc); + const psbt = Psbt.fromBase64(f.psbt); + assert.strictEqual( + psbt.validateSignaturesOfInput( + f.index, + schnorrValidator, + f.pubkey as any, + ), + true, + ); + assert.throws(() => { + psbt.validateSignaturesOfInput( + f.index, + schnorrValidator, + f.incorrectPubkey as any, + ); + }, new RegExp('No signatures for this pubkey')); + }); + }); + + describe('tapTreeToList/tapTreeFromList', () => { + it('Correctly converts a Taptree to a Tapleaf list and back', () => { + taprootFixtures.valid + .filter(f => f.arguments.scriptTree) + .map(f => f.arguments.scriptTree) + .forEach(scriptTree => { + const originalTree = convertScriptTree( + scriptTree, + LEAF_VERSION_TAPSCRIPT, + ); + const list = tapTreeToList(originalTree); + const treeFromList = tapTreeFromList(list); + + assert.deepStrictEqual(treeFromList, originalTree); + }); + }); + + it('Throws if too many leaves on a given level', () => { + const list = Array.from({ length: 5 }).map(() => ({ + depth: 2, + leafVersion: LEAF_VERSION_TAPSCRIPT, + script: Buffer.from([]), + })); + assert.throws(() => { + tapTreeFromList(list); + }, new RegExp('No room left to insert tapleaf in tree')); + }); + + it('Throws if taptree depth is exceeded', () => { + let tree: Taptree = [ + { output: Buffer.from([]) }, + { output: Buffer.from([]) }, + ]; + Array.from({ length: 129 }).forEach( + () => (tree = [tree, { output: Buffer.from([]) }]), + ); + assert.throws(() => { + tapTreeToList(tree as Taptree); + }, new RegExp('Max taptree depth exceeded.')); + }); + + it('Throws if tapleaf depth is to high', () => { + const list = [ + { + depth: 129, + leafVersion: LEAF_VERSION_TAPSCRIPT, + script: Buffer.from([]), + }, + ]; + assert.throws(() => { + tapTreeFromList(list); + }, new RegExp('Max taptree depth exceeded.')); + }); + + it('Throws if not a valid taptree structure', () => { + const tree = Array.from({ length: 3 }).map(() => ({ + output: Buffer.from([]), + })); + + assert.throws(() => { + tapTreeToList(tree as Taptree); + }, new RegExp('Cannot convert taptree to tapleaf list. Expecting a tapree structure.')); + }); + }); + describe('getFeeRate', () => { it('Throws error if called before inputs are finalized', () => { const f = fixtures.getFeeRate; diff --git a/ts_src/address.ts b/ts_src/address.ts index 753589d46..8004b2668 100644 --- a/ts_src/address.ts +++ b/ts_src/address.ts @@ -2,11 +2,9 @@ import { Network } from './networks'; import * as networks from './networks'; import * as payments from './payments'; import * as bscript from './script'; -import * as types from './types'; +import { typeforce, tuple, Hash160bit, UInt8 } from './types'; import { bech32, bech32m } from 'bech32'; import * as bs58check from 'bs58check'; -const { typeforce } = types; - export interface Base58CheckResult { hash: Buffer; version: number; @@ -21,7 +19,7 @@ export interface Bech32Result { const FUTURE_SEGWIT_MAX_SIZE: number = 40; const FUTURE_SEGWIT_MIN_SIZE: number = 2; const FUTURE_SEGWIT_MAX_VERSION: number = 16; -const FUTURE_SEGWIT_MIN_VERSION: number = 1; +const FUTURE_SEGWIT_MIN_VERSION: number = 2; const FUTURE_SEGWIT_VERSION_DIFF: number = 0x50; const FUTURE_SEGWIT_VERSION_WARNING: string = 'WARNING: Sending to a future segwit version address can lead to loss of funds. ' + @@ -93,7 +91,7 @@ export function fromBech32(address: string): Bech32Result { } export function toBase58Check(hash: Buffer, version: number): string { - typeforce(types.tuple(types.Hash160bit, types.UInt8), arguments); + typeforce(tuple(Hash160bit, UInt8), arguments); const payload = Buffer.allocUnsafe(21); payload.writeUInt8(version, 0); @@ -131,6 +129,9 @@ export function fromOutputScript(output: Buffer, network?: Network): string { try { return payments.p2wsh({ output, network }).address as string; } catch (e) {} + try { + return payments.p2tr({ output, network }).address as string; + } catch (e) {} try { return _toFutureSegwitAddress(output, network); } catch (e) {} @@ -165,6 +166,9 @@ export function toOutputScript(address: string, network?: Network): Buffer { return payments.p2wpkh({ hash: decodeBech32.data }).output as Buffer; if (decodeBech32.data.length === 32) return payments.p2wsh({ hash: decodeBech32.data }).output as Buffer; + } else if (decodeBech32.version === 1) { + if (decodeBech32.data.length === 32) + return payments.p2tr({ pubkey: decodeBech32.data }).output as Buffer; } else if ( decodeBech32.version >= FUTURE_SEGWIT_MIN_VERSION && decodeBech32.version <= FUTURE_SEGWIT_MAX_VERSION && diff --git a/ts_src/ecc_lib.ts b/ts_src/ecc_lib.ts new file mode 100644 index 000000000..eb4c59eeb --- /dev/null +++ b/ts_src/ecc_lib.ts @@ -0,0 +1,95 @@ +import { TinySecp256k1Interface } from './types'; + +const _ECCLIB_CACHE: { eccLib?: TinySecp256k1Interface } = {}; + +export function initEccLib(eccLib: TinySecp256k1Interface | undefined): void { + if (!eccLib) { + // allow clearing the library + _ECCLIB_CACHE.eccLib = eccLib; + } else if (eccLib !== _ECCLIB_CACHE.eccLib) { + // new instance, verify it + verifyEcc(eccLib!); + _ECCLIB_CACHE.eccLib = eccLib; + } +} + +export function getEccLib(): TinySecp256k1Interface { + if (!_ECCLIB_CACHE.eccLib) + throw new Error( + 'No ECC Library provided. You must call initEccLib() with a valid TinySecp256k1Interface instance', + ); + return _ECCLIB_CACHE.eccLib; +} + +const h = (hex: string): Buffer => Buffer.from(hex, 'hex'); + +function verifyEcc(ecc: TinySecp256k1Interface): void { + assert(typeof ecc.isXOnlyPoint === 'function'); + assert( + ecc.isXOnlyPoint( + h('79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798'), + ), + ); + assert( + ecc.isXOnlyPoint( + h('fffffffffffffffffffffffffffffffffffffffffffffffffffffffeeffffc2e'), + ), + ); + assert( + ecc.isXOnlyPoint( + h('f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9'), + ), + ); + assert( + ecc.isXOnlyPoint( + h('0000000000000000000000000000000000000000000000000000000000000001'), + ), + ); + assert( + !ecc.isXOnlyPoint( + h('0000000000000000000000000000000000000000000000000000000000000000'), + ), + ); + assert( + !ecc.isXOnlyPoint( + h('fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f'), + ), + ); + + assert(typeof ecc.xOnlyPointAddTweak === 'function'); + tweakAddVectors.forEach(t => { + const r = ecc.xOnlyPointAddTweak(h(t.pubkey), h(t.tweak)); + if (t.result === null) { + assert(r === null); + } else { + assert(r !== null); + assert(r!.parity === t.parity); + assert(Buffer.from(r!.xOnlyPubkey).equals(h(t.result))); + } + }); +} + +function assert(bool: boolean): void { + if (!bool) throw new Error('ecc library invalid'); +} + +const tweakAddVectors = [ + { + pubkey: '79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798', + tweak: 'fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364140', + parity: -1, + result: null, + }, + { + pubkey: '1617d38ed8d8657da4d4761e8057bc396ea9e4b9d29776d4be096016dbd2509b', + tweak: 'a8397a935f0dfceba6ba9618f6451ef4d80637abf4e6af2669fbc9de6a8fd2ac', + parity: 1, + result: 'e478f99dab91052ab39a33ea35fd5e6e4933f4d28023cd597c9a1f6760346adf', + }, + { + pubkey: '2c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e668680991', + tweak: '823c3cd2142744b075a87eade7e1b8678ba308d566226a0056ca2b7a76f86b47', + parity: 0, + result: '9534f8dc8c6deda2dc007655981c78b49c5d96c778fbf363462a11ec9dfd948c', + }, +]; diff --git a/ts_src/index.ts b/ts_src/index.ts index d8b8619d1..64c4294c2 100644 --- a/ts_src/index.ts +++ b/ts_src/index.ts @@ -29,3 +29,4 @@ export { StackElement, } from './payments'; export { Input as TxInput, Output as TxOutput } from './transaction'; +export { initEccLib } from './ecc_lib'; diff --git a/ts_src/ops.ts b/ts_src/ops.ts index 8e2c41c11..dd8b1e6da 100644 --- a/ts_src/ops.ts +++ b/ts_src/ops.ts @@ -127,6 +127,8 @@ const OPS: { [key: string]: number } = { OP_NOP9: 184, OP_NOP10: 185, + OP_CHECKSIGADD: 186, + OP_PUBKEYHASH: 253, OP_PUBKEY: 254, OP_INVALIDOPCODE: 255, diff --git a/ts_src/payments/bip341.ts b/ts_src/payments/bip341.ts new file mode 100644 index 000000000..5a1dd068d --- /dev/null +++ b/ts_src/payments/bip341.ts @@ -0,0 +1,148 @@ +import { Buffer as NBuffer } from 'buffer'; +import { getEccLib } from '../ecc_lib'; +import * as bcrypto from '../crypto'; + +import { varuint } from '../bufferutils'; +import { Tapleaf, Taptree, isTapleaf } from '../types'; + +export const LEAF_VERSION_TAPSCRIPT = 0xc0; +export const MAX_TAPTREE_DEPTH = 128; + +interface HashLeaf { + hash: Buffer; +} + +interface HashBranch { + hash: Buffer; + left: HashTree; + right: HashTree; +} + +interface TweakedPublicKey { + parity: number; + x: Buffer; +} + +const isHashBranch = (ht: HashTree): ht is HashBranch => + 'left' in ht && 'right' in ht; + +/** + * Binary tree representing leaf, branch, and root node hashes of a Taptree. + * Each node contains a hash, and potentially left and right branch hashes. + * This tree is used for 2 purposes: Providing the root hash for tweaking, + * and calculating merkle inclusion proofs when constructing a control block. + */ +export type HashTree = HashLeaf | HashBranch; + +export function rootHashFromPath( + controlBlock: Buffer, + leafHash: Buffer, +): Buffer { + if (controlBlock.length < 33) + throw new TypeError( + `The control-block length is too small. Got ${ + controlBlock.length + }, expected min 33.`, + ); + const m = (controlBlock.length - 33) / 32; + + let kj = leafHash; + for (let j = 0; j < m; j++) { + const ej = controlBlock.slice(33 + 32 * j, 65 + 32 * j); + if (kj.compare(ej) < 0) { + kj = tapBranchHash(kj, ej); + } else { + kj = tapBranchHash(ej, kj); + } + } + + return kj; +} + +/** + * Build a hash tree of merkle nodes from the scripts binary tree. + * @param scriptTree - the tree of scripts to pairwise hash. + */ +export function toHashTree(scriptTree: Taptree): HashTree { + if (isTapleaf(scriptTree)) return { hash: tapleafHash(scriptTree) }; + + const hashes = [toHashTree(scriptTree[0]), toHashTree(scriptTree[1])]; + hashes.sort((a, b) => a.hash.compare(b.hash)); + const [left, right] = hashes; + + return { + hash: tapBranchHash(left.hash, right.hash), + left, + right, + }; +} + +/** + * Given a HashTree, finds the path from a particular hash to the root. + * @param node - the root of the tree + * @param hash - the hash to search for + * @returns - array of sibling hashes, from leaf (inclusive) to root + * (exclusive) needed to prove inclusion of the specified hash. undefined if no + * path is found + */ +export function findScriptPath( + node: HashTree, + hash: Buffer, +): Buffer[] | undefined { + if (isHashBranch(node)) { + const leftPath = findScriptPath(node.left, hash); + if (leftPath !== undefined) return [...leftPath, node.right.hash]; + + const rightPath = findScriptPath(node.right, hash); + if (rightPath !== undefined) return [...rightPath, node.left.hash]; + } else if (node.hash.equals(hash)) { + return []; + } + + return undefined; +} + +export function tapleafHash(leaf: Tapleaf): Buffer { + const version = leaf.version || LEAF_VERSION_TAPSCRIPT; + return bcrypto.taggedHash( + 'TapLeaf', + NBuffer.concat([NBuffer.from([version]), serializeScript(leaf.output)]), + ); +} + +export function tapTweakHash(pubKey: Buffer, h: Buffer | undefined): Buffer { + return bcrypto.taggedHash( + 'TapTweak', + NBuffer.concat(h ? [pubKey, h] : [pubKey]), + ); +} + +export function tweakKey( + pubKey: Buffer, + h: Buffer | undefined, +): TweakedPublicKey | null { + if (!NBuffer.isBuffer(pubKey)) return null; + if (pubKey.length !== 32) return null; + if (h && h.length !== 32) return null; + + const tweakHash = tapTweakHash(pubKey, h); + + const res = getEccLib().xOnlyPointAddTweak(pubKey, tweakHash); + if (!res || res.xOnlyPubkey === null) return null; + + return { + parity: res.parity, + x: NBuffer.from(res.xOnlyPubkey), + }; +} + +function tapBranchHash(a: Buffer, b: Buffer): Buffer { + return bcrypto.taggedHash('TapBranch', NBuffer.concat([a, b])); +} + +function serializeScript(s: Buffer): Buffer { + const varintLen = varuint.encodingLength(s.length); + const buffer = NBuffer.allocUnsafe(varintLen); // better + varuint.encode(s.length, buffer); + return NBuffer.concat([buffer, s]); +} diff --git a/ts_src/payments/index.ts b/ts_src/payments/index.ts index 4b7f1117e..ca72f72cb 100644 --- a/ts_src/payments/index.ts +++ b/ts_src/payments/index.ts @@ -1,4 +1,5 @@ import { Network } from '../networks'; +import { Taptree } from '../types'; import { p2data as embed } from './embed'; import { p2ms } from './p2ms'; import { p2pk } from './p2pk'; @@ -6,6 +7,7 @@ import { p2pkh } from './p2pkh'; import { p2sh } from './p2sh'; import { p2wpkh } from './p2wpkh'; import { p2wsh } from './p2wsh'; +import { p2tr } from './p2tr'; export interface Payment { name?: string; @@ -17,11 +19,14 @@ export interface Payment { pubkeys?: Buffer[]; input?: Buffer; signatures?: Buffer[]; + internalPubkey?: Buffer; pubkey?: Buffer; signature?: Buffer; address?: string; hash?: Buffer; redeem?: Payment; + redeemVersion?: number; + scriptTree?: Taptree; witness?: Buffer[]; } @@ -38,7 +43,7 @@ export type StackElement = Buffer | number; export type Stack = StackElement[]; export type StackFunction = () => Stack; -export { embed, p2ms, p2pk, p2pkh, p2sh, p2wpkh, p2wsh }; +export { embed, p2ms, p2pk, p2pkh, p2sh, p2wpkh, p2wsh, p2tr }; // TODO // witness commitment diff --git a/ts_src/payments/p2tr.ts b/ts_src/payments/p2tr.ts new file mode 100644 index 000000000..64c591ffd --- /dev/null +++ b/ts_src/payments/p2tr.ts @@ -0,0 +1,326 @@ +import { Buffer as NBuffer } from 'buffer'; +import { bitcoin as BITCOIN_NETWORK } from '../networks'; +import * as bscript from '../script'; +import { typeforce as typef, isTaptree, TAPLEAF_VERSION_MASK } from '../types'; +import { getEccLib } from '../ecc_lib'; +import { + toHashTree, + rootHashFromPath, + findScriptPath, + tapleafHash, + tweakKey, + LEAF_VERSION_TAPSCRIPT, +} from './bip341'; +import { Payment, PaymentOpts } from './index'; +import * as lazy from './lazy'; +import { bech32m } from 'bech32'; + +const OPS = bscript.OPS; +const TAPROOT_WITNESS_VERSION = 0x01; +const ANNEX_PREFIX = 0x50; + +export function p2tr(a: Payment, opts?: PaymentOpts): Payment { + if ( + !a.address && + !a.output && + !a.pubkey && + !a.internalPubkey && + !(a.witness && a.witness.length > 1) + ) + throw new TypeError('Not enough data'); + + opts = Object.assign({ validate: true }, opts || {}); + + typef( + { + address: typef.maybe(typef.String), + input: typef.maybe(typef.BufferN(0)), + network: typef.maybe(typef.Object), + output: typef.maybe(typef.BufferN(34)), + internalPubkey: typef.maybe(typef.BufferN(32)), + hash: typef.maybe(typef.BufferN(32)), // merkle root hash, the tweak + pubkey: typef.maybe(typef.BufferN(32)), // tweaked with `hash` from `internalPubkey` + signature: typef.maybe(typef.BufferN(64)), + witness: typef.maybe(typef.arrayOf(typef.Buffer)), + scriptTree: typef.maybe(isTaptree), + redeem: typef.maybe({ + output: typef.maybe(typef.Buffer), // tapleaf script + redeemVersion: typef.maybe(typef.Number), // tapleaf version + witness: typef.maybe(typef.arrayOf(typef.Buffer)), + }), + redeemVersion: typef.maybe(typef.Number), + }, + a, + ); + + const _address = lazy.value(() => { + const result = bech32m.decode(a.address!); + const version = result.words.shift(); + const data = bech32m.fromWords(result.words); + return { + version, + prefix: result.prefix, + data: NBuffer.from(data), + }; + }); + + // remove annex if present, ignored by taproot + const _witness = lazy.value(() => { + if (!a.witness || !a.witness.length) return; + if ( + a.witness.length >= 2 && + a.witness[a.witness.length - 1][0] === ANNEX_PREFIX + ) { + return a.witness.slice(0, -1); + } + return a.witness.slice(); + }); + + const _hashTree = lazy.value(() => { + if (a.scriptTree) return toHashTree(a.scriptTree); + if (a.hash) return { hash: a.hash }; + return; + }); + + const network = a.network || BITCOIN_NETWORK; + const o: Payment = { name: 'p2tr', network }; + + lazy.prop(o, 'address', () => { + if (!o.pubkey) return; + + const words = bech32m.toWords(o.pubkey); + words.unshift(TAPROOT_WITNESS_VERSION); + return bech32m.encode(network.bech32, words); + }); + + lazy.prop(o, 'hash', () => { + const hashTree = _hashTree(); + if (hashTree) return hashTree.hash; + const w = _witness(); + if (w && w.length > 1) { + const controlBlock = w[w.length - 1]; + const leafVersion = controlBlock[0] & TAPLEAF_VERSION_MASK; + const script = w[w.length - 2]; + const leafHash = tapleafHash({ output: script, version: leafVersion }); + return rootHashFromPath(controlBlock, leafHash); + } + return null; + }); + lazy.prop(o, 'output', () => { + if (!o.pubkey) return; + return bscript.compile([OPS.OP_1, o.pubkey]); + }); + lazy.prop(o, 'redeemVersion', () => { + if (a.redeemVersion) return a.redeemVersion; + if ( + a.redeem && + a.redeem.redeemVersion !== undefined && + a.redeem.redeemVersion !== null + ) { + return a.redeem.redeemVersion; + } + + return LEAF_VERSION_TAPSCRIPT; + }); + lazy.prop(o, 'redeem', () => { + const witness = _witness(); // witness without annex + if (!witness || witness.length < 2) return; + + return { + output: witness[witness.length - 2], + witness: witness.slice(0, -2), + redeemVersion: witness[witness.length - 1][0] & TAPLEAF_VERSION_MASK, + }; + }); + lazy.prop(o, 'pubkey', () => { + if (a.pubkey) return a.pubkey; + if (a.output) return a.output.slice(2); + if (a.address) return _address().data; + if (o.internalPubkey) { + const tweakedKey = tweakKey(o.internalPubkey, o.hash); + if (tweakedKey) return tweakedKey.x; + } + }); + lazy.prop(o, 'internalPubkey', () => { + if (a.internalPubkey) return a.internalPubkey; + const witness = _witness(); + if (witness && witness.length > 1) + return witness[witness.length - 1].slice(1, 33); + }); + lazy.prop(o, 'signature', () => { + if (a.signature) return a.signature; + const witness = _witness(); // witness without annex + if (!witness || witness.length !== 1) return; + return witness[0]; + }); + + lazy.prop(o, 'witness', () => { + if (a.witness) return a.witness; + const hashTree = _hashTree(); + if (hashTree && a.redeem && a.redeem.output && a.internalPubkey) { + const leafHash = tapleafHash({ + output: a.redeem.output, + version: o.redeemVersion, + }); + const path = findScriptPath(hashTree, leafHash); + if (!path) return; + const outputKey = tweakKey(a.internalPubkey, hashTree.hash); + if (!outputKey) return; + const controlBock = NBuffer.concat( + [ + NBuffer.from([o.redeemVersion! | outputKey.parity]), + a.internalPubkey, + ].concat(path), + ); + return [a.redeem.output, controlBock]; + } + if (a.signature) return [a.signature]; + }); + + // extended validation + if (opts.validate) { + let pubkey: Buffer = NBuffer.from([]); + if (a.address) { + if (network && network.bech32 !== _address().prefix) + throw new TypeError('Invalid prefix or Network mismatch'); + if (_address().version !== TAPROOT_WITNESS_VERSION) + throw new TypeError('Invalid address version'); + if (_address().data.length !== 32) + throw new TypeError('Invalid address data'); + pubkey = _address().data; + } + + if (a.pubkey) { + if (pubkey.length > 0 && !pubkey.equals(a.pubkey)) + throw new TypeError('Pubkey mismatch'); + else pubkey = a.pubkey; + } + + if (a.output) { + if ( + a.output.length !== 34 || + a.output[0] !== OPS.OP_1 || + a.output[1] !== 0x20 + ) + throw new TypeError('Output is invalid'); + if (pubkey.length > 0 && !pubkey.equals(a.output.slice(2))) + throw new TypeError('Pubkey mismatch'); + else pubkey = a.output.slice(2); + } + + if (a.internalPubkey) { + const tweakedKey = tweakKey(a.internalPubkey, o.hash); + if (pubkey.length > 0 && !pubkey.equals(tweakedKey!.x)) + throw new TypeError('Pubkey mismatch'); + else pubkey = tweakedKey!.x; + } + + if (pubkey && pubkey.length) { + if (!getEccLib().isXOnlyPoint(pubkey)) + throw new TypeError('Invalid pubkey for p2tr'); + } + + const hashTree = _hashTree(); + + if (a.hash && hashTree) { + if (!a.hash.equals(hashTree.hash)) throw new TypeError('Hash mismatch'); + } + + if (a.redeem && a.redeem.output && hashTree) { + const leafHash = tapleafHash({ + output: a.redeem.output, + version: o.redeemVersion, + }); + if (!findScriptPath(hashTree, leafHash)) + throw new TypeError('Redeem script not in tree'); + } + + const witness = _witness(); + + // compare the provided redeem data with the one computed from witness + if (a.redeem && o.redeem) { + if (a.redeem.redeemVersion) { + if (a.redeem.redeemVersion !== o.redeem.redeemVersion) + throw new TypeError('Redeem.redeemVersion and witness mismatch'); + } + + if (a.redeem.output) { + if (bscript.decompile(a.redeem.output)!.length === 0) + throw new TypeError('Redeem.output is invalid'); + + // output redeem is constructed from the witness + if (o.redeem.output && !a.redeem.output.equals(o.redeem.output)) + throw new TypeError('Redeem.output and witness mismatch'); + } + if (a.redeem.witness) { + if ( + o.redeem.witness && + !stacksEqual(a.redeem.witness, o.redeem.witness) + ) + throw new TypeError('Redeem.witness and witness mismatch'); + } + } + + if (witness && witness.length) { + if (witness.length === 1) { + // key spending + if (a.signature && !a.signature.equals(witness[0])) + throw new TypeError('Signature mismatch'); + } else { + // script path spending + const controlBlock = witness[witness.length - 1]; + if (controlBlock.length < 33) + throw new TypeError( + `The control-block length is too small. Got ${ + controlBlock.length + }, expected min 33.`, + ); + + if ((controlBlock.length - 33) % 32 !== 0) + throw new TypeError( + `The control-block length of ${controlBlock.length} is incorrect!`, + ); + + const m = (controlBlock.length - 33) / 32; + if (m > 128) + throw new TypeError( + `The script path is too long. Got ${m}, expected max 128.`, + ); + + const internalPubkey = controlBlock.slice(1, 33); + if (a.internalPubkey && !a.internalPubkey.equals(internalPubkey)) + throw new TypeError('Internal pubkey mismatch'); + + if (!getEccLib().isXOnlyPoint(internalPubkey)) + throw new TypeError('Invalid internalPubkey for p2tr witness'); + + const leafVersion = controlBlock[0] & TAPLEAF_VERSION_MASK; + const script = witness[witness.length - 2]; + + const leafHash = tapleafHash({ output: script, version: leafVersion }); + const hash = rootHashFromPath(controlBlock, leafHash); + + const outputKey = tweakKey(internalPubkey, hash); + if (!outputKey) + // todo: needs test data + throw new TypeError('Invalid outputKey for p2tr witness'); + + if (pubkey.length && !pubkey.equals(outputKey.x)) + throw new TypeError('Pubkey mismatch for p2tr witness'); + + if (outputKey.parity !== (controlBlock[0] & 1)) + throw new Error('Incorrect parity'); + } + } + } + + return Object.assign(o, a); +} + +function stacksEqual(a: Buffer[], b: Buffer[]): boolean { + if (a.length !== b.length) return false; + + return a.every((x, i) => { + return x.equals(b[i]); + }); +} diff --git a/ts_src/psbt.ts b/ts_src/psbt.ts index b9af10fcf..aa546bc71 100644 --- a/ts_src/psbt.ts +++ b/ts_src/psbt.ts @@ -11,15 +11,38 @@ import { PsbtOutputUpdate, Transaction as ITransaction, TransactionFromBuffer, + TapKeySig, + TapScriptSig, } from 'bip174/src/lib/interfaces'; import { checkForInput, checkForOutput } from 'bip174/src/lib/utils'; import { fromOutputScript, toOutputScript } from './address'; import { cloneBuffer, reverseBuffer } from './bufferutils'; -import { hash160 } from './crypto'; import { bitcoin as btcNetwork, Network } from './networks'; import * as payments from './payments'; +import { tapleafHash } from './payments/bip341'; import * as bscript from './script'; import { Output, Transaction } from './transaction'; +import { + toXOnly, + tapScriptFinalizer, + serializeTaprootSignature, + isTaprootInput, + checkTaprootInputFields, + checkTaprootOutputFields, + tweakInternalPubKey, + checkTaprootInputForSigs, +} from './psbt/bip371'; +import { + witnessStackToScriptWitness, + checkInputForSig, + pubkeyInScript, + isP2MS, + isP2PK, + isP2PKH, + isP2WPKH, + isP2WSHScript, + isP2SHScript, +} from './psbt/psbtutils'; export interface TransactionInput { hash: string | Buffer; @@ -261,6 +284,7 @@ export class Psbt { `Requires single object with at least [hash] and [index]`, ); } + checkTaprootInputFields(inputData, inputData, 'addInput'); checkInputsForPartialSig(this.data.inputs, 'addInput'); if (inputData.witnessScript) checkInvalidP2WSH(inputData.witnessScript); const c = this.__CACHE; @@ -304,6 +328,8 @@ export class Psbt { const script = toOutputScript(address, network); outputData = Object.assign(outputData, { script }); } + checkTaprootOutputFields(outputData, outputData, 'addOutput'); + const c = this.__CACHE; this.data.addOutput(outputData); c.__FEE = undefined; @@ -345,9 +371,44 @@ export class Psbt { finalizeInput( inputIndex: number, - finalScriptsFunc: FinalScriptsFunc = getFinalScripts, + finalScriptsFunc?: FinalScriptsFunc | FinalTaprootScriptsFunc, ): this { const input = checkForInput(this.data.inputs, inputIndex); + if (isTaprootInput(input)) + return this._finalizeTaprootInput( + inputIndex, + input, + undefined, + finalScriptsFunc as FinalTaprootScriptsFunc, + ); + return this._finalizeInput( + inputIndex, + input, + finalScriptsFunc as FinalScriptsFunc, + ); + } + + finalizeTaprootInput( + inputIndex: number, + tapLeafHashToFinalize?: Buffer, + finalScriptsFunc: FinalTaprootScriptsFunc = tapScriptFinalizer, + ): this { + const input = checkForInput(this.data.inputs, inputIndex); + if (isTaprootInput(input)) + return this._finalizeTaprootInput( + inputIndex, + input, + tapLeafHashToFinalize, + finalScriptsFunc, + ); + throw new Error(`Cannot finalize input #${inputIndex}. Not Taproot.`); + } + + private _finalizeInput( + inputIndex: number, + input: PsbtInput, + finalScriptsFunc: FinalScriptsFunc = getFinalScripts, + ): this { const { script, isP2SH, isP2WSH, isSegwit } = getScriptFromInput( inputIndex, input, @@ -376,6 +437,39 @@ export class Psbt { return this; } + private _finalizeTaprootInput( + inputIndex: number, + input: PsbtInput, + tapLeafHashToFinalize?: Buffer, + finalScriptsFunc = tapScriptFinalizer, + ): this { + if (!input.witnessUtxo) + throw new Error( + `Cannot finalize input #${inputIndex}. Missing withness utxo.`, + ); + + // Check key spend first. Increased privacy and reduced block space. + if (input.tapKeySig) { + const payment = payments.p2tr({ + output: input.witnessUtxo.script, + signature: input.tapKeySig, + }); + const finalScriptWitness = witnessStackToScriptWitness(payment.witness!); + this.data.updateInput(inputIndex, { finalScriptWitness }); + } else { + const { finalScriptWitness } = finalScriptsFunc( + inputIndex, + input, + tapLeafHashToFinalize, + ); + this.data.updateInput(inputIndex, { finalScriptWitness }); + } + + this.data.clearFinalizedInput(inputIndex); + + return this; + } + getInputType(inputIndex: number): AllScriptType { const input = checkForInput(this.data.inputs, inputIndex); const script = getScriptFromUtxo(inputIndex, input, this.__CACHE); @@ -430,6 +524,22 @@ export class Psbt { inputIndex: number, validator: ValidateSigFunction, pubkey?: Buffer, + ): boolean { + const input = this.data.inputs[inputIndex]; + if (isTaprootInput(input)) + return this.validateSignaturesOfTaprootInput( + inputIndex, + validator, + pubkey, + ); + + return this._validateSignaturesOfInput(inputIndex, validator, pubkey); + } + + private _validateSignaturesOfInput( + inputIndex: number, + validator: ValidateSigFunction, + pubkey?: Buffer, ): boolean { const input = this.data.inputs[inputIndex]; const partialSig = (input || {}).partialSig; @@ -465,6 +575,64 @@ export class Psbt { return results.every(res => res === true); } + private validateSignaturesOfTaprootInput( + inputIndex: number, + validator: ValidateSigFunction, + pubkey?: Buffer, + ): boolean { + const input = this.data.inputs[inputIndex]; + const tapKeySig = (input || {}).tapKeySig; + const tapScriptSig = (input || {}).tapScriptSig; + if (!input && !tapKeySig && !(tapScriptSig && !tapScriptSig.length)) + throw new Error('No signatures to validate'); + if (typeof validator !== 'function') + throw new Error('Need validator function to validate signatures'); + + pubkey = pubkey && toXOnly(pubkey); + const allHashses = pubkey + ? getTaprootHashesForSig( + inputIndex, + input, + this.data.inputs, + pubkey, + this.__CACHE, + ) + : getAllTaprootHashesForSig( + inputIndex, + input, + this.data.inputs, + this.__CACHE, + ); + + if (!allHashses.length) throw new Error('No signatures for this pubkey'); + + const tapKeyHash = allHashses.find(h => !!h.leafHash); + if (tapKeySig && tapKeyHash) { + const isValidTapkeySig = validator( + tapKeyHash.pubkey, + tapKeyHash.hash, + tapKeySig, + ); + if (!isValidTapkeySig) return false; + } + + if (tapScriptSig) { + for (const tapSig of tapScriptSig) { + const tapSigHash = allHashses.find(h => tapSig.pubkey.equals(h.pubkey)); + if (tapSigHash) { + const isValidTapScriptSig = validator( + tapSig.pubkey, + tapSigHash.hash, + tapSig.signature, + ); + if (!isValidTapScriptSig) return false; + } + } + } + + return true; + } + signAllInputsHD( hdKeyPair: HDSigner, sighashTypes: number[] = [Transaction.SIGHASH_ALL], @@ -566,10 +734,7 @@ export class Psbt { ); } - signAllInputs( - keyPair: Signer, - sighashTypes: number[] = [Transaction.SIGHASH_ALL], - ): this { + signAllInputs(keyPair: Signer, sighashTypes?: number[]): this { if (!keyPair || !keyPair.publicKey) throw new Error('Need Signer to sign input'); @@ -593,7 +758,7 @@ export class Psbt { signAllInputsAsync( keyPair: Signer | SignerAsync, - sighashTypes: number[] = [Transaction.SIGHASH_ALL], + sighashTypes?: number[], ): Promise { return new Promise( (resolve, reject): any => { @@ -630,10 +795,51 @@ export class Psbt { signInput( inputIndex: number, keyPair: Signer, - sighashTypes: number[] = [Transaction.SIGHASH_ALL], + sighashTypes?: number[], + ): this { + if (!keyPair || !keyPair.publicKey) + throw new Error('Need Signer to sign input'); + + const input = checkForInput(this.data.inputs, inputIndex); + + if (isTaprootInput(input)) { + return this._signTaprootInput( + inputIndex, + input, + keyPair, + undefined, + sighashTypes, + ); + } + return this._signInput(inputIndex, keyPair, sighashTypes); + } + + signTaprootInput( + inputIndex: number, + keyPair: Signer, + tapLeafHashToSign?: Buffer, + sighashTypes?: number[], ): this { if (!keyPair || !keyPair.publicKey) throw new Error('Need Signer to sign input'); + const input = checkForInput(this.data.inputs, inputIndex); + + if (isTaprootInput(input)) + return this._signTaprootInput( + inputIndex, + input, + keyPair, + tapLeafHashToSign, + sighashTypes, + ); + throw new Error(`Input #${inputIndex} is not of type Taproot.`); + } + + private _signInput( + inputIndex: number, + keyPair: Signer, + sighashTypes: number[] = [Transaction.SIGHASH_ALL], + ): this { const { hash, sighashType } = getHashAndSighashType( this.data.inputs, inputIndex, @@ -653,35 +859,214 @@ export class Psbt { return this; } + private _signTaprootInput( + inputIndex: number, + input: PsbtInput, + keyPair: Signer, + tapLeafHashToSign?: Buffer, + allowedSighashTypes: number[] = [Transaction.SIGHASH_DEFAULT], + ): this { + const hashesForSig = this.checkTaprootHashesForSig( + inputIndex, + input, + keyPair, + tapLeafHashToSign, + allowedSighashTypes, + ); + + const tapKeySig: TapKeySig = hashesForSig + .filter(h => !h.leafHash) + .map(h => + serializeTaprootSignature( + keyPair.signSchnorr!(h.hash), + input.sighashType, + ), + )[0]; + + const tapScriptSig: TapScriptSig[] = hashesForSig + .filter(h => !!h.leafHash) + .map( + h => + ({ + pubkey: toXOnly(keyPair.publicKey), + signature: serializeTaprootSignature( + keyPair.signSchnorr!(h.hash), + input.sighashType, + ), + leafHash: h.leafHash, + } as TapScriptSig), + ); + + if (tapKeySig) { + this.data.updateInput(inputIndex, { tapKeySig }); + } + + if (tapScriptSig.length) { + this.data.updateInput(inputIndex, { tapScriptSig }); + } + + return this; + } + signInputAsync( inputIndex: number, keyPair: Signer | SignerAsync, - sighashTypes: number[] = [Transaction.SIGHASH_ALL], + sighashTypes?: number[], ): Promise { return Promise.resolve().then(() => { if (!keyPair || !keyPair.publicKey) throw new Error('Need Signer to sign input'); - const { hash, sighashType } = getHashAndSighashType( - this.data.inputs, - inputIndex, - keyPair.publicKey, - this.__CACHE, - sighashTypes, - ); - return Promise.resolve(keyPair.sign(hash)).then(signature => { - const partialSig = [ - { - pubkey: keyPair.publicKey, - signature: bscript.signature.encode(signature, sighashType), - }, - ]; + const input = checkForInput(this.data.inputs, inputIndex); + if (isTaprootInput(input)) + return this._signTaprootInputAsync( + inputIndex, + input, + keyPair, + undefined, + sighashTypes, + ); + + return this._signInputAsync(inputIndex, keyPair, sighashTypes); + }); + } + + signTaprootInputAsync( + inputIndex: number, + keyPair: Signer | SignerAsync, + tapLeafHash?: Buffer, + sighashTypes?: number[], + ): Promise { + return Promise.resolve().then(() => { + if (!keyPair || !keyPair.publicKey) + throw new Error('Need Signer to sign input'); + + const input = checkForInput(this.data.inputs, inputIndex); + if (isTaprootInput(input)) + return this._signTaprootInputAsync( + inputIndex, + input, + keyPair, + tapLeafHash, + sighashTypes, + ); + + throw new Error(`Input #${inputIndex} is not of type Taproot.`); + }); + } - this.data.updateInput(inputIndex, { partialSig }); + private _signInputAsync( + inputIndex: number, + keyPair: Signer | SignerAsync, + sighashTypes: number[] = [Transaction.SIGHASH_ALL], + ): Promise { + const { hash, sighashType } = getHashAndSighashType( + this.data.inputs, + inputIndex, + keyPair.publicKey, + this.__CACHE, + sighashTypes, + ); + + return Promise.resolve(keyPair.sign(hash)).then(signature => { + const partialSig = [ + { + pubkey: keyPair.publicKey, + signature: bscript.signature.encode(signature, sighashType), + }, + ]; + + this.data.updateInput(inputIndex, { partialSig }); + }); + } + + private async _signTaprootInputAsync( + inputIndex: number, + input: PsbtInput, + keyPair: Signer | SignerAsync, + tapLeafHash?: Buffer, + sighashTypes: number[] = [Transaction.SIGHASH_DEFAULT], + ): Promise { + const hashesForSig = this.checkTaprootHashesForSig( + inputIndex, + input, + keyPair, + tapLeafHash, + sighashTypes, + ); + + const signaturePromises: Promise< + { tapKeySig: Buffer } | { tapScriptSig: TapScriptSig[] } + >[] = []; + const tapKeyHash = hashesForSig.filter(h => !h.leafHash)[0]; + if (tapKeyHash) { + const tapKeySigPromise = Promise.resolve( + keyPair.signSchnorr!(tapKeyHash.hash), + ).then(sig => { + return { tapKeySig: serializeTaprootSignature(sig, input.sighashType) }; + }); + signaturePromises.push(tapKeySigPromise); + } + + const tapScriptHashes = hashesForSig.filter(h => !!h.leafHash); + if (tapScriptHashes.length) { + const tapScriptSigPromises = tapScriptHashes.map(tsh => { + return Promise.resolve(keyPair.signSchnorr!(tsh.hash)).then( + signature => { + const tapScriptSig = [ + { + pubkey: toXOnly(keyPair.publicKey), + signature: serializeTaprootSignature( + signature, + input.sighashType, + ), + leafHash: tsh.leafHash, + } as TapScriptSig, + ]; + return { tapScriptSig }; + }, + ); }); + signaturePromises.push(...tapScriptSigPromises); + } + + return Promise.all(signaturePromises).then(results => { + results.forEach(v => this.data.updateInput(inputIndex, v)); }); } + private checkTaprootHashesForSig( + inputIndex: number, + input: PsbtInput, + keyPair: Signer | SignerAsync, + tapLeafHashToSign?: Buffer, + allowedSighashTypes?: number[], + ): { hash: Buffer; leafHash?: Buffer }[] { + if (typeof keyPair.signSchnorr !== 'function') + throw new Error( + `Need Schnorr Signer to sign taproot input #${inputIndex}.`, + ); + + const hashesForSig = getTaprootHashesForSig( + inputIndex, + input, + this.data.inputs, + keyPair.publicKey, + this.__CACHE, + tapLeafHashToSign, + allowedSighashTypes, + ); + + if (!hashesForSig || !hashesForSig.length) + throw new Error( + `Can not sign for input #${inputIndex} with the key ${keyPair.publicKey.toString( + 'hex', + )}`, + ); + + return hashesForSig; + } + toBuffer(): Buffer { checkCache(this.__CACHE); return this.data.toBuffer(); @@ -704,6 +1089,11 @@ export class Psbt { updateInput(inputIndex: number, updateData: PsbtInputUpdate): this { if (updateData.witnessScript) checkInvalidP2WSH(updateData.witnessScript); + checkTaprootInputFields( + this.data.inputs[inputIndex], + updateData, + 'updateInput', + ); this.data.updateInput(inputIndex, updateData); if (updateData.nonWitnessUtxo) { addNonWitnessTxCache( @@ -716,6 +1106,9 @@ export class Psbt { } updateOutput(outputIndex: number, updateData: PsbtOutputUpdate): this { + const outputData = this.data.outputs[outputIndex]; + checkTaprootOutputFields(outputData, updateData, 'updateOutput'); + this.data.updateOutput(outputIndex, updateData); return this; } @@ -812,6 +1205,7 @@ export interface Signer { publicKey: Buffer; network?: any; sign(hash: Buffer, lowR?: boolean): Buffer; + signSchnorr?(hash: Buffer): Buffer; getPublicKey?(): Buffer; } @@ -819,6 +1213,7 @@ export interface SignerAsync { publicKey: Buffer; network?: any; sign(hash: Buffer, lowR?: boolean): Promise; + signSchnorr?(hash: Buffer): Promise; getPublicKey?(): Buffer; } @@ -939,23 +1334,6 @@ function isFinalized(input: PsbtInput): boolean { return !!input.finalScriptSig || !!input.finalScriptWitness; } -function isPaymentFactory(payment: any): (script: Buffer) => boolean { - return (script: Buffer): boolean => { - try { - payment({ output: script }); - return true; - } catch (err) { - return false; - } - }; -} -const isP2MS = isPaymentFactory(payments.p2ms); -const isP2PK = isPaymentFactory(payments.p2pk); -const isP2PKH = isPaymentFactory(payments.p2pkh); -const isP2WPKH = isPaymentFactory(payments.p2wpkh); -const isP2WSHScript = isPaymentFactory(payments.p2wsh); -const isP2SHScript = isPaymentFactory(payments.p2sh); - function bip32DerivationIsMine( root: HDSigner, ): (d: Bip32Derivation) => boolean { @@ -994,36 +1372,11 @@ function checkFees(psbt: Psbt, cache: PsbtCache, opts: PsbtOpts): void { function checkInputsForPartialSig(inputs: PsbtInput[], action: string): void { inputs.forEach(input => { - let throws = false; - let pSigs: PartialSig[] = []; - if ((input.partialSig || []).length === 0) { - if (!input.finalScriptSig && !input.finalScriptWitness) return; - pSigs = getPsigsFromInputFinalScripts(input); - } else { - pSigs = input.partialSig!; - } - pSigs.forEach(pSig => { - const { hashType } = bscript.signature.decode(pSig.signature); - const whitelist: string[] = []; - const isAnyoneCanPay = hashType & Transaction.SIGHASH_ANYONECANPAY; - if (isAnyoneCanPay) whitelist.push('addInput'); - const hashMod = hashType & 0x1f; - switch (hashMod) { - case Transaction.SIGHASH_ALL: - break; - case Transaction.SIGHASH_SINGLE: - case Transaction.SIGHASH_NONE: - whitelist.push('addOutput'); - whitelist.push('setInputSequence'); - break; - } - if (whitelist.indexOf(action) === -1) { - throws = true; - } - }); - if (throws) { + const throws = isTaprootInput(input) + ? checkTaprootInputForSigs(input, action) + : checkInputForSig(input, action); + if (throws) throw new Error('Can not modify transaction, signatures exist.'); - } }); } @@ -1147,6 +1500,13 @@ type FinalScriptsFunc = ( finalScriptSig: Buffer | undefined; finalScriptWitness: Buffer | undefined; }; +type FinalTaprootScriptsFunc = ( + inputIndex: number, // Which input is it? + input: PsbtInput, // The PSBT input contents + tapLeafHashToFinalize?: Buffer, // Only finalize this specific leaf +) => { + finalScriptWitness: Buffer | undefined; +}; function getFinalScripts( inputIndex: number, @@ -1251,13 +1611,8 @@ function getHashForSig( } { const unsignedTx = cache.__TX; const sighashType = input.sighashType || Transaction.SIGHASH_ALL; - if (sighashTypes && sighashTypes.indexOf(sighashType) < 0) { - const str = sighashTypeToString(sighashType); - throw new Error( - `Sighash type is not allowed. Retry the sign method passing the ` + - `sighashTypes array of whitelisted types. Sighash type: ${str}`, - ); - } + checkSighashTypeAllowed(sighashType, sighashTypes); + let hash: Buffer; let prevout: Output; @@ -1345,6 +1700,108 @@ function getHashForSig( }; } +function getAllTaprootHashesForSig( + inputIndex: number, + input: PsbtInput, + inputs: PsbtInput[], + cache: PsbtCache, +): { pubkey: Buffer; hash: Buffer; leafHash?: Buffer }[] { + const allPublicKeys = []; + if (input.tapInternalKey) { + const outputKey = tweakInternalPubKey(inputIndex, input); + allPublicKeys.push(outputKey); + } + + if (input.tapScriptSig) { + const tapScriptPubkeys = input.tapScriptSig.map(tss => tss.pubkey); + allPublicKeys.push(...tapScriptPubkeys); + } + + const allHashes = allPublicKeys.map(pubicKey => + getTaprootHashesForSig(inputIndex, input, inputs, pubicKey, cache), + ); + + return allHashes.flat(); +} + +function getTaprootHashesForSig( + inputIndex: number, + input: PsbtInput, + inputs: PsbtInput[], + pubkey: Buffer, + cache: PsbtCache, + tapLeafHashToSign?: Buffer, + allowedSighashTypes?: number[], +): { pubkey: Buffer; hash: Buffer; leafHash?: Buffer }[] { + const unsignedTx = cache.__TX; + + const sighashType = input.sighashType || Transaction.SIGHASH_DEFAULT; + checkSighashTypeAllowed(sighashType, allowedSighashTypes); + + const prevOuts: Output[] = inputs.map((i, index) => + getScriptAndAmountFromUtxo(index, i, cache), + ); + const signingScripts = prevOuts.map(o => o.script); + const values = prevOuts.map(o => o.value); + + const hashes = []; + if (input.tapInternalKey && !tapLeafHashToSign) { + const outputKey = tweakInternalPubKey(inputIndex, input); + if (toXOnly(pubkey).equals(outputKey)) { + const tapKeyHash = unsignedTx.hashForWitnessV1( + inputIndex, + signingScripts, + values, + sighashType, + ); + hashes.push({ pubkey, hash: tapKeyHash }); + } + } + + const tapLeafHashes = (input.tapLeafScript || []) + .filter(tapLeaf => pubkeyInScript(pubkey, tapLeaf.script)) + .map(tapLeaf => { + const hash = tapleafHash({ + output: tapLeaf.script, + version: tapLeaf.leafVersion, + }); + return Object.assign({ hash }, tapLeaf); + }) + .filter( + tapLeaf => !tapLeafHashToSign || tapLeafHashToSign.equals(tapLeaf.hash), + ) + .map(tapLeaf => { + const tapScriptHash = unsignedTx.hashForWitnessV1( + inputIndex, + signingScripts, + values, + Transaction.SIGHASH_DEFAULT, + tapLeaf.hash, + ); + + return { + pubkey, + hash: tapScriptHash, + leafHash: tapLeaf.hash, + }; + }); + + return hashes.concat(tapLeafHashes); +} + +function checkSighashTypeAllowed( + sighashType: number, + sighashTypes?: number[], +): void { + if (sighashTypes && sighashTypes.indexOf(sighashType) < 0) { + const str = sighashTypeToString(sighashType); + throw new Error( + `Sighash type is not allowed. Retry the sign method passing the ` + + `sighashTypes array of whitelisted types. Sighash type: ${str}`, + ); + } +} + function getPayment( script: Buffer, scriptType: string, @@ -1383,21 +1840,6 @@ function getPayment( return payment!; } -function getPsigsFromInputFinalScripts(input: PsbtInput): PartialSig[] { - const scriptItems = !input.finalScriptSig - ? [] - : bscript.decompile(input.finalScriptSig) || []; - const witnessItems = !input.finalScriptWitness - ? [] - : bscript.decompile(input.finalScriptWitness) || []; - return scriptItems - .concat(witnessItems) - .filter(item => { - return Buffer.isBuffer(item) && bscript.isCanonicalScriptSignature(item); - }) - .map(sig => ({ signature: sig })) as PartialSig[]; -} - interface GetScriptReturn { script: Buffer | null; isSegwit: boolean; @@ -1539,36 +1981,6 @@ function sighashTypeToString(sighashType: number): string { return text; } -function witnessStackToScriptWitness(witness: Buffer[]): Buffer { - let buffer = Buffer.allocUnsafe(0); - - function writeSlice(slice: Buffer): void { - buffer = Buffer.concat([buffer, Buffer.from(slice)]); - } - - function writeVarInt(i: number): void { - const currentLen = buffer.length; - const varintLen = varuint.encodingLength(i); - - buffer = Buffer.concat([buffer, Buffer.allocUnsafe(varintLen)]); - varuint.encode(i, buffer, currentLen); - } - - function writeVarSlice(slice: Buffer): void { - writeVarInt(slice.length); - writeSlice(slice); - } - - function writeVector(vector: Buffer[]): void { - writeVarInt(vector.length); - vector.forEach(writeVarSlice); - } - - writeVector(witness); - - return buffer; -} - function addNonWitnessTxCache( cache: PsbtCache, input: PsbtInput, @@ -1656,15 +2068,28 @@ function getScriptFromUtxo( input: PsbtInput, cache: PsbtCache, ): Buffer { + const { script } = getScriptAndAmountFromUtxo(inputIndex, input, cache); + return script; +} + +function getScriptAndAmountFromUtxo( + inputIndex: number, + input: PsbtInput, + cache: PsbtCache, +): { script: Buffer; value: number } { if (input.witnessUtxo !== undefined) { - return input.witnessUtxo.script; + return { + script: input.witnessUtxo.script, + value: input.witnessUtxo.value, + }; } else if (input.nonWitnessUtxo !== undefined) { const nonWitnessUtxoTx = nonWitnessUtxoTxFromCache( cache, input, inputIndex, ); - return nonWitnessUtxoTx.outs[cache.__TX.ins[inputIndex].index].script; + const o = nonWitnessUtxoTx.outs[cache.__TX.ins[inputIndex].index]; + return { script: o.script, value: o.value }; } else { throw new Error("Can't find pubkey in input without Utxo data"); } @@ -1808,18 +2233,6 @@ function checkInvalidP2WSH(script: Buffer): void { } } -function pubkeyInScript(pubkey: Buffer, script: Buffer): boolean { - const pubkeyHash = hash160(pubkey); - - const decompiled = bscript.decompile(script); - if (decompiled === null) throw new Error('Unknown script error'); - - return decompiled.some(element => { - if (typeof element === 'number') return false; - return element.equals(pubkey) || element.equals(pubkeyHash); - }); -} - type AllScriptType = | 'witnesspubkeyhash' | 'pubkeyhash' diff --git a/ts_src/psbt/bip371.ts b/ts_src/psbt/bip371.ts new file mode 100644 index 000000000..d4eaaa5f3 --- /dev/null +++ b/ts_src/psbt/bip371.ts @@ -0,0 +1,458 @@ +import { Taptree, Tapleaf, isTapleaf, isTaptree } from '../types'; +import { + PsbtInput, + PsbtOutput, + TapLeafScript, + TapScriptSig, + TapLeaf, + TapTree, + TapInternalKey, +} from 'bip174/src/lib/interfaces'; + +import { Transaction } from '../transaction'; + +import { + witnessStackToScriptWitness, + pubkeyPositionInScript, + isP2TR, +} from './psbtutils'; +import { + tweakKey, + tapleafHash, + rootHashFromPath, + LEAF_VERSION_TAPSCRIPT, + MAX_TAPTREE_DEPTH, +} from '../payments/bip341'; +import { p2tr } from '../payments'; + +import { signatureBlocksAction } from './psbtutils'; + +export const toXOnly = (pubKey: Buffer) => + pubKey.length === 32 ? pubKey : pubKey.slice(1, 33); + +/** + * Default tapscript finalizer. It searches for the `tapLeafHashToFinalize` if provided. + * Otherwise it will search for the tapleaf that has at least one signature and has the shortest path. + * @param inputIndex the position of the PSBT input. + * @param input the PSBT input. + * @param tapLeafHashToFinalize optional, if provided the finalizer will search for a tapleaf that has this hash + * and will try to build the finalScriptWitness. + * @returns the finalScriptWitness or throws an exception if no tapleaf found. + */ +export function tapScriptFinalizer( + inputIndex: number, + input: PsbtInput, + tapLeafHashToFinalize?: Buffer, +): { + finalScriptWitness: Buffer | undefined; +} { + const tapLeaf = findTapLeafToFinalize( + input, + inputIndex, + tapLeafHashToFinalize, + ); + + try { + const sigs = sortSignatures(input, tapLeaf); + const witness = sigs.concat(tapLeaf.script).concat(tapLeaf.controlBlock); + return { finalScriptWitness: witnessStackToScriptWitness(witness) }; + } catch (err) { + throw new Error(`Can not finalize taproot input #${inputIndex}: ${err}`); + } +} + +export function serializeTaprootSignature( + sig: Buffer, + sighashType?: number, +): Buffer { + const sighashTypeByte = sighashType + ? Buffer.from([sighashType!]) + : Buffer.from([]); + + return Buffer.concat([sig, sighashTypeByte]); +} + +export function isTaprootInput(input: PsbtInput): boolean { + return ( + input && + !!( + input.tapInternalKey || + input.tapMerkleRoot || + (input.tapLeafScript && input.tapLeafScript.length) || + (input.tapBip32Derivation && input.tapBip32Derivation.length) || + (input.witnessUtxo && isP2TR(input.witnessUtxo.script)) + ) + ); +} + +export function isTaprootOutput(output: PsbtOutput, script?: Buffer): boolean { + return ( + output && + !!( + output.tapInternalKey || + output.tapTree || + (output.tapBip32Derivation && output.tapBip32Derivation.length) || + (script && isP2TR(script)) + ) + ); +} + +export function checkTaprootInputFields( + inputData: PsbtInput, + newInputData: PsbtInput, + action: string, +): void { + checkMixedTaprootAndNonTaprootInputFields(inputData, newInputData, action); + checkIfTapLeafInTree(inputData, newInputData, action); +} + +export function checkTaprootOutputFields( + outputData: PsbtOutput, + newOutputData: PsbtOutput, + action: string, +): void { + checkMixedTaprootAndNonTaprootOutputFields(outputData, newOutputData, action); + checkTaprootScriptPubkey(outputData, newOutputData); +} + +function checkTaprootScriptPubkey( + outputData: PsbtOutput, + newOutputData: PsbtOutput, +): void { + if (!newOutputData.tapTree && !newOutputData.tapInternalKey) return; + + const tapInternalKey = + newOutputData.tapInternalKey || outputData.tapInternalKey; + const tapTree = newOutputData.tapTree || outputData.tapTree; + + if (tapInternalKey) { + const { script: scriptPubkey } = outputData as any; + const script = getTaprootScripPubkey(tapInternalKey, tapTree); + if (scriptPubkey && !scriptPubkey.equals(script)) + throw new Error('Error adding output. Script or address missmatch.'); + } +} + +function getTaprootScripPubkey( + tapInternalKey: TapInternalKey, + tapTree?: TapTree, +): Buffer { + const scriptTree = tapTree && tapTreeFromList(tapTree.leaves); + const { output } = p2tr({ + internalPubkey: tapInternalKey, + scriptTree, + }); + return output!; +} + +export function tweakInternalPubKey( + inputIndex: number, + input: PsbtInput, +): Buffer { + const tapInternalKey = input.tapInternalKey; + const outputKey = + tapInternalKey && tweakKey(tapInternalKey, input.tapMerkleRoot); + + if (!outputKey) + throw new Error( + `Cannot tweak tap internal key for input #${inputIndex}. Public key: ${tapInternalKey && + tapInternalKey.toString('hex')}`, + ); + return outputKey.x; +} + +/** + * Convert a binary tree to a BIP371 type list. Each element of the list is (according to BIP371): + * One or more tuples representing the depth, leaf version, and script for a leaf in the Taproot tree, + * allowing the entire tree to be reconstructed. The tuples must be in depth first search order so that + * the tree is correctly reconstructed. + * @param tree the binary tap tree + * @returns a list of BIP 371 tapleaves + */ +export function tapTreeToList(tree: Taptree): TapLeaf[] { + if (!isTaptree(tree)) + throw new Error( + 'Cannot convert taptree to tapleaf list. Expecting a tapree structure.', + ); + return _tapTreeToList(tree); +} + +/** + * Convert a BIP371 TapLeaf list to a TapTree (binary). + * @param leaves a list of tapleaves where each element of the list is (according to BIP371): + * One or more tuples representing the depth, leaf version, and script for a leaf in the Taproot tree, + * allowing the entire tree to be reconstructed. The tuples must be in depth first search order so that + * the tree is correctly reconstructed. + * @returns the corresponding taptree, or throws an exception if the tree cannot be reconstructed + */ +export function tapTreeFromList(leaves: TapLeaf[] = []): Taptree { + if (leaves.length === 1 && leaves[0].depth === 0) + return { + output: leaves[0].script, + version: leaves[0].leafVersion, + }; + + return instertLeavesInTree(leaves); +} + +export function checkTaprootInputForSigs( + input: PsbtInput, + action: string, +): boolean { + const sigs = extractTaprootSigs(input); + return sigs.some(sig => + signatureBlocksAction(sig, decodeSchnorrSignature, action), + ); +} + +function decodeSchnorrSignature( + signature: Buffer, +): { + signature: Buffer; + hashType: number; +} { + return { + signature: signature.slice(0, 64), + hashType: signature.slice(64)[0] || Transaction.SIGHASH_DEFAULT, + }; +} + +function extractTaprootSigs(input: PsbtInput): Buffer[] { + const sigs: Buffer[] = []; + if (input.tapKeySig) sigs.push(input.tapKeySig); + if (input.tapScriptSig) + sigs.push(...input.tapScriptSig.map(s => s.signature)); + if (!sigs.length) { + const finalTapKeySig = getTapKeySigFromWithness(input.finalScriptWitness); + if (finalTapKeySig) sigs.push(finalTapKeySig); + } + + return sigs; +} + +function getTapKeySigFromWithness( + finalScriptWitness?: Buffer, +): Buffer | undefined { + if (!finalScriptWitness) return; + const witness = finalScriptWitness.slice(2); + // todo: add schnorr signature validation + if (witness.length === 64 || witness.length === 65) return witness; +} + +function _tapTreeToList( + tree: Taptree, + leaves: TapLeaf[] = [], + depth = 0, +): TapLeaf[] { + if (depth > MAX_TAPTREE_DEPTH) throw new Error('Max taptree depth exceeded.'); + if (!tree) return []; + if (isTapleaf(tree)) { + leaves.push({ + depth, + leafVersion: tree.version || LEAF_VERSION_TAPSCRIPT, + script: tree.output, + }); + return leaves; + } + if (tree[0]) _tapTreeToList(tree[0], leaves, depth + 1); + if (tree[1]) _tapTreeToList(tree[1], leaves, depth + 1); + return leaves; +} + +// Just like Taptree, but it accepts empty branches +type PartialTaptree = + | [PartialTaptree | Tapleaf, PartialTaptree | Tapleaf] + | Tapleaf + | undefined; +function instertLeavesInTree(leaves: TapLeaf[]): Taptree { + let tree: PartialTaptree; + for (const leaf of leaves) { + tree = instertLeafInTree(leaf, tree); + if (!tree) throw new Error(`No room left to insert tapleaf in tree`); + } + + return tree as Taptree; +} + +function instertLeafInTree( + leaf: TapLeaf, + tree?: PartialTaptree, + depth = 0, +): PartialTaptree { + if (depth > MAX_TAPTREE_DEPTH) throw new Error('Max taptree depth exceeded.'); + if (leaf.depth === depth) { + if (!tree) + return { + output: leaf.script, + version: leaf.leafVersion, + }; + return; + } + + if (isTapleaf(tree)) return; + const leftSide = instertLeafInTree(leaf, tree && tree[0], depth + 1); + if (leftSide) return [leftSide, tree && tree[1]]; + + const rightSide = instertLeafInTree(leaf, tree && tree[1], depth + 1); + if (rightSide) return [tree && tree[0], rightSide]; +} + +function checkMixedTaprootAndNonTaprootInputFields( + inputData: PsbtOutput, + newInputData: PsbtInput, + action: string, +): void { + const isBadTaprootUpdate = + isTaprootInput(inputData) && hasNonTaprootFields(newInputData); + const isBadNonTaprootUpdate = + hasNonTaprootFields(inputData) && isTaprootInput(newInputData); + const hasMixedFields = + inputData === newInputData && + (isTaprootInput(newInputData) && hasNonTaprootFields(newInputData)); // todo: bad? use !=== + + if (isBadTaprootUpdate || isBadNonTaprootUpdate || hasMixedFields) + throw new Error( + `Invalid arguments for Psbt.${action}. ` + + `Cannot use both taproot and non-taproot fields.`, + ); +} +function checkMixedTaprootAndNonTaprootOutputFields( + inputData: PsbtOutput, + newInputData: PsbtOutput, + action: string, +): void { + const isBadTaprootUpdate = + isTaprootOutput(inputData) && hasNonTaprootFields(newInputData); + const isBadNonTaprootUpdate = + hasNonTaprootFields(inputData) && isTaprootOutput(newInputData); + const hasMixedFields = + inputData === newInputData && + (isTaprootOutput(newInputData) && hasNonTaprootFields(newInputData)); + + if (isBadTaprootUpdate || isBadNonTaprootUpdate || hasMixedFields) + throw new Error( + `Invalid arguments for Psbt.${action}. ` + + `Cannot use both taproot and non-taproot fields.`, + ); +} + +function checkIfTapLeafInTree( + inputData: PsbtInput, + newInputData: PsbtInput, + action: string, +): void { + if (newInputData.tapMerkleRoot) { + const newLeafsInTree = (newInputData.tapLeafScript || []).every(l => + isTapLeafInTree(l, newInputData.tapMerkleRoot), + ); + const oldLeafsInTree = (inputData.tapLeafScript || []).every(l => + isTapLeafInTree(l, newInputData.tapMerkleRoot), + ); + if (!newLeafsInTree || !oldLeafsInTree) + throw new Error( + `Invalid arguments for Psbt.${action}. Tapleaf not part of taptree.`, + ); + } else if (inputData.tapMerkleRoot) { + const newLeafsInTree = (newInputData.tapLeafScript || []).every(l => + isTapLeafInTree(l, inputData.tapMerkleRoot), + ); + if (!newLeafsInTree) + throw new Error( + `Invalid arguments for Psbt.${action}. Tapleaf not part of taptree.`, + ); + } +} + +function isTapLeafInTree(tapLeaf: TapLeafScript, merkleRoot?: Buffer): boolean { + if (!merkleRoot) return true; + + const leafHash = tapleafHash({ + output: tapLeaf.script, + version: tapLeaf.leafVersion, + }); + + const rootHash = rootHashFromPath(tapLeaf.controlBlock, leafHash); + return rootHash.equals(merkleRoot); +} + +function sortSignatures(input: PsbtInput, tapLeaf: TapLeafScript): Buffer[] { + const leafHash = tapleafHash({ + output: tapLeaf.script, + version: tapLeaf.leafVersion, + }); + + return (input.tapScriptSig || []) + .filter(tss => tss.leafHash.equals(leafHash)) + .map(tss => addPubkeyPositionInScript(tapLeaf.script, tss)) + .sort((t1, t2) => t2.positionInScript - t1.positionInScript) + .map(t => t.signature) as Buffer[]; +} + +function addPubkeyPositionInScript( + script: Buffer, + tss: TapScriptSig, +): TapScriptSigWitPosition { + return Object.assign( + { + positionInScript: pubkeyPositionInScript(tss.pubkey, script), + }, + tss, + ) as TapScriptSigWitPosition; +} + +/** + * Find tapleaf by hash, or get the signed tapleaf with the shortest path. + */ +function findTapLeafToFinalize( + input: PsbtInput, + inputIndex: number, + leafHashToFinalize?: Buffer, +): TapLeafScript { + if (!input.tapScriptSig || !input.tapScriptSig.length) + throw new Error( + `Can not finalize taproot input #${inputIndex}. No tapleaf script signature provided.`, + ); + const tapLeaf = (input.tapLeafScript || []) + .sort((a, b) => a.controlBlock.length - b.controlBlock.length) + .find(leaf => + canFinalizeLeaf(leaf, input.tapScriptSig!, leafHashToFinalize), + ); + + if (!tapLeaf) + throw new Error( + `Can not finalize taproot input #${inputIndex}. Signature for tapleaf script not found.`, + ); + + return tapLeaf; +} + +function canFinalizeLeaf( + leaf: TapLeafScript, + tapScriptSig: TapScriptSig[], + hash?: Buffer, +): boolean { + const leafHash = tapleafHash({ + output: leaf.script, + version: leaf.leafVersion, + }); + const whiteListedHash = !hash || hash.equals(leafHash); + return ( + whiteListedHash && + tapScriptSig!.find(tss => tss.leafHash.equals(leafHash)) !== undefined + ); +} + +function hasNonTaprootFields(io: PsbtInput | PsbtOutput): boolean { + return ( + io && + !!( + io.redeemScript || + io.witnessScript || + (io.bip32Derivation && io.bip32Derivation.length) + ) + ); +} + +interface TapScriptSigWitPosition extends TapScriptSig { + positionInScript: number; +} diff --git a/ts_src/psbt/psbtutils.ts b/ts_src/psbt/psbtutils.ts new file mode 100644 index 000000000..d51e2df54 --- /dev/null +++ b/ts_src/psbt/psbtutils.ts @@ -0,0 +1,139 @@ +import * as varuint from 'bip174/src/lib/converter/varint'; +import { PartialSig, PsbtInput } from 'bip174/src/lib/interfaces'; +import * as bscript from '../script'; +import { Transaction } from '../transaction'; +import { hash160 } from '../crypto'; +import * as payments from '../payments'; + +function isPaymentFactory(payment: any): (script: Buffer) => boolean { + return (script: Buffer): boolean => { + try { + payment({ output: script }); + return true; + } catch (err) { + return false; + } + }; +} +export const isP2MS = isPaymentFactory(payments.p2ms); +export const isP2PK = isPaymentFactory(payments.p2pk); +export const isP2PKH = isPaymentFactory(payments.p2pkh); +export const isP2WPKH = isPaymentFactory(payments.p2wpkh); +export const isP2WSHScript = isPaymentFactory(payments.p2wsh); +export const isP2SHScript = isPaymentFactory(payments.p2sh); +export const isP2TR = isPaymentFactory(payments.p2tr); + +export function witnessStackToScriptWitness(witness: Buffer[]): Buffer { + let buffer = Buffer.allocUnsafe(0); + + function writeSlice(slice: Buffer): void { + buffer = Buffer.concat([buffer, Buffer.from(slice)]); + } + + function writeVarInt(i: number): void { + const currentLen = buffer.length; + const varintLen = varuint.encodingLength(i); + + buffer = Buffer.concat([buffer, Buffer.allocUnsafe(varintLen)]); + varuint.encode(i, buffer, currentLen); + } + + function writeVarSlice(slice: Buffer): void { + writeVarInt(slice.length); + writeSlice(slice); + } + + function writeVector(vector: Buffer[]): void { + writeVarInt(vector.length); + vector.forEach(writeVarSlice); + } + + writeVector(witness); + + return buffer; +} + +export function pubkeyPositionInScript(pubkey: Buffer, script: Buffer): number { + const pubkeyHash = hash160(pubkey); + const pubkeyXOnly = pubkey.slice(1, 33); // slice before calling? + + const decompiled = bscript.decompile(script); + if (decompiled === null) throw new Error('Unknown script error'); + + return decompiled.findIndex(element => { + if (typeof element === 'number') return false; + return ( + element.equals(pubkey) || + element.equals(pubkeyHash) || + element.equals(pubkeyXOnly) + ); + }); +} + +export function pubkeyInScript(pubkey: Buffer, script: Buffer): boolean { + return pubkeyPositionInScript(pubkey, script) !== -1; +} + +export function checkInputForSig(input: PsbtInput, action: string): boolean { + const pSigs = extractPartialSigs(input); + return pSigs.some(pSig => + signatureBlocksAction(pSig, bscript.signature.decode, action), + ); +} + +type SignatureDecodeFunc = ( + buffer: Buffer, +) => { + signature: Buffer; + hashType: number; +}; +export function signatureBlocksAction( + signature: Buffer, + signatureDecodeFn: SignatureDecodeFunc, + action: string, +): boolean { + const { hashType } = signatureDecodeFn(signature); + const whitelist: string[] = []; + const isAnyoneCanPay = hashType & Transaction.SIGHASH_ANYONECANPAY; + if (isAnyoneCanPay) whitelist.push('addInput'); + const hashMod = hashType & 0x1f; + switch (hashMod) { + case Transaction.SIGHASH_ALL: + break; + case Transaction.SIGHASH_SINGLE: + case Transaction.SIGHASH_NONE: + whitelist.push('addOutput'); + whitelist.push('setInputSequence'); + break; + } + if (whitelist.indexOf(action) === -1) { + return true; + } + return false; +} + +function extractPartialSigs(input: PsbtInput): Buffer[] { + let pSigs: PartialSig[] = []; + if ((input.partialSig || []).length === 0) { + if (!input.finalScriptSig && !input.finalScriptWitness) return []; + pSigs = getPsigsFromInputFinalScripts(input); + } else { + pSigs = input.partialSig!; + } + return pSigs.map(p => p.signature); +} + +function getPsigsFromInputFinalScripts(input: PsbtInput): PartialSig[] { + const scriptItems = !input.finalScriptSig + ? [] + : bscript.decompile(input.finalScriptSig) || []; + const witnessItems = !input.finalScriptWitness + ? [] + : bscript.decompile(input.finalScriptWitness) || []; + return scriptItems + .concat(witnessItems) + .filter(item => { + return Buffer.isBuffer(item) && bscript.isCanonicalScriptSignature(item); + }) + .map(sig => ({ signature: sig })) as PartialSig[]; +} diff --git a/ts_src/types.ts b/ts_src/types.ts index c035b4008..b904fd49b 100644 --- a/ts_src/types.ts +++ b/ts_src/types.ts @@ -1,4 +1,5 @@ import { Buffer as NBuffer } from 'buffer'; + export const typeforce = require('typeforce'); const ZERO32 = NBuffer.alloc(32, 0); @@ -6,6 +7,7 @@ const EC_P = NBuffer.from( 'fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f', 'hex', ); + export function isPoint(p: Buffer | number | undefined | null): boolean { if (!NBuffer.isBuffer(p)) return false; if (p.length < 33) return false; @@ -65,6 +67,48 @@ export const Network = typeforce.compile({ wif: typeforce.UInt8, }); +export interface XOnlyPointAddTweakResult { + parity: 1 | 0; + xOnlyPubkey: Uint8Array; +} + +export interface Tapleaf { + output: Buffer; + version?: number; +} + +export const TAPLEAF_VERSION_MASK = 0xfe; +export function isTapleaf(o: any): o is Tapleaf { + if (!o || !('output' in o)) return false; + if (!NBuffer.isBuffer(o.output)) return false; + if (o.version !== undefined) + return (o.version & TAPLEAF_VERSION_MASK) === o.version; + return true; +} + +/** + * Binary tree repsenting script path spends for a Taproot input. + * Each node is either a single Tapleaf, or a pair of Tapleaf | Taptree. + * The tree has no balancing requirements. + */ +export type Taptree = [Taptree | Tapleaf, Taptree | Tapleaf] | Tapleaf; + +export function isTaptree(scriptTree: any): scriptTree is Taptree { + if (!Array(scriptTree)) return isTapleaf(scriptTree); + if (scriptTree.length !== 2) return false; + return scriptTree.every((t: any) => isTaptree(t)); +} + +export interface TinySecp256k1Interface { + isXOnlyPoint(p: Uint8Array): boolean; + xOnlyPointAddTweak( + p: Uint8Array, + tweak: Uint8Array, + ): XOnlyPointAddTweakResult | null; + privateAdd(d: Uint8Array, tweak: Uint8Array): Uint8Array | null; + privateNegate(d: Uint8Array): Uint8Array; +} + export const Buffer256bit = typeforce.BufferN(32); export const Hash160bit = typeforce.BufferN(20); export const Hash256bit = typeforce.BufferN(32);