From 418f9f8811cd70a5719ddb3878d6b8abc8dfa8e9 Mon Sep 17 00:00:00 2001 From: Daniel Peng Date: Mon, 27 Jul 2026 09:41:09 -0400 Subject: [PATCH 1/2] feat: refactor MPC/TSS wallet gen to use SDK callback Ticket: WCN-683 --- package-lock.json | 344 +++++++++++-- package.json | 6 +- .../api/master/generateWallet.test.ts | 79 ++- .../master/walletGenerationCallbacks.test.ts | 475 +++++++++++++++++- src/masterBitgoExpress/handlers/ecdsa.ts | 287 ----------- src/masterBitgoExpress/handlers/eddsa.ts | 118 ----- .../handlers/handleGenerateWallet.ts | 163 ++---- .../handlers/utils/walletCreationUtils.ts | 3 - .../handlers/walletGenerationCallbacks.ts | 394 ++++++++++++++- 9 files changed, 1281 insertions(+), 588 deletions(-) delete mode 100644 src/masterBitgoExpress/handlers/utils/walletCreationUtils.ts diff --git a/package-lock.json b/package-lock.json index cc36f3cd..8e215736 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,7 @@ "@bitgo-beta/abstract-cosmos": "1.0.1-beta.1905", "@bitgo-beta/abstract-eth": "1.0.2-beta.2150", "@bitgo-beta/abstract-utxo": "1.1.1-beta.2153", - "@bitgo-beta/sdk-api": "1.10.1-beta.1920", + "@bitgo-beta/sdk-api": "1.10.1-beta.1923", "@bitgo-beta/sdk-coin-ada": "2.3.14-beta.1918", "@bitgo-beta/sdk-coin-algo": "2.8.9-beta.398", "@bitgo-beta/sdk-coin-apt": "1.0.1-beta.1360", @@ -83,7 +83,7 @@ "@bitgo-beta/sdk-coin-zec": "1.1.1-beta.2144", "@bitgo-beta/sdk-coin-zeta": "1.0.1-beta.1836", "@bitgo-beta/sdk-coin-zketh": "1.0.1-beta.1700", - "@bitgo-beta/sdk-core": "8.2.1-beta.1925", + "@bitgo-beta/sdk-core": "8.2.1-beta.1928", "@bitgo-beta/sdk-lib-mpc": "8.2.0-beta.1924", "@bitgo-beta/statics": "15.1.1-beta.1939", "@bitgo/wasm-miniscript": "2.0.0-beta.7", @@ -3051,6 +3051,46 @@ "node": ">=20" } }, + "node_modules/@bitgo-beta/abstract-utxo/node_modules/@bitgo-beta/argon2": { + "version": "0.0.1-beta.240", + "resolved": "https://registry.npmjs.org/@bitgo-beta/argon2/-/argon2-0.0.1-beta.240.tgz", + "integrity": "sha512-kNh5MZAkYxX86wDce4lneNWHAEoS5l0Zh7+GTf47T8B/Tp3cP3dyz3pUcXQJZufZqYzYhPyYGJaKAsAP8znyGQ==", + "license": "MIT" + }, + "node_modules/@bitgo-beta/abstract-utxo/node_modules/@bitgo-beta/sdk-api": { + "version": "1.10.1-beta.1920", + "resolved": "https://registry.npmjs.org/@bitgo-beta/sdk-api/-/sdk-api-1.10.1-beta.1920.tgz", + "integrity": "sha512-PM8GEpd/ajHF+mNqhfvA1zAf8oyarY6bNpalJAcEg5npiDqNo/UslwJFpclXhWMiXdr8pTw/Z37MaiJPQhoplQ==", + "license": "MIT", + "dependencies": { + "@bitgo-beta/argon2": "0.0.1-beta.240", + "@bitgo-beta/sdk-core": "8.2.1-beta.1925", + "@bitgo-beta/sdk-hmac": "1.0.1-beta.1303", + "@bitgo-beta/sjcl": "1.0.2-beta.2176", + "@bitgo-beta/unspents": "0.13.2-beta.1928", + "@bitgo-beta/utxo-lib": "8.0.3-beta.1929", + "@types/superagent": "4.1.15", + "bitcoinjs-message": "npm:@bitgo-forks/bitcoinjs-message@1.0.0-master.3", + "debug": "3.1.0", + "eol": "^0.5.0", + "io-ts": "npm:@bitgo-forks/io-ts@2.1.4", + "lodash": "^4.18.0", + "proxy-agent": "6.4.0", + "sanitize-html": "^2.17.4", + "secp256k1": "5.0.1", + "secrets.js-grempe": "^1.1.0", + "superagent": "^9.0.1" + } + }, + "node_modules/@bitgo-beta/abstract-utxo/node_modules/@bitgo-beta/sdk-hmac": { + "version": "1.0.1-beta.1303", + "resolved": "https://registry.npmjs.org/@bitgo-beta/sdk-hmac/-/sdk-hmac-1.0.1-beta.1303.tgz", + "integrity": "sha512-Et22Jjo/fqpF2z9o6bG4yTmM9ybqzaJ/CXdcOa98hnYDtz0FykdoRVNPOQdw/GJLUKikUmbbbHGcIJOxocAzAw==", + "license": "MIT", + "dependencies": { + "@bitgo-beta/sjcl": "1.0.2-beta.2176" + } + }, "node_modules/@bitgo-beta/abstract-utxo/node_modules/@types/superagent": { "version": "4.1.15", "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-4.1.15.tgz", @@ -3060,6 +3100,21 @@ "@types/node": "*" } }, + "node_modules/@bitgo-beta/abstract-utxo/node_modules/debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/@bitgo-beta/abstract-utxo/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, "node_modules/@bitgo-beta/abstract-utxo/node_modules/formidable": { "version": "3.5.4", "resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.4.tgz", @@ -3138,9 +3193,10 @@ } }, "node_modules/@bitgo-beta/argon2": { - "version": "0.0.1-beta.240", - "resolved": "https://registry.npmjs.org/@bitgo-beta/argon2/-/argon2-0.0.1-beta.240.tgz", - "integrity": "sha512-kNh5MZAkYxX86wDce4lneNWHAEoS5l0Zh7+GTf47T8B/Tp3cP3dyz3pUcXQJZufZqYzYhPyYGJaKAsAP8znyGQ==" + "version": "0.0.1-beta.244", + "resolved": "https://registry.npmjs.org/@bitgo-beta/argon2/-/argon2-0.0.1-beta.244.tgz", + "integrity": "sha512-KDHDp9vABlb1vILQbjuAjZihQCa205KeYp+tQo2RX8zQ5APri5ct3ogHSqRI9C+K7tFlZUwcwHNHlVhpY4vLOQ==", + "license": "MIT" }, "node_modules/@bitgo-beta/blake2b": { "version": "3.0.4-beta.2167", @@ -3258,16 +3314,17 @@ "integrity": "sha512-pgZSVj7ojXt937FdZqeOk+RmOPt73nm//mi+bJ4DXsXuFjcbNBgBmt/gQlsxHJXhKugoeT3FUAgizlb+o+pMyQ==" }, "node_modules/@bitgo-beta/sdk-api": { - "version": "1.10.1-beta.1920", - "resolved": "https://registry.npmjs.org/@bitgo-beta/sdk-api/-/sdk-api-1.10.1-beta.1920.tgz", - "integrity": "sha512-PM8GEpd/ajHF+mNqhfvA1zAf8oyarY6bNpalJAcEg5npiDqNo/UslwJFpclXhWMiXdr8pTw/Z37MaiJPQhoplQ==", + "version": "1.10.1-beta.1923", + "resolved": "https://registry.npmjs.org/@bitgo-beta/sdk-api/-/sdk-api-1.10.1-beta.1923.tgz", + "integrity": "sha512-NaH3SVCFBcKFZIOIDb/ZzROzTX36wviFF0IdufFUG2CmGkSMNA6OUEFZzMsnSuyvVs003TSrdEPaAMuhqi7m2w==", + "license": "MIT", "dependencies": { - "@bitgo-beta/argon2": "0.0.1-beta.240", - "@bitgo-beta/sdk-core": "8.2.1-beta.1925", - "@bitgo-beta/sdk-hmac": "1.0.1-beta.1303", - "@bitgo-beta/sjcl": "1.0.2-beta.2176", - "@bitgo-beta/unspents": "0.13.2-beta.1928", - "@bitgo-beta/utxo-lib": "8.0.3-beta.1929", + "@bitgo-beta/argon2": "0.0.1-beta.244", + "@bitgo-beta/sdk-core": "8.2.1-beta.1928", + "@bitgo-beta/sdk-hmac": "1.0.1-beta.1307", + "@bitgo-beta/sjcl": "1.0.2-beta.2180", + "@bitgo-beta/unspents": "0.13.2-beta.1932", + "@bitgo-beta/utxo-lib": "8.0.3-beta.1933", "@types/superagent": "4.1.15", "bitcoinjs-message": "npm:@bitgo-forks/bitcoinjs-message@1.0.0-master.3", "debug": "3.1.0", @@ -3281,6 +3338,93 @@ "superagent": "^9.0.1" } }, + "node_modules/@bitgo-beta/sdk-api/node_modules/@bitgo-beta/blake2b": { + "version": "3.0.4-beta.2171", + "resolved": "https://registry.npmjs.org/@bitgo-beta/blake2b/-/blake2b-3.0.4-beta.2171.tgz", + "integrity": "sha512-VluUn8duvR0E0xuFHmV3scvOJu1kyBDJfgG/X3zjsbOD1CvhijiWmxSJFBbN2+QOKMG/DPE9jqF6OrCL7yfhLQ==", + "license": "ISC", + "dependencies": { + "@bitgo-beta/blake2b-wasm": "3.0.4-beta.2178", + "nanoassert": "^2.0.0" + } + }, + "node_modules/@bitgo-beta/sdk-api/node_modules/@bitgo-beta/blake2b-wasm": { + "version": "3.0.4-beta.2178", + "resolved": "https://registry.npmjs.org/@bitgo-beta/blake2b-wasm/-/blake2b-wasm-3.0.4-beta.2178.tgz", + "integrity": "sha512-wEHe+ce1tpF/nBVmGUcwn/kpyJggAVaLBAGULoUy1t7X/AM/vZqdchylt8idNov/z7vkCQPBrNv+qT1OBCTo0Q==", + "license": "MIT", + "dependencies": { + "nanoassert": "^1.0.0" + } + }, + "node_modules/@bitgo-beta/sdk-api/node_modules/@bitgo-beta/blake2b-wasm/node_modules/nanoassert": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/nanoassert/-/nanoassert-1.1.0.tgz", + "integrity": "sha512-C40jQ3NzfkP53NsO8kEOFd79p4b9kDXQMwgiY1z8ZwrDZgUyom0AHwGegF4Dm99L+YoYhuaB0ceerUcXmqr1rQ==", + "license": "ISC" + }, + "node_modules/@bitgo-beta/sdk-api/node_modules/@bitgo-beta/secp256k1": { + "version": "1.0.2-beta.1968", + "resolved": "https://registry.npmjs.org/@bitgo-beta/secp256k1/-/secp256k1-1.0.2-beta.1968.tgz", + "integrity": "sha512-H3sa2IS8vkjllBrxdtHMSeal0zhR3XPoQ+U1vGJb5+N6ucmR5uSQsCXMw6TgYwH1NqiAEzwq1Oa8PSV6rOd8pg==", + "license": "MIT", + "dependencies": { + "@brandonblack/musig": "^0.0.1-alpha.0", + "@noble/secp256k1": "1.6.3", + "bip32": "^3.0.1", + "bitcoinjs-message": "npm:@bitgo-forks/bitcoinjs-message@1.0.0-master.3", + "bs58check": "^2.1.2", + "create-hash": "^1.2.0", + "create-hmac": "^1.1.7", + "ecpair": "npm:@bitgo/ecpair@2.1.0-rc.0" + }, + "engines": { + "node": ">=20", + "npm": ">=3.10.10" + } + }, + "node_modules/@bitgo-beta/sdk-api/node_modules/@bitgo-beta/sjcl": { + "version": "1.0.2-beta.2180", + "resolved": "https://registry.npmjs.org/@bitgo-beta/sjcl/-/sjcl-1.0.2-beta.2180.tgz", + "integrity": "sha512-K4u6YXFPgQHQ9GmPKsNT0nc9Q+zc4VYNDMEwjK2W3QikpsCaaWalA5TQjhjl59TijXRuSNxcQdRJvaaY8YbQeg==", + "license": "(BSD-2-Clause OR GPL-2.0-only)" + }, + "node_modules/@bitgo-beta/sdk-api/node_modules/@bitgo-beta/unspents": { + "version": "0.13.2-beta.1932", + "resolved": "https://registry.npmjs.org/@bitgo-beta/unspents/-/unspents-0.13.2-beta.1932.tgz", + "integrity": "sha512-LBwvLW370kO8Kl3cy0c7n+pCQ0S2z8fRtZ6Gmz9wZ0d5fAHtAwbAeV8PJ78qZwS91I4LGAtz2BT1kq0ac3TbLw==", + "license": "Apache-2.0", + "dependencies": { + "@bitgo-beta/utxo-lib": "8.0.3-beta.1933", + "lodash": "^4.18.0", + "tcomb": "~3.2.29", + "varuint-bitcoin": "^1.0.4" + } + }, + "node_modules/@bitgo-beta/sdk-api/node_modules/@bitgo-beta/utxo-lib": { + "version": "8.0.3-beta.1933", + "resolved": "https://registry.npmjs.org/@bitgo-beta/utxo-lib/-/utxo-lib-8.0.3-beta.1933.tgz", + "integrity": "sha512-8l/stbE6KY/vMraLzXVsEALewM4F5j3LZig5TbeOJ1nRRZAQI2OWXkXSyKgYuHFLxu3cEWL59U7mL7ISBQ3rrw==", + "license": "MIT", + "dependencies": { + "@bitgo-beta/blake2b": "3.0.4-beta.2171", + "@bitgo-beta/secp256k1": "1.0.2-beta.1968", + "@brandonblack/musig": "^0.0.1-alpha.0", + "bech32": "^2.0.0", + "bip174": "npm:@bitgo-forks/bip174@3.1.0-master.4", + "bitcoin-ops": "^1.3.0", + "bitcoinjs-lib": "npm:@bitgo-forks/bitcoinjs-lib@7.1.0-master.11", + "bs58check": "^2.1.2", + "cashaddress": "^1.1.0", + "fastpriorityqueue": "^0.7.1", + "typeforce": "^1.11.3", + "varuint-bitcoin": "^1.1.2" + }, + "engines": { + "node": ">=20", + "npm": ">=3.10.10" + } + }, "node_modules/@bitgo-beta/sdk-api/node_modules/@types/superagent": { "version": "4.1.15", "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-4.1.15.tgz", @@ -5851,15 +5995,16 @@ } }, "node_modules/@bitgo-beta/sdk-core": { - "version": "8.2.1-beta.1925", - "resolved": "https://registry.npmjs.org/@bitgo-beta/sdk-core/-/sdk-core-8.2.1-beta.1925.tgz", - "integrity": "sha512-YoJlycAa7kar3bSQQeG1kHjrsc8UWCJFCvvdL3QpwHO3Gz+tK/0iygsxb6ucMlrg8L6UdlJdvb2GgOMh8Efp7w==", + "version": "8.2.1-beta.1928", + "resolved": "https://registry.npmjs.org/@bitgo-beta/sdk-core/-/sdk-core-8.2.1-beta.1928.tgz", + "integrity": "sha512-BIvxruaI4FreiJ4C4tD+EgXsZD1cvc5CCB3uoMnwD0hjl6qtUd6ER5jjLLqzFv74f3bUWS/qMMdG3e09V9akvA==", + "license": "MIT", "dependencies": { - "@bitgo-beta/sdk-lib-mpc": "8.2.0-beta.1924", - "@bitgo-beta/secp256k1": "1.0.2-beta.1963", - "@bitgo-beta/sjcl": "1.0.2-beta.2176", - "@bitgo-beta/statics": "15.1.1-beta.1939", - "@bitgo-beta/utxo-lib": "8.0.3-beta.1929", + "@bitgo-beta/sdk-lib-mpc": "8.2.0-beta.1927", + "@bitgo-beta/secp256k1": "1.0.2-beta.1968", + "@bitgo-beta/sjcl": "1.0.2-beta.2180", + "@bitgo-beta/statics": "15.1.1-beta.1944", + "@bitgo-beta/utxo-lib": "8.0.3-beta.1933", "@bitgo/public-types": "6.48.0", "@noble/curves": "1.8.1", "@stablelib/hex": "^1.0.0", @@ -5887,10 +6032,117 @@ "uuid": "11.1.1" } }, + "node_modules/@bitgo-beta/sdk-core/node_modules/@bitgo-beta/blake2b": { + "version": "3.0.4-beta.2171", + "resolved": "https://registry.npmjs.org/@bitgo-beta/blake2b/-/blake2b-3.0.4-beta.2171.tgz", + "integrity": "sha512-VluUn8duvR0E0xuFHmV3scvOJu1kyBDJfgG/X3zjsbOD1CvhijiWmxSJFBbN2+QOKMG/DPE9jqF6OrCL7yfhLQ==", + "license": "ISC", + "dependencies": { + "@bitgo-beta/blake2b-wasm": "3.0.4-beta.2178", + "nanoassert": "^2.0.0" + } + }, + "node_modules/@bitgo-beta/sdk-core/node_modules/@bitgo-beta/blake2b-wasm": { + "version": "3.0.4-beta.2178", + "resolved": "https://registry.npmjs.org/@bitgo-beta/blake2b-wasm/-/blake2b-wasm-3.0.4-beta.2178.tgz", + "integrity": "sha512-wEHe+ce1tpF/nBVmGUcwn/kpyJggAVaLBAGULoUy1t7X/AM/vZqdchylt8idNov/z7vkCQPBrNv+qT1OBCTo0Q==", + "license": "MIT", + "dependencies": { + "nanoassert": "^1.0.0" + } + }, + "node_modules/@bitgo-beta/sdk-core/node_modules/@bitgo-beta/blake2b-wasm/node_modules/nanoassert": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/nanoassert/-/nanoassert-1.1.0.tgz", + "integrity": "sha512-C40jQ3NzfkP53NsO8kEOFd79p4b9kDXQMwgiY1z8ZwrDZgUyom0AHwGegF4Dm99L+YoYhuaB0ceerUcXmqr1rQ==", + "license": "ISC" + }, + "node_modules/@bitgo-beta/sdk-core/node_modules/@bitgo-beta/sdk-lib-mpc": { + "version": "8.2.0-beta.1927", + "resolved": "https://registry.npmjs.org/@bitgo-beta/sdk-lib-mpc/-/sdk-lib-mpc-8.2.0-beta.1927.tgz", + "integrity": "sha512-gGsohw569Qd1ynVCbjRUvBo8yJnBHcuMUsU1/4ABoom8XcnaLDqzWgg6L4W94vnE252QlF5A/a2X3rFqfxI0oQ==", + "license": "MIT", + "dependencies": { + "@bitgo/wasm-mps": "1.10.0", + "@noble/curves": "1.8.1", + "@silencelaboratories/dkls-wasm-ll-node": "1.2.0-pre.4", + "@silencelaboratories/dkls-wasm-ll-web": "1.2.0-pre.4", + "@types/superagent": "4.1.15", + "@wasmer/wasi": "^1.2.2", + "bigint-crypto-utils": "3.1.4", + "bigint-mod-arith": "3.1.2", + "cbor-x": "1.5.9", + "fp-ts": "2.16.2", + "io-ts": "npm:@bitgo-forks/io-ts@2.1.4", + "libsodium-wrappers-sumo": "^0.7.9", + "openpgp": "5.11.3", + "paillier-bigint": "3.3.0", + "secp256k1": "5.0.1" + }, + "peerDependencies": { + "@silencelaboratories/dkls-wasm-ll-bundler": "1.2.0-pre.4" + }, + "peerDependenciesMeta": { + "@silencelaboratories/dkls-wasm-ll-bundler": { + "optional": true + } + } + }, + "node_modules/@bitgo-beta/sdk-core/node_modules/@bitgo-beta/secp256k1": { + "version": "1.0.2-beta.1968", + "resolved": "https://registry.npmjs.org/@bitgo-beta/secp256k1/-/secp256k1-1.0.2-beta.1968.tgz", + "integrity": "sha512-H3sa2IS8vkjllBrxdtHMSeal0zhR3XPoQ+U1vGJb5+N6ucmR5uSQsCXMw6TgYwH1NqiAEzwq1Oa8PSV6rOd8pg==", + "license": "MIT", + "dependencies": { + "@brandonblack/musig": "^0.0.1-alpha.0", + "@noble/secp256k1": "1.6.3", + "bip32": "^3.0.1", + "bitcoinjs-message": "npm:@bitgo-forks/bitcoinjs-message@1.0.0-master.3", + "bs58check": "^2.1.2", + "create-hash": "^1.2.0", + "create-hmac": "^1.1.7", + "ecpair": "npm:@bitgo/ecpair@2.1.0-rc.0" + }, + "engines": { + "node": ">=20", + "npm": ">=3.10.10" + } + }, + "node_modules/@bitgo-beta/sdk-core/node_modules/@bitgo-beta/sjcl": { + "version": "1.0.2-beta.2180", + "resolved": "https://registry.npmjs.org/@bitgo-beta/sjcl/-/sjcl-1.0.2-beta.2180.tgz", + "integrity": "sha512-K4u6YXFPgQHQ9GmPKsNT0nc9Q+zc4VYNDMEwjK2W3QikpsCaaWalA5TQjhjl59TijXRuSNxcQdRJvaaY8YbQeg==", + "license": "(BSD-2-Clause OR GPL-2.0-only)" + }, + "node_modules/@bitgo-beta/sdk-core/node_modules/@bitgo-beta/utxo-lib": { + "version": "8.0.3-beta.1933", + "resolved": "https://registry.npmjs.org/@bitgo-beta/utxo-lib/-/utxo-lib-8.0.3-beta.1933.tgz", + "integrity": "sha512-8l/stbE6KY/vMraLzXVsEALewM4F5j3LZig5TbeOJ1nRRZAQI2OWXkXSyKgYuHFLxu3cEWL59U7mL7ISBQ3rrw==", + "license": "MIT", + "dependencies": { + "@bitgo-beta/blake2b": "3.0.4-beta.2171", + "@bitgo-beta/secp256k1": "1.0.2-beta.1968", + "@brandonblack/musig": "^0.0.1-alpha.0", + "bech32": "^2.0.0", + "bip174": "npm:@bitgo-forks/bip174@3.1.0-master.4", + "bitcoin-ops": "^1.3.0", + "bitcoinjs-lib": "npm:@bitgo-forks/bitcoinjs-lib@7.1.0-master.11", + "bs58check": "^2.1.2", + "cashaddress": "^1.1.0", + "fastpriorityqueue": "^0.7.1", + "typeforce": "^1.11.3", + "varuint-bitcoin": "^1.1.2" + }, + "engines": { + "node": ">=20", + "npm": ">=3.10.10" + } + }, "node_modules/@bitgo-beta/sdk-core/node_modules/@noble/curves": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.8.1.tgz", "integrity": "sha512-warwspo+UYUPep0Q+vtdVB4Ugn8GGQj8iyB3gnRWsztmUHTI3S1nhdiWNsPUGL0vud7JlRRk1XEu7Lq1KGTnMQ==", + "license": "MIT", "dependencies": { "@noble/hashes": "1.7.1" }, @@ -5905,6 +6157,7 @@ "version": "1.7.1", "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.7.1.tgz", "integrity": "sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ==", + "license": "MIT", "engines": { "node": "^14.21.3 || >=16" }, @@ -5916,6 +6169,7 @@ "version": "4.1.15", "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-4.1.15.tgz", "integrity": "sha512-mu/N4uvfDN2zVQQ5AYJI/g4qxn2bHB6521t1UuH09ShNWjebTqN0ZFuYK9uYjcgmI0dTQEs+Owi1EO6U0OkOZQ==", + "license": "MIT", "dependencies": { "@types/cookiejar": "*", "@types/node": "*" @@ -5925,6 +6179,7 @@ "version": "3.0.11", "resolved": "https://registry.npmjs.org/base-x/-/base-x-3.0.11.tgz", "integrity": "sha512-xz7wQ8xDhdyP7tQxwdteLYeFfS68tSMNCZ/Y37WJ4bhGfKPpqEIlmIyueQHqOyoPhE6xNUqjzRr8ra0eF9VRvA==", + "license": "MIT", "dependencies": { "safe-buffer": "^5.0.1" } @@ -5933,14 +6188,25 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz", "integrity": "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==", + "license": "MIT", "dependencies": { "base-x": "^3.0.2" } }, + "node_modules/@bitgo-beta/sdk-core/node_modules/cbor-x": { + "version": "1.5.9", + "resolved": "https://registry.npmjs.org/cbor-x/-/cbor-x-1.5.9.tgz", + "integrity": "sha512-OEI5rEu3MeR0WWNUXuIGkxmbXVhABP+VtgAXzm48c9ulkrsvxshjjk94XSOGphyAKeNGLPfAxxzEtgQ6rEVpYQ==", + "license": "MIT", + "optionalDependencies": { + "cbor-extract": "^2.2.0" + } + }, "node_modules/@bitgo-beta/sdk-core/node_modules/formidable": { "version": "3.5.4", "resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.4.tgz", "integrity": "sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug==", + "license": "MIT", "dependencies": { "@paralleldrive/cuid2": "^2.2.2", "dezalgo": "^1.0.4", @@ -5953,11 +6219,18 @@ "url": "https://ko-fi.com/tunnckoCore/commissions" } }, + "node_modules/@bitgo-beta/sdk-core/node_modules/fp-ts": { + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/fp-ts/-/fp-ts-2.16.2.tgz", + "integrity": "sha512-CkqAjnIKFqvo3sCyoBTqgJvF+bHrSik584S9nhTjtBESLx26cbtVMR/T9a6ApChOcSDAaM3JydDmWDUn4EEXng==", + "license": "MIT" + }, "node_modules/@bitgo-beta/sdk-core/node_modules/io-ts": { "name": "@bitgo-forks/io-ts", "version": "2.1.4", "resolved": "https://registry.npmjs.org/@bitgo-forks/io-ts/-/io-ts-2.1.4.tgz", "integrity": "sha512-jCt3WPfDM+wM0SJMGJkY0TS6JmaQ78ATAYtsppJYJfts8geOS/N/UftwAROXwv6azKAMz8uo163t6dWWwfsYug==", + "license": "MIT", "peerDependencies": { "fp-ts": "^2.0.0" } @@ -5967,6 +6240,7 @@ "resolved": "https://registry.npmjs.org/keccak/-/keccak-3.0.3.tgz", "integrity": "sha512-JZrLIAJWuZxKbCilMpNz5Vj7Vtb4scDG3dMXLOsbzBmQGyjwE61BbW7bJkfKKCShXiQZt3T6sBgALRtmd+nZaQ==", "hasInstallScript": true, + "license": "MIT", "dependencies": { "node-addon-api": "^2.0.0", "node-gyp-build": "^4.2.0", @@ -5980,6 +6254,7 @@ "version": "2.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "license": "MIT", "bin": { "mime": "cli.js" }, @@ -5990,13 +6265,15 @@ "node_modules/@bitgo-beta/sdk-core/node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" }, "node_modules/@bitgo-beta/sdk-core/node_modules/superagent": { "version": "9.0.2", "resolved": "https://registry.npmjs.org/superagent/-/superagent-9.0.2.tgz", "integrity": "sha512-xuW7dzkUpcJq7QnhOsnNUgtYp3xRwpt2F7abdRYIpCsAt0hhUqia0EdxyXZQQpNmGtsCzYHryaKSV3q3GJnq7w==", "deprecated": "Please upgrade to superagent v10.2.2+, see release notes at https://github.com/forwardemail/superagent/releases/tag/v10.2.2 - maintenance is supported by Forward Email @ https://forwardemail.net", + "license": "MIT", "dependencies": { "component-emitter": "^1.3.0", "cookiejar": "^2.1.4", @@ -6016,6 +6293,7 @@ "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", "dependencies": { "ms": "^2.1.3" }, @@ -6036,18 +6314,26 @@ "https://github.com/sponsors/broofa", "https://github.com/sponsors/ctavan" ], + "license": "MIT", "bin": { "uuid": "dist/esm/bin/uuid" } }, "node_modules/@bitgo-beta/sdk-hmac": { - "version": "1.0.1-beta.1303", - "resolved": "https://registry.npmjs.org/@bitgo-beta/sdk-hmac/-/sdk-hmac-1.0.1-beta.1303.tgz", - "integrity": "sha512-Et22Jjo/fqpF2z9o6bG4yTmM9ybqzaJ/CXdcOa98hnYDtz0FykdoRVNPOQdw/GJLUKikUmbbbHGcIJOxocAzAw==", + "version": "1.0.1-beta.1307", + "resolved": "https://registry.npmjs.org/@bitgo-beta/sdk-hmac/-/sdk-hmac-1.0.1-beta.1307.tgz", + "integrity": "sha512-RxEb9jLbqy5dvINjSnLwDyvmcM0x4f7btUeLGJmaNzWBVbDplhPLgJ5roeuVltIm5na+KitY6cV7nISe40UQyA==", + "license": "MIT", "dependencies": { - "@bitgo-beta/sjcl": "1.0.2-beta.2176" + "@bitgo-beta/sjcl": "1.0.2-beta.2180" } }, + "node_modules/@bitgo-beta/sdk-hmac/node_modules/@bitgo-beta/sjcl": { + "version": "1.0.2-beta.2180", + "resolved": "https://registry.npmjs.org/@bitgo-beta/sjcl/-/sjcl-1.0.2-beta.2180.tgz", + "integrity": "sha512-K4u6YXFPgQHQ9GmPKsNT0nc9Q+zc4VYNDMEwjK2W3QikpsCaaWalA5TQjhjl59TijXRuSNxcQdRJvaaY8YbQeg==", + "license": "(BSD-2-Clause OR GPL-2.0-only)" + }, "node_modules/@bitgo-beta/sdk-lib-mpc": { "version": "8.2.0-beta.1924", "resolved": "https://registry.npmjs.org/@bitgo-beta/sdk-lib-mpc/-/sdk-lib-mpc-8.2.0-beta.1924.tgz", @@ -17624,6 +17910,7 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/big.js/-/big.js-3.2.0.tgz", "integrity": "sha512-+hN/Zh2D08Mx65pZ/4g5bsmNiZUuChDiQfTUQ7qJr4/kuopCr88xZsAXv6mBoZEsUI4OuGHlX59qE94K2mMW8Q==", + "license": "MIT", "engines": { "node": "*" } @@ -28361,7 +28648,8 @@ "version": "0.7.2", "resolved": "https://registry.npmjs.org/noble-bls12-381/-/noble-bls12-381-0.7.2.tgz", "integrity": "sha512-Z5isbU6opuWPL3dxsGqO5BdOE8WP1XUM7HFIn/xeE5pATTnml/PEIy4MFQQrktHiitkuJdsCDtzEOnS9eIpC3Q==", - "deprecated": "Switch to \"@noble/curves\" for security updates" + "deprecated": "Switch to \"@noble/curves\" for security updates", + "license": "MIT" }, "node_modules/nock": { "version": "13.5.6", diff --git a/package.json b/package.json index a0b8cd27..ad82d1bc 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ "@bitgo-beta/abstract-cosmos": "1.0.1-beta.1905", "@bitgo-beta/abstract-eth": "1.0.2-beta.2150", "@bitgo-beta/abstract-utxo": "1.1.1-beta.2153", - "@bitgo-beta/sdk-api": "1.10.1-beta.1920", + "@bitgo-beta/sdk-api": "1.10.1-beta.1923", "@bitgo-beta/sdk-coin-ada": "2.3.14-beta.1918", "@bitgo-beta/sdk-coin-algo": "2.8.9-beta.398", "@bitgo-beta/sdk-coin-apt": "1.0.1-beta.1360", @@ -101,7 +101,7 @@ "@bitgo-beta/sdk-coin-zec": "1.1.1-beta.2144", "@bitgo-beta/sdk-coin-zeta": "1.0.1-beta.1836", "@bitgo-beta/sdk-coin-zketh": "1.0.1-beta.1700", - "@bitgo-beta/sdk-core": "8.2.1-beta.1925", + "@bitgo-beta/sdk-core": "8.2.1-beta.1928", "@bitgo-beta/sdk-lib-mpc": "8.2.0-beta.1924", "@bitgo-beta/statics": "15.1.1-beta.1939", "@bitgo/wasm-miniscript": "2.0.0-beta.7", @@ -122,7 +122,7 @@ "zod": "^3.25.48" }, "overrides": { - "@bitgo-beta/sdk-core": "8.2.1-beta.1925", + "@bitgo-beta/sdk-core": "8.2.1-beta.1928", "@bitgo-beta/statics": "15.1.1-beta.1939", "elliptic": "^6.6.1", "expo": "^48.0.0", diff --git a/src/__tests__/api/master/generateWallet.test.ts b/src/__tests__/api/master/generateWallet.test.ts index 032de995..8a01d81f 100644 --- a/src/__tests__/api/master/generateWallet.test.ts +++ b/src/__tests__/api/master/generateWallet.test.ts @@ -6,7 +6,7 @@ import nock from 'nock'; import sinon from 'sinon'; import { app as expressApp } from '../../../masterBitGoExpressApp'; import { AppMode, MasterExpressConfig, TlsMode } from '../../../shared/types'; -import { Environments } from '@bitgo-beta/sdk-core'; +import { BitgoMpcGpgPubKeys, Environments } from '@bitgo-beta/sdk-core'; import { BitGoAPI } from '@bitgo-beta/sdk-api'; import * as middleware from '../../../shared/middleware'; import { BitGoRequest } from '../../../types/request'; @@ -64,6 +64,10 @@ describe('POST /api/v1/:coin/advancedwallet/generate', () => { const ecdsaCoin = 'hteth'; const accessToken = 'test-token'; + // The SDK parses BitGo's GPG keys and rejects anything that is not a known BitGo MPC key + const bitgoMpcv1GpgKey = BitgoMpcGpgPubKeys.getBitgoMpcGpgPubKey('test', 'onprem', 'mpcv1'); + const bitgoMpcv2GpgKey = BitgoMpcGpgPubKeys.getBitgoMpcGpgPubKey('test', 'onprem', 'mpcv2'); + // Valid BIP32 extended public keys required by the SDK's isValidPub check const validUserPub = 'xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8'; @@ -352,7 +356,7 @@ describe('POST /api/v1/:coin/advancedwallet/generate', () => { nock(bitgoApiUrl) .persist() .get('/api/v1/client/constants') - .reply(200, { constants: { mpc: { bitgoPublicKey: 'test-bitgo-public-key' } } }); + .reply(200, { constants: { mpc: { bitgoPublicKey: bitgoMpcv1GpgKey } } }); const backupBitgo = new BitGoAPI({ env: 'test' }); const configWithBackup = makeConfig({ advancedWalletManagerBackupUrl: backupAwmUrl }); @@ -369,7 +373,7 @@ describe('POST /api/v1/:coin/advancedwallet/generate', () => { const userInitNock = nock(advancedWalletManagerUrl) .post(`/api/${eddsaCoin}/mpc/key/initialize`, { source: 'user', - bitgoGpgPub: 'test-bitgo-public-key', + bitgoGpgPub: bitgoMpcv1GpgKey, }) .reply(200, { encryptedDataKey: 'key', @@ -389,7 +393,7 @@ describe('POST /api/v1/:coin/advancedwallet/generate', () => { const backupInitNock = nock(backupAwmUrl) .post(`/api/${eddsaCoin}/mpc/key/initialize`, { source: 'backup', - bitgoGpgPub: 'test-bitgo-public-key', + bitgoGpgPub: bitgoMpcv1GpgKey, counterPartyGpgPub: 'user-key', }) .reply(200, { @@ -538,7 +542,7 @@ describe('POST /api/v1/:coin/advancedwallet/generate', () => { .post(`/api/v2/${eddsaCoin}/key`, { commonKeychain: 'commonKeychain', source: 'user', - type: 'tss', + keyType: 'tss', }) .reply(200, { id: 'user-key-id', @@ -602,7 +606,7 @@ describe('POST /api/v1/:coin/advancedwallet/generate', () => { const addBackupKeyNock = nock(bitgoApiUrl) .post(`/api/v2/${eddsaCoin}/key`, { source: 'backup', - type: 'tss', + keyType: 'tss', commonKeychain: 'commonKeychain', }) .reply(200, { @@ -651,12 +655,12 @@ describe('POST /api/v1/:coin/advancedwallet/generate', () => { nock(bitgoApiUrl) .persist() .get('/api/v1/client/constants') - .reply(200, { constants: { mpc: { bitgoPublicKey: 'test-bitgo-public-key' } } }); + .reply(200, { constants: { mpc: { bitgoPublicKey: bitgoMpcv1GpgKey } } }); const userInitNock = nock(advancedWalletManagerUrl) .post(`/api/${eddsaCoin}/mpc/key/initialize`, { source: 'user', - bitgoGpgPub: 'test-bitgo-public-key', + bitgoGpgPub: bitgoMpcv1GpgKey, }) .reply(200, { encryptedDataKey: 'key', @@ -675,7 +679,7 @@ describe('POST /api/v1/:coin/advancedwallet/generate', () => { const backupInitNock = nock(advancedWalletManagerUrl) .post(`/api/${eddsaCoin}/mpc/key/initialize`, { source: 'backup', - bitgoGpgPub: 'test-bitgo-public-key', + bitgoGpgPub: bitgoMpcv1GpgKey, counterPartyGpgPub: 'user-key', }) .reply(200, { @@ -822,7 +826,7 @@ describe('POST /api/v1/:coin/advancedwallet/generate', () => { .post(`/api/v2/${eddsaCoin}/key`, { commonKeychain: 'commonKeychain', source: 'user', - type: 'tss', + keyType: 'tss', }) .reply(200, { id: 'id', @@ -884,7 +888,7 @@ describe('POST /api/v1/:coin/advancedwallet/generate', () => { const addBackupKeyNock = nock(bitgoApiUrl) .post(`/api/v2/${eddsaCoin}/key`, { source: 'backup', - type: 'tss', + keyType: 'tss', commonKeychain: 'commonKeychain', }) .reply(200, { @@ -899,7 +903,6 @@ describe('POST /api/v1/:coin/advancedwallet/generate', () => { label: 'test_wallet', enterprise: 'test_enterprise', multisigType: 'tss', - coin: eddsaCoin, m: 2, n: 3, keys: ['id', 'id', 'id'], @@ -990,7 +993,18 @@ describe('POST /api/v1/:coin/advancedwallet/generate', () => { nock(bitgoApiUrl) .persist() .get('/api/v1/client/constants') - .reply(200, { constants: { mpc: { bitgoMPCv2PublicKey: 'test-bitgo-public-key' } } }); + .reply(200, { constants: { mpc: { bitgoMPCv2PublicKey: bitgoMpcv2GpgKey } } }); + // The SDK resolves BitGo's MPCv2 GPG key and the enterprise's wallet creation settings itself + const tssPubkeyNock = nock(bitgoApiUrl) + .get(`/api/v2/${ecdsaCoin}/tss/pubkey`) + .query({ enterpriseId: 'test-enterprise' }) + .reply(200, { mpcv2PublicKey: bitgoMpcv2GpgKey }); + + const tssSettingsNock = nock(bitgoApiUrl) + .get('/api/v2/tss/settings') + .reply(200, { + coinSettings: { eth: { walletCreationSettings: { multiSigTypeVersion: 'MPCv2' } } }, + }); const backupBitgo = new BitGoAPI({ env: 'test' }); const configWithBackup = makeConfig({ advancedWalletManagerBackupUrl: backupAwmUrl }); @@ -1027,7 +1041,7 @@ describe('POST /api/v1/:coin/advancedwallet/generate', () => { encryptedDataKey: 'key', encryptedData: 'data', round: 1, - bitgoGpgPub: 'test-bitgo-public-key', + bitgoGpgPub: bitgoMpcv2GpgKey, counterPartyGpgPub: 'test-backup-public-key', }) .reply(200, { @@ -1046,7 +1060,7 @@ describe('POST /api/v1/:coin/advancedwallet/generate', () => { encryptedDataKey: 'key', encryptedData: 'data', round: 1, - bitgoGpgPub: 'test-bitgo-public-key', + bitgoGpgPub: bitgoMpcv2GpgKey, counterPartyGpgPub: 'test-user-public-key', }) .reply(200, { @@ -1527,7 +1541,7 @@ describe('POST /api/v1/:coin/advancedwallet/generate', () => { .post(`/api/v2/${ecdsaCoin}/key`, { commonKeychain: 'commonKeychain', source: 'user', - type: 'tss', + keyType: 'tss', isMPCv2: true, }) .reply(200, { id: 'user-key-id', source: 'user', type: 'tss' }); @@ -1536,7 +1550,7 @@ describe('POST /api/v1/:coin/advancedwallet/generate', () => { .post(`/api/v2/${ecdsaCoin}/key`, { commonKeychain: 'commonKeychain', source: 'backup', - type: 'tss', + keyType: 'tss', isMPCv2: true, }) .reply(200, { id: 'backup-key-id', source: 'backup', type: 'tss' }); @@ -1545,7 +1559,7 @@ describe('POST /api/v1/:coin/advancedwallet/generate', () => { .post(`/api/v2/${ecdsaCoin}/key`, { commonKeychain: 'commonKeychain', source: 'bitgo', - type: 'tss', + keyType: 'tss', isMPCv2: true, }) .reply(200, { @@ -1595,6 +1609,8 @@ describe('POST /api/v1/:coin/advancedwallet/generate', () => { backupRound4Nock.done(); backupFinalizeNock.done(); // Verify BitGo API calls + tssPubkeyNock.done(); + tssSettingsNock.done(); bitgoRound1And2Nock.done(); bitgoRound3Nock.done(); bitgoRound4Nock.done(); @@ -1608,7 +1624,18 @@ describe('POST /api/v1/:coin/advancedwallet/generate', () => { nock(bitgoApiUrl) .persist() .get('/api/v1/client/constants') - .reply(200, { constants: { mpc: { bitgoMPCv2PublicKey: 'test-bitgo-public-key' } } }); + .reply(200, { constants: { mpc: { bitgoMPCv2PublicKey: bitgoMpcv2GpgKey } } }); + // The SDK resolves BitGo's MPCv2 GPG key and the enterprise's wallet creation settings itself + const tssPubkeyNock = nock(bitgoApiUrl) + .get(`/api/v2/${ecdsaCoin}/tss/pubkey`) + .query({ enterpriseId: 'test-enterprise' }) + .reply(200, { mpcv2PublicKey: bitgoMpcv2GpgKey }); + + const tssSettingsNock = nock(bitgoApiUrl) + .get('/api/v2/tss/settings') + .reply(200, { + coinSettings: { eth: { walletCreationSettings: { multiSigTypeVersion: 'MPCv2' } } }, + }); // init round const userInitNock = nock(advancedWalletManagerUrl) @@ -1637,7 +1664,7 @@ describe('POST /api/v1/:coin/advancedwallet/generate', () => { encryptedDataKey: 'key', encryptedData: 'data', round: 1, - bitgoGpgPub: 'test-bitgo-public-key', + bitgoGpgPub: bitgoMpcv2GpgKey, counterPartyGpgPub: 'test-backup-public-key', }) .reply(200, { @@ -1659,7 +1686,7 @@ describe('POST /api/v1/:coin/advancedwallet/generate', () => { encryptedDataKey: 'key', encryptedData: 'data', round: 1, - bitgoGpgPub: 'test-bitgo-public-key', + bitgoGpgPub: bitgoMpcv2GpgKey, counterPartyGpgPub: 'test-user-public-key', }) .reply(200, { @@ -2143,7 +2170,7 @@ describe('POST /api/v1/:coin/advancedwallet/generate', () => { .post(`/api/v2/${ecdsaCoin}/key`, { commonKeychain: 'commonKeychain', source: 'user', - type: 'tss', + keyType: 'tss', isMPCv2: true, }) .reply(200, { id: 'user-key-id', source: 'user', type: 'tss' }); @@ -2152,7 +2179,7 @@ describe('POST /api/v1/:coin/advancedwallet/generate', () => { .post(`/api/v2/${ecdsaCoin}/key`, { commonKeychain: 'commonKeychain', source: 'backup', - type: 'tss', + keyType: 'tss', isMPCv2: true, }) .reply(200, { id: 'backup-key-id', source: 'backup', type: 'tss' }); @@ -2161,7 +2188,7 @@ describe('POST /api/v1/:coin/advancedwallet/generate', () => { .post(`/api/v2/${ecdsaCoin}/key`, { commonKeychain: 'commonKeychain', source: 'bitgo', - type: 'tss', + keyType: 'tss', isMPCv2: true, }) .reply(200, { @@ -2179,10 +2206,10 @@ describe('POST /api/v1/:coin/advancedwallet/generate', () => { label: 'test-wallet', // ? enterprise: 'test-enterprise', multisigType: 'tss', - coin: ecdsaCoin, m: 2, n: 3, keys: ['user-key-id', 'backup-key-id', 'bitgo-key-id'], + walletVersion: 5, type: 'advanced', }) .reply(200, { @@ -2254,6 +2281,8 @@ describe('POST /api/v1/:coin/advancedwallet/generate', () => { backupInitNock.done(); userRound1Nock.done(); backupRound1Nock.done(); + tssPubkeyNock.done(); + tssSettingsNock.done(); bitgoRound1And2Nock.done(); userRound2Nock.done(); backupRound2Nock.done(); diff --git a/src/__tests__/api/master/walletGenerationCallbacks.test.ts b/src/__tests__/api/master/walletGenerationCallbacks.test.ts index 5f81aaca..1c93bea6 100644 --- a/src/__tests__/api/master/walletGenerationCallbacks.test.ts +++ b/src/__tests__/api/master/walletGenerationCallbacks.test.ts @@ -6,7 +6,11 @@ import { createAwmClient, AdvancedWalletManagerClient, } from '../../../masterBitgoExpress/clients/advancedWalletManagerClient'; -import { createOnchainKeyGenCallback } from '../../../masterBitgoExpress/handlers/walletGenerationCallbacks'; +import { + createEcdsaMPCv2KeyGenCallbacks, + createEddsaKeyGenCallbacks, + createOnchainKeyGenCallback, +} from '../../../masterBitgoExpress/handlers/walletGenerationCallbacks'; import { AppMode, KeySource, MasterExpressConfig, TlsMode } from '../../../shared/types'; import { DEFAULT_ASYNC_MODE_CONFIG } from './testUtils'; @@ -150,4 +154,473 @@ describe('walletGenerationCallbacks', () => { }).should.be.rejectedWith('Unexpected key source for onchain key generation: bitgo'); }); }); + + describe('createEcdsaMPCv2KeyGenCallbacks', () => { + const ecdsaCoin = 'hteth'; + const bitgoGpgPub = 'bitgo-mpcv2-gpg-pub'; + const state = { encryptedData: 'data', encryptedDataKey: 'key' }; + const sessionId = 'session-id'; + + // AWM message payloads, keyed by the party that produced them + const broadcast = (label: string) => ({ + from: 0, + payload: { message: `broadcast-${label}`, signature: `sig-${label}` }, + }); + const p2p = (label: string) => ({ + from: 0, + to: 2, + payload: { encryptedMessage: `p2p-${label}`, signature: `sig-${label}` }, + commitment: `commitment-${label}`, + }); + + // BitGo messages, as the SDK hands them to the callbacks, and the AWM's expected shape for them + const bitgoMsg1 = { from: 2 as const, message: 'bitgo-1', signature: 'bitgo-sig-1' }; + const bitgoMsg4 = { from: 2 as const, message: 'bitgo-4', signature: 'bitgo-sig-4' }; + const bitgoP2p = (to: 0 | 1, round: number) => ({ + from: 2 as const, + to, + encryptedMessage: `bitgo-to-${to}-${round}`, + signature: `bitgo-sig-to-${to}-${round}`, + }); + const formattedBroadcast = (msg: { from: number; message: string; signature: string }) => ({ + from: msg.from, + payload: { message: msg.message, signature: msg.signature }, + }); + const formattedP2p = ( + msg: { from: number; to: number; encryptedMessage: string; signature: string }, + commitment?: string, + ) => ({ + from: msg.from, + to: msg.to, + payload: { encryptedMessage: msg.encryptedMessage, signature: msg.signature }, + ...(commitment ? { commitment } : {}), + }); + + function nockRound( + url: string, + source: KeySource, + round: number, + messages: nock.DataMatcherMap, + ) { + return nock(url).post(`/api/${ecdsaCoin}/mpcv2/round`, { + source, + ...state, + round, + ...messages, + }); + } + + let callbacks: ReturnType; + + beforeEach(() => { + const config = makeConfig({ advancedWalletManagerBackupUrl: backupAwmUrl }); + callbacks = createEcdsaMPCv2KeyGenCallbacks( + createAwmClient(config, ecdsaCoin)!, + createAwmBackupClient(config, ecdsaCoin)!, + ); + }); + + /** Runs initialize plus round 1 on both AWMs; the returned nock scopes must be satisfied. */ + async function initialize() { + const userInitNock = nock(advancedWalletManagerUrl) + .post(`/api/${ecdsaCoin}/mpcv2/initialize`, { source: KeySource.USER }) + .reply(200, { ...state, gpgPub: 'user-gpg-pub' }); + const backupInitNock = nock(backupAwmUrl) + .post(`/api/${ecdsaCoin}/mpcv2/initialize`, { source: KeySource.BACKUP }) + .reply(200, { ...state, gpgPub: 'backup-gpg-pub' }); + const userRound1Nock = nockRound(advancedWalletManagerUrl, KeySource.USER, 1, { + bitgoGpgPub, + counterPartyGpgPub: 'backup-gpg-pub', + }).reply(200, { ...state, round: 2, broadcastMessage: broadcast('user-1') }); + const backupRound1Nock = nockRound(backupAwmUrl, KeySource.BACKUP, 1, { + bitgoGpgPub, + counterPartyGpgPub: 'user-gpg-pub', + }).reply(200, { ...state, round: 2, broadcastMessage: broadcast('backup-1') }); + + const result = await callbacks.initializeCallback({ + enterprise: 'test-enterprise', + bitgoPublicGpgKey: bitgoGpgPub, + }); + + [userInitNock, backupInitNock, userRound1Nock, backupRound1Nock].forEach((n) => n.done()); + return result; + } + + /** Runs AWM rounds 2 and 3 on both AWMs. */ + async function round2() { + const userRound2Nock = nockRound(advancedWalletManagerUrl, KeySource.USER, 2, { + broadcastMessages: { + bitgo: formattedBroadcast(bitgoMsg1), + counterParty: broadcast('backup-1'), + }, + }).reply(200, { + ...state, + round: 3, + p2pMessages: { bitgo: p2p('user-2-bitgo'), counterParty: p2p('user-2-backup') }, + }); + const backupRound2Nock = nockRound(backupAwmUrl, KeySource.BACKUP, 2, { + broadcastMessages: { + bitgo: formattedBroadcast(bitgoMsg1), + counterParty: broadcast('user-1'), + }, + }).reply(200, { + ...state, + round: 3, + p2pMessages: { bitgo: p2p('backup-2-bitgo'), counterParty: p2p('backup-2-user') }, + }); + const userRound3Nock = nockRound(advancedWalletManagerUrl, KeySource.USER, 3, { + p2pMessages: { + bitgo: formattedP2p(bitgoP2p(0, 2)), + counterParty: p2p('backup-2-user'), + }, + }).reply(200, { + ...state, + round: 4, + p2pMessages: { bitgo: p2p('user-3-bitgo'), counterParty: p2p('user-3-backup') }, + }); + const backupRound3Nock = nockRound(backupAwmUrl, KeySource.BACKUP, 3, { + p2pMessages: { + bitgo: formattedP2p(bitgoP2p(1, 2)), + counterParty: p2p('user-2-backup'), + }, + }).reply(200, { + ...state, + round: 4, + p2pMessages: { bitgo: p2p('backup-3-bitgo'), counterParty: p2p('backup-3-user') }, + }); + + const result = await callbacks.round2Callback({ + sessionId, + bitgoMsg1, + bitgoToUserMsg2: bitgoP2p(0, 2), + bitgoToBackupMsg2: bitgoP2p(1, 2), + userState: state, + backupState: state, + }); + + [userRound2Nock, backupRound2Nock, userRound3Nock, backupRound3Nock].forEach((n) => n.done()); + return result; + } + + /** Runs AWM round 4 on both AWMs. */ + async function round3() { + const userRound4Nock = nockRound(advancedWalletManagerUrl, KeySource.USER, 4, { + p2pMessages: { + bitgo: formattedP2p(bitgoP2p(0, 3), 'bitgo-commitment-2'), + counterParty: p2p('backup-3-user'), + }, + }).reply(200, { ...state, round: 5, broadcastMessage: broadcast('user-4') }); + const backupRound4Nock = nockRound(backupAwmUrl, KeySource.BACKUP, 4, { + p2pMessages: { + bitgo: formattedP2p(bitgoP2p(1, 3), 'bitgo-commitment-2'), + counterParty: p2p('user-3-backup'), + }, + }).reply(200, { ...state, round: 5, broadcastMessage: broadcast('backup-4') }); + + const result = await callbacks.round3Callback({ + sessionId, + bitgoCommitment2: 'bitgo-commitment-2', + bitgoToUserMsg3: bitgoP2p(0, 3), + bitgoToBackupMsg3: bitgoP2p(1, 3), + userState: state, + backupState: state, + }); + + [userRound4Nock, backupRound4Nock].forEach((n) => n.done()); + return result; + } + + function nockFinalize( + url: string, + source: KeySource, + counterPartyBroadcast: nock.DataMatcherMap, + ) { + return nock(url).post(`/api/${ecdsaCoin}/mpcv2/finalize`, { + source, + ...state, + broadcastMessages: { + bitgo: formattedBroadcast(bitgoMsg4), + counterParty: counterPartyBroadcast, + }, + bitgoCommonKeychain: 'commonKeychain', + }); + } + + it('should relay round 1 messages and GPG keys from both AWMs to the SDK', async () => { + const result = await initialize(); + + result.userGpgPublicKey.should.equal('user-gpg-pub'); + result.backupGpgPublicKey.should.equal('backup-gpg-pub'); + result.round1Messages.should.eql({ + broadcastMessages: [broadcast('user-1'), broadcast('backup-1')], + p2pMessages: [], + }); + result.userState.should.eql(state); + result.backupState.should.eql(state); + }); + + it('should return the round 2 BitGo p2p messages after running AWM rounds 2 and 3', async () => { + await initialize(); + const result = await round2(); + + result.round2Messages.should.eql({ + broadcastMessages: [], + p2pMessages: [p2p('user-2-bitgo'), p2p('backup-2-bitgo')], + }); + }); + + it('should return the round 3 p2p and round 4 broadcast messages', async () => { + await initialize(); + await round2(); + const result = await round3(); + + result.round3Messages.should.eql({ + broadcastMessages: [broadcast('user-4'), broadcast('backup-4')], + p2pMessages: [p2p('user-3-bitgo'), p2p('backup-3-bitgo')], + }); + }); + + it('should finalize both AWMs and return the agreed common keychain', async () => { + await initialize(); + await round2(); + await round3(); + + const userFinalizeNock = nockFinalize( + advancedWalletManagerUrl, + KeySource.USER, + broadcast('backup-4'), + ).reply(200, { source: KeySource.USER, commonKeychain: 'commonKeychain' }); + const backupFinalizeNock = nockFinalize( + backupAwmUrl, + KeySource.BACKUP, + broadcast('user-4'), + ).reply(200, { source: KeySource.BACKUP, commonKeychain: 'commonKeychain' }); + + const result = await callbacks.finalizeCallback({ + sessionId, + bitgoMsg4, + bitgoCommonKeychain: 'commonKeychain', + userState: state, + backupState: state, + }); + + result.commonKeychain.should.equal('commonKeychain'); + userFinalizeNock.done(); + backupFinalizeNock.done(); + }); + + it('should reject when the user and backup common keychains do not match', async () => { + await initialize(); + await round2(); + await round3(); + + nockFinalize(advancedWalletManagerUrl, KeySource.USER, broadcast('backup-4')).reply(200, { + source: KeySource.USER, + commonKeychain: 'commonKeychain', + }); + nockFinalize(backupAwmUrl, KeySource.BACKUP, broadcast('user-4')).reply(200, { + source: KeySource.BACKUP, + commonKeychain: 'otherCommonKeychain', + }); + + await callbacks + .finalizeCallback({ + sessionId, + bitgoMsg4, + bitgoCommonKeychain: 'commonKeychain', + userState: state, + backupState: state, + }) + .should.be.rejectedWith(/User and backup common keychains do not match/); + }); + + it('should reject when an AWM round 1 response has no broadcast message', async () => { + nock(advancedWalletManagerUrl) + .post(`/api/${ecdsaCoin}/mpcv2/initialize`, { source: KeySource.USER }) + .reply(200, { ...state, gpgPub: 'user-gpg-pub' }); + nock(backupAwmUrl) + .post(`/api/${ecdsaCoin}/mpcv2/initialize`, { source: KeySource.BACKUP }) + .reply(200, { ...state, gpgPub: 'backup-gpg-pub' }); + nockRound(advancedWalletManagerUrl, KeySource.USER, 1, { + bitgoGpgPub, + counterPartyGpgPub: 'backup-gpg-pub', + }).reply(200, { ...state, round: 2 }); + nockRound(backupAwmUrl, KeySource.BACKUP, 1, { + bitgoGpgPub, + counterPartyGpgPub: 'user-gpg-pub', + }).reply(200, { ...state, round: 2, broadcastMessage: broadcast('backup-1') }); + + await callbacks + .initializeCallback({ enterprise: 'test-enterprise', bitgoPublicGpgKey: bitgoGpgPub }) + .should.be.rejectedWith('Missing broadcast message in user round 1 response'); + }); + }); + + describe('createEddsaKeyGenCallbacks', () => { + const eddsaCoin = 'tsol'; + const bitgoGpgPub = 'bitgo-mpcv1-gpg-pub'; + const state = { encryptedData: 'data', encryptedDataKey: 'key' }; + + const keyShare = (from: KeySource, to: KeySource) => ({ + from, + to, + publicShare: `public-${from}`, + privateShare: `private-${from}-to-${to}`, + privateShareProof: 'proof', + vssProof: 'proof', + gpgKey: `${from}-gpg-key`, + }); + + const bitgoKeychain = { + id: 'bitgo-key-id', + source: KeySource.BITGO, + type: 'tss' as const, + commonKeychain: 'commonKeychain', + keyShares: [keyShare(KeySource.BITGO, KeySource.USER)], + }; + + let callbacks: ReturnType; + + beforeEach(() => { + const config = makeConfig({ advancedWalletManagerBackupUrl: backupAwmUrl }); + callbacks = createEddsaKeyGenCallbacks( + createAwmClient(config, eddsaCoin)!, + createAwmBackupClient(config, eddsaCoin)!, + ); + }); + + it('should initialize both AWMs, passing the user GPG key to the backup AWM', async () => { + const userInitNock = nock(advancedWalletManagerUrl) + .post(`/api/${eddsaCoin}/mpc/key/initialize`, { + source: KeySource.USER, + bitgoGpgPub, + }) + .reply(200, { ...state, bitgoPayload: keyShare(KeySource.USER, KeySource.BITGO) }); + const backupInitNock = nock(backupAwmUrl) + .post(`/api/${eddsaCoin}/mpc/key/initialize`, { + source: KeySource.BACKUP, + bitgoGpgPub, + counterPartyGpgPub: 'user-gpg-key', + }) + .reply(200, { + ...state, + bitgoPayload: keyShare(KeySource.BACKUP, KeySource.BITGO), + counterPartyKeyShare: keyShare(KeySource.BACKUP, KeySource.USER), + }); + + const result = await callbacks.initializeCallback({ + enterprise: 'test-enterprise', + bitgoPublicGpgKey: bitgoGpgPub, + }); + + result.userGpgPublicKey.should.equal('user-gpg-key'); + result.backupGpgPublicKey.should.equal('backup-gpg-key'); + result.userToBitgoKeyShare.should.eql(keyShare(KeySource.USER, KeySource.BITGO)); + result.backupToBitgoKeyShare.should.eql(keyShare(KeySource.BACKUP, KeySource.BITGO)); + result.backupToUserCounterPartyKeyShare.should.eql( + keyShare(KeySource.BACKUP, KeySource.USER), + ); + result.userState.should.eql(state); + result.backupState.should.eql(state); + userInitNock.done(); + backupInitNock.done(); + }); + + it('should reject when the backup AWM does not return a key share for the user', async () => { + nock(advancedWalletManagerUrl) + .post(`/api/${eddsaCoin}/mpc/key/initialize`) + .reply(200, { ...state, bitgoPayload: keyShare(KeySource.USER, KeySource.BITGO) }); + nock(backupAwmUrl) + .post(`/api/${eddsaCoin}/mpc/key/initialize`) + .reply(200, { ...state, bitgoPayload: keyShare(KeySource.BACKUP, KeySource.BITGO) }); + + await callbacks + .initializeCallback({ enterprise: 'test-enterprise', bitgoPublicGpgKey: bitgoGpgPub }) + .should.be.rejectedWith( + 'Backup key share for the user is missing from the initialization response', + ); + }); + + it('should route the user finalize to the primary AWM and restore the key share routing', async () => { + const finalizeNock = nock(advancedWalletManagerUrl) + .post(`/api/${eddsaCoin}/mpc/key/finalize`, { + source: KeySource.USER, + coin: eddsaCoin, + ...state, + bitgoKeyChain: { + ...bitgoKeychain, + verifiedVssProof: true, + isBitGo: true, + isTrust: false, + }, + counterPartyGpgPub: 'backup-gpg-key', + counterPartyKeyShare: keyShare(KeySource.BACKUP, KeySource.USER), + }) + .reply(200, { + source: KeySource.USER, + commonKeychain: 'commonKeychain', + counterpartyKeyShare: keyShare(KeySource.USER, KeySource.BACKUP), + }); + + const result = await callbacks.finalizeCallback({ + source: KeySource.USER, + coin: eddsaCoin, + bitgoKeychain, + counterPartyGPGKey: 'backup-gpg-key', + counterPartyKeyShare: keyShare(KeySource.BACKUP, KeySource.USER), + state, + }); + + result.commonKeychain.should.equal('commonKeychain'); + result.counterpartyKeyShare!.should.eql(keyShare(KeySource.USER, KeySource.BACKUP)); + finalizeNock.done(); + }); + + it('should reject when the user finalize does not return a key share for backup', async () => { + nock(advancedWalletManagerUrl) + .post(`/api/${eddsaCoin}/mpc/key/finalize`) + .reply(200, { source: KeySource.USER, commonKeychain: 'commonKeychain' }); + + await callbacks + .finalizeCallback({ + source: KeySource.USER, + coin: eddsaCoin, + bitgoKeychain, + counterPartyGPGKey: 'backup-gpg-key', + counterPartyKeyShare: keyShare(KeySource.BACKUP, KeySource.USER), + state, + }) + .should.be.rejectedWith('Key share for backup missing from user finalize response'); + }); + + it('should route the backup finalize to the backup AWM', async () => { + const finalizeNock = nock(backupAwmUrl) + .post(`/api/${eddsaCoin}/mpc/key/finalize`, { + source: KeySource.BACKUP, + coin: eddsaCoin, + ...state, + bitgoKeyChain: { + ...bitgoKeychain, + verifiedVssProof: true, + isBitGo: true, + isTrust: false, + }, + counterPartyGpgPub: 'user-gpg-key', + counterPartyKeyShare: keyShare(KeySource.USER, KeySource.BACKUP), + }) + .reply(200, { source: KeySource.BACKUP, commonKeychain: 'commonKeychain' }); + + const result = await callbacks.finalizeCallback({ + source: KeySource.BACKUP, + coin: eddsaCoin, + bitgoKeychain, + counterPartyGPGKey: 'user-gpg-key', + counterPartyKeyShare: keyShare(KeySource.USER, KeySource.BACKUP), + state, + }); + + result.commonKeychain.should.equal('commonKeychain'); + finalizeNock.done(); + }); + }); }); diff --git a/src/masterBitgoExpress/handlers/ecdsa.ts b/src/masterBitgoExpress/handlers/ecdsa.ts index 74e10baa..1b7d5c71 100644 --- a/src/masterBitgoExpress/handlers/ecdsa.ts +++ b/src/masterBitgoExpress/handlers/ecdsa.ts @@ -1,10 +1,8 @@ import { - BaseCoin, BitGoBase, EcdsaMPCv2Utils, IRequestTracer, RequestType, - SupplementGenerateWalletOptions, Wallet, TxRequest, } from '@bitgo-beta/sdk-core'; @@ -102,288 +100,3 @@ export async function signAndSendEcdsaMPCv2FromTxRequest( RequestType.tx, ); } - -interface OrchestrateEcdsaKeyGenParams { - bitgo: BitGoBase; - baseCoin: BaseCoin; - awmClient: AdvancedWalletManagerClient; - awmBackupClient: AdvancedWalletManagerClient; - enterprise: string; - walletParams: SupplementGenerateWalletOptions; -} - -export async function orchestrateEcdsaKeyGen({ - bitgo, - baseCoin, - awmClient, - awmBackupClient, - enterprise, - walletParams, -}: OrchestrateEcdsaKeyGenParams) { - const constants = await bitgo.fetchConstants(); - if (!constants.mpc.bitgoMPCv2PublicKey) { - throw new Error('Unable to create MPCv2 keys - bitgoMPCv2PublicKey is missing in constants'); - } - const ecdsaUtils = new EcdsaMPCv2Utils(bitgo, baseCoin); - - // INITIALIZE ROUND: GENERATE ALL GPG KEYS AND RETRIEVE GPG PUBS FROM ALL PARTIES - const userInitResponse = await awmClient.initEcdsaMpcV2KeyGenMpcV2({ - source: 'user', - }); - if ( - !userInitResponse.gpgPub || - !userInitResponse.encryptedData || - !userInitResponse.encryptedDataKey - ) { - throw new Error('Missing required fields in user init response'); - } - const backupInitResponse = await awmBackupClient.initEcdsaMpcV2KeyGenMpcV2({ - source: 'backup', - }); - if ( - !backupInitResponse.gpgPub || - !backupInitResponse.encryptedData || - !backupInitResponse.encryptedDataKey - ) { - throw new Error('Missing required fields in backup init response'); - } - - // ROUND 1 - const userRound1Promise = awmClient.roundEcdsaMPCv2KeyGen({ - source: 'user', - encryptedData: userInitResponse.encryptedData, - encryptedDataKey: userInitResponse.encryptedDataKey, - round: 1, - bitgoGpgPub: constants.mpc.bitgoMPCv2PublicKey, - counterPartyGpgPub: backupInitResponse.gpgPub, - }); - const backupRound1Promise = awmBackupClient.roundEcdsaMPCv2KeyGen({ - source: 'backup', - encryptedData: backupInitResponse.encryptedData, - encryptedDataKey: backupInitResponse.encryptedDataKey, - round: 1, - bitgoGpgPub: constants.mpc.bitgoMPCv2PublicKey, - counterPartyGpgPub: userInitResponse.gpgPub, - }); - const [userRound1Response, backupRound1Response] = await Promise.all([ - userRound1Promise, - backupRound1Promise, - ]); - if (!userRound1Response.broadcastMessage) { - throw new Error('Missing broadcast message in user round 1 response'); - } - if (!backupRound1Response.broadcastMessage) { - throw new Error('Missing broadcast message in backup round 1 response'); - } - - // ROUND 1 & 2 BitGo - const round1And2BitGoResponse = await ecdsaUtils.sendKeyGenerationRound1( - enterprise, - userInitResponse.gpgPub, - backupInitResponse.gpgPub, - { - broadcastMessages: [ - userRound1Response.broadcastMessage, - backupRound1Response.broadcastMessage, - ], - p2pMessages: [], - }, - ); - const { sessionId, bitgoMsg1, bitgoToUserMsg2, bitgoToBackupMsg2 } = round1And2BitGoResponse; - - // ROUND 2 - const userRound2Promise = awmClient.roundEcdsaMPCv2KeyGen({ - source: 'user', - encryptedData: userRound1Response.encryptedData, - encryptedDataKey: userRound1Response.encryptedDataKey, - round: 2, - broadcastMessages: { - bitgo: ecdsaUtils.formatBitgoBroadcastMessage(bitgoMsg1), - counterParty: backupRound1Response.broadcastMessage, - }, - }); - const backupRound2Promise = awmBackupClient.roundEcdsaMPCv2KeyGen({ - source: 'backup', - encryptedData: backupRound1Response.encryptedData, - encryptedDataKey: backupRound1Response.encryptedDataKey, - round: 2, - broadcastMessages: { - bitgo: ecdsaUtils.formatBitgoBroadcastMessage(bitgoMsg1), - counterParty: userRound1Response.broadcastMessage, - }, - }); - const [userRound2Response, backupRound2Response] = await Promise.all([ - userRound2Promise, - backupRound2Promise, - ]); - if (!userRound2Response.p2pMessages?.bitgo) { - throw new Error('Missing BitGo p2p message in user round 2 response'); - } - if (!backupRound2Response.p2pMessages?.bitgo) { - throw new Error('Missing BitGo p2p message in backup round 2 response'); - } - - // ROUND 3 - const userRound3Promise = awmClient.roundEcdsaMPCv2KeyGen({ - source: 'user', - encryptedData: userRound2Response.encryptedData, - encryptedDataKey: userRound2Response.encryptedDataKey, - round: 3, - p2pMessages: { - bitgo: ecdsaUtils.formatP2PMessage(bitgoToUserMsg2), - counterParty: backupRound2Response.p2pMessages?.counterParty, - }, - }); - const backupRound3Promise = awmBackupClient.roundEcdsaMPCv2KeyGen({ - source: 'backup', - encryptedData: backupRound2Response.encryptedData, - encryptedDataKey: backupRound2Response.encryptedDataKey, - round: 3, - p2pMessages: { - bitgo: ecdsaUtils.formatP2PMessage(bitgoToBackupMsg2), - counterParty: userRound2Response.p2pMessages?.counterParty, - }, - }); - const round3BitGoPromise = ecdsaUtils.sendKeyGenerationRound2(enterprise, sessionId, { - p2pMessages: [ - userRound2Response.p2pMessages?.bitgo, - backupRound2Response.p2pMessages?.bitgo, - ].filter((msg) => msg !== undefined), - broadcastMessages: [], - }); - const [userRound3Response, backupRound3Response, round3BitGoResponse] = await Promise.all([ - userRound3Promise, - backupRound3Promise, - round3BitGoPromise, - ]); - const { - sessionId: sessionIdRound3, - bitgoToUserMsg3, - bitgoToBackupMsg3, - bitgoCommitment2: bitgoCommitment3, - } = round3BitGoResponse; - if (!userRound3Response.p2pMessages?.bitgo) { - throw new Error('Missing BitGo p2p message in user round 3 response'); - } - if (!backupRound3Response.p2pMessages?.bitgo) { - throw new Error('Missing BitGo p2p message in backup round 3 response'); - } - if (sessionId !== sessionIdRound3) { - throw new Error('Round 1 and 2 Session IDs do not match'); - } - - // ROUND 4 - const userRound4Promise = awmClient.roundEcdsaMPCv2KeyGen({ - source: 'user', - encryptedData: userRound3Response.encryptedData, - encryptedDataKey: userRound3Response.encryptedDataKey, - round: 4, - p2pMessages: { - bitgo: ecdsaUtils.formatP2PMessage(bitgoToUserMsg3, bitgoCommitment3), - counterParty: backupRound3Response.p2pMessages?.counterParty, - }, - }); - const backupRound4Promise = awmBackupClient.roundEcdsaMPCv2KeyGen({ - source: 'backup', - encryptedData: backupRound3Response.encryptedData, - encryptedDataKey: backupRound3Response.encryptedDataKey, - round: 4, - p2pMessages: { - bitgo: ecdsaUtils.formatP2PMessage(bitgoToBackupMsg3, bitgoCommitment3), - counterParty: userRound3Response.p2pMessages?.counterParty, - }, - }); - const [userRound4Response, backupRound4Response] = await Promise.all([ - userRound4Promise, - backupRound4Promise, - ]); - if (!userRound4Response.broadcastMessage) { - throw new Error('Missing broadcast message in user round 4 response'); - } - if (!backupRound4Response.broadcastMessage) { - throw new Error('Missing broadcast message in backup round 4 response'); - } - - // FINALIZE - const round4BitGoResponse = await ecdsaUtils.sendKeyGenerationRound3(enterprise, sessionId, { - p2pMessages: [ - userRound3Response.p2pMessages?.bitgo, - backupRound3Response.p2pMessages?.bitgo, - ].filter((msg) => msg !== undefined), - broadcastMessages: [ - userRound4Response.broadcastMessage, - backupRound4Response.broadcastMessage, - ].filter((msg) => msg !== undefined), - }); - const { - sessionId: sessionIdRound4, - bitgoMsg4, - commonKeychain: bitgoCommonKeychain, - } = round4BitGoResponse; - const userFinalizePromise = awmClient.finalizeEcdsaMPCv2KeyGen({ - source: 'user', - encryptedData: userRound4Response.encryptedData, - encryptedDataKey: userRound4Response.encryptedDataKey, - broadcastMessages: { - bitgo: ecdsaUtils.formatBitgoBroadcastMessage(bitgoMsg4), - counterParty: backupRound4Response.broadcastMessage, - }, - bitgoCommonKeychain, - }); - const backupFinalizePromise = awmBackupClient.finalizeEcdsaMPCv2KeyGen({ - source: 'backup', - encryptedData: backupRound4Response.encryptedData, - encryptedDataKey: backupRound4Response.encryptedDataKey, - broadcastMessages: { - bitgo: ecdsaUtils.formatBitgoBroadcastMessage(bitgoMsg4), - counterParty: userRound4Response.broadcastMessage, - }, - bitgoCommonKeychain, - }); - const [userFinalizeResponse, backupFinalizeResponse] = await Promise.all([ - userFinalizePromise, - backupFinalizePromise, - ]); - if (sessionId !== sessionIdRound4) { - throw new Error('Round 4 Session IDs do not match'); - } - if (!userFinalizeResponse.commonKeychain) { - throw new Error('Missing common keychain in user finalize response'); - } - if (!backupFinalizeResponse.commonKeychain) { - throw new Error('Missing common keychain in backup finalize response'); - } - if (userFinalizeResponse.commonKeychain !== backupFinalizeResponse.commonKeychain) { - throw new Error('User and backup common keychains do not match'); - } - if (userFinalizeResponse.commonKeychain !== bitgoCommonKeychain) { - throw new Error('User and BitGo common keychains do not match'); - } - - // CREATE KEYCHAINS - const userMpcKey = await baseCoin.keychains().add({ - commonKeychain: userFinalizeResponse.commonKeychain, - source: 'user', - type: 'tss', - isMPCv2: true, - }); - const backupMpcKey = await baseCoin.keychains().add({ - commonKeychain: backupFinalizeResponse.commonKeychain, - source: 'backup', - type: 'tss', - isMPCv2: true, - }); - const bitgoKeychain = await baseCoin.keychains().add({ - commonKeychain: bitgoCommonKeychain, - source: 'bitgo', - type: 'tss', - isMPCv2: true, - }); - walletParams.keys = [userMpcKey.id, backupMpcKey.id, bitgoKeychain.id]; - const keychains = { - userKeychain: userMpcKey, - backupKeychain: backupMpcKey, - bitgoKeychain, - }; - return { walletParams, keychains }; -} diff --git a/src/masterBitgoExpress/handlers/eddsa.ts b/src/masterBitgoExpress/handlers/eddsa.ts index 5b91eec0..8802d39f 100644 --- a/src/masterBitgoExpress/handlers/eddsa.ts +++ b/src/masterBitgoExpress/handlers/eddsa.ts @@ -3,8 +3,6 @@ import { Wallet, IRequestTracer, EddsaUtils, - BaseCoin, - ApiKeyShare, TxRequest, CommitmentShareRecord, EncryptedSignerShareRecord, @@ -115,119 +113,3 @@ export async function handleEddsaSigning( reqId, ); } - -interface OrchestrateEddsaKeyGenParams { - bitgo: BitGoBase; - baseCoin: BaseCoin; - awmClient: AdvancedWalletManagerClient; - awmBackupClient: AdvancedWalletManagerClient; - enterprise: string; - walletParams: any; -} - -export async function orchestrateEddsaKeyGen({ - bitgo, - baseCoin, - awmClient, - awmBackupClient, - enterprise, - walletParams, -}: OrchestrateEddsaKeyGenParams) { - const constants = await bitgo.fetchConstants(); - if (!constants.mpc.bitgoPublicKey) { - throw new Error('Unable to create MPC keys - bitgoPublicKey is missing in constants'); - } - // Initialize key generation for user and backup - const userInitResponse = await awmClient.initMpcKeyGeneration({ - source: 'user', - bitgoGpgKey: constants.mpc.bitgoPublicKey, - }); - const backupInitResponse = await awmBackupClient.initMpcKeyGeneration({ - source: 'backup', - bitgoGpgKey: constants.mpc.bitgoPublicKey, - userGpgKey: userInitResponse.bitgoPayload.gpgKey, - }); - if (!backupInitResponse.counterPartyKeyShare) { - throw new Error('User key share is missing from initialization response'); - } - // Extract GPG keys based on payload type - const userGPGKey = - userInitResponse.bitgoPayload.from === 'user' - ? userInitResponse.bitgoPayload.gpgKey - : undefined; - const backupGPGKey = - backupInitResponse.bitgoPayload.from === 'backup' - ? backupInitResponse.bitgoPayload.gpgKey - : undefined; - if (!userGPGKey || !backupGPGKey) { - throw new Error('Missing required GPG keys from payloads'); - } - // Create BitGo keychain using the initialization responses - const bitgoKeychain = await baseCoin.keychains().add({ - keyType: 'tss', - source: 'bitgo', - keyShares: [userInitResponse.bitgoPayload, backupInitResponse.bitgoPayload], - enterprise: enterprise, - userGPGPublicKey: userGPGKey, - backupGPGPublicKey: backupGPGKey, - }); - // Finalize user and backup keychains - const userKeychainPromise = await awmClient.finalizeMpcKeyGeneration({ - source: 'user', - coin: baseCoin.getFamily(), - encryptedDataKey: userInitResponse.encryptedDataKey, - encryptedData: userInitResponse.encryptedData, - bitGoKeychain: { - ...bitgoKeychain, - commonKeychain: bitgoKeychain.commonKeychain ?? '', - hsmType: bitgoKeychain.hsmType, - type: 'tss', - source: 'bitgo', - verifiedVssProof: true, - isBitGo: true, - isTrust: false, - keyShares: bitgoKeychain.keyShares as ApiKeyShare[], - }, - counterPartyGPGKey: backupGPGKey, - counterPartyKeyShare: backupInitResponse.counterPartyKeyShare, - }); - if (!userKeychainPromise.counterpartyKeyShare) { - throw new Error('Backup key share is missing from user keychain promise'); - } - const userMpcKey = await baseCoin.keychains().add({ - commonKeychain: userKeychainPromise.commonKeychain, - source: 'user', - type: 'tss', - }); - const backupKeychainPromise = await awmBackupClient.finalizeMpcKeyGeneration({ - source: 'backup', - coin: baseCoin.getFamily(), - encryptedDataKey: backupInitResponse.encryptedDataKey, - encryptedData: backupInitResponse.encryptedData, - bitGoKeychain: { - ...bitgoKeychain, - commonKeychain: bitgoKeychain.commonKeychain ?? '', - hsmType: bitgoKeychain.hsmType, - type: 'tss', - source: 'bitgo', - verifiedVssProof: true, - isBitGo: true, - isTrust: false, - keyShares: bitgoKeychain.keyShares as ApiKeyShare[], - }, - counterPartyGPGKey: userGPGKey, - counterPartyKeyShare: userKeychainPromise.counterpartyKeyShare, - }); - const backupMpcKey = await baseCoin.keychains().add({ - commonKeychain: backupKeychainPromise.commonKeychain, - source: 'backup', - type: 'tss', - }); - const keychains = { - userKeychain: userMpcKey, - backupKeychain: backupMpcKey, - bitgoKeychain, - }; - walletParams.keys = [userMpcKey.id, backupMpcKey.id, bitgoKeychain.id]; - return { walletParams, keychains }; -} diff --git a/src/masterBitgoExpress/handlers/handleGenerateWallet.ts b/src/masterBitgoExpress/handlers/handleGenerateWallet.ts index 6cd56afd..058a4331 100644 --- a/src/masterBitgoExpress/handlers/handleGenerateWallet.ts +++ b/src/masterBitgoExpress/handlers/handleGenerateWallet.ts @@ -1,19 +1,15 @@ -import { - RequestTracer, - SupplementGenerateWalletOptions, - Wallet, - WalletWithKeychains, -} from '@bitgo-beta/sdk-core'; -import _ from 'lodash'; +import assert from 'assert'; +import { GenerateWalletWithExternalSignerOptions, MPCAlgorithm } from '@bitgo-beta/sdk-core'; import { MasterApiSpecRouteRequest } from '../routers/masterBitGoExpressApiSpec'; -import { orchestrateEcdsaKeyGen } from './ecdsa'; -import { orchestrateEddsaKeyGen } from './eddsa'; import coinFactory from '../../shared/coinFactory'; import { BadRequestError } from '../../shared/errors'; import { KeySource } from '../../shared/types'; import { submitJobViaBridgeClient } from './utils/asyncUtils'; -import { createOnchainKeyGenCallback } from './walletGenerationCallbacks'; -import { getBaseWalletParams } from './utils/walletCreationUtils'; +import { + createEcdsaMPCv2KeyGenCallbacks, + createEddsaKeyGenCallbacks, + createOnchainKeyGenCallback, +} from './walletGenerationCallbacks'; /** * Request handler for generating an advanced wallet. @@ -27,127 +23,70 @@ export async function handleGenerateWallet( return handleGenerateEvmKeyRingWallet(req); } - if (multisigType === 'tss') { - return handleGenerateMpcWallet(req); - } - - return handleGenerateOnChainWallet(req); -} + const isTss = multisigType === 'tss'; -/** - * This route is used to generate a multisig wallet when advanced wallet manager is enabled - */ -async function handleGenerateOnChainWallet( - req: MasterApiSpecRouteRequest<'v1.wallet.generate', 'post'>, -) { - const asyncResult = await submitJobViaBridgeClient(req, { - path: `/api/${req.params.coin}/key/independent`, - body: req.decoded, - sources: [KeySource.USER, KeySource.BACKUP], - operationType: 'multisig_keygen', - }); - if (asyncResult) { - return asyncResult; + if (isTss) { + if (req.config.asyncModeConfig.enabled) { + throw new BadRequestError('Async mode is not yet supported for TSS wallet generation'); + } + } else { + const asyncResult = await submitJobViaBridgeClient(req, { + path: `/api/${req.params.coin}/key/independent`, + body: req.decoded, + sources: [KeySource.USER, KeySource.BACKUP], + operationType: 'multisig_keygen', + }); + if (asyncResult) { + return asyncResult; + } } const bitgo = req.bitgo; const baseCoin = await coinFactory.getCoin(req.params.coin, bitgo); - const createKeychainCallback = createOnchainKeyGenCallback( - req.awmUserClient, - req.awmBackupClient, - ); + if (isTss && !baseCoin.supportsTss()) { + throw new BadRequestError(`MPC wallet generation is not supported for coin ${req.params.coin}`); + } - const result = await baseCoin.wallets().generateWallet({ + const result = await baseCoin.wallets().generateWalletWithExternalSigner({ ...req.decoded, type: 'advanced', - multisigType: 'onchain', - createKeychainCallback, + multisigType: isTss ? 'tss' : 'onchain', + ...keyGenCallbacks(req, isTss ? baseCoin.getMPCAlgorithm() : undefined), }); + return { ...result, wallet: result.wallet.toJSON() }; } /** - * Generates a MPC wallet + * Picks the AWM key generation callbacks the SDK should drive, based on the coin's MPC algorithm. + * An undefined algorithm means an onchain multisig wallet. */ -async function handleGenerateMpcWallet( +function keyGenCallbacks( req: MasterApiSpecRouteRequest<'v1.wallet.generate', 'post'>, -) { - if (req.config.asyncModeConfig.enabled) { - throw new BadRequestError('Async mode is not yet supported for TSS wallet generation'); - } - - const bitgo = req.bitgo; - const baseCoin = await coinFactory.getCoin(req.decoded.coin, bitgo); - const awmClient = req.awmUserClient; - const awmBackupClient = req.awmBackupClient; + algorithm: MPCAlgorithm | undefined, +): Pick< + GenerateWalletWithExternalSignerOptions, + 'createKeychainCallback' | 'ecdsaMPCv2Callbacks' | 'eddsaCallbacks' +> { + const { awmUserClient, awmBackupClient } = req; + assert(awmUserClient, 'User AWM client not initialized'); + assert(awmBackupClient, 'Backup AWM client not initialized'); - if (!baseCoin.supportsTss()) { - throw new BadRequestError( - `MPC wallet generation is not supported for coin ${req.decoded.coin}`, - ); - } - - if (!awmClient) { - throw new Error('Advanced Wallet Manager client is required for MPC wallet generation'); - } - - const reqId = new RequestTracer(); - const { label, enterprise } = req.decoded; - - const walletParams: SupplementGenerateWalletOptions = { - ...req.decoded, - label, - ...getBaseWalletParams('tss'), - }; - - if (!_.isUndefined(enterprise)) { - if (!_.isString(enterprise)) { - throw new BadRequestError('invalid enterprise argument, expecting string'); - } - walletParams.enterprise = enterprise; - } - - const algorithm = baseCoin.getMPCAlgorithm(); - let orchestrateResult; switch (algorithm) { + case undefined: + return { + createKeychainCallback: createOnchainKeyGenCallback(awmUserClient, awmBackupClient), + }; case 'ecdsa': - orchestrateResult = await orchestrateEcdsaKeyGen({ - bitgo, - baseCoin, - awmClient, - awmBackupClient, - enterprise, - walletParams, - }); - break; + return { + ecdsaMPCv2Callbacks: createEcdsaMPCv2KeyGenCallbacks(awmUserClient, awmBackupClient), + }; case 'eddsa': - orchestrateResult = await orchestrateEddsaKeyGen({ - bitgo, - baseCoin, - awmClient, - awmBackupClient, - walletParams, - enterprise, - }); - break; + return { eddsaCallbacks: createEddsaKeyGenCallbacks(awmUserClient, awmBackupClient) }; default: - throw new Error(`Unsupported MPC algorithm: ${algorithm}`); + throw new BadRequestError(`Unsupported MPC algorithm: ${algorithm}`); } - - const { keychains, walletParams: finalWalletParams } = orchestrateResult; - bitgo.setRequestTracer(reqId); - const newWallet = await bitgo.post(baseCoin.url('/wallet/add')).send(finalWalletParams).result(); - - const result: WalletWithKeychains = { - wallet: new Wallet(bitgo, baseCoin, newWallet), - userKeychain: keychains.userKeychain, - backupKeychain: keychains.backupKeychain, - bitgoKeychain: keychains.bitgoKeychain, - responseType: 'WalletWithKeychains', - }; - - return { ...result, wallet: result.wallet.toJSON() }; } /** @@ -161,10 +100,10 @@ async function handleGenerateEvmKeyRingWallet( } const bitgo = req.bitgo; - const baseCoin = await coinFactory.getCoin(req.decoded.coin, bitgo); + const baseCoin = await coinFactory.getCoin(req.params.coin, bitgo); if (!baseCoin.isEVM()) { throw new BadRequestError( - `EVM keyring wallet generation is not supported for coin ${req.decoded.coin}`, + `EVM keyring wallet generation is not supported for coin ${req.params.coin}`, ); } diff --git a/src/masterBitgoExpress/handlers/utils/walletCreationUtils.ts b/src/masterBitgoExpress/handlers/utils/walletCreationUtils.ts deleted file mode 100644 index 1fddb9e6..00000000 --- a/src/masterBitgoExpress/handlers/utils/walletCreationUtils.ts +++ /dev/null @@ -1,3 +0,0 @@ -export function getBaseWalletParams(multisigType: 'onchain' | 'tss') { - return { m: 2, n: 3, keys: [] as string[], type: 'advanced', multisigType } as const; -} diff --git a/src/masterBitgoExpress/handlers/walletGenerationCallbacks.ts b/src/masterBitgoExpress/handlers/walletGenerationCallbacks.ts index 9f8bf37a..f2b580af 100644 --- a/src/masterBitgoExpress/handlers/walletGenerationCallbacks.ts +++ b/src/masterBitgoExpress/handlers/walletGenerationCallbacks.ts @@ -1,26 +1,31 @@ -import { CreateKeychainCallback } from '@bitgo-beta/sdk-core'; +import assert from 'assert'; +import { DklsTypes } from '@bitgo-beta/sdk-lib-mpc'; +import { + CreateKeychainCallback, + EcdsaMPCv2KeyGenCallbacks, + EddsaKeyGenCallbacks, +} from '@bitgo-beta/sdk-core'; import { KeySource } from '../../shared/types'; import { AdvancedWalletManagerClient, IndependentKeychainResponse, } from '../clients/advancedWalletManagerClient'; +// DKLS23 party indices: user=0, backup=1, bitgo=2 +const PARTY = { USER: 0, BACKUP: 1, BITGO: 2 } as const; + export function createOnchainKeyGenCallback( awmUserClient: AdvancedWalletManagerClient, awmBackupClient: AdvancedWalletManagerClient, ): CreateKeychainCallback { return async ({ source, coin }) => { - let client: AdvancedWalletManagerClient; - if (source === KeySource.USER) { - client = awmUserClient; - } else if (source === KeySource.BACKUP) { - client = awmBackupClient; - } else { - throw new Error(`Unexpected key source for onchain key generation: ${source}`); - } - + assert( + source === KeySource.USER || source === KeySource.BACKUP, + `Unexpected key source for onchain key generation: ${source}`, + ); + const client = source === KeySource.USER ? awmUserClient : awmBackupClient; const keychain = await client.createIndependentKeychain({ source, coin, type: 'independent' }); - return keychain as { pub: string; type: 'independent'; source: typeof source }; + return { pub: keychain.pub, type: 'independent' as const, source }; }; } @@ -40,3 +45,370 @@ export function createOnchainKeyGenCallbackForPreGeneratedKeychains( }; }; } + +/** Narrows an AWM response to the encrypted session state the SDK threads between callbacks. */ +function toMpcState(response: { encryptedData: string; encryptedDataKey: string }) { + return { encryptedData: response.encryptedData, encryptedDataKey: response.encryptedDataKey }; +} + +/** Shape the AWM MPCv2 round endpoints expect for a BitGo broadcast message. */ +function formatBroadcastMessage(message: { from: number; message: string; signature: string }) { + return { + from: message.from, + payload: { message: message.message, signature: message.signature }, + }; +} + +/** Shape the AWM MPCv2 round endpoints expect for a BitGo p2p message. */ +function formatP2PMessage( + message: { from: number; to: number; encryptedMessage: string; signature: string }, + commitment?: string, +) { + return { + from: message.from, + to: message.to, + payload: { encryptedMessage: message.encryptedMessage, signature: message.signature }, + commitment, + }; +} + +/** + * Creates ECDSA MPCv2 key generation callbacks for the SDK external signer flow. + * + * Each callback drives both the user and the backup AWM. The SDK relays messages to and from BitGo, + * while messages exchanged directly between user and backup are carried in this closure. The AWM + * protocol has more rounds than the SDK has callbacks, so `round2Callback` runs AWM rounds 2 and 3 + * (both of its inputs arrive together from BitGo round 1) and `round3Callback` runs AWM round 4. + */ +export function createEcdsaMPCv2KeyGenCallbacks( + awmUserClient: AdvancedWalletManagerClient, + awmBackupClient: AdvancedWalletManagerClient, +): EcdsaMPCv2KeyGenCallbacks { + // Messages the SDK never sees: user/backup broadcasts and the p2p messages they exchange. + // round-2 counterparty messages are only used within round2Callback and are locals there. + let userRound1Broadcast: DklsTypes.AuthBroadcastMessage; + let backupRound1Broadcast: DklsTypes.AuthBroadcastMessage; + let userRound3BitgoP2p: DklsTypes.AuthEncP2PMessage; + let backupRound3BitgoP2p: DklsTypes.AuthEncP2PMessage; + let userRound3CounterPartyP2p: DklsTypes.AuthEncP2PMessage; + let backupRound3CounterPartyP2p: DklsTypes.AuthEncP2PMessage; + let userRound4Broadcast: DklsTypes.AuthBroadcastMessage; + let backupRound4Broadcast: DklsTypes.AuthBroadcastMessage; + + return { + initializeCallback: async ({ bitgoPublicGpgKey }) => { + const userInit = await awmUserClient.initEcdsaMpcV2KeyGenMpcV2({ source: KeySource.USER }); + const backupInit = await awmBackupClient.initEcdsaMpcV2KeyGenMpcV2({ + source: KeySource.BACKUP, + }); + + const [userRound1, backupRound1] = await Promise.all([ + awmUserClient.roundEcdsaMPCv2KeyGen({ + source: KeySource.USER, + encryptedData: userInit.encryptedData, + encryptedDataKey: userInit.encryptedDataKey, + round: 1, + bitgoGpgPub: bitgoPublicGpgKey, + counterPartyGpgPub: backupInit.gpgPub, + }), + awmBackupClient.roundEcdsaMPCv2KeyGen({ + source: KeySource.BACKUP, + encryptedData: backupInit.encryptedData, + encryptedDataKey: backupInit.encryptedDataKey, + round: 1, + bitgoGpgPub: bitgoPublicGpgKey, + counterPartyGpgPub: userInit.gpgPub, + }), + ]); + assert(userRound1.broadcastMessage, 'Missing broadcast message in user round 1 response'); + assert(backupRound1.broadcastMessage, 'Missing broadcast message in backup round 1 response'); + userRound1Broadcast = userRound1.broadcastMessage; + backupRound1Broadcast = backupRound1.broadcastMessage; + + return { + userGpgPublicKey: userInit.gpgPub, + backupGpgPublicKey: backupInit.gpgPub, + round1Messages: { + broadcastMessages: [userRound1.broadcastMessage, backupRound1.broadcastMessage], + p2pMessages: [], + }, + userState: toMpcState(userRound1), + backupState: toMpcState(backupRound1), + }; + }, + + round2Callback: async ({ + bitgoMsg1, + bitgoToUserMsg2, + bitgoToBackupMsg2, + userState, + backupState, + }) => { + assert(userRound1Broadcast, 'round2Callback called before initializeCallback completed'); + assert(backupRound1Broadcast, 'round2Callback called before initializeCallback completed'); + const bitgoBroadcast = formatBroadcastMessage(bitgoMsg1); + const [userRound2, backupRound2] = await Promise.all([ + awmUserClient.roundEcdsaMPCv2KeyGen({ + source: KeySource.USER, + ...userState, + round: 2, + broadcastMessages: { bitgo: bitgoBroadcast, counterParty: backupRound1Broadcast }, + }), + awmBackupClient.roundEcdsaMPCv2KeyGen({ + source: KeySource.BACKUP, + ...backupState, + round: 2, + broadcastMessages: { bitgo: bitgoBroadcast, counterParty: userRound1Broadcast }, + }), + ]); + assert(userRound2.p2pMessages?.bitgo, 'Missing BitGo p2p message in user round 2 response'); + assert( + backupRound2.p2pMessages?.bitgo, + 'Missing BitGo p2p message in backup round 2 response', + ); + assert( + userRound2.p2pMessages.counterParty, + 'Missing counterParty p2p in user round 2 response', + ); + assert( + backupRound2.p2pMessages.counterParty, + 'Missing counterParty p2p in backup round 2 response', + ); + const userRound2CounterPartyP2p = userRound2.p2pMessages.counterParty; + const backupRound2CounterPartyP2p = backupRound2.p2pMessages.counterParty; + + assert(bitgoToUserMsg2.from === PARTY.BITGO, 'bitgoToUserMsg2 is not from BitGo'); + assert(bitgoToUserMsg2.to === PARTY.USER, 'bitgoToUserMsg2 is not addressed to user'); + assert(bitgoToBackupMsg2.from === PARTY.BITGO, 'bitgoToBackupMsg2 is not from BitGo'); + assert(bitgoToBackupMsg2.to === PARTY.BACKUP, 'bitgoToBackupMsg2 is not addressed to backup'); + + const [userRound3, backupRound3] = await Promise.all([ + awmUserClient.roundEcdsaMPCv2KeyGen({ + source: KeySource.USER, + ...toMpcState(userRound2), + round: 3, + p2pMessages: { + bitgo: formatP2PMessage(bitgoToUserMsg2), + counterParty: backupRound2CounterPartyP2p, + }, + }), + awmBackupClient.roundEcdsaMPCv2KeyGen({ + source: KeySource.BACKUP, + ...toMpcState(backupRound2), + round: 3, + p2pMessages: { + bitgo: formatP2PMessage(bitgoToBackupMsg2), + counterParty: userRound2CounterPartyP2p, + }, + }), + ]); + assert(userRound3.p2pMessages?.bitgo, 'Missing BitGo p2p message in user round 3 response'); + assert( + backupRound3.p2pMessages?.bitgo, + 'Missing BitGo p2p message in backup round 3 response', + ); + assert( + userRound3.p2pMessages.counterParty, + 'Missing counterParty p2p in user round 3 response', + ); + assert( + backupRound3.p2pMessages.counterParty, + 'Missing counterParty p2p in backup round 3 response', + ); + userRound3BitgoP2p = userRound3.p2pMessages.bitgo; + backupRound3BitgoP2p = backupRound3.p2pMessages.bitgo; + userRound3CounterPartyP2p = userRound3.p2pMessages.counterParty; + backupRound3CounterPartyP2p = backupRound3.p2pMessages.counterParty; + + return { + round2Messages: { + broadcastMessages: [], + p2pMessages: [userRound2.p2pMessages.bitgo, backupRound2.p2pMessages.bitgo], + }, + userState: toMpcState(userRound3), + backupState: toMpcState(backupRound3), + }; + }, + + round3Callback: async ({ + bitgoCommitment2, + bitgoToUserMsg3, + bitgoToBackupMsg3, + userState, + backupState, + }) => { + assert(userRound3CounterPartyP2p, 'round3Callback called before round2Callback completed'); + assert(backupRound3CounterPartyP2p, 'round3Callback called before round2Callback completed'); + assert(userRound3BitgoP2p, 'round3Callback called before round2Callback completed'); + assert(backupRound3BitgoP2p, 'round3Callback called before round2Callback completed'); + assert(bitgoToUserMsg3.from === PARTY.BITGO, 'bitgoToUserMsg3 is not from BitGo'); + assert(bitgoToUserMsg3.to === PARTY.USER, 'bitgoToUserMsg3 is not addressed to user'); + assert(bitgoToBackupMsg3.from === PARTY.BITGO, 'bitgoToBackupMsg3 is not from BitGo'); + assert(bitgoToBackupMsg3.to === PARTY.BACKUP, 'bitgoToBackupMsg3 is not addressed to backup'); + const [userRound4, backupRound4] = await Promise.all([ + awmUserClient.roundEcdsaMPCv2KeyGen({ + source: KeySource.USER, + ...userState, + round: 4, + p2pMessages: { + bitgo: formatP2PMessage(bitgoToUserMsg3, bitgoCommitment2), + counterParty: backupRound3CounterPartyP2p, + }, + }), + awmBackupClient.roundEcdsaMPCv2KeyGen({ + source: KeySource.BACKUP, + ...backupState, + round: 4, + p2pMessages: { + bitgo: formatP2PMessage(bitgoToBackupMsg3, bitgoCommitment2), + counterParty: userRound3CounterPartyP2p, + }, + }), + ]); + assert(userRound4.broadcastMessage, 'Missing broadcast message in user round 4 response'); + assert(backupRound4.broadcastMessage, 'Missing broadcast message in backup round 4 response'); + userRound4Broadcast = userRound4.broadcastMessage; + backupRound4Broadcast = backupRound4.broadcastMessage; + + return { + round3Messages: { + broadcastMessages: [userRound4.broadcastMessage, backupRound4.broadcastMessage], + p2pMessages: [userRound3BitgoP2p, backupRound3BitgoP2p], + }, + userState: toMpcState(userRound4), + backupState: toMpcState(backupRound4), + }; + }, + + finalizeCallback: async ({ bitgoMsg4, bitgoCommonKeychain, userState, backupState }) => { + assert(userRound4Broadcast, 'finalizeCallback called before round3Callback completed'); + assert(backupRound4Broadcast, 'finalizeCallback called before round3Callback completed'); + const bitgoBroadcast = formatBroadcastMessage(bitgoMsg4); + const [userFinalize, backupFinalize] = await Promise.all([ + awmUserClient.finalizeEcdsaMPCv2KeyGen({ + source: KeySource.USER, + ...userState, + broadcastMessages: { bitgo: bitgoBroadcast, counterParty: backupRound4Broadcast }, + bitgoCommonKeychain, + }), + awmBackupClient.finalizeEcdsaMPCv2KeyGen({ + source: KeySource.BACKUP, + ...backupState, + broadcastMessages: { bitgo: bitgoBroadcast, counterParty: userRound4Broadcast }, + bitgoCommonKeychain, + }), + ]); + assert(userFinalize.commonKeychain, 'Missing common keychain in user finalize response'); + assert(backupFinalize.commonKeychain, 'Missing common keychain in backup finalize response'); + assert.strictEqual( + userFinalize.commonKeychain, + backupFinalize.commonKeychain, + 'User and backup common keychains do not match', + ); + assert.strictEqual( + userFinalize.commonKeychain, + bitgoCommonKeychain, + 'User and BitGo common keychains do not match', + ); + + return { commonKeychain: userFinalize.commonKeychain }; + }, + }; +} + +/** + * Creates EdDSA (MPCv1) key generation callbacks for the SDK external signer flow. + * + * `initializeCallback` runs both parties — the backup AWM needs the user's GPG key — and + * `finalizeCallback` is invoked once per source, user first, so the user's counterparty key share + * can be handed to the backup AWM. + */ +export function createEddsaKeyGenCallbacks( + awmUserClient: AdvancedWalletManagerClient, + awmBackupClient: AdvancedWalletManagerClient, +): EddsaKeyGenCallbacks { + return { + initializeCallback: async ({ bitgoPublicGpgKey }) => { + const userInit = await awmUserClient.initMpcKeyGeneration({ + source: KeySource.USER, + bitgoGpgKey: bitgoPublicGpgKey, + }); + const backupInit = await awmBackupClient.initMpcKeyGeneration({ + source: KeySource.BACKUP, + bitgoGpgKey: bitgoPublicGpgKey, + userGpgKey: userInit.bitgoPayload.gpgKey, + }); + assert.strictEqual( + userInit.bitgoPayload.from, + KeySource.USER, + 'User payload is not from user', + ); + assert.strictEqual( + backupInit.bitgoPayload.from, + KeySource.BACKUP, + 'Backup payload is not from backup', + ); + assert( + backupInit.counterPartyKeyShare, + 'Backup key share for the user is missing from the initialization response', + ); + + return { + userGpgPublicKey: userInit.bitgoPayload.gpgKey, + backupGpgPublicKey: backupInit.bitgoPayload.gpgKey, + userToBitgoKeyShare: userInit.bitgoPayload, + backupToBitgoKeyShare: backupInit.bitgoPayload, + backupToUserCounterPartyKeyShare: backupInit.counterPartyKeyShare, + userState: toMpcState(userInit), + backupState: toMpcState(backupInit), + }; + }, + + finalizeCallback: async ({ + source, + coin, + bitgoKeychain, + counterPartyGPGKey, + counterPartyKeyShare, + state, + }) => { + const isUser = source === KeySource.USER; + const client = isUser ? awmUserClient : awmBackupClient; + assert(bitgoKeychain.commonKeychain, 'Missing commonKeychain in BitGo keychain'); + assert(bitgoKeychain.keyShares, 'Missing keyShares in BitGo keychain'); + const response = await client.finalizeMpcKeyGeneration({ + source, + coin, + encryptedData: state.encryptedData, + encryptedDataKey: state.encryptedDataKey, + bitGoKeychain: { + ...bitgoKeychain, + commonKeychain: bitgoKeychain.commonKeychain, + source: KeySource.BITGO, + type: 'tss', + verifiedVssProof: true, + isBitGo: true, + isTrust: false, + keyShares: bitgoKeychain.keyShares, + }, + counterPartyGPGKey, + counterPartyKeyShare: { + ...counterPartyKeyShare, + from: isUser ? KeySource.BACKUP : KeySource.USER, + to: source, + }, + }); + + if (isUser) { + assert( + response.counterpartyKeyShare, + 'Key share for backup missing from user finalize response', + ); + } + return { + commonKeychain: response.commonKeychain, + counterpartyKeyShare: response.counterpartyKeyShare, + }; + }, + }; +} From bbcaf9a652dbebe19e3ec70b91c362a9063aeb8a Mon Sep 17 00:00:00 2001 From: Daniel Peng Date: Mon, 27 Jul 2026 10:01:22 -0400 Subject: [PATCH 2/2] chore: bump deps identified by scanner to fixed version Ticket: WCN-683 --- package-lock.json | 64 +++++++++++++++++++---------------------------- package.json | 5 ++-- 2 files changed, 29 insertions(+), 40 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8e215736..dc01df1d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17761,7 +17761,8 @@ "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true }, "node_modules/base-64": { "version": "0.1.0", @@ -18196,12 +18197,24 @@ } }, "node_modules/brace-expansion": { - "version": "1.1.16", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz", - "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==", + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.8.tgz", + "integrity": "sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==", + "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/brace-expansion/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" } }, "node_modules/braces": { @@ -19362,11 +19375,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" - }, "node_modules/config-chain": { "version": "1.1.13", "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz", @@ -23534,26 +23542,6 @@ "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", "license": "BSD-2-Clause" }, - "node_modules/glob/node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", - "license": "MIT", - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/glob/node_modules/brace-expansion": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.8.tgz", - "integrity": "sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==", - "dependencies": { - "balanced-match": "^4.0.2" - }, - "engines": { - "node": "20 || >=22" - } - }, "node_modules/glob/node_modules/foreground-child": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", @@ -28367,9 +28355,9 @@ "integrity": "sha512-7vO7n28+aYO4J+8w96AzhmU8G+Y/xpPDJz/se19ICsqj/momRbb9mh9ZUtkoJ5X3nTnPdhEJyc0qnM6yAsHBaA==" }, "node_modules/nanoid": { - "version": "3.3.12", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", - "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "version": "3.3.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", "funding": [ { "type": "github", @@ -32962,9 +32950,9 @@ } }, "node_modules/postcss": { - "version": "8.5.15", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", - "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "version": "8.5.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.23.tgz", + "integrity": "sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==", "funding": [ { "type": "opencollective", @@ -32981,7 +32969,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.12", + "nanoid": "^3.3.16", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, diff --git a/package.json b/package.json index ad82d1bc..57a764fb 100644 --- a/package.json +++ b/package.json @@ -155,9 +155,10 @@ "lodash": "^4.18.0", "fast-uri": "^3.1.4", "shell-quote": "^1.9.0", - "brace-expansion@1": "^1.1.16", + "brace-expansion@1": "^5.0.8", "brace-expansion@2": "^2.1.2", - "brace-expansion@5": "^5.0.7", + "brace-expansion@5": "^5.0.8", + "postcss": ">=8.5.18", "js-yaml@3": "^3.15.0", "js-yaml@4": "^4.3.0" },