From ac15c77327487a54ea1207c192e22c2dfd75c9ac Mon Sep 17 00:00:00 2001 From: Manas Ladha Date: Tue, 21 Apr 2026 18:14:48 +0530 Subject: [PATCH 01/13] feat(sdk-coin-eth): add registerWithCoinMap for dynamic token registration TICKET: CSHLD-24 Co-Authored-By: Claude Opus 4.6 (1M context) --- modules/sdk-coin-eth/src/register.ts | 14 +++- modules/sdk-coin-eth/test/unit/register.ts | 81 ++++++++++++++++++++++ 2 files changed, 94 insertions(+), 1 deletion(-) create mode 100644 modules/sdk-coin-eth/test/unit/register.ts diff --git a/modules/sdk-coin-eth/src/register.ts b/modules/sdk-coin-eth/src/register.ts index 7420142e10..4b51ff1c3b 100644 --- a/modules/sdk-coin-eth/src/register.ts +++ b/modules/sdk-coin-eth/src/register.ts @@ -1,4 +1,4 @@ -import { BitGoBase } from '@bitgo/sdk-core'; +import { BitGoBase, GlobalCoinFactory } from '@bitgo/sdk-core'; import { Erc20Token } from './erc20Token'; import { Eth } from './eth'; import { Gteth } from './gteth'; @@ -21,7 +21,19 @@ export const register = (sdk: BitGoBase): void => { }; export const registerWithCoinMap = (sdk: BitGoBase, coinMap: CoinMap): void => { + sdk.register('eth', Eth.createInstance); + sdk.register('gteth', Gteth.createInstance); + sdk.register('teth', Teth.createInstance); + sdk.register('hteth', Hteth.createInstance); + Erc721Token.createTokenConstructors().forEach(({ name, coinConstructor }) => { + sdk.register(name, coinConstructor); + }); + + // Registration for ERC20 tokens from the coin map (includes both hardcoded and dynamic tokens from AMS). Erc20Token.createTokenConstructors(getFormattedErc20Tokens(coinMap)).forEach(({ name, coinConstructor }) => { sdk.register(name, coinConstructor); + if (coinMap.has(name)) { + GlobalCoinFactory.registerToken(coinMap.get(name), coinConstructor); + } }); }; diff --git a/modules/sdk-coin-eth/test/unit/register.ts b/modules/sdk-coin-eth/test/unit/register.ts new file mode 100644 index 0000000000..5cfecadfa5 --- /dev/null +++ b/modules/sdk-coin-eth/test/unit/register.ts @@ -0,0 +1,81 @@ +import sinon from 'sinon'; +import assert from 'assert'; +import { BitGoAPI } from '@bitgo/sdk-api'; +import { GlobalCoinFactory } from '@bitgo/sdk-core'; +import { coins, Erc20Coin } from '@bitgo/statics'; +import { register, registerWithCoinMap } from '../../src/register'; +import { Erc20Token } from '../../src/erc20Token'; +import { Erc721Token } from '../../src/erc721Token'; + +describe('ETH Register', function () { + let bitgo: BitGoAPI; + let registerSpy: sinon.SinonSpy; + let registerTokenSpy: sinon.SinonSpy; + + beforeEach(function () { + bitgo = new BitGoAPI({ env: 'test' }); + registerSpy = sinon.spy(bitgo, 'register'); + registerTokenSpy = sinon.spy(GlobalCoinFactory, 'registerToken'); + }); + + afterEach(function () { + registerSpy.restore(); + registerTokenSpy.restore(); + }); + + describe('register', function () { + it('should register base coins and token constructors', function () { + register(bitgo); + + const registeredNames = registerSpy.getCalls().map((call) => call.args[0]); + + // Base coins should be registered + assert.ok(registeredNames.includes('eth')); + assert.ok(registeredNames.includes('gteth')); + assert.ok(registeredNames.includes('teth')); + assert.ok(registeredNames.includes('hteth')); + + // ERC20 and ERC721 tokens should be registered + const erc20Count = Erc20Token.createTokenConstructors().length; + const erc721Count = Erc721Token.createTokenConstructors().length; + assert.strictEqual(registerSpy.callCount, 4 + erc20Count + erc721Count); + }); + }); + + describe('registerWithCoinMap', function () { + it('should call register internally for base coins and tokens', function () { + registerWithCoinMap(bitgo, coins); + + const registeredNames = registerSpy.getCalls().map((call) => call.args[0]); + + // Base coins should be registered via register() + assert.ok(registeredNames.includes('eth')); + assert.ok(registeredNames.includes('gteth')); + assert.ok(registeredNames.includes('teth')); + assert.ok(registeredNames.includes('hteth')); + }); + + it('should add dynamic ERC20 tokens to the global coin map', function () { + registerWithCoinMap(bitgo, coins); + + // registerToken should have been called for dynamic tokens + assert.ok(registerTokenSpy.callCount > 0); + + // Each call should pass a valid coin from the coinMap + for (let i = 0; i < registerTokenSpy.callCount; i++) { + const call = registerTokenSpy.getCall(i); + const staticsCoin = call.args[0]; + assert.ok(coins.has(staticsCoin.name), `${staticsCoin.name} should exist in the coin map`); + } + }); + + it('should not add tokens to the global coin map when coin map has no ERC20 tokens', function () { + const limitedCoinMap = coins.filter((coin) => !(coin instanceof Erc20Coin)); + + registerWithCoinMap(bitgo, limitedCoinMap); + + // registerToken should not be called since no ERC20 tokens are in the map + assert.strictEqual(registerTokenSpy.callCount, 0); + }); + }); +}); From 428b2587ac2b62f1a327b2124a469cf318c676dc Mon Sep 17 00:00:00 2001 From: "asset-metadata-bot[bot]" <226385837+asset-metadata-bot[bot]@users.noreply.github.com> Date: Tue, 28 Apr 2026 13:05:23 +0000 Subject: [PATCH 02/13] feat: add new tokens for CSHLD-701 --- modules/statics/src/coins/botOfcTokens.ts | 4 ++-- modules/statics/src/coins/botTokens.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/modules/statics/src/coins/botOfcTokens.ts b/modules/statics/src/coins/botOfcTokens.ts index 9ff5b4d398..cde112535d 100644 --- a/modules/statics/src/coins/botOfcTokens.ts +++ b/modules/statics/src/coins/botOfcTokens.ts @@ -794,7 +794,7 @@ export const botOfcTokens = [ undefined ), AccountCtors.ofcerc20( - 'c673dd10-076e-47b7-a53f-3a622c7cf38c', + '02af63f1-21b9-41a6-836a-a273f0faa276', 'ofceth:krwq', 'KRWQ', 18, @@ -806,7 +806,7 @@ export const botOfcTokens = [ undefined ), AccountCtors.ofcerc20( - 'cb98a20f-f22a-494a-b8e6-53212aa96f5a', + 'adf94a02-974e-4fec-8132-d066ac52e49d', 'ofceth:hybond', 'HYBOND', 18, diff --git a/modules/statics/src/coins/botTokens.ts b/modules/statics/src/coins/botTokens.ts index f31cf9dede..66baceb229 100644 --- a/modules/statics/src/coins/botTokens.ts +++ b/modules/statics/src/coins/botTokens.ts @@ -797,7 +797,7 @@ export const botTokens = [ undefined ), AccountCtors.erc20( - 'ffb0d91e-c6d9-4cef-b5ea-f3781a757197', + '4bbcfef1-dde0-4931-a5d2-e7da10c8a1c6', 'eth:krwq', 'KRWQ', 18, @@ -809,7 +809,7 @@ export const botTokens = [ undefined ), AccountCtors.erc20( - 'f940b8f1-4911-4632-94ce-f4c0d950defb', + '910c83a4-7a4e-4892-8be6-89a0c3e99394', 'eth:hybond', 'HYBOND', 18, From 5ae22c0e725282d9e2be7aa541ec4aa6d008fcb6 Mon Sep 17 00:00:00 2001 From: Otto Allmendinger Date: Tue, 28 Apr 2026 15:55:54 +0200 Subject: [PATCH 03/13] feat(abstract-utxo): bump wasm-utxo to 4.8.0 Update wasm-utxo dependency across multiple modules to version 4.8.0. Contains fixes for abstract-utxo bip322. Co-authored-by: llm-git Issue: BTC-0 --- modules/abstract-utxo/package.json | 2 +- modules/utxo-bin/package.json | 2 +- modules/utxo-core/package.json | 2 +- modules/utxo-ord/package.json | 2 +- modules/utxo-staking/package.json | 2 +- yarn.lock | 8 ++++---- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/modules/abstract-utxo/package.json b/modules/abstract-utxo/package.json index d2f32b9685..b5f1470b10 100644 --- a/modules/abstract-utxo/package.json +++ b/modules/abstract-utxo/package.json @@ -66,7 +66,7 @@ "@bitgo/utxo-core": "^1.36.0", "@bitgo/utxo-lib": "^11.22.0", "@bitgo/utxo-ord": "^1.29.0", - "@bitgo/wasm-utxo": "^4.7.0", + "@bitgo/wasm-utxo": "^4.8.0", "@types/lodash": "^4.14.121", "@types/superagent": "4.1.15", "bignumber.js": "^9.0.2", diff --git a/modules/utxo-bin/package.json b/modules/utxo-bin/package.json index d8ef5963c3..9568139c3e 100644 --- a/modules/utxo-bin/package.json +++ b/modules/utxo-bin/package.json @@ -31,7 +31,7 @@ "@bitgo/unspents": "^0.51.3", "@bitgo/utxo-core": "^1.36.0", "@bitgo/utxo-lib": "^11.22.0", - "@bitgo/wasm-utxo": "^4.7.0", + "@bitgo/wasm-utxo": "^4.8.0", "@noble/curves": "1.8.1", "archy": "^1.0.0", "bech32": "^2.0.0", diff --git a/modules/utxo-core/package.json b/modules/utxo-core/package.json index bac833a3cc..3834f3c326 100644 --- a/modules/utxo-core/package.json +++ b/modules/utxo-core/package.json @@ -81,7 +81,7 @@ "@bitgo/secp256k1": "^1.11.0", "@bitgo/unspents": "^0.51.3", "@bitgo/utxo-lib": "^11.22.0", - "@bitgo/wasm-utxo": "^4.7.0", + "@bitgo/wasm-utxo": "^4.8.0", "bip174": "npm:@bitgo-forks/bip174@3.1.0-master.4", "fast-sha256": "^1.3.0" }, diff --git a/modules/utxo-ord/package.json b/modules/utxo-ord/package.json index 57f0e65d16..3a2e4a66c3 100644 --- a/modules/utxo-ord/package.json +++ b/modules/utxo-ord/package.json @@ -45,7 +45,7 @@ "directory": "modules/utxo-ord" }, "dependencies": { - "@bitgo/wasm-utxo": "^4.7.0" + "@bitgo/wasm-utxo": "^4.8.0" }, "devDependencies": { "@bitgo/utxo-lib": "^11.22.0" diff --git a/modules/utxo-staking/package.json b/modules/utxo-staking/package.json index c5bc71a5f9..c97e1a681a 100644 --- a/modules/utxo-staking/package.json +++ b/modules/utxo-staking/package.json @@ -63,7 +63,7 @@ "@bitgo/babylonlabs-io-btc-staking-ts": "^3.5.0", "@bitgo/utxo-core": "^1.36.0", "@bitgo/utxo-lib": "^11.22.0", - "@bitgo/wasm-utxo": "^4.7.0", + "@bitgo/wasm-utxo": "^4.8.0", "bip174": "npm:@bitgo-forks/bip174@3.1.0-master.4", "bip322-js": "^2.0.0", "bitcoinjs-lib": "^6.1.7", diff --git a/yarn.lock b/yarn.lock index b7906a70c8..290502db89 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1030,10 +1030,10 @@ resolved "https://registry.npmjs.org/@bitgo/wasm-ton/-/wasm-ton-1.1.1.tgz" integrity sha512-Y4x2V2ZcYWlmx42v7dlrKDtT2DuUt8smk8E98mh7RhpiifJhLk2v5RmXDwBl0A3v9TzUOU6qMOnSS/iZ8Pq52w== -"@bitgo/wasm-utxo@^4.7.0": - version "4.7.0" - resolved "https://registry.npmjs.org/@bitgo/wasm-utxo/-/wasm-utxo-4.7.0.tgz#2ec1103c840b3be1a2ed29fae4ebc03fd57160a6" - integrity sha512-7T1vZNxM1dGPi2EqbWAFzHN0A8uWlR05c9Q7UAmZv1dQt6SBTsGc5rPyoEmwvkyPJSdbvcPS3NCoTyWIcbqUUA== +"@bitgo/wasm-utxo@^4.8.0": + version "4.8.0" + resolved "https://registry.npmjs.org/@bitgo/wasm-utxo/-/wasm-utxo-4.8.0.tgz#744b20d239e5430402d61bd4ba7b1fbd77cc9ff3" + integrity sha512-FV4ll1nAiR8RKtkJAsjauz+bmcllDuo8Cx0aFTbnS+0yKQAGQCjc9AJXW9CvZC65fi1dTj9+hmcLugWrQ0h92A== "@brandonblack/musig@^0.0.1-alpha.0": version "0.0.1-alpha.1" From e7dfa8e8650e7825944bf4a29927e9e3a10b75fe Mon Sep 17 00:00:00 2001 From: Zhongxi Shen Date: Fri, 17 Apr 2026 14:48:46 -0600 Subject: [PATCH 04/13] feat: add round domain separator to adata TICKET: HSM-1513 --- .../src/bitgo/utils/tss/ecdsa/ecdsaMPCv2.ts | 41 +- .../unit/bitgo/utils/tss/ecdsa/ecdsaMPCv2.ts | 608 ++++++++++++++++++ .../unit/bitgo/utils/tss/ecdsa/gpgKeys.ts | 34 + 3 files changed, 673 insertions(+), 10 deletions(-) create mode 100644 modules/sdk-core/test/unit/bitgo/utils/tss/ecdsa/ecdsaMPCv2.ts create mode 100644 modules/sdk-core/test/unit/bitgo/utils/tss/ecdsa/gpgKeys.ts diff --git a/modules/sdk-core/src/bitgo/utils/tss/ecdsa/ecdsaMPCv2.ts b/modules/sdk-core/src/bitgo/utils/tss/ecdsa/ecdsaMPCv2.ts index e76eee3e09..f5dbe75eeb 100644 --- a/modules/sdk-core/src/bitgo/utils/tss/ecdsa/ecdsaMPCv2.ts +++ b/modules/sdk-core/src/bitgo/utils/tss/ecdsa/ecdsaMPCv2.ts @@ -52,6 +52,10 @@ import { EcdsaMPCv2KeyGenSendFn, KeyGenSenderForEnterprise } from './ecdsaMPCv2K import { envRequiresBitgoPubGpgKeyConfig, isBitgoMpcPubKey } from '../../../tss/bitgoPubKeys'; export class EcdsaMPCv2Utils extends BaseEcdsaUtils { + private static readonly DKLS23_SIGNING_USER_GPG_KEY = 'DKLS23_SIGNING_USER_GPG_KEY'; + private static readonly DKLS23_SIGNING_ROUND1_STATE = 'DKLS23_SIGNING_ROUND1_STATE'; + private static readonly DKLS23_SIGNING_ROUND2_STATE = 'DKLS23_SIGNING_ROUND2_STATE'; + /** @inheritdoc */ async createKeychains(params: { passphrase: string; @@ -968,17 +972,20 @@ export class EcdsaMPCv2Utils extends BaseEcdsaUtils { * @param {string} bitgoPublicGpgKey - the BitGo public GPG key * @param {string} encryptedUserGpgPrvKey - the encrypted user GPG private key * @param {string} walletPassphrase - the wallet passphrase + * @param {string} adata - the additional data to validate the GPG keys * @returns {Promise<{ bitgoGpgKey: pgp.Key; userGpgKey: pgp.SerializedKeyPair }>} - the BitGo and user GPG keys */ private async getBitgoAndUserGpgKeys( bitgoPublicGpgKey: string, encryptedUserGpgPrvKey: string, - walletPassphrase: string + walletPassphrase: string, + adata: string ): Promise<{ bitgoGpgKey: pgp.Key; userGpgKey: pgp.SerializedKeyPair; }> { const bitgoGpgKey = await pgp.readKey({ armoredKey: bitgoPublicGpgKey }); + this.validateAdata(adata, encryptedUserGpgPrvKey, EcdsaMPCv2Utils.DKLS23_SIGNING_USER_GPG_KEY); const userDecryptedKey = await pgp.readKey({ armoredKey: this.bitgo.decrypt({ input: encryptedUserGpgPrvKey, password: walletPassphrase }), }); @@ -999,7 +1006,7 @@ export class EcdsaMPCv2Utils extends BaseEcdsaUtils { * @returns void * @throws {Error} if the adata or cyphertext is invalid */ - private validateAdata(adata: string, cyphertext: string): void { + private validateAdata(adata: string, cyphertext: string, roundDomainSeparator: string): void { let cypherJson; try { cypherJson = JSON.parse(cyphertext); @@ -1007,7 +1014,10 @@ export class EcdsaMPCv2Utils extends BaseEcdsaUtils { throw new Error('Failed to parse cyphertext to JSON, got: ' + cyphertext); } // using decodeURIComponent to handle special characters - if (decodeURIComponent(cypherJson.adata) !== decodeURIComponent(adata)) { + if ( + decodeURIComponent(cypherJson.adata) !== decodeURIComponent(`${roundDomainSeparator}:${adata}`) && + decodeURIComponent(cypherJson.adata) !== decodeURIComponent(adata) + ) { throw new Error('Adata does not match cyphertext adata'); } } @@ -1128,13 +1138,17 @@ export class EcdsaMPCv2Utils extends BaseEcdsaUtils { const userSignerBroadcastMsg1 = await userSigner.init(); const signatureShareRound1 = await getSignatureShareRoundOne(userSignerBroadcastMsg1, userGpgKey); const session = userSigner.getSession(); - const encryptedRound1Session = this.bitgo.encrypt({ input: session, password: walletPassphrase, adata }); + const encryptedRound1Session = this.bitgo.encrypt({ + input: session, + password: walletPassphrase, + adata: `${EcdsaMPCv2Utils.DKLS23_SIGNING_ROUND1_STATE}:${adata}`, + }); const userGpgPubKey = userGpgKey.publicKey; const encryptedUserGpgPrvKey = this.bitgo.encrypt({ input: userGpgKey.privateKey, password: walletPassphrase, - adata, + adata: `${EcdsaMPCv2Utils.DKLS23_SIGNING_USER_GPG_KEY}:${adata}`, }); return { signatureShareRound1, userGpgPubKey, encryptedRound1Session, encryptedUserGpgPrvKey }; @@ -1159,7 +1173,8 @@ export class EcdsaMPCv2Utils extends BaseEcdsaUtils { const { bitgoGpgKey, userGpgKey } = await this.getBitgoAndUserGpgKeys( bitgoPublicGpgKey, encryptedUserGpgPrvKey, - walletPassphrase + walletPassphrase, + adata ); const signatureShares = txRequest.transactions?.[0].signatureShares; @@ -1176,9 +1191,9 @@ export class EcdsaMPCv2Utils extends BaseEcdsaUtils { bitgoGpgKey ); + this.validateAdata(adata, encryptedRound1Session, EcdsaMPCv2Utils.DKLS23_SIGNING_ROUND1_STATE); const round1Session = this.bitgo.decrypt({ input: encryptedRound1Session, password: walletPassphrase }); - this.validateAdata(adata, encryptedRound1Session); const userKeyShare = Buffer.from(prv, 'base64'); const userSigner = new DklsDsg.Dsg(userKeyShare, 0, derivationPath, hashBuffer); await userSigner.setSession(round1Session); @@ -1199,7 +1214,11 @@ export class EcdsaMPCv2Utils extends BaseEcdsaUtils { bitgoGpgKey ); const session = userSigner.getSession(); - const encryptedRound2Session = this.bitgo.encrypt({ input: session, password: walletPassphrase, adata }); + const encryptedRound2Session = this.bitgo.encrypt({ + input: session, + password: walletPassphrase, + adata: `${EcdsaMPCv2Utils.DKLS23_SIGNING_ROUND2_STATE}:${adata}`, + }); return { signatureShareRound2, @@ -1227,7 +1246,8 @@ export class EcdsaMPCv2Utils extends BaseEcdsaUtils { const { bitgoGpgKey, userGpgKey } = await this.getBitgoAndUserGpgKeys( bitgoPublicGpgKey, encryptedUserGpgPrvKey, - walletPassphrase + walletPassphrase, + adata ); const signatureShares = txRequest.transactions?.[0].signatureShares; @@ -1249,8 +1269,9 @@ export class EcdsaMPCv2Utils extends BaseEcdsaUtils { broadcastMessages: [], }); + this.validateAdata(adata, encryptedRound2Session, EcdsaMPCv2Utils.DKLS23_SIGNING_ROUND2_STATE); const round2Session = this.bitgo.decrypt({ input: encryptedRound2Session, password: walletPassphrase }); - this.validateAdata(adata, encryptedRound2Session); + const userKeyShare = Buffer.from(prv, 'base64'); const userSigner = new DklsDsg.Dsg(userKeyShare, 0, derivationPath, hashBuffer); await userSigner.setSession(round2Session); diff --git a/modules/sdk-core/test/unit/bitgo/utils/tss/ecdsa/ecdsaMPCv2.ts b/modules/sdk-core/test/unit/bitgo/utils/tss/ecdsa/ecdsaMPCv2.ts new file mode 100644 index 0000000000..4c2304b208 --- /dev/null +++ b/modules/sdk-core/test/unit/bitgo/utils/tss/ecdsa/ecdsaMPCv2.ts @@ -0,0 +1,608 @@ +import * as assert from 'assert'; +import * as sinon from 'sinon'; +import { Hash, randomBytes } from 'crypto'; +import createKeccakHash from 'keccak'; +import { + MPCv2PartyFromStringOrNumber, + MPCv2SignatureShareRound1Input, + MPCv2SignatureShareRound1Output, + MPCv2SignatureShareRound2Input, + MPCv2SignatureShareRound2Output, + MPCv2SignatureShareRound3Input, +} from '@bitgo/public-types'; +import { DklsComms, DklsDsg, DklsTypes, DklsUtils } from '@bitgo/sdk-lib-mpc'; +import * as sjcl from '@bitgo/sjcl'; +import { + BitGoBase, + EcdsaMPCv2Utils, + IBaseCoin, + SignatureShareRecord, + SignatureShareType, + TxRequest, +} from '../../../../../../src'; +import { bitgoGpgKey } from './gpgKeys'; + +describe('ECDSA MPC v2', async () => { + let userShare: Buffer; + let bitgoShare: Buffer; + + before('generate key shares for testing', async () => { + const [userDkgSession, backupDkgSession, bitgoDkgSession] = await DklsUtils.generateDKGKeyShares(); + assert.ok(userDkgSession); + assert.ok(backupDkgSession); + assert.ok(bitgoDkgSession); + + userShare = userDkgSession.getKeyShare(); + bitgoShare = bitgoDkgSession.getKeyShare(); + }); + + let ecdsaMPCv2Utils: EcdsaMPCv2Utils; + + before('initialize EcdsaMPCv2Utils', async () => { + const mockBg = {} as BitGoBase; + mockBg.getEnv = sinon.stub().returns('test'); + mockBg.encrypt = sinon.stub().callsFake((params) => { + const salt = randomBytes(8); + const iv = randomBytes(16); + return sjcl.encrypt(params.password, params.input, { + salt: [bytesToWord(salt.subarray(0, 4)), bytesToWord(salt.subarray(4))], + iv: [ + bytesToWord(iv.subarray(0, 4)), + bytesToWord(iv.subarray(4, 8)), + bytesToWord(iv.subarray(8, 12)), + bytesToWord(iv.subarray(12, 16)), + ], + adata: params.adata, + }); + }); + mockBg.decrypt = sinon.stub().callsFake((params) => { + return sjcl.decrypt(params.password, params.input); + }); + + const mockCoin = {} as IBaseCoin; + mockCoin.getHashFunction = sinon.stub().callsFake(() => createKeccakHash('keccak256') as Hash); + + ecdsaMPCv2Utils = new EcdsaMPCv2Utils(mockBg, mockCoin); + }); + + const walletID = '62fe536a6b4cf70007acb48c0e7bb0b0'; + const walletPassphrase = 'testPass'; + + it('should sign a message hash using ECDSA MPC v2 offline rounds', async () => { + const tMessage = 'testMessage'; + const derivationPath = 'm/0'; + + // round 1 + const reqMPCv2SigningRound1 = { + txRequest: { + txRequestId: '123456', + apiVersion: 'full', + walletId: walletID, + transactions: [ + { + unsignedTx: { + derivationPath, + signableHex: tMessage, + }, + signatureShares: [], + }, + ], + }, + prv: userShare.toString('base64'), + walletPassphrase, + }; + + const resMPCv2SigningRound1 = await ecdsaMPCv2Utils.createOfflineRound1Share(reqMPCv2SigningRound1 as any); + resMPCv2SigningRound1.should.have.property('signatureShareRound1'); + resMPCv2SigningRound1.should.have.property('userGpgPubKey'); + resMPCv2SigningRound1.should.have.property('encryptedRound1Session'); + resMPCv2SigningRound1.should.have.property('encryptedUserGpgPrvKey'); + + const encryptedRound1Session = resMPCv2SigningRound1.encryptedRound1Session; + const encryptedUserGpgPrvKey = resMPCv2SigningRound1.encryptedUserGpgPrvKey; + + const hashBuffer = createKeccakHash('keccak256').update(Buffer.from(tMessage, 'hex')).digest(); + const bitgoSession = new DklsDsg.Dsg(bitgoShare, 2, derivationPath, hashBuffer); + + const txRequestRound1 = await signBitgoMPCv2Round1( + bitgoSession, + reqMPCv2SigningRound1.txRequest as any, + resMPCv2SigningRound1.signatureShareRound1, + resMPCv2SigningRound1.userGpgPubKey + ); + assert.ok( + txRequestRound1.transactions && + txRequestRound1.transactions.length === 1 && + txRequestRound1.transactions[0].signatureShares.length === 2, + 'txRequestRound1.transactions is not an array of length 1 with 2 signatureShares' + ); + + // round 2 + const reqMPCv2SigningRound2 = { + ...reqMPCv2SigningRound1, + txRequest: txRequestRound1, + encryptedRound1Session, + encryptedUserGpgPrvKey, + bitgoPublicGpgKey: bitgoGpgKey.public, + }; + + const resMPCv2SigningRound2 = await ecdsaMPCv2Utils.createOfflineRound2Share(reqMPCv2SigningRound2 as any); + resMPCv2SigningRound2.should.have.property('signatureShareRound2'); + resMPCv2SigningRound2.should.have.property('encryptedRound2Session'); + + const encryptedRound2Session = resMPCv2SigningRound2.encryptedRound2Session; + + const { txRequest: txRequestRound2, bitgoMsg4 } = await signBitgoMPCv2Round2( + bitgoSession, + reqMPCv2SigningRound2.txRequest, + resMPCv2SigningRound2.signatureShareRound2, + resMPCv2SigningRound1.userGpgPubKey + ); + assert.ok( + txRequestRound2.transactions && + txRequestRound2.transactions.length === 1 && + txRequestRound2.transactions[0].signatureShares.length === 4, + 'txRequestRound2.transactions is not an array of length 1 with 4 signatureShares' + ); + bitgoMsg4.should.have.property('signatureR'); + + // round 3 + const reqMPCv2SigningRound3 = { + ...reqMPCv2SigningRound2, + txRequest: txRequestRound2, + encryptedRound1Session: null, // not needed for round 3 + encryptedRound2Session, + }; + + const resMPCv2SigningRound3 = await ecdsaMPCv2Utils.createOfflineRound3Share(reqMPCv2SigningRound3 as any); + resMPCv2SigningRound3.should.have.property('signatureShareRound3'); + + const { userMsg4 } = await signBitgoMPCv2Round3( + bitgoSession, + resMPCv2SigningRound3.signatureShareRound3, + resMPCv2SigningRound1.userGpgPubKey + ); + + // signature generation and validation + assert.ok(userMsg4.data.msg4.signatureR === bitgoMsg4.signatureR, 'User and BitGo signaturesR do not match'); + + const deserializedBitgoMsg4 = DklsTypes.deserializeMessages({ + p2pMessages: [], + broadcastMessages: [bitgoMsg4], + }); + + const deserializedUserMsg4 = DklsTypes.deserializeMessages({ + p2pMessages: [], + broadcastMessages: [ + { + from: userMsg4.data.msg4.from, + payload: userMsg4.data.msg4.message, + }, + ], + }); + + const combinedSigUsingUtil = DklsUtils.combinePartialSignatures( + [deserializedUserMsg4.broadcastMessages[0].payload, deserializedBitgoMsg4.broadcastMessages[0].payload], + Buffer.from(userMsg4.data.msg4.signatureR, 'base64').toString('hex') + ); + + const convertedSignature = DklsUtils.verifyAndConvertDklsSignature( + Buffer.from(tMessage, 'hex'), + combinedSigUsingUtil, + DklsTypes.getCommonKeychain(userShare), + derivationPath, + createKeccakHash('keccak256') as Hash + ); + assert.ok(convertedSignature, 'Signature is not valid'); + assert.ok(convertedSignature.split(':').length === 4, 'Signature is not valid'); + }); + + it('should fail to sign using session after round X when session after round Y is expected', async () => { + const tMessage = 'testMessage'; + const derivationPath = 'm/1/2'; + + // round 1 + const reqMPCv2SigningRound1 = { + txRequest: { + txRequestId: '123456', + apiVersion: 'full', + walletId: walletID, + transactions: [ + { + unsignedTx: { + derivationPath, + signableHex: tMessage, + }, + signatureShares: [], + }, + ], + }, + prv: userShare.toString('base64'), + walletPassphrase, + }; + + const resMPCv2SigningRound1 = await ecdsaMPCv2Utils.createOfflineRound1Share(reqMPCv2SigningRound1 as any); + resMPCv2SigningRound1.should.have.property('signatureShareRound1'); + resMPCv2SigningRound1.should.have.property('userGpgPubKey'); + resMPCv2SigningRound1.should.have.property('encryptedRound1Session'); + resMPCv2SigningRound1.should.have.property('encryptedUserGpgPrvKey'); + + const encryptedRound1Session = resMPCv2SigningRound1.encryptedRound1Session; + const encryptedUserGpgPrvKey = resMPCv2SigningRound1.encryptedUserGpgPrvKey; + + const hashBuffer = createKeccakHash('keccak256').update(Buffer.from(tMessage, 'hex')).digest(); + const bitgoSession = new DklsDsg.Dsg(bitgoShare, 2, derivationPath, hashBuffer); + + const txRequestRound1 = await signBitgoMPCv2Round1( + bitgoSession, + reqMPCv2SigningRound1.txRequest as any, + resMPCv2SigningRound1.signatureShareRound1, + resMPCv2SigningRound1.userGpgPubKey + ); + assert.ok( + txRequestRound1.transactions && + txRequestRound1.transactions.length === 1 && + txRequestRound1.transactions[0].signatureShares.length === 2, + 'txRequestRound1.transactions is not an array of length 1 with 2 signatureShares' + ); + + // round 2 + const reqMPCv2SigningRound2 = { + ...reqMPCv2SigningRound1, + txRequest: txRequestRound1, + encryptedRound1Session, + encryptedUserGpgPrvKey, + bitgoPublicGpgKey: bitgoGpgKey.public, + }; + + const resMPCv2SigningRound2 = await ecdsaMPCv2Utils.createOfflineRound2Share(reqMPCv2SigningRound2 as any); + resMPCv2SigningRound2.should.have.property('signatureShareRound2'); + resMPCv2SigningRound2.should.have.property('encryptedRound2Session'); + + const { txRequest: txRequestRound2, bitgoMsg4 } = await signBitgoMPCv2Round2( + bitgoSession, + reqMPCv2SigningRound2.txRequest, + resMPCv2SigningRound2.signatureShareRound2, + resMPCv2SigningRound1.userGpgPubKey + ); + assert.ok( + txRequestRound2.transactions && + txRequestRound2.transactions.length === 1 && + txRequestRound2.transactions[0].signatureShares.length === 4, + 'txRequestRound2.transactions is not an array of length 1 with 4 signatureShares' + ); + bitgoMsg4.should.have.property('signatureR'); + + const encryptedRound2Session = resMPCv2SigningRound2.encryptedRound2Session; + + // A bogus round 3 signing request containing encrypted session from round 1 instead of round 2 should fail. + const bogusReqMPCv2SigningRound3 = { + ...reqMPCv2SigningRound2, + txRequest: txRequestRound2, + encryptedRound1Session: null, // not needed for round 3 + encryptedRound2Session: encryptedRound1Session, // instaed of encryptedRound2Session + }; + + await ecdsaMPCv2Utils + .createOfflineRound3Share(bogusReqMPCv2SigningRound3 as any) + .should.be.rejectedWith('Adata does not match cyphertext adata'); + + // A bogus round 2 signing request containing encrypted session from round 2 instead of round 1 should fail. + const bogusReqMPCv2SigningRound2 = { + ...reqMPCv2SigningRound2, + encryptedRound1Session: encryptedRound2Session, // instaed of encryptedRound1Session + }; + + await ecdsaMPCv2Utils + .createOfflineRound2Share(bogusReqMPCv2SigningRound2 as any) + .should.be.rejectedWith('Unexpected signature share response. Unable to parse data.'); + }); + + it('should fail to sign reusing a session on different message', async () => { + const tMessage1 = 'testMessage1'; + const tMessage2 = 'testMessage2'; + const derivationPath = 'm/3/4'; + + // round 1 of signing tMessage1 + const reqMPCv2SigningMsg1Round1 = { + txRequest: { + txRequestId: '123456', + apiVersion: 'full', + walletId: walletID, + transactions: [ + { + unsignedTx: { + derivationPath, + signableHex: tMessage1, + }, + signatureShares: [], + }, + ], + }, + prv: userShare.toString('base64'), + walletPassphrase, + }; + + const resMPCv2SigningMsg1Round1 = await ecdsaMPCv2Utils.createOfflineRound1Share(reqMPCv2SigningMsg1Round1 as any); + resMPCv2SigningMsg1Round1.should.have.property('signatureShareRound1'); + resMPCv2SigningMsg1Round1.should.have.property('userGpgPubKey'); + resMPCv2SigningMsg1Round1.should.have.property('encryptedRound1Session'); + resMPCv2SigningMsg1Round1.should.have.property('encryptedUserGpgPrvKey'); + + const encryptedMsg1Round1Session = resMPCv2SigningMsg1Round1.encryptedRound1Session; + + const hashBuffer1 = createKeccakHash('keccak256').update(Buffer.from(tMessage1, 'hex')).digest(); + const bitgoSession1 = new DklsDsg.Dsg(bitgoShare, 2, derivationPath, hashBuffer1); + + const txRequestMsg1Round1 = await signBitgoMPCv2Round1( + bitgoSession1, + reqMPCv2SigningMsg1Round1.txRequest as any, + resMPCv2SigningMsg1Round1.signatureShareRound1, + resMPCv2SigningMsg1Round1.userGpgPubKey + ); + assert.ok( + txRequestMsg1Round1.transactions && + txRequestMsg1Round1.transactions.length === 1 && + txRequestMsg1Round1.transactions[0].signatureShares.length === 2, + 'txRequestMsg1Round1.transactions is not an array of length 1 with 2 signatureShares' + ); + + // round 1 of signing tMessage2 + const reqMPCv2SigningMsg2Round1 = { + ...reqMPCv2SigningMsg1Round1, + txRequest: { + ...reqMPCv2SigningMsg1Round1.txRequest, + transactions: [ + { + unsignedTx: { + derivationPath, + signableHex: tMessage2, + }, + signatureShares: [], + }, + ], + }, + }; + + const resMPCv2SigningMsg2Round1 = await ecdsaMPCv2Utils.createOfflineRound1Share(reqMPCv2SigningMsg2Round1 as any); + resMPCv2SigningMsg2Round1.should.have.property('signatureShareRound1'); + resMPCv2SigningMsg2Round1.should.have.property('userGpgPubKey'); + resMPCv2SigningMsg2Round1.should.have.property('encryptedRound1Session'); + resMPCv2SigningMsg2Round1.should.have.property('encryptedUserGpgPrvKey'); + + const encryptedMsg2UserGpgPrvKey = resMPCv2SigningMsg2Round1.encryptedUserGpgPrvKey; + + const hashBuffer2 = createKeccakHash('keccak256').update(Buffer.from(tMessage2, 'hex')).digest(); + const bitgoSession2 = new DklsDsg.Dsg(bitgoShare, 2, derivationPath, hashBuffer2); + + const txRequestMsg2Round1 = await signBitgoMPCv2Round1( + bitgoSession2, + reqMPCv2SigningMsg2Round1.txRequest as any, + resMPCv2SigningMsg2Round1.signatureShareRound1, + resMPCv2SigningMsg2Round1.userGpgPubKey + ); + assert.ok( + txRequestMsg2Round1.transactions && + txRequestMsg2Round1.transactions.length === 1 && + txRequestMsg2Round1.transactions[0].signatureShares.length === 2, + 'txRequestMsg2Round1.transactions is not an array of length 1 with 2 signatureShares' + ); + + // Attempting to reuse round 1 session from signing tMessage1 for signing tMessage2 should fail at round 2. + const reqMPCv2SigningMsg2Round2WithMsg1Session = { + ...reqMPCv2SigningMsg2Round1, + txRequest: txRequestMsg2Round1, + encryptedRound1Session: encryptedMsg1Round1Session, // instead of resMPCv2SigningMsg2Round1.encryptedRound1Session + encryptedUserGpgPrvKey: encryptedMsg2UserGpgPrvKey, + bitgoPublicGpgKey: bitgoGpgKey.public, + }; + + await ecdsaMPCv2Utils + .createOfflineRound2Share(reqMPCv2SigningMsg2Round2WithMsg1Session as any) + .should.be.rejectedWith('Error while creating messages from party 0, round 2: Error: Invalid final_session_id'); + }); +}); + +function bytesToWord(bytes?: Uint8Array | number[]): number { + if (!(bytes instanceof Uint8Array) || bytes.length !== 4) { + throw new Error('bytes must be a Uint8Array with length 4'); + } + + return bytes.reduce((num, byte) => num * 0x100 + byte, 0); +} + +function getUserPartyGpgKeyPublic(userPubKey: string): DklsTypes.PartyGpgKey { + return { + partyId: 0, + gpgKey: userPubKey, + }; +} + +function getBitGoPartyGpgKeyPrv(bitgoPrvKey: string): DklsTypes.PartyGpgKey { + return { + partyId: 2, + gpgKey: bitgoPrvKey, + }; +} + +async function signBitgoMPCv2Round1( + bitgoSession: DklsDsg.Dsg, + txRequest: TxRequest, + userShare: SignatureShareRecord, + userGPGPubKey: string +): Promise { + assert.ok( + txRequest.transactions && txRequest.transactions.length === 1, + 'txRequest.transactions is not an array of length 1' + ); + txRequest.transactions[0].signatureShares.push(userShare); + // Do the actual signing on BitGo's side based on User's messages + const signatureShare = JSON.parse(userShare.share) as MPCv2SignatureShareRound1Input; + const deserializedMessages = DklsTypes.deserializeMessages({ + p2pMessages: [], + broadcastMessages: [ + { + from: signatureShare.data.msg1.from, + payload: signatureShare.data.msg1.message, + }, + ], + }); + const bitgoToUserRound1BroadcastMsg = await bitgoSession.init(); + const bitgoToUserRound2Msg = bitgoSession.handleIncomingMessages({ + p2pMessages: [], + broadcastMessages: deserializedMessages.broadcastMessages, + }); + const serializedBitGoToUserRound1And2Msgs = DklsTypes.serializeMessages({ + p2pMessages: bitgoToUserRound2Msg.p2pMessages, + broadcastMessages: [bitgoToUserRound1BroadcastMsg], + }); + + const authEncMessages = await DklsComms.encryptAndAuthOutgoingMessages( + serializedBitGoToUserRound1And2Msgs, + [getUserPartyGpgKeyPublic(userGPGPubKey)], + [getBitGoPartyGpgKeyPrv(bitgoGpgKey.private)] + ); + + const bitgoToUserSignatureShare: MPCv2SignatureShareRound1Output = { + type: 'round1Output', + data: { + msg1: { + from: authEncMessages.broadcastMessages[0].from as MPCv2PartyFromStringOrNumber, + signature: authEncMessages.broadcastMessages[0].payload.signature, + message: authEncMessages.broadcastMessages[0].payload.message, + }, + msg2: { + from: authEncMessages.p2pMessages[0].from as MPCv2PartyFromStringOrNumber, + to: authEncMessages.p2pMessages[0].to as MPCv2PartyFromStringOrNumber, + encryptedMessage: authEncMessages.p2pMessages[0].payload.encryptedMessage, + signature: authEncMessages.p2pMessages[0].payload.signature, + }, + }, + }; + txRequest.transactions[0].signatureShares.push({ + from: SignatureShareType.BITGO, + to: SignatureShareType.USER, + share: JSON.stringify(bitgoToUserSignatureShare), + }); + return txRequest; +} + +async function signBitgoMPCv2Round2( + bitgoSession: DklsDsg.Dsg, + txRequest: TxRequest, + userShare: SignatureShareRecord, + userGPGPubKey: string +): Promise<{ txRequest: TxRequest; bitgoMsg4: DklsTypes.SerializedBroadcastMessage }> { + assert.ok( + txRequest.transactions && txRequest.transactions.length === 1, + 'txRequest.transactions is not an array of length 1' + ); + txRequest.transactions[0].signatureShares.push(userShare); + + // Do the actual signing on BitGo's side based on User's messages + const parsedSignatureShare = JSON.parse(userShare.share) as MPCv2SignatureShareRound2Input; + const serializedMessages = await DklsComms.decryptAndVerifyIncomingMessages( + { + p2pMessages: [ + { + from: parsedSignatureShare.data.msg2.from, + to: parsedSignatureShare.data.msg2.to, + payload: { + encryptedMessage: parsedSignatureShare.data.msg2.encryptedMessage, + signature: parsedSignatureShare.data.msg2.signature, + }, + }, + { + from: parsedSignatureShare.data.msg3.from, + to: parsedSignatureShare.data.msg3.to, + payload: { + encryptedMessage: parsedSignatureShare.data.msg3.encryptedMessage, + signature: parsedSignatureShare.data.msg3.signature, + }, + }, + ], + broadcastMessages: [], + }, + [getUserPartyGpgKeyPublic(userGPGPubKey)], + [getBitGoPartyGpgKeyPrv(bitgoGpgKey.private)] + ); + const deserializedMessages2 = DklsTypes.deserializeMessages({ + p2pMessages: [serializedMessages.p2pMessages[0]], + broadcastMessages: [], + }); + + const bitgoToUserRound3Msg = bitgoSession.handleIncomingMessages(deserializedMessages2); + const serializedBitGoToUserRound3Msgs = DklsTypes.serializeMessages(bitgoToUserRound3Msg); + + const authEncMessages = await DklsComms.encryptAndAuthOutgoingMessages( + serializedBitGoToUserRound3Msgs, + [getUserPartyGpgKeyPublic(userGPGPubKey)], + [getBitGoPartyGpgKeyPrv(bitgoGpgKey.private)] + ); + + const bitgoToUserSignatureShare: MPCv2SignatureShareRound2Output = { + type: 'round2Output', + data: { + msg3: { + from: authEncMessages.p2pMessages[0].from as MPCv2PartyFromStringOrNumber, + to: authEncMessages.p2pMessages[0].to as MPCv2PartyFromStringOrNumber, + encryptedMessage: authEncMessages.p2pMessages[0].payload.encryptedMessage, + signature: authEncMessages.p2pMessages[0].payload.signature, + }, + }, + }; + + // handling user msg3 but not returning bitgo msg4 since its stored on bitgo side only + const deserializedMessages3 = DklsTypes.deserializeMessages({ + p2pMessages: [serializedMessages.p2pMessages[1]], + broadcastMessages: [], + }); + const deserializedBitgoMsg4 = bitgoSession.handleIncomingMessages(deserializedMessages3); + const serializedBitGoToUserRound4Msgs = DklsTypes.serializeMessages(deserializedBitgoMsg4); + + txRequest.transactions[0].signatureShares.push({ + from: SignatureShareType.BITGO, + to: SignatureShareType.USER, + share: JSON.stringify(bitgoToUserSignatureShare), + }); + return { txRequest, bitgoMsg4: serializedBitGoToUserRound4Msgs.broadcastMessages[0] }; +} + +async function signBitgoMPCv2Round3( + bitgoSession: DklsDsg.Dsg, + userShare: SignatureShareRecord, + userGPGPubKey: string +): Promise<{ userMsg4: MPCv2SignatureShareRound3Input }> { + const parsedSignatureShare = JSON.parse(userShare.share) as MPCv2SignatureShareRound3Input; + const msg4 = parsedSignatureShare.data.msg4; + const signatureRAuthMessage = + msg4.signatureR && msg4.signatureRSignature + ? { message: msg4.signatureR, signature: msg4.signatureRSignature } + : undefined; + const serializedMessages = await DklsComms.decryptAndVerifyIncomingMessages( + { + p2pMessages: [], + broadcastMessages: [ + { + from: msg4.from, + payload: { + message: msg4.message, + signature: msg4.signature, + }, + signatureR: signatureRAuthMessage, + }, + ], + }, + [getUserPartyGpgKeyPublic(userGPGPubKey)], + [getBitGoPartyGpgKeyPrv(bitgoGpgKey.private)] + ); + const deserializedMessages = DklsTypes.deserializeMessages({ + p2pMessages: [], + broadcastMessages: [serializedMessages.broadcastMessages[0]], + }); + bitgoSession.handleIncomingMessages(deserializedMessages); + + return { + userMsg4: parsedSignatureShare, + }; +} diff --git a/modules/sdk-core/test/unit/bitgo/utils/tss/ecdsa/gpgKeys.ts b/modules/sdk-core/test/unit/bitgo/utils/tss/ecdsa/gpgKeys.ts new file mode 100644 index 0000000000..a95732c559 --- /dev/null +++ b/modules/sdk-core/test/unit/bitgo/utils/tss/ecdsa/gpgKeys.ts @@ -0,0 +1,34 @@ +export const bitgoGpgKey = { + private: + '-----BEGIN PGP PRIVATE KEY BLOCK-----\n' + + '\n' + + 'xXQEZo2rshMFK4EEAAoCAwQC6HQa7PXiX2nnpZr/asCcEbgCOcjsR8gcSI8v\n' + + 'vMADk59KsFweg+kIzCR3UqfMe2uG6JHwOYpvDREHp/hqtA+hAAD/XgjiTu4D\n' + + '0d9YzSx3ZP8lUAcruvJbyMIIlr26QIeHq5kRQM0FYml0Z2/CjAQQEwgAPgWC\n' + + 'Zo2rsgQLCQcICZA8EZGJCCwPOAMVCAoEFgACAQIZAQKbAwIeARYhBLSGUeOq\n' + + 'bM5ym4aSnjwRkYkILA84AABXoQD+KkO5kWGw8GgWN142t+pGULPLzGo6353r\n' + + 'H8FwgKxe9ikBAKEjJI17aVlozG0RzFVxctBLLVqjYO5tBZQhoQbHHkGdx3gE\n' + + 'Zo2rshIFK4EEAAoCAwTBwmMa+htUmjUoqlKTuQaoWcY0Det+ee/6fV9+vnis\n' + + 'EyphRUFXnA0K0LyGpSnNlqKisSoArwUkZTiWwTbMWjTdAwEIBwABAJmAlxnB\n' + + 'IZ5bw88Duvw0yaRRcgXt5tDP0z23l6cvJWgKEJbCeAQYEwgAKgWCZo2rsgmQ\n' + + 'PBGRiQgsDzgCmwwWIQS0hlHjqmzOcpuGkp48EZGJCCwPOAAA3/4BAIozuxF1\n' + + 'JEoSQXe8YFIFqowwCiVwr2K6NqqRn+mGM1NjAQCYWIsZq+4+UCBIKScVknTG\n' + + 'uu2Utd5ZMyNYZTWCxLk9+g==\n' + + '=iXOB\n' + + '-----END PGP PRIVATE KEY BLOCK-----\n', + public: + '-----BEGIN PGP PUBLIC KEY BLOCK-----\n' + + '\n' + + 'xk8EZo2rshMFK4EEAAoCAwQC6HQa7PXiX2nnpZr/asCcEbgCOcjsR8gcSI8v\n' + + 'vMADk59KsFweg+kIzCR3UqfMe2uG6JHwOYpvDREHp/hqtA+hzQViaXRnb8KM\n' + + 'BBATCAA+BYJmjauyBAsJBwgJkDwRkYkILA84AxUICgQWAAIBAhkBApsDAh4B\n' + + 'FiEEtIZR46psznKbhpKePBGRiQgsDzgAAFehAP4qQ7mRYbDwaBY3Xja36kZQ\n' + + 's8vMajrfnesfwXCArF72KQEAoSMkjXtpWWjMbRHMVXFy0EstWqNg7m0FlCGh\n' + + 'BsceQZ3OUwRmjauyEgUrgQQACgIDBMHCYxr6G1SaNSiqUpO5BqhZxjQN6355\n' + + '7/p9X36+eKwTKmFFQVecDQrQvIalKc2WoqKxKgCvBSRlOJbBNsxaNN0DAQgH\n' + + 'wngEGBMIACoFgmaNq7IJkDwRkYkILA84ApsMFiEEtIZR46psznKbhpKePBGR\n' + + 'iQgsDzgAAN/+AQCKM7sRdSRKEkF3vGBSBaqMMAolcK9iujaqkZ/phjNTYwEA\n' + + 'mFiLGavuPlAgSCknFZJ0xrrtlLXeWTMjWGU1gsS5Pfo=\n' + + '=7uRX\n' + + '-----END PGP PUBLIC KEY BLOCK-----\n', +}; From 5f0d1edcfa2021ff4580d41730b955f96a530921 Mon Sep 17 00:00:00 2001 From: Mohammad Al Faiyaz Date: Tue, 28 Apr 2026 15:43:01 -0400 Subject: [PATCH 05/13] fix(abstract-utxo): guard address in checkRecipient for OP_RETURN outputs What changed: - make address optional in checkRecipient parameter type - add recipient.address guard before calling isScriptRecipient - pass explicit object to super.checkRecipient to satisfy its address: string type - add unit tests covering OP_RETURN and script-prefixed recipient cases Why: OP_RETURN recipients have no address field at runtime; calling isScriptRecipient(undefined) unconditionally invoked undefined.toLowerCase() causing a crash when approving pending approvals containing OP_RETURN outputs. TICKET: WAL-826 Co-Authored-By: Claude Sonnet 4.6 --- modules/abstract-utxo/src/abstractUtxoCoin.ts | 6 +-- .../test/unit/transaction/recipient.ts | 39 +++++++++++++++++++ 2 files changed, 42 insertions(+), 3 deletions(-) create mode 100644 modules/abstract-utxo/test/unit/transaction/recipient.ts diff --git a/modules/abstract-utxo/src/abstractUtxoCoin.ts b/modules/abstract-utxo/src/abstractUtxoCoin.ts index b5d04a47a7..6248c96e08 100644 --- a/modules/abstract-utxo/src/abstractUtxoCoin.ts +++ b/modules/abstract-utxo/src/abstractUtxoCoin.ts @@ -572,10 +572,10 @@ export abstract class AbstractUtxoCoin return (chainhead as any).height; } - checkRecipient(recipient: { address: string; amount: number | string }): void { + checkRecipient(recipient: { address?: string; amount: number | string }): void { assertValidTransactionRecipient(recipient); - if (!isScriptRecipient(recipient.address)) { - super.checkRecipient(recipient); + if (recipient.address && !isScriptRecipient(recipient.address)) { + super.checkRecipient({ address: recipient.address, amount: recipient.amount }); } } diff --git a/modules/abstract-utxo/test/unit/transaction/recipient.ts b/modules/abstract-utxo/test/unit/transaction/recipient.ts new file mode 100644 index 0000000000..1dbf838fba --- /dev/null +++ b/modules/abstract-utxo/test/unit/transaction/recipient.ts @@ -0,0 +1,39 @@ +import assert from 'assert'; + +import { getUtxoCoin } from '../util/utxoCoins'; + +describe('AbstractUtxoCoin.checkRecipient', function () { + const coin = getUtxoCoin('btc'); + + it('does not throw for OP_RETURN output with no address field', function () { + // Simulates { amount: '0', script: '6a0c...' } coming from buildParams.recipients + assert.doesNotThrow(() => { + coin.checkRecipient({ amount: '0' }); + }); + }); + + it('does not throw for script-prefixed address with zero amount', function () { + assert.doesNotThrow(() => { + coin.checkRecipient({ address: 'scriptPubKey:6a0c68656c6c6f20776f726c64', amount: '0' }); + }); + }); + + it('does not throw for a regular address', function () { + // A valid mainnet P2PKH address + assert.doesNotThrow(() => { + coin.checkRecipient({ address: '1A1zP1eP5QGefi2DMPTfTL5SLmv7Divf', amount: '1000' }); + }); + }); + + it('throws when OP_RETURN output (no address) has non-zero amount', function () { + assert.throws(() => { + coin.checkRecipient({ amount: '1000' }); + }, /Only zero amounts allowed for non-encodeable scriptPubkeys/); + }); + + it('throws when script-prefixed address has non-zero amount', function () { + assert.throws(() => { + coin.checkRecipient({ address: 'scriptPubKey:6a0c68656c6c6f20776f726c64', amount: '500' }); + }, /Only zero amounts allowed for non-encodeable scriptPubkeys/); + }); +}); From c474524fc4cc5f2903ccd84a4249bca4e3a4a45d Mon Sep 17 00:00:00 2001 From: Marzooqa Kather Date: Tue, 28 Apr 2026 10:35:08 +0000 Subject: [PATCH 06/13] fix(sdk-lib-mpc): use @bitgo/wasm-mps/web in browser for EdDSA DKG MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In browsers (webpack), importing @bitgo/wasm-mps resolves to the --target bundler ESM build which has a race condition between webpack's async WASM instantiation and __wbg_set_wasm being called. This causes wasm.ed25519_dkg_round0_process to be undefined when initDkg() runs. Mirror the ECDSA pattern in ecdsa-dkls/dkg.ts: detect browser via `typeof window !== 'undefined'` (excluding Electron via window.process checks), then use @bitgo/wasm-mps/web which exposes an explicit init() function — guaranteed ready after await. Adds a local .d.ts shim for @bitgo/wasm-mps/web until WCI-250 publishes the ./web subpath export with its own types. Ticket: WCI-251 --- modules/sdk-lib-mpc/package.json | 2 +- modules/sdk-lib-mpc/src/tss/eddsa-mps/dkg.ts | 20 ++++++++++++++++++-- webpack/bitgojs.config.js | 1 + yarn.lock | 8 ++++---- 4 files changed, 24 insertions(+), 7 deletions(-) diff --git a/modules/sdk-lib-mpc/package.json b/modules/sdk-lib-mpc/package.json index 6bf73ee07b..5fd84ba37d 100644 --- a/modules/sdk-lib-mpc/package.json +++ b/modules/sdk-lib-mpc/package.json @@ -36,7 +36,7 @@ ] }, "dependencies": { - "@bitgo/wasm-mps": "1.7.0", + "@bitgo/wasm-mps": "1.8.1", "@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", diff --git a/modules/sdk-lib-mpc/src/tss/eddsa-mps/dkg.ts b/modules/sdk-lib-mpc/src/tss/eddsa-mps/dkg.ts index 6845f78a6e..d59e047235 100644 --- a/modules/sdk-lib-mpc/src/tss/eddsa-mps/dkg.ts +++ b/modules/sdk-lib-mpc/src/tss/eddsa-mps/dkg.ts @@ -3,7 +3,9 @@ import { encode } from 'cbor-x'; import crypto from 'crypto'; import { DeserializedMessage, DeserializedMessages, DkgState, EddsaReducedKeyShare } from './types'; -type WasmMps = typeof import('@bitgo/wasm-mps'); +type NodeWasmer = typeof import('@bitgo/wasm-mps'); +type WebWasmer = typeof import('@bitgo/wasm-mps/web'); +type WasmMps = NodeWasmer | WebWasmer; /** * EdDSA Distributed Key Generation (DKG) implementation using @bitgo/wasm-mps. @@ -53,7 +55,21 @@ export class DKG { private async loadWasmMps(): Promise { if (!this.wasmMps) { - this.wasmMps = await import('@bitgo/wasm-mps'); + if ( + typeof window !== 'undefined' && + /* checks for electron processes */ + !window.process && + !window.process?.['type'] + ) { + // Browser: web build has explicit init() — guaranteed ready after await + // eslint-disable-next-line import/no-internal-modules -- @bitgo/wasm-mps exposes environment-specific subpath exports. + const webWasm = await import('@bitgo/wasm-mps/web'); + await webWasm.default(); + this.wasmMps = webWasm; + } else { + // Node.js: dynamic import() rewritten to require() by tsc → CJS build → readFileSync + this.wasmMps = await import('@bitgo/wasm-mps'); + } } } diff --git a/webpack/bitgojs.config.js b/webpack/bitgojs.config.js index 94fbfec112..b40af33464 100644 --- a/webpack/bitgojs.config.js +++ b/webpack/bitgojs.config.js @@ -22,6 +22,7 @@ module.exports = { '@bitgo/wasm-ton': path.resolve('../../node_modules/@bitgo/wasm-ton/dist/esm/js/index.js'), '@bitgo/wasm-utxo': path.resolve('../../node_modules/@bitgo/wasm-utxo/dist/esm/js/index.js'), '@bitgo/wasm-solana': path.resolve('../../node_modules/@bitgo/wasm-solana/dist/esm/js/index.js'), + '@bitgo/wasm-mps/web': path.resolve('../../node_modules/@bitgo/wasm-mps/dist/web/js/wasm/wasm_mps.js'), '@bitgo/wasm-mps': path.resolve('../../node_modules/@bitgo/wasm-mps/dist/esm/js/wasm/wasm_mps.js'), '@bitgo/utxo-ord': path.resolve('../utxo-ord/dist/esm/index.js'), }, diff --git a/yarn.lock b/yarn.lock index b7906a70c8..797232913e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1015,10 +1015,10 @@ resolved "https://registry.npmjs.org/@bitgo/wasm-dot/-/wasm-dot-1.7.0.tgz" integrity sha512-KoXavJvyDHlEN+sWcigbgxYJtdFaU7gS0EkYQbNH4npVjNlzo6rL6gwjyWbyOy7oEs65DhpJ9vY5kRbE/bKiTQ== -"@bitgo/wasm-mps@1.7.0": - version "1.7.0" - resolved "https://registry.npmjs.org/@bitgo/wasm-mps/-/wasm-mps-1.7.0.tgz#e7ebca1afd2df757e69c5cdac702d6a06156867c" - integrity sha512-SNO7as4UvnE2ptDXp1oUXjABA8Y3/71lgVpAQyAGSfSaURjz4rG19+JZR54GBRIaA6hvUPr029b4gFyqoZPcgg== +"@bitgo/wasm-mps@1.8.1": + version "1.8.1" + resolved "https://registry.npmjs.org/@bitgo/wasm-mps/-/wasm-mps-1.8.1.tgz#946673f5845696cdcf744f8122fd1fc2be3edce1" + integrity sha512-CV8EXYc1BGYtXdCRDxJ5h04nj/LpMgu3VlkfowlodI6UKcj1zotAvk4OMIdgiPPbKVr1l+xibHDXZYx/uf3rnw== "@bitgo/wasm-solana@^2.6.0": version "2.6.0" From c0dbca6f49172d762b159eab123667d10273c6df Mon Sep 17 00:00:00 2001 From: Zahin Mohammad Date: Tue, 28 Apr 2026 18:47:52 -0400 Subject: [PATCH 07/13] feat(root): add recovery-mode to npmjs-release workflow Adds a `recovery-mode` workflow_dispatch input that publishes only the versions missing from npm (via `lerna publish from-package`) when a prior release left npm in a partial-publish state. Pins the checkout to a SHA captured by a pre-flight job so the publish cannot drift from what the env reviewer approved, asserts the SHA up front, and verifies post-publish that every public package version on rel/latest is now on the registry. Ticket: WCN-308 --- .github/workflows/npmjs-release.yml | 120 +++++++++++++++++++++++++++- 1 file changed, 117 insertions(+), 3 deletions(-) diff --git a/.github/workflows/npmjs-release.yml b/.github/workflows/npmjs-release.yml index 1300414e0a..b44151e247 100644 --- a/.github/workflows/npmjs-release.yml +++ b/.github/workflows/npmjs-release.yml @@ -10,12 +10,33 @@ on: type: boolean required: false default: false + recovery-mode: + description: | + Recover from a partial-publish failure. Skips version bumping, + master→rel/latest merge, and GPG signing. Runs `lerna publish + from-package` against rel/latest, publishing only versions + missing from npm. Release notes, GitHub release, and Express + Docker publish still run. + + IMPORTANT: from-package publishes whatever versions are in the + rel/latest package.json files at trigger time. Verify rel/latest + HEAD matches the failed release before triggering — the workflow + logs the resolved SHA and the planned publish list. + type: boolean + required: false + default: false permissions: contents: write id-token: write pull-requests: read +# Prevent overlapping releases. workflow_dispatch runs are serialized; +# a normal release and a recovery run cannot race against rel/latest. +concurrency: + group: npmjs-release + cancel-in-progress: false + env: NX_NO_CLOUD: true NX_SKIP_NX_CACHE: true @@ -24,6 +45,7 @@ env: jobs: get-release-context: name: Get release context + if: ${{ !inputs.recovery-mode }} runs-on: ${{ vars.BASE_RUNNER_TYPE || 'ubuntu-latest' }} timeout-minutes: 10 outputs: @@ -109,10 +131,58 @@ jobs: echo "" >> "$GITHUB_STEP_SUMMARY" + get-recovery-context: + name: Get recovery context + if: inputs.recovery-mode + runs-on: ${{ vars.BASE_RUNNER_TYPE || 'ubuntu-latest' }} + timeout-minutes: 10 + outputs: + # Pinned SHA. release-bitgojs checks out this exact commit, not the + # rel/latest branch tip, so the publish cannot drift from what the + # env reviewer approved. + sha: ${{ steps.resolve.outputs.sha }} + steps: + - name: Checkout rel/latest + uses: actions/checkout@v6 + with: + ref: rel/latest + fetch-depth: 1 + + - name: Resolve SHA and show recovery target + id: resolve + run: | + # Pin the SHA at preview time and surface it (plus the planned + # publish list) BEFORE the env-gated publish job runs, so + # reviewers approving the `npmjs-release` environment can + # sanity-check what will be published. + sha="$(git rev-parse HEAD)" + if [ -z "$sha" ]; then + echo "::error::Failed to resolve rel/latest SHA. Refusing to proceed." + exit 1 + fi + echo "sha=$sha" >> "$GITHUB_OUTPUT" + { + echo "## Recovery target" + echo "" + echo "Branch: \`rel/latest\`" + echo "Resolved SHA: \`$sha\`" + echo "Subject: $(git log -1 --pretty=format:'%s')" + echo "" + echo "### Versions in rel/latest package.jsons" + echo "" + echo '```' + for f in modules/*/package.json; do + jq -r '"\(.name)@\(.version)\(if .private then " (private)" else "" end)"' "$f" + done | sort + echo '```' + } >> "$GITHUB_STEP_SUMMARY" + release-bitgojs: name: Release BitGoJS needs: - get-release-context + - get-recovery-context + if: ${{ always() && needs.get-release-context.result != 'failure' && needs.get-recovery-context.result != 'failure' }} runs-on: ${{ vars.BASE_RUNNER_TYPE || 'ubuntu-latest' }} timeout-minutes: 60 environment: npmjs-release @@ -120,12 +190,20 @@ jobs: - name: Checkout repository uses: actions/checkout@v6 with: - ref: ${{ needs.get-release-context.outputs.current-master-sha }} + # Recovery mode pins to the SHA resolved by get-recovery-context so + # the publish cannot drift from the commit the env reviewer approved + # (rel/latest could otherwise advance during the approval wait). + # Normal mode uses the master SHA captured by get-release-context. + ref: ${{ inputs.recovery-mode && needs.get-recovery-context.outputs.sha || needs.get-release-context.outputs.current-master-sha }} token: ${{ secrets.BITGOBOT_PAT_TOKEN || github.token }} fetch-depth: 0 + # version-bump-summary uses `git tag --points-at HEAD`. In recovery + # mode the bump tags were created by a prior failed run and live + # only on origin, so we must fetch them. + fetch-tags: true - name: Configure GPG - if: inputs.dry-run == false + if: ${{ inputs.dry-run == false && !inputs.recovery-mode }} run: | echo "${{ secrets.BITGOBOT_GPG_PRIVATE_KEY }}" | gpg --batch --import git config --global user.signingkey 67A9A0B77F0BD445E45CC8B719828A304678A92F @@ -143,11 +221,13 @@ jobs: node-version-file: ".nvmrc" - name: Switch to rel/latest branch + if: ${{ !inputs.recovery-mode }} run: | git checkout rel/latest git pull origin rel/latest - name: Merge master into rel/latest + if: ${{ !inputs.recovery-mode }} run: | echo "Merging master commit ${{ needs.get-release-context.outputs.current-master-sha }} into rel/latest" git merge ${{ needs.get-release-context.outputs.current-master-sha }} --no-edit @@ -171,12 +251,46 @@ jobs: uses: ./.github/actions/verify-npm-packages - name: Publish new version - if: inputs.dry-run == false + if: ${{ inputs.dry-run == false && !inputs.recovery-mode }} run: | yarn lerna publish --sign-git-tag --sign-git-commit --include-merged-tags --conventional-commits --conventional-graduate --yes env: NPM_CONFIG_PROVENANCE: true + - name: Publish missing versions (recovery) + if: ${{ inputs.dry-run == false && inputs.recovery-mode }} + run: | + # `from-package` reads each package.json's `version`, queries npm, + # and publishes only versions missing from the registry. No bump, + # no tag, no git push. + yarn lerna publish from-package --yes + env: + NPM_CONFIG_PROVENANCE: true + + - name: Verify recovery published the missing versions + if: ${{ inputs.dry-run == false && inputs.recovery-mode }} + run: | + # Walk every non-private package and confirm the version on + # rel/latest is now reachable on the npm registry. Catches the + # case where lerna reports success but a package didn't land + # (e.g., another transient registry error). + missing=() + for f in modules/*/package.json; do + if [ "$(jq -r '.private // false' "$f")" = "true" ]; then continue; fi + name=$(jq -r '.name' "$f") + version=$(jq -r '.version' "$f") + code=$(curl -sL -o /dev/null -w "%{http_code}" -- "https://registry.npmjs.org/${name}/${version}") + if [ "$code" != "200" ]; then + missing+=("${name}@${version} (HTTP ${code})") + fi + done + if [ "${#missing[@]}" -ne 0 ]; then + echo "::error::Recovery left versions still missing from npm:" + printf ' - %s\n' "${missing[@]}" + exit 1 + fi + echo "✅ All public package versions on rel/latest are present on npm." + - name: Generate version bump summary id: version-bump-summary if: inputs.dry-run == false From 5c30948522ee6ddcc2f55dba89dbf8b3bf987bb3 Mon Sep 17 00:00:00 2001 From: Abhijeet Singh Date: Wed, 29 Apr 2026 11:42:12 +0530 Subject: [PATCH 08/13] feat(statics): add OFC coins for kavacosmos and dydxcosmos Go Accounts Ticket: CHALO-355 --- modules/statics/src/coins/ofcCoins.ts | 32 +++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/modules/statics/src/coins/ofcCoins.ts b/modules/statics/src/coins/ofcCoins.ts index f16d144e76..c5b70c47d7 100644 --- a/modules/statics/src/coins/ofcCoins.ts +++ b/modules/statics/src/coins/ofcCoins.ts @@ -126,6 +126,22 @@ export const ofcCoins = [ CoinKind.CRYPTO ), ofc('3977b3bd-abf2-476b-9d2a-4666d3b0aa10', 'ofcosmo', 'Osmosis', 6, UnderlyingAsset.OSMO, CoinKind.CRYPTO), + ofc( + 'd99c0388-f72c-4673-b2ec-e063df412a0b', + 'ofckavacosmos', + 'Kava Cosmos', + 6, + UnderlyingAsset.KAVACOSMOS, + CoinKind.CRYPTO + ), + ofc( + '67d3bade-7a8d-486d-8148-4d03ce7d72fa', + 'ofcdydxcosmos', + 'dYdX Cosmos', + 18, + UnderlyingAsset.DYDXCOSMOS, + CoinKind.CRYPTO + ), ofc('5958e6e9-c6d7-4372-8d1d-c681f595c481', 'ofchash', 'Provenance', 9, UnderlyingAsset.HASH, CoinKind.CRYPTO), ofc('4616eb4e-9244-449c-a503-02cb2d715b2c', 'ofcsei', 'Sei', 6, UnderlyingAsset.SEI, CoinKind.CRYPTO), ofc('50a00889-47d2-44b5-8dc8-1fb3b4f47b86', 'ofczeta', 'Zeta', 18, UnderlyingAsset.ZETA, CoinKind.CRYPTO), @@ -874,6 +890,22 @@ export const ofcCoins = [ CoinKind.CRYPTO ), tofc('1573da4d-15a8-4dae-9368-84ec0507e251', 'ofctosmo', 'Testnet Osmosis', 6, UnderlyingAsset.OSMO, CoinKind.CRYPTO), + tofc( + '0bf91684-4e28-4472-84d2-309973e3a2e8', + 'ofctkavacosmos', + 'Testnet Kava Cosmos', + 6, + UnderlyingAsset.KAVACOSMOS, + CoinKind.CRYPTO + ), + tofc( + 'a08b329b-65b6-47d8-9c65-877850f784b8', + 'ofctdydxcosmos', + 'Testnet dYdX Cosmos', + 18, + UnderlyingAsset.DYDXCOSMOS, + CoinKind.CRYPTO + ), tofc( '4bbb64d1-6bd2-4c53-8be0-f99229362c3d', 'ofcthash', From 8fcf1da04bb4bc0ca67537c7d5af1437d0fdefe2 Mon Sep 17 00:00:00 2001 From: Vibhav Simha G Date: Wed, 22 Apr 2026 13:46:01 +0530 Subject: [PATCH 09/13] feat(sdk-lib-mpc): add EdDSA DSG MPS class MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduces the DSG (Distributed Sign Generation) class for EdDSA MPCv2 using the @bitgo/wasm-mps WASM bindings. The class mirrors the existing DKG pattern with explicit state management and session export/restore support for server-side persistence across rounds. - Add `DSG` class with 4-round signing protocol state machine (Init → WaitMsg1 → WaitMsg2 → WaitMsg3 → Complete) - Add `DsgState` enum to types.ts - Export `EddsaMPSDsg` namespace from index.ts - Add `runEdDsaDSG` test helper to util.ts - Add unit tests covering full 2-of-3 signing, session restore, message serialization, error handling, and derivation paths Ticket: WCI-164 --- modules/sdk-lib-mpc/src/tss/eddsa-mps/dsg.ts | 298 ++++++++++++++++++ .../sdk-lib-mpc/src/tss/eddsa-mps/index.ts | 1 + .../sdk-lib-mpc/src/tss/eddsa-mps/types.ts | 18 ++ .../sdk-lib-mpc/test/unit/tss/eddsa/dsg.ts | 286 +++++++++++++++++ .../sdk-lib-mpc/test/unit/tss/eddsa/util.ts | 44 ++- 5 files changed, 646 insertions(+), 1 deletion(-) create mode 100644 modules/sdk-lib-mpc/src/tss/eddsa-mps/dsg.ts create mode 100644 modules/sdk-lib-mpc/test/unit/tss/eddsa/dsg.ts diff --git a/modules/sdk-lib-mpc/src/tss/eddsa-mps/dsg.ts b/modules/sdk-lib-mpc/src/tss/eddsa-mps/dsg.ts new file mode 100644 index 0000000000..717a8b3386 --- /dev/null +++ b/modules/sdk-lib-mpc/src/tss/eddsa-mps/dsg.ts @@ -0,0 +1,298 @@ +import assert from 'assert'; +import { + ed25519_dsg_round0_process, + ed25519_dsg_round1_process, + ed25519_dsg_round2_process, + ed25519_dsg_round3_process, +} from '@bitgo/wasm-mps'; +import { DeserializedMessage, DeserializedMessages, DsgState } from './types'; + +/** + * EdDSA Distributed Sign Generation (DSG) implementation using @bitgo/wasm-mps. + * + * State is explicit: each WASM round function returns + * `{ msg, state }` bytes; the state bytes are stored between rounds and passed to the + * next round function (this is what a server would persist to a database between API + * rounds). + * + * The protocol is hard-coded 2-of-3: each signing party communicates with exactly one + * counterpart. `handleIncomingMessages` accepts both messages (own + counterpart), and + * filters own out internally. + * + * @example + * ```typescript + * const dsg = new DSG(0); // partyIdx 0 + * dsg.initDsg(keyShare, message, 'm', 2); // counterpart is party 2 + * const msg1 = dsg.getFirstMessage(); + * const msg2 = dsg.handleIncomingMessages([msg1, peerMsg1]); // emits SignMsg2 + * const msg3 = dsg.handleIncomingMessages([msg2[0], peerMsg2]); // emits SignMsg3 + * dsg.handleIncomingMessages([msg3[0], peerMsg3]); // completes DSG + * const signature = dsg.getSignature(); // 64-byte Ed25519 signature + * ``` + */ +export class DSG { + protected partyIdx: number; + protected otherPartyIdx: number | null = null; + + /** Opaque bincode-serialised Keyshare from a prior DKG */ + private keyShare: Buffer | null = null; + /** Raw message bytes to sign (Ed25519 hashes internally; no prehashing required) */ + private message: Buffer | null = null; + /** BIP-32-style derivation path, e.g. "m" or "m/0/1". Folded in via Keyshare::derive_with_offset */ + private derivationPath: string | null = null; + + /** Serialised round state bytes returned by the previous round function */ + private dsgStateBytes: Buffer | null = null; + /** Final 64-byte Ed25519 signature, available after WaitMsg3 -> Complete */ + private signature: Buffer | null = null; + + protected dsgState: DsgState = DsgState.Uninitialized; + + constructor(partyIdx: number) { + this.partyIdx = partyIdx; + } + + getState(): DsgState { + return this.dsgState; + } + + /** + * Initialises the DSG session. The keyshare must come from a prior DKG run, and + * `otherPartyIdx` must be the single counterpart who will co-sign with this party. + * + * @param keyShare - Opaque bincode-serialised Keyshare bytes from `DKG.getKeyShare()`. + * @param message - Raw message bytes to sign (no prehashing). + * @param derivationPath - BIP-32-style derivation path. Use `"m"` for the root key. + * @param otherPartyIdx - Party index of the single counterpart in this signing session. + * Must differ from this party's own `partyIdx` and be in `[0, 2]`. + */ + initDsg(keyShare: Buffer, message: Buffer, derivationPath: string, otherPartyIdx: number): void { + if (!keyShare || keyShare.length === 0) { + throw Error('Missing or invalid keyShare'); + } + if (!message || message.length === 0) { + throw Error('Missing or invalid message'); + } + if (this.partyIdx < 0 || this.partyIdx > 2) { + throw Error(`Invalid partyIdx ${this.partyIdx}: must be in [0, 2]`); + } + if (otherPartyIdx < 0 || otherPartyIdx > 2 || otherPartyIdx === this.partyIdx) { + throw Error(`Invalid otherPartyIdx ${otherPartyIdx}: must be in [0, 2] and != partyIdx`); + } + + this.keyShare = keyShare; + this.message = message; + this.derivationPath = derivationPath; + this.otherPartyIdx = otherPartyIdx; + this.dsgState = DsgState.Init; + } + + /** + * Runs round 0 of the DSG protocol. Returns this party's broadcast message + * (a `SignMsg1` containing the commitment to `R_i`). Stores the round state + * bytes internally for the next round. + */ + getFirstMessage(): DeserializedMessage { + if (this.dsgState !== DsgState.Init) { + throw Error('DSG session not initialized'); + } + assert(this.keyShare, 'keyShare must be set after initDsg'); + assert(this.derivationPath !== null, 'derivationPath must be set after initDsg'); + assert(this.message, 'message must be set after initDsg'); + + let result; + try { + result = ed25519_dsg_round0_process(this.keyShare, this.derivationPath, this.message); + } catch (err) { + throw new Error(`Error while creating the first message from party ${this.partyIdx}: ${err}`); + } + + this.dsgStateBytes = Buffer.from(result.state); + this.dsgState = DsgState.WaitMsg1; + return { payload: new Uint8Array(result.msg), from: this.partyIdx }; + } + + /** + * Handles incoming messages for the current round and advances the protocol. + * + * - In `WaitMsg1`: runs round 1, returns this party's `SignMsg2` broadcast. + * - In `WaitMsg2`: runs round 2 (which internally fuses two Silence Labs transitions), + * returns this party's `SignMsg3` broadcast (partial signature). + * - In `WaitMsg3`: runs round 3, completes DSG, returns `[]`. + * + * The caller passes both messages (own + counterpart) for symmetry with + * `DKG.handleIncomingMessages`. Own message is filtered out internally; only the + * counterpart's payload is forwarded to the WASM round function. + * + * @param messagesForIthRound - Both messages for this round (own + counterpart). + */ + handleIncomingMessages(messagesForIthRound: DeserializedMessages): DeserializedMessages { + if (this.dsgState === DsgState.Complete) { + throw Error('DSG session already completed'); + } + if (this.dsgState === DsgState.Uninitialized) { + throw Error('DSG session not initialized'); + } + if (this.dsgState === DsgState.Init) { + throw Error( + 'DSG session must call getFirstMessage() before handling incoming messages. Call getFirstMessage() first.' + ); + } + if (messagesForIthRound.length !== 2) { + throw Error('Invalid number of messages for the round. Expected 2 messages (own + counterpart) for 2-of-3 DSG'); + } + + const peerMessages = messagesForIthRound.filter((m) => m.from !== this.partyIdx); + if (peerMessages.length !== 1) { + throw Error(`Expected exactly 1 counterpart message; got ${peerMessages.length}`); + } + const peerMsg = peerMessages[0]; + if (peerMsg.from !== this.otherPartyIdx) { + throw Error(`Unexpected counterpart party index: got ${peerMsg.from}, expected ${this.otherPartyIdx}`); + } + const peerPayload = Buffer.from(peerMsg.payload); + + if (this.dsgState === DsgState.WaitMsg1) { + assert(this.dsgStateBytes, 'dsgStateBytes must be set in WaitMsg1'); + let result; + try { + result = ed25519_dsg_round1_process(peerPayload, this.dsgStateBytes); + } catch (err) { + throw new Error(`Error while creating messages from party ${this.partyIdx}, round ${this.dsgState}: ${err}`); + } + this.dsgStateBytes = Buffer.from(result.state); + this.dsgState = DsgState.WaitMsg2; + return [{ payload: new Uint8Array(result.msg), from: this.partyIdx }]; + } + + if (this.dsgState === DsgState.WaitMsg2) { + assert(this.dsgStateBytes, 'dsgStateBytes must be set in WaitMsg2'); + let result; + try { + result = ed25519_dsg_round2_process(peerPayload, this.dsgStateBytes); + } catch (err) { + throw new Error(`Error while creating messages from party ${this.partyIdx}, round ${this.dsgState}: ${err}`); + } + this.dsgStateBytes = Buffer.from(result.state); + this.dsgState = DsgState.WaitMsg3; + return [{ payload: new Uint8Array(result.msg), from: this.partyIdx }]; + } + + if (this.dsgState === DsgState.WaitMsg3) { + assert(this.dsgStateBytes, 'dsgStateBytes must be set in WaitMsg3'); + let sigBytes; + try { + sigBytes = ed25519_dsg_round3_process(peerPayload, this.dsgStateBytes); + } catch (err) { + throw new Error(`Error while creating messages from party ${this.partyIdx}, round ${this.dsgState}: ${err}`); + } + this.signature = Buffer.from(sigBytes); + this.dsgStateBytes = null; + this.dsgState = DsgState.Complete; + return []; + } + + throw Error('Unexpected DSG state'); + } + + /** + * Returns the final 64-byte Ed25519 signature produced by round 3. + * Only available once the protocol reaches `Complete`. + */ + getSignature(): Buffer { + if (!this.signature) { + throw Error('DSG session has not produced a signature yet'); + } + return this.signature; + } + + /** + * Exports the current session state as a JSON string for persistence. + * Includes the opaque round state bytes plus everything needed to re-enter the + * protocol after a restart (keyshare, message, derivation path, counterpart). + */ + getSession(): string { + if (this.dsgState === DsgState.Complete) { + throw Error('DSG session is complete. Exporting the session is not allowed.'); + } + if (this.dsgState === DsgState.Uninitialized) { + throw Error('DSG session not initialized'); + } + if (this.dsgState === DsgState.Init) { + throw Error('DSG session must produce its first message before exporting.'); + } + return JSON.stringify({ + dsgStateBytes: this.dsgStateBytes?.toString('base64') ?? null, + dsgRound: this.dsgState, + keyShare: this.keyShare?.toString('base64') ?? null, + message: this.message?.toString('base64') ?? null, + derivationPath: this.derivationPath, + partyIdx: this.partyIdx, + otherPartyIdx: this.otherPartyIdx, + }); + } + + /** + * Restores a previously exported session. Allows the protocol to continue from + * where it left off, as if the round state was loaded from a database. + */ + restoreSession(session: string): void { + const data = JSON.parse(session); + if (!Object.values(DsgState).includes(data.dsgRound)) { + throw Error(`Invalid dsgRound in session: ${data.dsgRound}`); + } + if (data.dsgRound === DsgState.Uninitialized || data.dsgRound === DsgState.Init) { + throw Error(`Cannot restore DSG session in state ${data.dsgRound}`); + } + if (data.dsgRound === DsgState.Complete) { + throw Error('DSG session is complete. Restoring the session is not allowed.'); + } + if (typeof data.partyIdx !== 'number' || data.partyIdx < 0 || data.partyIdx > 2) { + throw Error(`Invalid partyIdx in session: ${data.partyIdx}`); + } + if ( + typeof data.otherPartyIdx !== 'number' || + data.otherPartyIdx < 0 || + data.otherPartyIdx > 2 || + data.otherPartyIdx === data.partyIdx + ) { + throw Error(`Invalid otherPartyIdx in session: ${data.otherPartyIdx}`); + } + if (this.partyIdx !== data.partyIdx) { + throw Error(`Session partyIdx ${data.partyIdx} does not match instance ${this.partyIdx}`); + } + if (typeof data.dsgStateBytes !== 'string' || data.dsgStateBytes.length === 0) { + throw Error(`Round ${data.dsgRound} requires dsgStateBytes`); + } + if (typeof data.keyShare !== 'string' || data.keyShare.length === 0) { + throw Error('Restored session missing keyShare'); + } + if (typeof data.message !== 'string' || data.message.length === 0) { + throw Error('Restored session missing message'); + } + if (typeof data.derivationPath !== 'string') { + throw Error('Restored session missing derivationPath'); + } + + const dsgStateBytes = Buffer.from(data.dsgStateBytes, 'base64'); + const keyShare = Buffer.from(data.keyShare, 'base64'); + const message = Buffer.from(data.message, 'base64'); + if (dsgStateBytes.length === 0) { + throw Error(`Round ${data.dsgRound} requires dsgStateBytes`); + } + if (keyShare.length === 0) { + throw Error('Restored session missing keyShare'); + } + if (message.length === 0) { + throw Error('Restored session missing message'); + } + + this.dsgStateBytes = dsgStateBytes; + this.dsgState = data.dsgRound; + this.keyShare = keyShare; + this.message = message; + this.derivationPath = data.derivationPath; + this.partyIdx = data.partyIdx; + this.otherPartyIdx = data.otherPartyIdx; + } +} diff --git a/modules/sdk-lib-mpc/src/tss/eddsa-mps/index.ts b/modules/sdk-lib-mpc/src/tss/eddsa-mps/index.ts index eadfa9e80f..cc355458a4 100644 --- a/modules/sdk-lib-mpc/src/tss/eddsa-mps/index.ts +++ b/modules/sdk-lib-mpc/src/tss/eddsa-mps/index.ts @@ -1,4 +1,5 @@ export * as EddsaMPSDkg from './dkg'; +export * as EddsaMPSDsg from './dsg'; export * as MPSUtil from './util'; export * as MPSTypes from './types'; export * as MPSComms from './commsLayer'; diff --git a/modules/sdk-lib-mpc/src/tss/eddsa-mps/types.ts b/modules/sdk-lib-mpc/src/tss/eddsa-mps/types.ts index 3392793f76..f026f322f1 100644 --- a/modules/sdk-lib-mpc/src/tss/eddsa-mps/types.ts +++ b/modules/sdk-lib-mpc/src/tss/eddsa-mps/types.ts @@ -26,6 +26,24 @@ export enum DkgState { Complete = 'Complete', } +/** + * Represents the state of a DSG (Distributed Sign Generation) session. + */ +export enum DsgState { + /** DSG session has not been initialized */ + Uninitialized = 'Uninitialized', + /** initDsg() has been called; ready for getFirstMessage() */ + Init = 'Init', + /** R0 broadcast emitted; waiting for counterpart's R0 broadcast (SignMsg1) */ + WaitMsg1 = 'WaitMsg1', + /** R1 broadcast emitted; waiting for counterpart's R1 broadcast (SignMsg2) */ + WaitMsg2 = 'WaitMsg2', + /** R2 broadcast emitted; waiting for counterpart's R2 broadcast (SignMsg3, the partial sig) */ + WaitMsg3 = 'WaitMsg3', + /** Final 64-byte Ed25519 signature is available via getSignature() */ + Complete = 'Complete', +} + export interface Message { payload: T; from: number; diff --git a/modules/sdk-lib-mpc/test/unit/tss/eddsa/dsg.ts b/modules/sdk-lib-mpc/test/unit/tss/eddsa/dsg.ts new file mode 100644 index 0000000000..49fbe64e6f --- /dev/null +++ b/modules/sdk-lib-mpc/test/unit/tss/eddsa/dsg.ts @@ -0,0 +1,286 @@ +import assert from 'assert'; +import { ed25519 } from '@noble/curves/ed25519'; +import { EddsaMPSDkg, EddsaMPSDsg, MPSTypes } from '../../../../src/tss/eddsa-mps'; +import { generateEdDsaDKGKeyShares, runEdDsaDSG } from './util'; + +const MESSAGE = Buffer.from('The Times 03/Jan/2009 Chancellor on brink of second bailout for banks'); + +describe('EdDSA MPS DSG', function () { + // DKG is expensive; generate keyshares once and reuse across tests. + let userDkg: EddsaMPSDkg.DKG; + let backupDkg: EddsaMPSDkg.DKG; + let bitgoDkg: EddsaMPSDkg.DKG; + + let userKeyShare: Buffer; + let backupKeyShare: Buffer; + let bitgoKeyShare: Buffer; + let dkgPubKey: Buffer; + + before(async function () { + [userDkg, backupDkg, bitgoDkg] = await generateEdDsaDKGKeyShares(); + userKeyShare = userDkg.getKeyShare(); + backupKeyShare = backupDkg.getKeyShare(); + bitgoKeyShare = bitgoDkg.getKeyShare(); + dkgPubKey = userDkg.getSharePublicKey(); + }); + + describe('DSG Initialization', function () { + it('should accept valid inputs and produce a first message', function () { + const dsg = new EddsaMPSDsg.DSG(0); + dsg.initDsg(userKeyShare, MESSAGE, 'm', 2); + + const msg = dsg.getFirstMessage(); + assert.strictEqual(msg.from, 0, 'First message should be from party 0'); + assert(msg.payload.length > 0, 'First message should have non-empty payload'); + }); + + it('should throw when getFirstMessage is called before initDsg', function () { + const dsg = new EddsaMPSDsg.DSG(0); + assert.throws(() => dsg.getFirstMessage(), /DSG session not initialized/); + }); + + it('should throw when handleIncomingMessages is called before initDsg', function () { + const dsg = new EddsaMPSDsg.DSG(0); + assert.throws(() => dsg.handleIncomingMessages([]), /DSG session not initialized/); + }); + + it('should throw when getSignature is called before completion', function () { + const dsg = new EddsaMPSDsg.DSG(0); + dsg.initDsg(userKeyShare, MESSAGE, 'm', 2); + assert.throws(() => dsg.getSignature(), /has not produced a signature yet/); + }); + + it('should throw on empty keyShare', function () { + const dsg = new EddsaMPSDsg.DSG(0); + assert.throws(() => dsg.initDsg(Buffer.alloc(0), MESSAGE, 'm', 2), /Missing or invalid keyShare/); + }); + + it('should throw on empty message', function () { + const dsg = new EddsaMPSDsg.DSG(0); + assert.throws(() => dsg.initDsg(userKeyShare, Buffer.alloc(0), 'm', 2), /Missing or invalid message/); + }); + + it('should throw when otherPartyIdx equals own partyIdx', function () { + const dsg = new EddsaMPSDsg.DSG(0); + assert.throws(() => dsg.initDsg(userKeyShare, MESSAGE, 'm', 0), /Invalid otherPartyIdx/); + }); + + it('should throw when otherPartyIdx is out of range', function () { + const dsg = new EddsaMPSDsg.DSG(0); + assert.throws(() => dsg.initDsg(userKeyShare, MESSAGE, 'm', 5), /Invalid otherPartyIdx/); + }); + + it('should throw when partyIdx is out of range', function () { + const dsg = new EddsaMPSDsg.DSG(7); + assert.throws(() => dsg.initDsg(userKeyShare, MESSAGE, 'm', 0), /Invalid partyIdx/); + }); + + it('should throw when handleIncomingMessages is called before getFirstMessage', function () { + const dsg = new EddsaMPSDsg.DSG(0); + dsg.initDsg(userKeyShare, MESSAGE, 'm', 2); + assert.throws(() => dsg.handleIncomingMessages([]), /must call getFirstMessage/); + }); + }); + + describe('DSG Protocol Execution (2-of-3)', function () { + it('should complete full DSG between user (0) and bitgo (2) and produce identical signatures', function () { + const { dsgA, dsgB } = runEdDsaDSG(userKeyShare, bitgoKeyShare, 0, 2, MESSAGE); + + assert.strictEqual(dsgA.getState(), 'Complete'); + assert.strictEqual(dsgB.getState(), 'Complete'); + + const sigA = dsgA.getSignature(); + const sigB = dsgB.getSignature(); + + assert.strictEqual(sigA.length, 64, 'Signature must be 64 bytes'); + assert.strictEqual(sigA.toString('hex'), sigB.toString('hex'), 'Both parties must produce identical signatures'); + }); + + it('should produce a signature that verifies under the DKG public key', function () { + const { dsgA } = runEdDsaDSG(userKeyShare, bitgoKeyShare, 0, 2, MESSAGE); + const sig = dsgA.getSignature(); + + const isValid = ed25519.verify(sig, MESSAGE, dkgPubKey); + assert(isValid, 'Signature should verify under DKG public key'); + }); + + it('should sign the same message identically across all 2-of-3 party combinations', function () { + const userBackupSig = runEdDsaDSG(userKeyShare, backupKeyShare, 0, 1, MESSAGE).dsgA.getSignature(); + const backupBitgoSig = runEdDsaDSG(backupKeyShare, bitgoKeyShare, 1, 2, MESSAGE).dsgA.getSignature(); + const userBitgoSig = runEdDsaDSG(userKeyShare, bitgoKeyShare, 0, 2, MESSAGE).dsgA.getSignature(); + + // Per-session nonce randomisation means signatures across DIFFERENT signing + // sessions WILL differ. The invariant we test is that every 2-of-3 subset + // produces a signature that verifies under the SAME DKG public key. + assert(ed25519.verify(userBackupSig, MESSAGE, dkgPubKey), 'user+backup signature should verify'); + assert(ed25519.verify(backupBitgoSig, MESSAGE, dkgPubKey), 'backup+bitgo signature should verify'); + assert(ed25519.verify(userBitgoSig, MESSAGE, dkgPubKey), 'user+bitgo signature should verify'); + }); + + it('should sign arbitrary message lengths', function () { + const shortMsg = Buffer.from([0x01]); + const longMsg = Buffer.alloc(4096, 0xab); + + const { dsgA: short } = runEdDsaDSG(userKeyShare, bitgoKeyShare, 0, 2, shortMsg); + const { dsgA: long } = runEdDsaDSG(userKeyShare, bitgoKeyShare, 0, 2, longMsg); + + assert(ed25519.verify(short.getSignature(), shortMsg, dkgPubKey), '1-byte message signature should verify'); + assert(ed25519.verify(long.getSignature(), longMsg, dkgPubKey), '4096-byte message signature should verify'); + }); + + it('should throw when handleIncomingMessages is called after completion', function () { + const { dsgA } = runEdDsaDSG(userKeyShare, bitgoKeyShare, 0, 2, MESSAGE); + assert.throws(() => dsgA.handleIncomingMessages([]), /already completed/); + }); + }); + + describe('Derivation Paths', function () { + it('should produce different signatures for different derivation paths', function () { + const { dsgA: rootSig } = runEdDsaDSG(userKeyShare, bitgoKeyShare, 0, 2, MESSAGE, 'm'); + const { dsgA: derivedSig } = runEdDsaDSG(userKeyShare, bitgoKeyShare, 0, 2, MESSAGE, 'm/0/1'); + + assert.notStrictEqual( + rootSig.getSignature().toString('hex'), + derivedSig.getSignature().toString('hex'), + 'Different derivation paths should produce different signatures' + ); + }); + }); + + describe('Error Handling', function () { + it('should throw when handleIncomingMessages receives the wrong number of messages', function () { + const dsg = new EddsaMPSDsg.DSG(0); + dsg.initDsg(userKeyShare, MESSAGE, 'm', 2); + const own = dsg.getFirstMessage(); + + assert.throws(() => dsg.handleIncomingMessages([own]), /Expected 2 messages/); + assert.throws(() => dsg.handleIncomingMessages([own, own, own]), /Expected 2 messages/); + }); + + it('should throw when counterpart message comes from an unexpected party', function () { + const dsg = new EddsaMPSDsg.DSG(0); + dsg.initDsg(userKeyShare, MESSAGE, 'm', 2); + + const own = dsg.getFirstMessage(); + // Forge a "counterpart" message from party 1 instead of expected party 2 + const wrongPeer = { from: 1, payload: own.payload }; + + assert.throws(() => dsg.handleIncomingMessages([own, wrongPeer]), /Unexpected counterpart party index/); + }); + + it('should throw when both messages claim to come from this party', function () { + const dsg = new EddsaMPSDsg.DSG(0); + dsg.initDsg(userKeyShare, MESSAGE, 'm', 2); + const own = dsg.getFirstMessage(); + + assert.throws(() => dsg.handleIncomingMessages([own, own]), /Expected exactly 1 counterpart message/); + }); + }); + + describe('Message Serialization', function () { + it('should serialize and deserialize DSG messages round-trip', function () { + const dsgA = new EddsaMPSDsg.DSG(0); + const dsgB = new EddsaMPSDsg.DSG(2); + dsgA.initDsg(userKeyShare, MESSAGE, 'm', 2); + dsgB.initDsg(bitgoKeyShare, MESSAGE, 'm', 0); + + const a0 = dsgA.getFirstMessage(); + const b0 = dsgB.getFirstMessage(); + + const serialized = MPSTypes.serializeMessages([a0, b0]); + assert( + serialized.every((m) => typeof m.payload === 'string'), + 'Serialized payloads should be strings' + ); + + const deserialized = MPSTypes.deserializeMessages(serialized); + assert.strictEqual(deserialized.length, 2); + deserialized.forEach((msg, i) => { + const original = i === 0 ? a0 : b0; + assert.strictEqual(msg.from, original.from); + assert.deepStrictEqual(Buffer.from(msg.payload), Buffer.from(original.payload)); + }); + }); + }); + + describe('Session Management', function () { + it('should export and restore DSG session and continue protocol to a valid signature', function () { + const dsgA = new EddsaMPSDsg.DSG(0); + const dsgB = new EddsaMPSDsg.DSG(2); + dsgA.initDsg(userKeyShare, MESSAGE, 'm', 2); + dsgB.initDsg(bitgoKeyShare, MESSAGE, 'm', 0); + + const a0 = dsgA.getFirstMessage(); + const b0 = dsgB.getFirstMessage(); + + const sessionA = dsgA.getSession(); + assert(typeof sessionA === 'string' && sessionA.length > 0); + + // Restore A in a fresh instance and finish the protocol from there. + const restoredA = new EddsaMPSDsg.DSG(0); + restoredA.restoreSession(sessionA); + assert.strictEqual(restoredA.getState(), dsgA.getState(), 'Restored state should match original'); + + const [a1] = restoredA.handleIncomingMessages([a0, b0]); + const [b1] = dsgB.handleIncomingMessages([a0, b0]); + + const [a2] = restoredA.handleIncomingMessages([a1, b1]); + const [b2] = dsgB.handleIncomingMessages([a1, b1]); + + restoredA.handleIncomingMessages([a2, b2]); + dsgB.handleIncomingMessages([a2, b2]); + + const sigA = restoredA.getSignature(); + const sigB = dsgB.getSignature(); + + assert.strictEqual(sigA.toString('hex'), sigB.toString('hex'), 'Restored signer must agree with counterpart'); + assert(ed25519.verify(sigA, MESSAGE, dkgPubKey), 'Restored-session signature should verify under DKG pubkey'); + }); + + it('should throw when exporting session after completion', function () { + const { dsgA, dsgB } = runEdDsaDSG(userKeyShare, bitgoKeyShare, 0, 2, MESSAGE); + assert.throws(() => dsgA.getSession(), /DSG session is complete\. Exporting the session is not allowed\./); + assert.throws(() => dsgB.getSession(), /DSG session is complete\. Exporting the session is not allowed\./); + }); + + it('should throw when exporting session before the first message', function () { + const dsg = new EddsaMPSDsg.DSG(0); + dsg.initDsg(userKeyShare, MESSAGE, 'm', 2); + assert.throws(() => dsg.getSession(), /must produce its first message before exporting/); + }); + + it('should throw when exporting session before initialization', function () { + const dsg = new EddsaMPSDsg.DSG(0); + assert.throws(() => dsg.getSession(), /DSG session not initialized/); + }); + + it('should throw when restoring a session with invalid fields', function () { + const dsg = new EddsaMPSDsg.DSG(0); + dsg.initDsg(userKeyShare, MESSAGE, 'm', 2); + dsg.getFirstMessage(); + + const session = JSON.parse(dsg.getSession()); + + assert.throws( + () => new EddsaMPSDsg.DSG(0).restoreSession(JSON.stringify({ ...session, dsgRound: 'Invalid' })), + /Invalid dsgRound in session/ + ); + assert.throws( + () => new EddsaMPSDsg.DSG(0).restoreSession(JSON.stringify({ ...session, partyIdx: 4 })), + /Invalid partyIdx in session/ + ); + assert.throws( + () => new EddsaMPSDsg.DSG(0).restoreSession(JSON.stringify({ ...session, otherPartyIdx: 0 })), + /Invalid otherPartyIdx in session/ + ); + assert.throws( + () => new EddsaMPSDsg.DSG(0).restoreSession(JSON.stringify({ ...session, dsgStateBytes: null })), + /requires dsgStateBytes/ + ); + assert.throws( + () => new EddsaMPSDsg.DSG(1).restoreSession(JSON.stringify(session)), + /Session partyIdx 0 does not match instance 1/ + ); + }); + }); +}); diff --git a/modules/sdk-lib-mpc/test/unit/tss/eddsa/util.ts b/modules/sdk-lib-mpc/test/unit/tss/eddsa/util.ts index 237f2b12a3..4e00505197 100644 --- a/modules/sdk-lib-mpc/test/unit/tss/eddsa/util.ts +++ b/modules/sdk-lib-mpc/test/unit/tss/eddsa/util.ts @@ -1,6 +1,7 @@ import crypto from 'crypto'; import { x25519 } from '@noble/curves/ed25519'; -import { EddsaMPSDkg } from '../../../../src/tss/eddsa-mps'; +import { EddsaMPSDkg, EddsaMPSDsg } from '../../../../src/tss/eddsa-mps'; +import { DeserializedMessage } from '../../../../src/tss/eddsa-mps/types'; /** * Generates an X25519 keypair. If a seed is provided (32 bytes), it is used as the @@ -54,3 +55,44 @@ export async function generateEdDsaDKGKeyShares( return [user, backup, bitgo]; } + +/** + * Runs a full 2-of-3 EdDSA DSG protocol between two parties holding `keyShareA` + * and `keyShareB`, signing `message` under `derivationPath`. + * + * Returns both parties' resulting `DSG` instances so callers can compare signatures + * (`dsgA.getSignature()` and `dsgB.getSignature()` should be byte-identical) or + * verify against a public key. + */ +export function runEdDsaDSG( + keyShareA: Buffer, + keyShareB: Buffer, + partyAIdx: number, + partyBIdx: number, + message: Buffer, + derivationPath = 'm' +): { dsgA: EddsaMPSDsg.DSG; dsgB: EddsaMPSDsg.DSG } { + const dsgA = new EddsaMPSDsg.DSG(partyAIdx); + const dsgB = new EddsaMPSDsg.DSG(partyBIdx); + + dsgA.initDsg(keyShareA, message, derivationPath, partyBIdx); + dsgB.initDsg(keyShareB, message, derivationPath, partyAIdx); + + // Round 0 -> SignMsg1 + const a0: DeserializedMessage = dsgA.getFirstMessage(); + const b0: DeserializedMessage = dsgB.getFirstMessage(); + + // Round 1 -> SignMsg2 + const [a1] = dsgA.handleIncomingMessages([a0, b0]); + const [b1] = dsgB.handleIncomingMessages([a0, b0]); + + // Round 2 -> SignMsg3 (partial sig) + const [a2] = dsgA.handleIncomingMessages([a1, b1]); + const [b2] = dsgB.handleIncomingMessages([a1, b1]); + + // Round 3 -> Complete (no output messages) + dsgA.handleIncomingMessages([a2, b2]); + dsgB.handleIncomingMessages([a2, b2]); + + return { dsgA, dsgB }; +} From d1e2a12df857ef4684c5cfe59a9e29f6d871a4a1 Mon Sep 17 00:00:00 2001 From: Otto Allmendinger Date: Tue, 28 Apr 2026 11:38:30 +0200 Subject: [PATCH 10/13] test(abstract-utxo): move BIP-322 Proof tests Issue: BTC-2650 --- modules/abstract-utxo/test/unit/bip322.ts | 62 +++++++++++++++++++ .../test/unit/explainTransaction.ts | 61 ------------------ 2 files changed, 62 insertions(+), 61 deletions(-) diff --git a/modules/abstract-utxo/test/unit/bip322.ts b/modules/abstract-utxo/test/unit/bip322.ts index 819ac02877..e5d1960188 100644 --- a/modules/abstract-utxo/test/unit/bip322.ts +++ b/modules/abstract-utxo/test/unit/bip322.ts @@ -4,6 +4,9 @@ import * as utxolib from '@bitgo/utxo-lib'; import { bip322 as coreBip322 } from '@bitgo/utxo-core'; import { bip322 as wasmBip322, fixedScriptWallet, BIP32, type Triple } from '@bitgo/wasm-utxo'; +import { bip322Fixtures } from './fixtures/bip322/fixtures'; +import { getUtxoCoin } from './util'; + import { BIP322MessageBroadcastable, BIP322MessageInfo, @@ -403,6 +406,65 @@ describe('BIP322', function () { }); }); + describe('BIP322 Proof', function () { + const coin = getUtxoCoin('btc'); + const pubs = bip322Fixtures.valid.rootWalletKeys.triple.map((b) => b.neutered().toBase58()) as Triple; + + it('should successfully run with a user nonce', async function () { + const psbtHex = bip322Fixtures.valid.userNonce; + const result = await coin.explainTransaction({ txHex: psbtHex, pubs }); + assert.strictEqual(result.outputAmount, '0'); + assert.strictEqual(result.changeAmount, '0'); + assert.strictEqual(result.outputs.length, 1); + assert.strictEqual(result.outputs[0].address, 'scriptPubKey:6a'); + assert.strictEqual(result.fee, '0'); + assert.ok('signatures' in result); + assert.strictEqual(result.signatures, 0); + assert.ok(result.messages); + result.messages?.forEach((obj) => { + assert.ok(obj.address); + assert.ok(obj.message); + assert.strictEqual(obj.message, bip322Fixtures.valid.message); + }); + }); + + it('should successfully run with a user signature', async function () { + const psbtHex = bip322Fixtures.valid.userSignature; + const result = await coin.explainTransaction({ txHex: psbtHex, pubs }); + assert.strictEqual(result.outputAmount, '0'); + assert.strictEqual(result.changeAmount, '0'); + assert.strictEqual(result.outputs.length, 1); + assert.strictEqual(result.outputs[0].address, 'scriptPubKey:6a'); + assert.strictEqual(result.fee, '0'); + assert.ok('signatures' in result); + assert.strictEqual(result.signatures, 1); + assert.ok(result.messages); + result.messages?.forEach((obj) => { + assert.ok(obj.address); + assert.ok(obj.message); + assert.strictEqual(obj.message, bip322Fixtures.valid.message); + }); + }); + + it('should successfully run with a hsm signature', async function () { + const psbtHex = bip322Fixtures.valid.hsmSignature; + const result = await coin.explainTransaction({ txHex: psbtHex, pubs }); + assert.strictEqual(result.outputAmount, '0'); + assert.strictEqual(result.changeAmount, '0'); + assert.strictEqual(result.outputs.length, 1); + assert.strictEqual(result.outputs[0].address, 'scriptPubKey:6a'); + assert.strictEqual(result.fee, '0'); + assert.ok('signatures' in result); + assert.strictEqual(result.signatures, 2); + assert.ok(result.messages); + result.messages?.forEach((obj) => { + assert.ok(obj.address); + assert.ok(obj.message); + assert.strictEqual(obj.message, bip322Fixtures.valid.message); + }); + }); + }); + describe('utxolib verification stack - wasm-utxo respects input.sighashType', function () { // This test verifies that wasm-utxo correctly respects the input.sighashType field // when creating musig2 partial signatures. diff --git a/modules/abstract-utxo/test/unit/explainTransaction.ts b/modules/abstract-utxo/test/unit/explainTransaction.ts index bcf1c0486a..aaf41d097c 100644 --- a/modules/abstract-utxo/test/unit/explainTransaction.ts +++ b/modules/abstract-utxo/test/unit/explainTransaction.ts @@ -1,9 +1,6 @@ -import assert from 'assert'; - import { common, Triple, Wallet } from '@bitgo/sdk-core'; import nock = require('nock'); -import { bip322Fixtures } from './fixtures/bip322/fixtures'; import { psbtTxHex } from './fixtures/psbtHexProof'; import { defaultBitGo, getUtxoCoin } from './util'; @@ -44,62 +41,4 @@ describe('Explain Transaction', function () { }); }); - describe('BIP322 Proof', function () { - const coin = getUtxoCoin('btc'); - const pubs = bip322Fixtures.valid.rootWalletKeys.triple.map((b) => b.neutered().toBase58()) as Triple; - - it('should successfully run with a user nonce', async function () { - const psbtHex = bip322Fixtures.valid.userNonce; - const result = await coin.explainTransaction({ txHex: psbtHex, pubs }); - assert.strictEqual(result.outputAmount, '0'); - assert.strictEqual(result.changeAmount, '0'); - assert.strictEqual(result.outputs.length, 1); - assert.strictEqual(result.outputs[0].address, 'scriptPubKey:6a'); - assert.strictEqual(result.fee, '0'); - assert.ok('signatures' in result); - assert.strictEqual(result.signatures, 0); - assert.ok(result.messages); - result.messages?.forEach((obj) => { - assert.ok(obj.address); - assert.ok(obj.message); - assert.strictEqual(obj.message, bip322Fixtures.valid.message); - }); - }); - - it('should successfully run with a user signature', async function () { - const psbtHex = bip322Fixtures.valid.userSignature; - const result = await coin.explainTransaction({ txHex: psbtHex, pubs }); - assert.strictEqual(result.outputAmount, '0'); - assert.strictEqual(result.changeAmount, '0'); - assert.strictEqual(result.outputs.length, 1); - assert.strictEqual(result.outputs[0].address, 'scriptPubKey:6a'); - assert.strictEqual(result.fee, '0'); - assert.ok('signatures' in result); - assert.strictEqual(result.signatures, 1); - assert.ok(result.messages); - result.messages?.forEach((obj) => { - assert.ok(obj.address); - assert.ok(obj.message); - assert.strictEqual(obj.message, bip322Fixtures.valid.message); - }); - }); - - it('should successfully run with a hsm signature', async function () { - const psbtHex = bip322Fixtures.valid.hsmSignature; - const result = await coin.explainTransaction({ txHex: psbtHex, pubs }); - assert.strictEqual(result.outputAmount, '0'); - assert.strictEqual(result.changeAmount, '0'); - assert.strictEqual(result.outputs.length, 1); - assert.strictEqual(result.outputs[0].address, 'scriptPubKey:6a'); - assert.strictEqual(result.fee, '0'); - assert.ok('signatures' in result); - assert.strictEqual(result.signatures, 2); - assert.ok(result.messages); - result.messages?.forEach((obj) => { - assert.ok(obj.address); - assert.ok(obj.message); - assert.strictEqual(obj.message, bip322Fixtures.valid.message); - }); - }); - }); }); From b6b61662372e701a2858744d258472c06b6169f4 Mon Sep 17 00:00:00 2001 From: Otto Allmendinger Date: Tue, 28 Apr 2026 10:54:05 +0200 Subject: [PATCH 11/13] fix(abstract-utxo): enhance BIP-322 with signature verification Extend explainPsbtWasm to count valid signatures per input: - Add scriptId to ExplainedInput (from parseTransactionWithWalletKeys) - Add signedBy map showing validation status for each key - Extract BIP-322 messages with bip322.getBip322Message() - Count signatures via bip322.verifyBip322PsbtInput() Replace BIP-322 test fixtures with dynamic PSBT generation using wasm-utxo. Remove fixtures/bip322/fixtures.ts and inline PSBT construction directly in tests. Call explainPsbtWasm with BitGoPsbt instances instead of serializing via coin.explainTransaction. Remove utxolib dependency from BIP-322 tests. Use @bitgo/wasm-utxo directly for key generation, address creation, and signature verification. Rewrite p2trMusig2 sighashType test as pure wasm-utxo round-trip. Add cross-library compatibility test verifying utxolib-created BIP-322 PSBTs can be signed by wasm-utxo and validated by utxolib. Issue: BTC-2650 Co-authored-by: llm-git --- .../fixedScript/explainPsbtWasm.ts | 55 +++++- .../fixedScript/explainTransaction.ts | 2 +- modules/abstract-utxo/test/unit/bip322.ts | 167 ++++++++++-------- .../test/unit/explainTransaction.ts | 1 - .../test/unit/fixtures/bip322/fixtures.ts | 22 --- .../transaction/fixedScript/explainPsbt.ts | 12 +- 6 files changed, 151 insertions(+), 108 deletions(-) delete mode 100644 modules/abstract-utxo/test/unit/fixtures/bip322/fixtures.ts diff --git a/modules/abstract-utxo/src/transaction/fixedScript/explainPsbtWasm.ts b/modules/abstract-utxo/src/transaction/fixedScript/explainPsbtWasm.ts index e2eec34bce..813dc5013f 100644 --- a/modules/abstract-utxo/src/transaction/fixedScript/explainPsbtWasm.ts +++ b/modules/abstract-utxo/src/transaction/fixedScript/explainPsbtWasm.ts @@ -1,7 +1,8 @@ -import { fixedScriptWallet } from '@bitgo/wasm-utxo'; +import { fixedScriptWallet, bip322 } from '@bitgo/wasm-utxo'; import { Triple } from '@bitgo/sdk-core'; import type { FixedScriptWalletOutput, Output, BitGoPsbt } from '../types'; +import type { Bip322Message } from '../../abstractUtxoCoin'; import type { TransactionExplanationWasm } from './explainTransaction'; @@ -49,6 +50,8 @@ interface ExplainPsbtWasmParams { export interface ExplainedInput { address: string; value: TAmount; + scriptId: fixedScriptWallet.ScriptId | null; + signedBy: { [key: string]: boolean }; } export interface TransactionExplanationBigInt { @@ -64,9 +67,28 @@ export interface TransactionExplanationBigInt { fee: bigint; } +function getSignedByForInput( + psbt: BitGoPsbt, + inputIndex: number, + walletXpubs: fixedScriptWallet.RootWalletKeys, + replayProtectionPublicKeys: Buffer[], + scriptId: fixedScriptWallet.ScriptId | null +): { [key: string]: boolean } { + if (scriptId !== null) { + return { + user: psbt.verifySignature(inputIndex, walletXpubs.userKey()), + backup: psbt.verifySignature(inputIndex, walletXpubs.backupKey()), + bitgo: psbt.verifySignature(inputIndex, walletXpubs.bitgoKey()), + }; + } + return Object.fromEntries( + replayProtectionPublicKeys.map((key, j) => [`replayProtection${j}`, psbt.verifySignature(inputIndex, key)]) + ); +} + export function explainPsbtWasmBigInt( psbt: BitGoPsbt, - walletXpubs: Triple | fixedScriptWallet.RootWalletKeys, + walletXpubs: fixedScriptWallet.RootWalletKeys, params: ExplainPsbtWasmParams ): TransactionExplanationBigInt { const parsed = psbt.parseTransactionWithWalletKeys(walletXpubs, { replayProtection: params.replayProtection }); @@ -92,7 +114,12 @@ export function explainPsbtWasmBigInt( } }); - const inputs = parsed.inputs.map((input) => ({ address: input.address, value: input.value })); + const inputs = parsed.inputs.map((input, i) => ({ + address: input.address, + value: input.value, + scriptId: input.scriptId, + signedBy: getSignedByForInput(psbt, i, walletXpubs, params.replayProtection.publicKeys, input.scriptId), + })); const inputAmount = inputs.reduce((sum, input) => sum + input.value, 0n); const outputAmount = outputs.reduce((sum, output) => sum + output.amount, 0n); const changeAmount = changeOutputs.reduce((sum, output) => sum + output.amount, 0n); @@ -120,15 +147,32 @@ function stringifyChangeOutput(output: FixedScriptWalletOutput): FixedSc return { ...output, amount: output.amount.toString() }; } +function extractBip322Messages(psbt: BitGoPsbt, inputs: ExplainedInput[]): { messages: Bip322Message[] | undefined } { + const messages: Bip322Message[] = inputs.flatMap((input, i) => { + const message = bip322.getBip322Message(psbt, i); + return message ? [{ message, address: input.address }] : []; + }); + + if (messages.length === 0) { + return { messages: undefined }; + } + + return { messages }; +} + export function explainPsbtWasm( psbt: BitGoPsbt, walletXpubs: Triple | fixedScriptWallet.RootWalletKeys, params: ExplainPsbtWasmParams ): TransactionExplanationWasm { - const result = explainPsbtWasmBigInt(psbt, walletXpubs, params); + const result = explainPsbtWasmBigInt(psbt, fixedScriptWallet.RootWalletKeys.from(walletXpubs), params); + const inputs = result.inputs.map((i) => ({ address: i.address, value: i.value.toString(), signedBy: i.signedBy })); + + const { messages } = extractBip322Messages(psbt, result.inputs); + return { id: result.id, - inputs: result.inputs.map((i) => ({ address: i.address, value: i.value.toString() })), + inputs, inputAmount: result.inputAmount.toString(), outputAmount: result.outputAmount.toString(), changeAmount: result.changeAmount.toString(), @@ -137,6 +181,7 @@ export function explainPsbtWasm( changeOutputs: result.changeOutputs.map(stringifyChangeOutput), customChangeOutputs: result.customChangeOutputs.map(stringifyChangeOutput), fee: result.fee.toString(), + messages, }; } diff --git a/modules/abstract-utxo/src/transaction/fixedScript/explainTransaction.ts b/modules/abstract-utxo/src/transaction/fixedScript/explainTransaction.ts index 1698989e16..0165816e7d 100644 --- a/modules/abstract-utxo/src/transaction/fixedScript/explainTransaction.ts +++ b/modules/abstract-utxo/src/transaction/fixedScript/explainTransaction.ts @@ -52,7 +52,7 @@ interface TransactionExplanationWithSignatures & { - inputs: Array<{ address: string; value: string }>; + inputs: Array<{ address: string; value: string; signedBy: { [key: string]: boolean } }>; inputAmount: string; }; diff --git a/modules/abstract-utxo/test/unit/bip322.ts b/modules/abstract-utxo/test/unit/bip322.ts index e5d1960188..6c70d02747 100644 --- a/modules/abstract-utxo/test/unit/bip322.ts +++ b/modules/abstract-utxo/test/unit/bip322.ts @@ -3,10 +3,9 @@ import assert from 'assert'; import * as utxolib from '@bitgo/utxo-lib'; import { bip322 as coreBip322 } from '@bitgo/utxo-core'; import { bip322 as wasmBip322, fixedScriptWallet, BIP32, type Triple } from '@bitgo/wasm-utxo'; +import { getKeyTriple } from '@bitgo/wasm-utxo/testutils'; -import { bip322Fixtures } from './fixtures/bip322/fixtures'; -import { getUtxoCoin } from './util'; - +import { explainPsbtWasm } from '../../src/transaction/fixedScript'; import { BIP322MessageBroadcastable, BIP322MessageInfo, @@ -20,7 +19,7 @@ function createTestWalletKeys(seed: string): { xpubs: Triple; xprivs: Triple; } { - const keys = utxolib.testutil.getKeyTriple(seed); + const keys = getKeyTriple(seed); return { xpubs: keys.map((k) => k.neutered().toBase58()) as Triple, xprivs: keys.map((k) => k.toBase58()) as Triple, @@ -28,12 +27,14 @@ function createTestWalletKeys(seed: string): { } function getDerivedPubkeys(seed: string, chain: number, index: number): Triple { - const keys = utxolib.testutil.getKeyTriple(seed); - return keys.map((k) => k.derivePath(`m/0/0/${chain}/${index}`).publicKey.toString('hex')) as Triple; + const keys = getKeyTriple(seed); + return keys.map((k) => + Buffer.from(k.derivePath(`m/0/0/${chain}/${index}`).publicKey).toString('hex') + ) as Triple; } function getAddress(walletKeys: fixedScriptWallet.RootWalletKeys, chain: number, index: number): string { - return fixedScriptWallet.address(walletKeys, chain, index, utxolib.networks.bitcoin); + return fixedScriptWallet.address(walletKeys, chain, index, 'btc'); } describe('BIP322', function () { @@ -379,8 +380,8 @@ describe('BIP322', function () { scriptId: { chain, index }, rootWalletKeys: walletKeys, }); - psbt.sign(0, BIP32.fromBase58(xprivs[0])); - psbt.sign(0, BIP32.fromBase58(xprivs[2])); + psbt.sign(BIP32.fromBase58(xprivs[0])); + psbt.sign(BIP32.fromBase58(xprivs[2])); const pubkeys = getDerivedPubkeys(seed, chain, index); const address = getAddress(walletKeys, chain, index); @@ -407,76 +408,99 @@ describe('BIP322', function () { }); describe('BIP322 Proof', function () { - const coin = getUtxoCoin('btc'); - const pubs = bip322Fixtures.valid.rootWalletKeys.triple.map((b) => b.neutered().toBase58()) as Triple; + const message = 'I can believe it is not butter'; + const chain = 10; + const index = 0; + const { xpubs, xprivs } = createTestWalletKeys('bip322-proof'); + const walletKeys = fixedScriptWallet.RootWalletKeys.from(xpubs); - it('should successfully run with a user nonce', async function () { - const psbtHex = bip322Fixtures.valid.userNonce; - const result = await coin.explainTransaction({ txHex: psbtHex, pubs }); + function createUnsignedPsbt(): fixedScriptWallet.BitGoPsbt { + const psbt = fixedScriptWallet.BitGoPsbt.createEmpty('btc', walletKeys, { version: 0 }); + wasmBip322.addBip322Input(psbt, { message, scriptId: { chain, index }, rootWalletKeys: walletKeys }); + return psbt; + } + + function assertCommon(result: ReturnType, expectedSignerCount: number): void { assert.strictEqual(result.outputAmount, '0'); assert.strictEqual(result.changeAmount, '0'); assert.strictEqual(result.outputs.length, 1); assert.strictEqual(result.outputs[0].address, 'scriptPubKey:6a'); assert.strictEqual(result.fee, '0'); - assert.ok('signatures' in result); - assert.strictEqual(result.signatures, 0); + for (const input of result.inputs) { + const signerCount = Object.values(input.signedBy).filter(Boolean).length; + assert.strictEqual(signerCount, expectedSignerCount); + } assert.ok(result.messages); - result.messages?.forEach((obj) => { + for (const obj of result.messages ?? []) { assert.ok(obj.address); - assert.ok(obj.message); - assert.strictEqual(obj.message, bip322Fixtures.valid.message); - }); + assert.strictEqual(obj.message, message); + } + } + + it('should successfully run with a user nonce', function () { + const psbt = createUnsignedPsbt(); + assertCommon(explainPsbtWasm(psbt, walletKeys, { replayProtection: { publicKeys: [] } }), 0); }); - it('should successfully run with a user signature', async function () { - const psbtHex = bip322Fixtures.valid.userSignature; - const result = await coin.explainTransaction({ txHex: psbtHex, pubs }); - assert.strictEqual(result.outputAmount, '0'); - assert.strictEqual(result.changeAmount, '0'); - assert.strictEqual(result.outputs.length, 1); - assert.strictEqual(result.outputs[0].address, 'scriptPubKey:6a'); - assert.strictEqual(result.fee, '0'); - assert.ok('signatures' in result); - assert.strictEqual(result.signatures, 1); - assert.ok(result.messages); - result.messages?.forEach((obj) => { - assert.ok(obj.address); - assert.ok(obj.message); - assert.strictEqual(obj.message, bip322Fixtures.valid.message); - }); + it('should successfully run with a user signature', function () { + const psbt = createUnsignedPsbt(); + psbt.sign(BIP32.fromBase58(xprivs[0])); + assertCommon(explainPsbtWasm(psbt, walletKeys, { replayProtection: { publicKeys: [] } }), 1); }); - it('should successfully run with a hsm signature', async function () { - const psbtHex = bip322Fixtures.valid.hsmSignature; - const result = await coin.explainTransaction({ txHex: psbtHex, pubs }); - assert.strictEqual(result.outputAmount, '0'); - assert.strictEqual(result.changeAmount, '0'); - assert.strictEqual(result.outputs.length, 1); - assert.strictEqual(result.outputs[0].address, 'scriptPubKey:6a'); - assert.strictEqual(result.fee, '0'); - assert.ok('signatures' in result); - assert.strictEqual(result.signatures, 2); - assert.ok(result.messages); - result.messages?.forEach((obj) => { - assert.ok(obj.address); - assert.ok(obj.message); - assert.strictEqual(obj.message, bip322Fixtures.valid.message); + it('should successfully run with a hsm signature', function () { + const psbt = createUnsignedPsbt(); + psbt.sign(BIP32.fromBase58(xprivs[0])); + psbt.sign(BIP32.fromBase58(xprivs[2])); + assertCommon(explainPsbtWasm(psbt, walletKeys, { replayProtection: { publicKeys: [] } }), 2); + }); + }); + + describe('p2trMusig2 BIP322 signing', function () { + it('should produce verifiable musig2 signatures', function () { + const seed = 'p2trMusig2_sighash_test'; + const { xpubs, xprivs } = createTestWalletKeys(seed); + const walletKeys = fixedScriptWallet.RootWalletKeys.from(xpubs); + + const chain = 40; // p2trMusig2 external + const index = 0; + const messageText = 'BIP322 sighash test'; + + const psbt = fixedScriptWallet.BitGoPsbt.createEmpty('btc', walletKeys, { version: 0 }); + wasmBip322.addBip322Input(psbt, { + message: messageText, + scriptId: { chain, index }, + rootWalletKeys: walletKeys, + signPath: { signer: 'user', cosigner: 'bitgo' }, }); + + const userKey = BIP32.fromBase58(xprivs[0]); + const bitgoKey = BIP32.fromBase58(xprivs[2]); + psbt.generateMusig2Nonces(userKey); + psbt.generateMusig2Nonces(bitgoKey); + psbt.sign(userKey); + psbt.sign(bitgoKey); + + const signers = wasmBip322.verifyBip322PsbtInput(psbt, 0, { + message: messageText, + scriptId: { chain, index }, + rootWalletKeys: walletKeys, + }); + assert.ok(signers.includes('user')); + assert.ok(signers.includes('bitgo')); }); }); - describe('utxolib verification stack - wasm-utxo respects input.sighashType', function () { - // This test verifies that wasm-utxo correctly respects the input.sighashType field - // when creating musig2 partial signatures. - // - // Previously (before fix), wasm-utxo would always create signatures with SIGHASH_DEFAULT (0) - // regardless of the input.sighashType field, causing validation to fail. + describe('utxolib interoperability - wasm-utxo can verify utxolib-generated BIP322 proofs', function () { + // This test verifies cross-library compatibility: + // 1. utxo-core (utxolib) creates a BIP322 PSBT + // 2. wasm-utxo signs it with musig2 + // 3. utxo-core validates the wasm-utxo signatures // - // Now (after fix), wasm-utxo reads input.sighashType and creates signatures with the - // correct sighash type, allowing validation to succeed. + // This ensures that wasm-utxo and utxolib generate compatible BIP322 proofs. - it('should validate signatures when wasm-utxo respects input.sighashType', function () { - const seed = 'p2trMusig2_sighash_test'; + it('should sign utxolib-created BIP322 PSBT and validate with utxolib', function () { + const seed = 'p2trMusig2_utxolib_compat_test'; const { xprivs } = createTestWalletKeys(seed); // Create utxolib RootWalletKeys for utxo-core PSBT construction @@ -485,28 +509,26 @@ describe('BIP322', function () { // p2trMusig2 external chain code const chain = utxolib.bitgo.getExternalChainCode('p2trMusig2'); const index = 0; - const messageText = 'BIP322 sighash test'; + const messageText = 'BIP322 utxolib interop test'; // Create BIP322 PSBT using utxo-core const psbt = coreBip322.createBaseToSignPsbt(utxolibRootWalletKeys, utxolib.networks.bitcoin); - coreBip322.addBip322InputWithChainAndIndex(psbt, messageText, utxolibRootWalletKeys, { chain, index }); - - // Note: utxo-core sets sighashType: Transaction.SIGHASH_ALL (1) for BIP322 inputs - const SIGHASH_ALL = 1; - assert.strictEqual(psbt.data.inputs[0].sighashType, SIGHASH_ALL); + coreBip322.addBip322InputWithChainAndIndex(psbt, messageText, utxolibRootWalletKeys, { + chain, + index, + }); - // Convert to wasm-utxo PSBT for cosigning + // Convert to wasm-utxo PSBT for signing const wasmPsbt = fixedScriptWallet.BitGoPsbt.fromBytes(psbt.toBuffer(), 'btc'); // Generate musig2 nonces and sign with wasm-utxo - // wasm-utxo now respects input.sighashType and creates signatures with SIGHASH_ALL const userKey = BIP32.fromBase58(xprivs[0]); const bitgoKey = BIP32.fromBase58(xprivs[2]); wasmPsbt.generateMusig2Nonces(userKey); wasmPsbt.generateMusig2Nonces(bitgoKey); - wasmPsbt.sign(0, userKey); - wasmPsbt.sign(0, bitgoKey); + wasmPsbt.sign(userKey); + wasmPsbt.sign(bitgoKey); // Convert back to utxolib PSBT for validation const signedPsbt = utxolib.bitgo.createPsbtFromBuffer( @@ -514,8 +536,7 @@ describe('BIP322', function () { utxolib.networks.bitcoin ); - // Validation should succeed because wasm-utxo now creates signatures - // with the correct sighash type (SIGHASH_ALL) matching input.sighashType + // Validation should succeed - wasm-utxo signatures are compatible with utxolib const validationResult = utxolib.bitgo.getSignatureValidationArrayPsbt(signedPsbt, utxolibRootWalletKeys); // Verify that both user (index 0) and bitgo (index 2) signatures are valid diff --git a/modules/abstract-utxo/test/unit/explainTransaction.ts b/modules/abstract-utxo/test/unit/explainTransaction.ts index aaf41d097c..b4c9bc6e74 100644 --- a/modules/abstract-utxo/test/unit/explainTransaction.ts +++ b/modules/abstract-utxo/test/unit/explainTransaction.ts @@ -40,5 +40,4 @@ describe('Explain Transaction', function () { await coin.explainTransaction(psbtTxHex, wallet); }); }); - }); diff --git a/modules/abstract-utxo/test/unit/fixtures/bip322/fixtures.ts b/modules/abstract-utxo/test/unit/fixtures/bip322/fixtures.ts deleted file mode 100644 index 3a8464eae7..0000000000 --- a/modules/abstract-utxo/test/unit/fixtures/bip322/fixtures.ts +++ /dev/null @@ -1,22 +0,0 @@ -import * as utxolib from '@bitgo/utxo-lib'; - -export const bip322Fixtures = { - valid: { - rootWalletKeys: utxolib.testutil.getDefaultWalletKeys(), - message: 'I can believe it is not butter', - unsigned: - '70736274ff0100fdae01000000000a97aac049b2b52216aecb2bc78c8af46201212ee96e20e914df2e36d69a543c5a0000000000000000000cdca00b194e8ca29eb3bf17c78bbeeefa318b6d84c315359f7d19106615681b000000000000000000c8f6693d34c4e98ef1fee4970e8342a12e67f81a4e74f7c5f5203c7fd6bcf2b90000000000000000009f29963b1fa0a10e5fd6d35893ed4342755c5446055a3bb3f11db5dbfb50c4e2000000000000000000f76fd6f563a17407845ad8f4e5654a520db5ebe1248b05a7e0926037cce94057000000000000000000764f19f344db53359718284d4bb03bb0bbb83ae4125d45047faf3bf26a97ca95000000000000000000e2b53078c15bebe0bc8152af38bb355d08adeae9b5b86ec6770ce66d92e9d969000000000000000000a2dcc6006e599cb473166667035ba9b6c662cfed865daebb425a89d40af2dffe00000000000000000047b378408d0f5a9be521ce8e37113a0202b0e0a8c8d3409e3e1e4c27038c29560000000000000000005019def787deba1bac0c861ecea5bdbd6269f373d9daeaa31396350b2c84d2ca000000000000000000010000000000000000016a000000004f010488b21e0000000000000000003a922e29f0c8eb0db2a60484cbdcb631f6b107c9caae3ffdcf3e7d2ec1f6bcd00312148715f361dab685a669d42431e5d6d3f973404dab9c9fd1b950b279ad763404cc18ae084f010488b21e0000000000000000006d1d656d3ddd91c194c04565a3603702a21016ced14a265f38982d6275e67b6403d3bac2313a7c6b21cbb11b14b0d10341f922c0a403a8bd8c87f0dc820f35af6e04f65cd8694f010488b21e000000000000000000cb04fd63ab34d90fe6466b880e2a02ccf8a863374312991af8911b1aaab443340336ef228ffe9b8efffba052c32d334660dd1f8366cf8fe44ae5aa672b6b62909504f2ef03890001007500000000010000000000000000000000000000000000000000000000000000000000000000ffffffff220020e816464a0141176fb1b582b012c5b2a7b3ab3f708465b5cf9cf81999f4e6aff10000000001000000000000000017a914d909474404c124a3d04c3fbff61faa49cf43c58b8700000000010120000000000000000017a914d909474404c124a3d04c3fbff61faa49cf43c58b87010469522102cc4d0fa411cac244486f8eb2c08e035ff7410f460a359ca7f8810991bd3b42092102d72fd0d0d90293434ad5fca160f278e03c614497aa4e425cf454e2c1330f96ab210344d884136df550202865ffbc6218c7f9c88fe6ce39c945798190badb38a752f153ae220602cc4d0fa411cac244486f8eb2c08e035ff7410f460a359ca7f8810991bd3b420914f2ef038900000000000000000000000000000000220602d72fd0d0d90293434ad5fca160f278e03c614497aa4e425cf454e2c1330f96ab14cc18ae080000000000000000000000000000000022060344d884136df550202865ffbc6218c7f9c88fe6ce39c945798190badb38a752f114f65cd8690000000000000000000000000000000008fc05424954474f051e492063616e2062656c69657665206974206973206e6f74206275747465720001007500000000010000000000000000000000000000000000000000000000000000000000000000ffffffff220020e816464a0141176fb1b582b012c5b2a7b3ab3f708465b5cf9cf81999f4e6aff10000000001000000000000000017a9145768efe6a71468736dce66cba5929c6350609c358700000000010120000000000000000017a9145768efe6a71468736dce66cba5929c6350609c3587010469522103a0334f9fbb9da9888e9ea3cc458c2a9ef659aec340bfb1d2d6616f01e5e5c4e421028fd779569e5588cabc672fc3c64fe12501490439a8e1990e079003a1a66921ea2103761b51c305db18bf65bb7703e47bb31e40372bf58a06b8970d3a3442f867741e53ae2206028fd779569e5588cabc672fc3c64fe12501490439a8e1990e079003a1a66921ea14cc18ae0800000000000000000100000001000000220603761b51c305db18bf65bb7703e47bb31e40372bf58a06b8970d3a3442f867741e14f65cd86900000000000000000100000001000000220603a0334f9fbb9da9888e9ea3cc458c2a9ef659aec340bfb1d2d6616f01e5e5c4e414f2ef03890000000000000000010000000100000008fc05424954474f051e492063616e2062656c69657665206974206973206e6f74206275747465720001007500000000010000000000000000000000000000000000000000000000000000000000000000ffffffff220020e816464a0141176fb1b582b012c5b2a7b3ab3f708465b5cf9cf81999f4e6aff10000000001000000000000000017a914836e0b75e731521db07aa2dfc5a15c9b7e279ab38700000000010120000000000000000017a914836e0b75e731521db07aa2dfc5a15c9b7e279ab38701042200208224f2e5de25d91de7ae08f8744f98ac30a7c86e9c0fcf169279e520c2b8313a010569522102d5878a4d1c23ea735d7146ef2e1094f08d118bf0e20ea09366077c6b086c49d62102804dae97fd1acbb46d2816f530f83119dccdcaf3f5dd4c4dabb8edccd04bc65c21022ddee7d0b3f7f894fca44650a72137908eb843432e6874cb873cc0f2d27a54d653ae2206022ddee7d0b3f7f894fca44650a72137908eb843432e6874cb873cc0f2d27a54d614f65cd86900000000000000000a00000002000000220602804dae97fd1acbb46d2816f530f83119dccdcaf3f5dd4c4dabb8edccd04bc65c14cc18ae0800000000000000000a00000002000000220602d5878a4d1c23ea735d7146ef2e1094f08d118bf0e20ea09366077c6b086c49d614f2ef038900000000000000000a0000000200000008fc05424954474f051e492063616e2062656c69657665206974206973206e6f74206275747465720001007500000000010000000000000000000000000000000000000000000000000000000000000000ffffffff220020e816464a0141176fb1b582b012c5b2a7b3ab3f708465b5cf9cf81999f4e6aff10000000001000000000000000017a914a7621ee3549de756e9048183171d02a48e1f18a28700000000010120000000000000000017a914a7621ee3549de756e9048183171d02a48e1f18a2870104220020fff6d24d943ecfea3b2cab30a1db70ef66b765be13b13e4e1758410217edf5710105695221034f0c87cd48157c37d26c296937b816218f6093d038ae6c3deba6b9f5776abfb32103eebab40c070ebe90e2806af5a4e365116c013a4d81ee7c167663649ef7a91d5321031b6cad2325e074b6f7419e7d1bfe37f317b309707630c0c313f89221cc8113c753ae2206031b6cad2325e074b6f7419e7d1bfe37f317b309707630c0c313f89221cc8113c714f65cd86900000000000000000b000000030000002206034f0c87cd48157c37d26c296937b816218f6093d038ae6c3deba6b9f5776abfb314f2ef038900000000000000000b00000003000000220603eebab40c070ebe90e2806af5a4e365116c013a4d81ee7c167663649ef7a91d5314cc18ae0800000000000000000b0000000300000008fc05424954474f051e492063616e2062656c69657665206974206973206e6f74206275747465720001008000000000010000000000000000000000000000000000000000000000000000000000000000ffffffff220020e816464a0141176fb1b582b012c5b2a7b3ab3f708465b5cf9cf81999f4e6aff10000000001000000000000000022002080ebe4e2d2d33e7898cb0ddabb1a6dbc2466d6333820886fb6842b49e2ac76ba0000000001012b000000000000000022002080ebe4e2d2d33e7898cb0ddabb1a6dbc2466d6333820886fb6842b49e2ac76ba01056952210383a7be358204e1f8dfba8dc7cdc0dca66a00b14eaf679d91b1a92f656c26bf8421027c8c55f1c02918ebf0daed6dc58f5e0b1074e46d8310d162570044084a0967692103623dc211dc28212e83fdf9f808e1dec770e461b0af1ba65458be106222be6a6153ae2206027c8c55f1c02918ebf0daed6dc58f5e0b1074e46d8310d162570044084a09676914cc18ae0800000000000000001400000004000000220603623dc211dc28212e83fdf9f808e1dec770e461b0af1ba65458be106222be6a6114f65cd8690000000000000000140000000400000022060383a7be358204e1f8dfba8dc7cdc0dca66a00b14eaf679d91b1a92f656c26bf8414f2ef03890000000000000000140000000400000008fc05424954474f051e492063616e2062656c69657665206974206973206e6f74206275747465720001008000000000010000000000000000000000000000000000000000000000000000000000000000ffffffff220020e816464a0141176fb1b582b012c5b2a7b3ab3f708465b5cf9cf81999f4e6aff10000000001000000000000000022002096462d2916f4907312fb02c74412b5a8d92f566f5d4f34d744916720aec977be0000000001012b000000000000000022002096462d2916f4907312fb02c74412b5a8d92f566f5d4f34d744916720aec977be010569522102f1af33db32f4c97b5f9381129f47e9f622a427144518eae3f8eca814741241f62102671f7f94e0adbc5be6893191ab88405b7aaa5cba3ef98ae4fd136316aad4cef32102d71c110eed1ae7e1c51377ac72f94339caa280a6d9dd7a843aad65cf4509288b53ae220602671f7f94e0adbc5be6893191ab88405b7aaa5cba3ef98ae4fd136316aad4cef314cc18ae0800000000000000001500000005000000220602d71c110eed1ae7e1c51377ac72f94339caa280a6d9dd7a843aad65cf4509288b14f65cd86900000000000000001500000005000000220602f1af33db32f4c97b5f9381129f47e9f622a427144518eae3f8eca814741241f614f2ef03890000000000000000150000000500000008fc05424954474f051e492063616e2062656c69657665206974206973206e6f74206275747465720001008000000000010000000000000000000000000000000000000000000000000000000000000000ffffffff220020e816464a0141176fb1b582b012c5b2a7b3ab3f708465b5cf9cf81999f4e6aff100000000010000000000000000225120db284bf5c2d0ba0a03fe29240f29bda603a705aa090584b637474ec2c09f7c290000000001012b0000000000000000225120db284bf5c2d0ba0a03fe29240f29bda603a705aa090584b637474ec2c09f7c294215c11ce1dfba2ee2d1cab438a36b7e39f77da38229cc8f220570166a04bf0f29daf10f3d3e320bd977cf4de055594a1b93b6bf3bf35d9e5b8ac3bae37ca23f76f91d4520c69e0b14bfba3101e0f336480a4617964ddb34d14755406529bc3f1f57fa49c8ad202a42e9d0284d5515d404dd84024b3d2eb291e2f971da02262a91551e35ed7e17acc021162a42e9d0284d5515d404dd84024b3d2eb291e2f971da02262a91551e35ed7e17350142a464371efcc831b425e671f82488821ec1fbd1610cd5017540d3f2b17eb791f65cd86900000000000000001e000000060000002116c69e0b14bfba3101e0f336480a4617964ddb34d14755406529bc3f1f57fa49c8350142a464371efcc831b425e671f82488821ec1fbd1610cd5017540d3f2b17eb791f2ef038900000000000000001e0000000600000008fc05424954474f051e492063616e2062656c69657665206974206973206e6f74206275747465720001008000000000010000000000000000000000000000000000000000000000000000000000000000ffffffff220020e816464a0141176fb1b582b012c5b2a7b3ab3f708465b5cf9cf81999f4e6aff1000000000100000000000000002251201a7acb31cf6bbf739dcc38b6d889ed3c2f3c8dfe219bd53347de500e7a421c7d0000000001012b00000000000000002251201a7acb31cf6bbf739dcc38b6d889ed3c2f3c8dfe219bd53347de500e7a421c7d4215c0a30fa2cbc9c0febc03c43d3d4d5f450ca164039364081cc681cc308388d249947ae56f815f9d25f5c2b33edeaad613c1a7d14fba7a2d48db9523b06c5eea55074520e08fd25466b7ddb5239bd54c4823f877c137bda413fb5bd8a12bc5242a18900fad209b8c27e9f9f279390ab304cc3415e29d59e15f6ba34d463446fc90f8ea3ce315acc021169b8c27e9f9f279390ab304cc3415e29d59e15f6ba34d463446fc90f8ea3ce31535014b5521d0235ac6798e0ec20fcab5572e4853cc74b2ac5623d8c8472a9b8c3d74f65cd86900000000000000001f000000070000002116e08fd25466b7ddb5239bd54c4823f877c137bda413fb5bd8a12bc5242a18900f35014b5521d0235ac6798e0ec20fcab5572e4853cc74b2ac5623d8c8472a9b8c3d74f2ef038900000000000000001f0000000700000008fc05424954474f051e492063616e2062656c69657665206974206973206e6f74206275747465720001008000000000010000000000000000000000000000000000000000000000000000000000000000ffffffff220020e816464a0141176fb1b582b012c5b2a7b3ab3f708465b5cf9cf81999f4e6aff1000000000100000000000000002251201d4f08c91c860545e970325543b283f8ebb036877337a2c96907ebd7cd2f03540000000001012b00000000000000002251201d4f08c91c860545e970325543b283f8ebb036877337a2c96907ebd7cd2f0354211633bc494f3ea86c13a9fba20abfa888593110db9e23c268105c8988d87957b9101500f2ef0389000000000000000028000000080000002116eb3a656215fa62b1d240096f695e3184eec77ee08fbbc5ced600b5af875950941500f65cd869000000000000000028000000080000000117207f5b14ae6f1cb9426136073c6962eef187f4af390aaa7ea11da6be101558cb670118202d5ae45def1d1e5c0e119b107275755f75386d7a86dec79d6f4621b385cf6cd648fc05424954474f011d4f08c91c860545e970325543b283f8ebb036877337a2c96907ebd7cd2f03547f5b14ae6f1cb9426136073c6962eef187f4af390aaa7ea11da6be101558cb67420233bc494f3ea86c13a9fba20abfa888593110db9e23c268105c8988d87957b91003eb3a656215fa62b1d240096f695e3184eec77ee08fbbc5ced600b5af8759509408fc05424954474f051e492063616e2062656c69657665206974206973206e6f74206275747465720001008000000000010000000000000000000000000000000000000000000000000000000000000000ffffffff220020e816464a0141176fb1b582b012c5b2a7b3ab3f708465b5cf9cf81999f4e6aff100000000010000000000000000225120bdc70773283923be0e6d17777d35cf901e02822a445637ad80ae694b557ab93d0000000001012b0000000000000000225120bdc70773283923be0e6d17777d35cf901e02822a445637ad80ae694b557ab93d2116f5370f52856166f7ec086a3e95cd7f1e848301b90681a7d0146ecb9ac5e1c59d1500f2ef0389000000000000000029000000090000002116f6b36d425bd445717706f8a1dbe4dd813fb0c50339fbec367eb0ed8c39b3d9dc1500f65cd86900000000000000002900000009000000011720c4a6939e74c11c496fdcee803e3292117760a19fe42a3dd089ad801e9f2cb0430118200da8eab7f651495d404cf9c51e15747719a42e3b4078ca87c5412e84563bd47b48fc05424954474f01bdc70773283923be0e6d17777d35cf901e02822a445637ad80ae694b557ab93dc4a6939e74c11c496fdcee803e3292117760a19fe42a3dd089ad801e9f2cb0434202f5370f52856166f7ec086a3e95cd7f1e848301b90681a7d0146ecb9ac5e1c59d02f6b36d425bd445717706f8a1dbe4dd813fb0c50339fbec367eb0ed8c39b3d9dc08fc05424954474f051e492063616e2062656c69657665206974206973206e6f74206275747465720000', - userNonce: - '70736274ff0100fdae01000000000a97aac049b2b52216aecb2bc78c8af46201212ee96e20e914df2e36d69a543c5a0000000000000000000cdca00b194e8ca29eb3bf17c78bbeeefa318b6d84c315359f7d19106615681b000000000000000000c8f6693d34c4e98ef1fee4970e8342a12e67f81a4e74f7c5f5203c7fd6bcf2b90000000000000000009f29963b1fa0a10e5fd6d35893ed4342755c5446055a3bb3f11db5dbfb50c4e2000000000000000000f76fd6f563a17407845ad8f4e5654a520db5ebe1248b05a7e0926037cce94057000000000000000000764f19f344db53359718284d4bb03bb0bbb83ae4125d45047faf3bf26a97ca95000000000000000000e2b53078c15bebe0bc8152af38bb355d08adeae9b5b86ec6770ce66d92e9d969000000000000000000a2dcc6006e599cb473166667035ba9b6c662cfed865daebb425a89d40af2dffe00000000000000000047b378408d0f5a9be521ce8e37113a0202b0e0a8c8d3409e3e1e4c27038c29560000000000000000005019def787deba1bac0c861ecea5bdbd6269f373d9daeaa31396350b2c84d2ca000000000000000000010000000000000000016a000000004f010488b21e0000000000000000003a922e29f0c8eb0db2a60484cbdcb631f6b107c9caae3ffdcf3e7d2ec1f6bcd00312148715f361dab685a669d42431e5d6d3f973404dab9c9fd1b950b279ad763404cc18ae084f010488b21e0000000000000000006d1d656d3ddd91c194c04565a3603702a21016ced14a265f38982d6275e67b6403d3bac2313a7c6b21cbb11b14b0d10341f922c0a403a8bd8c87f0dc820f35af6e04f65cd8694f010488b21e000000000000000000cb04fd63ab34d90fe6466b880e2a02ccf8a863374312991af8911b1aaab443340336ef228ffe9b8efffba052c32d334660dd1f8366cf8fe44ae5aa672b6b62909504f2ef03890001007500000000010000000000000000000000000000000000000000000000000000000000000000ffffffff220020e816464a0141176fb1b582b012c5b2a7b3ab3f708465b5cf9cf81999f4e6aff10000000001000000000000000017a914d909474404c124a3d04c3fbff61faa49cf43c58b8700000000010120000000000000000017a914d909474404c124a3d04c3fbff61faa49cf43c58b87010469522102cc4d0fa411cac244486f8eb2c08e035ff7410f460a359ca7f8810991bd3b42092102d72fd0d0d90293434ad5fca160f278e03c614497aa4e425cf454e2c1330f96ab210344d884136df550202865ffbc6218c7f9c88fe6ce39c945798190badb38a752f153ae220602cc4d0fa411cac244486f8eb2c08e035ff7410f460a359ca7f8810991bd3b420914f2ef038900000000000000000000000000000000220602d72fd0d0d90293434ad5fca160f278e03c614497aa4e425cf454e2c1330f96ab14cc18ae080000000000000000000000000000000022060344d884136df550202865ffbc6218c7f9c88fe6ce39c945798190badb38a752f114f65cd8690000000000000000000000000000000008fc05424954474f051e492063616e2062656c69657665206974206973206e6f74206275747465720001007500000000010000000000000000000000000000000000000000000000000000000000000000ffffffff220020e816464a0141176fb1b582b012c5b2a7b3ab3f708465b5cf9cf81999f4e6aff10000000001000000000000000017a9145768efe6a71468736dce66cba5929c6350609c358700000000010120000000000000000017a9145768efe6a71468736dce66cba5929c6350609c3587010469522103a0334f9fbb9da9888e9ea3cc458c2a9ef659aec340bfb1d2d6616f01e5e5c4e421028fd779569e5588cabc672fc3c64fe12501490439a8e1990e079003a1a66921ea2103761b51c305db18bf65bb7703e47bb31e40372bf58a06b8970d3a3442f867741e53ae2206028fd779569e5588cabc672fc3c64fe12501490439a8e1990e079003a1a66921ea14cc18ae0800000000000000000100000001000000220603761b51c305db18bf65bb7703e47bb31e40372bf58a06b8970d3a3442f867741e14f65cd86900000000000000000100000001000000220603a0334f9fbb9da9888e9ea3cc458c2a9ef659aec340bfb1d2d6616f01e5e5c4e414f2ef03890000000000000000010000000100000008fc05424954474f051e492063616e2062656c69657665206974206973206e6f74206275747465720001007500000000010000000000000000000000000000000000000000000000000000000000000000ffffffff220020e816464a0141176fb1b582b012c5b2a7b3ab3f708465b5cf9cf81999f4e6aff10000000001000000000000000017a914836e0b75e731521db07aa2dfc5a15c9b7e279ab38700000000010120000000000000000017a914836e0b75e731521db07aa2dfc5a15c9b7e279ab38701042200208224f2e5de25d91de7ae08f8744f98ac30a7c86e9c0fcf169279e520c2b8313a010569522102d5878a4d1c23ea735d7146ef2e1094f08d118bf0e20ea09366077c6b086c49d62102804dae97fd1acbb46d2816f530f83119dccdcaf3f5dd4c4dabb8edccd04bc65c21022ddee7d0b3f7f894fca44650a72137908eb843432e6874cb873cc0f2d27a54d653ae2206022ddee7d0b3f7f894fca44650a72137908eb843432e6874cb873cc0f2d27a54d614f65cd86900000000000000000a00000002000000220602804dae97fd1acbb46d2816f530f83119dccdcaf3f5dd4c4dabb8edccd04bc65c14cc18ae0800000000000000000a00000002000000220602d5878a4d1c23ea735d7146ef2e1094f08d118bf0e20ea09366077c6b086c49d614f2ef038900000000000000000a0000000200000008fc05424954474f051e492063616e2062656c69657665206974206973206e6f74206275747465720001007500000000010000000000000000000000000000000000000000000000000000000000000000ffffffff220020e816464a0141176fb1b582b012c5b2a7b3ab3f708465b5cf9cf81999f4e6aff10000000001000000000000000017a914a7621ee3549de756e9048183171d02a48e1f18a28700000000010120000000000000000017a914a7621ee3549de756e9048183171d02a48e1f18a2870104220020fff6d24d943ecfea3b2cab30a1db70ef66b765be13b13e4e1758410217edf5710105695221034f0c87cd48157c37d26c296937b816218f6093d038ae6c3deba6b9f5776abfb32103eebab40c070ebe90e2806af5a4e365116c013a4d81ee7c167663649ef7a91d5321031b6cad2325e074b6f7419e7d1bfe37f317b309707630c0c313f89221cc8113c753ae2206031b6cad2325e074b6f7419e7d1bfe37f317b309707630c0c313f89221cc8113c714f65cd86900000000000000000b000000030000002206034f0c87cd48157c37d26c296937b816218f6093d038ae6c3deba6b9f5776abfb314f2ef038900000000000000000b00000003000000220603eebab40c070ebe90e2806af5a4e365116c013a4d81ee7c167663649ef7a91d5314cc18ae0800000000000000000b0000000300000008fc05424954474f051e492063616e2062656c69657665206974206973206e6f74206275747465720001008000000000010000000000000000000000000000000000000000000000000000000000000000ffffffff220020e816464a0141176fb1b582b012c5b2a7b3ab3f708465b5cf9cf81999f4e6aff10000000001000000000000000022002080ebe4e2d2d33e7898cb0ddabb1a6dbc2466d6333820886fb6842b49e2ac76ba0000000001012b000000000000000022002080ebe4e2d2d33e7898cb0ddabb1a6dbc2466d6333820886fb6842b49e2ac76ba01056952210383a7be358204e1f8dfba8dc7cdc0dca66a00b14eaf679d91b1a92f656c26bf8421027c8c55f1c02918ebf0daed6dc58f5e0b1074e46d8310d162570044084a0967692103623dc211dc28212e83fdf9f808e1dec770e461b0af1ba65458be106222be6a6153ae2206027c8c55f1c02918ebf0daed6dc58f5e0b1074e46d8310d162570044084a09676914cc18ae0800000000000000001400000004000000220603623dc211dc28212e83fdf9f808e1dec770e461b0af1ba65458be106222be6a6114f65cd8690000000000000000140000000400000022060383a7be358204e1f8dfba8dc7cdc0dca66a00b14eaf679d91b1a92f656c26bf8414f2ef03890000000000000000140000000400000008fc05424954474f051e492063616e2062656c69657665206974206973206e6f74206275747465720001008000000000010000000000000000000000000000000000000000000000000000000000000000ffffffff220020e816464a0141176fb1b582b012c5b2a7b3ab3f708465b5cf9cf81999f4e6aff10000000001000000000000000022002096462d2916f4907312fb02c74412b5a8d92f566f5d4f34d744916720aec977be0000000001012b000000000000000022002096462d2916f4907312fb02c74412b5a8d92f566f5d4f34d744916720aec977be010569522102f1af33db32f4c97b5f9381129f47e9f622a427144518eae3f8eca814741241f62102671f7f94e0adbc5be6893191ab88405b7aaa5cba3ef98ae4fd136316aad4cef32102d71c110eed1ae7e1c51377ac72f94339caa280a6d9dd7a843aad65cf4509288b53ae220602671f7f94e0adbc5be6893191ab88405b7aaa5cba3ef98ae4fd136316aad4cef314cc18ae0800000000000000001500000005000000220602d71c110eed1ae7e1c51377ac72f94339caa280a6d9dd7a843aad65cf4509288b14f65cd86900000000000000001500000005000000220602f1af33db32f4c97b5f9381129f47e9f622a427144518eae3f8eca814741241f614f2ef03890000000000000000150000000500000008fc05424954474f051e492063616e2062656c69657665206974206973206e6f74206275747465720001008000000000010000000000000000000000000000000000000000000000000000000000000000ffffffff220020e816464a0141176fb1b582b012c5b2a7b3ab3f708465b5cf9cf81999f4e6aff100000000010000000000000000225120db284bf5c2d0ba0a03fe29240f29bda603a705aa090584b637474ec2c09f7c290000000001012b0000000000000000225120db284bf5c2d0ba0a03fe29240f29bda603a705aa090584b637474ec2c09f7c294215c11ce1dfba2ee2d1cab438a36b7e39f77da38229cc8f220570166a04bf0f29daf10f3d3e320bd977cf4de055594a1b93b6bf3bf35d9e5b8ac3bae37ca23f76f91d4520c69e0b14bfba3101e0f336480a4617964ddb34d14755406529bc3f1f57fa49c8ad202a42e9d0284d5515d404dd84024b3d2eb291e2f971da02262a91551e35ed7e17acc021162a42e9d0284d5515d404dd84024b3d2eb291e2f971da02262a91551e35ed7e17350142a464371efcc831b425e671f82488821ec1fbd1610cd5017540d3f2b17eb791f65cd86900000000000000001e000000060000002116c69e0b14bfba3101e0f336480a4617964ddb34d14755406529bc3f1f57fa49c8350142a464371efcc831b425e671f82488821ec1fbd1610cd5017540d3f2b17eb791f2ef038900000000000000001e0000000600000008fc05424954474f051e492063616e2062656c69657665206974206973206e6f74206275747465720001008000000000010000000000000000000000000000000000000000000000000000000000000000ffffffff220020e816464a0141176fb1b582b012c5b2a7b3ab3f708465b5cf9cf81999f4e6aff1000000000100000000000000002251201a7acb31cf6bbf739dcc38b6d889ed3c2f3c8dfe219bd53347de500e7a421c7d0000000001012b00000000000000002251201a7acb31cf6bbf739dcc38b6d889ed3c2f3c8dfe219bd53347de500e7a421c7d4215c0a30fa2cbc9c0febc03c43d3d4d5f450ca164039364081cc681cc308388d249947ae56f815f9d25f5c2b33edeaad613c1a7d14fba7a2d48db9523b06c5eea55074520e08fd25466b7ddb5239bd54c4823f877c137bda413fb5bd8a12bc5242a18900fad209b8c27e9f9f279390ab304cc3415e29d59e15f6ba34d463446fc90f8ea3ce315acc021169b8c27e9f9f279390ab304cc3415e29d59e15f6ba34d463446fc90f8ea3ce31535014b5521d0235ac6798e0ec20fcab5572e4853cc74b2ac5623d8c8472a9b8c3d74f65cd86900000000000000001f000000070000002116e08fd25466b7ddb5239bd54c4823f877c137bda413fb5bd8a12bc5242a18900f35014b5521d0235ac6798e0ec20fcab5572e4853cc74b2ac5623d8c8472a9b8c3d74f2ef038900000000000000001f0000000700000008fc05424954474f051e492063616e2062656c69657665206974206973206e6f74206275747465720001008000000000010000000000000000000000000000000000000000000000000000000000000000ffffffff220020e816464a0141176fb1b582b012c5b2a7b3ab3f708465b5cf9cf81999f4e6aff1000000000100000000000000002251201d4f08c91c860545e970325543b283f8ebb036877337a2c96907ebd7cd2f03540000000001012b00000000000000002251201d4f08c91c860545e970325543b283f8ebb036877337a2c96907ebd7cd2f0354211633bc494f3ea86c13a9fba20abfa888593110db9e23c268105c8988d87957b9101500f2ef0389000000000000000028000000080000002116eb3a656215fa62b1d240096f695e3184eec77ee08fbbc5ced600b5af875950941500f65cd869000000000000000028000000080000000117207f5b14ae6f1cb9426136073c6962eef187f4af390aaa7ea11da6be101558cb670118202d5ae45def1d1e5c0e119b107275755f75386d7a86dec79d6f4621b385cf6cd648fc05424954474f011d4f08c91c860545e970325543b283f8ebb036877337a2c96907ebd7cd2f03547f5b14ae6f1cb9426136073c6962eef187f4af390aaa7ea11da6be101558cb67420233bc494f3ea86c13a9fba20abfa888593110db9e23c268105c8988d87957b91003eb3a656215fa62b1d240096f695e3184eec77ee08fbbc5ced600b5af8759509449fc05424954474f020233bc494f3ea86c13a9fba20abfa888593110db9e23c268105c8988d87957b9101d4f08c91c860545e970325543b283f8ebb036877337a2c96907ebd7cd2f0354420214145dfddd77b53e56128ec0b4a4f05ce87d15901d9faf96d1adac8424275bf602be4ea2b65970c459da9a1d518a62c30b0b91bf49da7cffd15ef6d50f3d14f7eb08fc05424954474f051e492063616e2062656c69657665206974206973206e6f74206275747465720001008000000000010000000000000000000000000000000000000000000000000000000000000000ffffffff220020e816464a0141176fb1b582b012c5b2a7b3ab3f708465b5cf9cf81999f4e6aff100000000010000000000000000225120bdc70773283923be0e6d17777d35cf901e02822a445637ad80ae694b557ab93d0000000001012b0000000000000000225120bdc70773283923be0e6d17777d35cf901e02822a445637ad80ae694b557ab93d2116f5370f52856166f7ec086a3e95cd7f1e848301b90681a7d0146ecb9ac5e1c59d1500f2ef0389000000000000000029000000090000002116f6b36d425bd445717706f8a1dbe4dd813fb0c50339fbec367eb0ed8c39b3d9dc1500f65cd86900000000000000002900000009000000011720c4a6939e74c11c496fdcee803e3292117760a19fe42a3dd089ad801e9f2cb0430118200da8eab7f651495d404cf9c51e15747719a42e3b4078ca87c5412e84563bd47b48fc05424954474f01bdc70773283923be0e6d17777d35cf901e02822a445637ad80ae694b557ab93dc4a6939e74c11c496fdcee803e3292117760a19fe42a3dd089ad801e9f2cb0434202f5370f52856166f7ec086a3e95cd7f1e848301b90681a7d0146ecb9ac5e1c59d02f6b36d425bd445717706f8a1dbe4dd813fb0c50339fbec367eb0ed8c39b3d9dc49fc05424954474f0202f5370f52856166f7ec086a3e95cd7f1e848301b90681a7d0146ecb9ac5e1c59dbdc70773283923be0e6d17777d35cf901e02822a445637ad80ae694b557ab93d42039cf40d4e38434812b1504189ec66a861a47f7e2930c209c9f51dc3b7db5edd98029e8aa6d757c6ad481402c2f2130c430a84e117e9c30e216651595a47ff10d21f08fc05424954474f051e492063616e2062656c69657665206974206973206e6f74206275747465720000', - hsmNonce: - '70736274ff0100fdae01000000000a97aac049b2b52216aecb2bc78c8af46201212ee96e20e914df2e36d69a543c5a0000000000000000000cdca00b194e8ca29eb3bf17c78bbeeefa318b6d84c315359f7d19106615681b000000000000000000c8f6693d34c4e98ef1fee4970e8342a12e67f81a4e74f7c5f5203c7fd6bcf2b90000000000000000009f29963b1fa0a10e5fd6d35893ed4342755c5446055a3bb3f11db5dbfb50c4e2000000000000000000f76fd6f563a17407845ad8f4e5654a520db5ebe1248b05a7e0926037cce94057000000000000000000764f19f344db53359718284d4bb03bb0bbb83ae4125d45047faf3bf26a97ca95000000000000000000e2b53078c15bebe0bc8152af38bb355d08adeae9b5b86ec6770ce66d92e9d969000000000000000000a2dcc6006e599cb473166667035ba9b6c662cfed865daebb425a89d40af2dffe00000000000000000047b378408d0f5a9be521ce8e37113a0202b0e0a8c8d3409e3e1e4c27038c29560000000000000000005019def787deba1bac0c861ecea5bdbd6269f373d9daeaa31396350b2c84d2ca000000000000000000010000000000000000016a000000004f010488b21e0000000000000000003a922e29f0c8eb0db2a60484cbdcb631f6b107c9caae3ffdcf3e7d2ec1f6bcd00312148715f361dab685a669d42431e5d6d3f973404dab9c9fd1b950b279ad763404cc18ae084f010488b21e0000000000000000006d1d656d3ddd91c194c04565a3603702a21016ced14a265f38982d6275e67b6403d3bac2313a7c6b21cbb11b14b0d10341f922c0a403a8bd8c87f0dc820f35af6e04f65cd8694f010488b21e000000000000000000cb04fd63ab34d90fe6466b880e2a02ccf8a863374312991af8911b1aaab443340336ef228ffe9b8efffba052c32d334660dd1f8366cf8fe44ae5aa672b6b62909504f2ef03890001007500000000010000000000000000000000000000000000000000000000000000000000000000ffffffff220020e816464a0141176fb1b582b012c5b2a7b3ab3f708465b5cf9cf81999f4e6aff10000000001000000000000000017a914d909474404c124a3d04c3fbff61faa49cf43c58b8700000000010120000000000000000017a914d909474404c124a3d04c3fbff61faa49cf43c58b87010469522102cc4d0fa411cac244486f8eb2c08e035ff7410f460a359ca7f8810991bd3b42092102d72fd0d0d90293434ad5fca160f278e03c614497aa4e425cf454e2c1330f96ab210344d884136df550202865ffbc6218c7f9c88fe6ce39c945798190badb38a752f153ae220602cc4d0fa411cac244486f8eb2c08e035ff7410f460a359ca7f8810991bd3b420914f2ef038900000000000000000000000000000000220602d72fd0d0d90293434ad5fca160f278e03c614497aa4e425cf454e2c1330f96ab14cc18ae080000000000000000000000000000000022060344d884136df550202865ffbc6218c7f9c88fe6ce39c945798190badb38a752f114f65cd8690000000000000000000000000000000008fc05424954474f051e492063616e2062656c69657665206974206973206e6f74206275747465720001007500000000010000000000000000000000000000000000000000000000000000000000000000ffffffff220020e816464a0141176fb1b582b012c5b2a7b3ab3f708465b5cf9cf81999f4e6aff10000000001000000000000000017a9145768efe6a71468736dce66cba5929c6350609c358700000000010120000000000000000017a9145768efe6a71468736dce66cba5929c6350609c3587010469522103a0334f9fbb9da9888e9ea3cc458c2a9ef659aec340bfb1d2d6616f01e5e5c4e421028fd779569e5588cabc672fc3c64fe12501490439a8e1990e079003a1a66921ea2103761b51c305db18bf65bb7703e47bb31e40372bf58a06b8970d3a3442f867741e53ae2206028fd779569e5588cabc672fc3c64fe12501490439a8e1990e079003a1a66921ea14cc18ae0800000000000000000100000001000000220603761b51c305db18bf65bb7703e47bb31e40372bf58a06b8970d3a3442f867741e14f65cd86900000000000000000100000001000000220603a0334f9fbb9da9888e9ea3cc458c2a9ef659aec340bfb1d2d6616f01e5e5c4e414f2ef03890000000000000000010000000100000008fc05424954474f051e492063616e2062656c69657665206974206973206e6f74206275747465720001007500000000010000000000000000000000000000000000000000000000000000000000000000ffffffff220020e816464a0141176fb1b582b012c5b2a7b3ab3f708465b5cf9cf81999f4e6aff10000000001000000000000000017a914836e0b75e731521db07aa2dfc5a15c9b7e279ab38700000000010120000000000000000017a914836e0b75e731521db07aa2dfc5a15c9b7e279ab38701042200208224f2e5de25d91de7ae08f8744f98ac30a7c86e9c0fcf169279e520c2b8313a010569522102d5878a4d1c23ea735d7146ef2e1094f08d118bf0e20ea09366077c6b086c49d62102804dae97fd1acbb46d2816f530f83119dccdcaf3f5dd4c4dabb8edccd04bc65c21022ddee7d0b3f7f894fca44650a72137908eb843432e6874cb873cc0f2d27a54d653ae2206022ddee7d0b3f7f894fca44650a72137908eb843432e6874cb873cc0f2d27a54d614f65cd86900000000000000000a00000002000000220602804dae97fd1acbb46d2816f530f83119dccdcaf3f5dd4c4dabb8edccd04bc65c14cc18ae0800000000000000000a00000002000000220602d5878a4d1c23ea735d7146ef2e1094f08d118bf0e20ea09366077c6b086c49d614f2ef038900000000000000000a0000000200000008fc05424954474f051e492063616e2062656c69657665206974206973206e6f74206275747465720001007500000000010000000000000000000000000000000000000000000000000000000000000000ffffffff220020e816464a0141176fb1b582b012c5b2a7b3ab3f708465b5cf9cf81999f4e6aff10000000001000000000000000017a914a7621ee3549de756e9048183171d02a48e1f18a28700000000010120000000000000000017a914a7621ee3549de756e9048183171d02a48e1f18a2870104220020fff6d24d943ecfea3b2cab30a1db70ef66b765be13b13e4e1758410217edf5710105695221034f0c87cd48157c37d26c296937b816218f6093d038ae6c3deba6b9f5776abfb32103eebab40c070ebe90e2806af5a4e365116c013a4d81ee7c167663649ef7a91d5321031b6cad2325e074b6f7419e7d1bfe37f317b309707630c0c313f89221cc8113c753ae2206031b6cad2325e074b6f7419e7d1bfe37f317b309707630c0c313f89221cc8113c714f65cd86900000000000000000b000000030000002206034f0c87cd48157c37d26c296937b816218f6093d038ae6c3deba6b9f5776abfb314f2ef038900000000000000000b00000003000000220603eebab40c070ebe90e2806af5a4e365116c013a4d81ee7c167663649ef7a91d5314cc18ae0800000000000000000b0000000300000008fc05424954474f051e492063616e2062656c69657665206974206973206e6f74206275747465720001008000000000010000000000000000000000000000000000000000000000000000000000000000ffffffff220020e816464a0141176fb1b582b012c5b2a7b3ab3f708465b5cf9cf81999f4e6aff10000000001000000000000000022002080ebe4e2d2d33e7898cb0ddabb1a6dbc2466d6333820886fb6842b49e2ac76ba0000000001012b000000000000000022002080ebe4e2d2d33e7898cb0ddabb1a6dbc2466d6333820886fb6842b49e2ac76ba01056952210383a7be358204e1f8dfba8dc7cdc0dca66a00b14eaf679d91b1a92f656c26bf8421027c8c55f1c02918ebf0daed6dc58f5e0b1074e46d8310d162570044084a0967692103623dc211dc28212e83fdf9f808e1dec770e461b0af1ba65458be106222be6a6153ae2206027c8c55f1c02918ebf0daed6dc58f5e0b1074e46d8310d162570044084a09676914cc18ae0800000000000000001400000004000000220603623dc211dc28212e83fdf9f808e1dec770e461b0af1ba65458be106222be6a6114f65cd8690000000000000000140000000400000022060383a7be358204e1f8dfba8dc7cdc0dca66a00b14eaf679d91b1a92f656c26bf8414f2ef03890000000000000000140000000400000008fc05424954474f051e492063616e2062656c69657665206974206973206e6f74206275747465720001008000000000010000000000000000000000000000000000000000000000000000000000000000ffffffff220020e816464a0141176fb1b582b012c5b2a7b3ab3f708465b5cf9cf81999f4e6aff10000000001000000000000000022002096462d2916f4907312fb02c74412b5a8d92f566f5d4f34d744916720aec977be0000000001012b000000000000000022002096462d2916f4907312fb02c74412b5a8d92f566f5d4f34d744916720aec977be010569522102f1af33db32f4c97b5f9381129f47e9f622a427144518eae3f8eca814741241f62102671f7f94e0adbc5be6893191ab88405b7aaa5cba3ef98ae4fd136316aad4cef32102d71c110eed1ae7e1c51377ac72f94339caa280a6d9dd7a843aad65cf4509288b53ae220602671f7f94e0adbc5be6893191ab88405b7aaa5cba3ef98ae4fd136316aad4cef314cc18ae0800000000000000001500000005000000220602d71c110eed1ae7e1c51377ac72f94339caa280a6d9dd7a843aad65cf4509288b14f65cd86900000000000000001500000005000000220602f1af33db32f4c97b5f9381129f47e9f622a427144518eae3f8eca814741241f614f2ef03890000000000000000150000000500000008fc05424954474f051e492063616e2062656c69657665206974206973206e6f74206275747465720001008000000000010000000000000000000000000000000000000000000000000000000000000000ffffffff220020e816464a0141176fb1b582b012c5b2a7b3ab3f708465b5cf9cf81999f4e6aff100000000010000000000000000225120db284bf5c2d0ba0a03fe29240f29bda603a705aa090584b637474ec2c09f7c290000000001012b0000000000000000225120db284bf5c2d0ba0a03fe29240f29bda603a705aa090584b637474ec2c09f7c294215c11ce1dfba2ee2d1cab438a36b7e39f77da38229cc8f220570166a04bf0f29daf10f3d3e320bd977cf4de055594a1b93b6bf3bf35d9e5b8ac3bae37ca23f76f91d4520c69e0b14bfba3101e0f336480a4617964ddb34d14755406529bc3f1f57fa49c8ad202a42e9d0284d5515d404dd84024b3d2eb291e2f971da02262a91551e35ed7e17acc021162a42e9d0284d5515d404dd84024b3d2eb291e2f971da02262a91551e35ed7e17350142a464371efcc831b425e671f82488821ec1fbd1610cd5017540d3f2b17eb791f65cd86900000000000000001e000000060000002116c69e0b14bfba3101e0f336480a4617964ddb34d14755406529bc3f1f57fa49c8350142a464371efcc831b425e671f82488821ec1fbd1610cd5017540d3f2b17eb791f2ef038900000000000000001e0000000600000008fc05424954474f051e492063616e2062656c69657665206974206973206e6f74206275747465720001008000000000010000000000000000000000000000000000000000000000000000000000000000ffffffff220020e816464a0141176fb1b582b012c5b2a7b3ab3f708465b5cf9cf81999f4e6aff1000000000100000000000000002251201a7acb31cf6bbf739dcc38b6d889ed3c2f3c8dfe219bd53347de500e7a421c7d0000000001012b00000000000000002251201a7acb31cf6bbf739dcc38b6d889ed3c2f3c8dfe219bd53347de500e7a421c7d4215c0a30fa2cbc9c0febc03c43d3d4d5f450ca164039364081cc681cc308388d249947ae56f815f9d25f5c2b33edeaad613c1a7d14fba7a2d48db9523b06c5eea55074520e08fd25466b7ddb5239bd54c4823f877c137bda413fb5bd8a12bc5242a18900fad209b8c27e9f9f279390ab304cc3415e29d59e15f6ba34d463446fc90f8ea3ce315acc021169b8c27e9f9f279390ab304cc3415e29d59e15f6ba34d463446fc90f8ea3ce31535014b5521d0235ac6798e0ec20fcab5572e4853cc74b2ac5623d8c8472a9b8c3d74f65cd86900000000000000001f000000070000002116e08fd25466b7ddb5239bd54c4823f877c137bda413fb5bd8a12bc5242a18900f35014b5521d0235ac6798e0ec20fcab5572e4853cc74b2ac5623d8c8472a9b8c3d74f2ef038900000000000000001f0000000700000008fc05424954474f051e492063616e2062656c69657665206974206973206e6f74206275747465720001008000000000010000000000000000000000000000000000000000000000000000000000000000ffffffff220020e816464a0141176fb1b582b012c5b2a7b3ab3f708465b5cf9cf81999f4e6aff1000000000100000000000000002251201d4f08c91c860545e970325543b283f8ebb036877337a2c96907ebd7cd2f03540000000001012b00000000000000002251201d4f08c91c860545e970325543b283f8ebb036877337a2c96907ebd7cd2f0354211633bc494f3ea86c13a9fba20abfa888593110db9e23c268105c8988d87957b9101500f2ef0389000000000000000028000000080000002116eb3a656215fa62b1d240096f695e3184eec77ee08fbbc5ced600b5af875950941500f65cd869000000000000000028000000080000000117207f5b14ae6f1cb9426136073c6962eef187f4af390aaa7ea11da6be101558cb670118202d5ae45def1d1e5c0e119b107275755f75386d7a86dec79d6f4621b385cf6cd648fc05424954474f011d4f08c91c860545e970325543b283f8ebb036877337a2c96907ebd7cd2f03547f5b14ae6f1cb9426136073c6962eef187f4af390aaa7ea11da6be101558cb67420233bc494f3ea86c13a9fba20abfa888593110db9e23c268105c8988d87957b91003eb3a656215fa62b1d240096f695e3184eec77ee08fbbc5ced600b5af8759509449fc05424954474f020233bc494f3ea86c13a9fba20abfa888593110db9e23c268105c8988d87957b9101d4f08c91c860545e970325543b283f8ebb036877337a2c96907ebd7cd2f0354420214145dfddd77b53e56128ec0b4a4f05ce87d15901d9faf96d1adac8424275bf602be4ea2b65970c459da9a1d518a62c30b0b91bf49da7cffd15ef6d50f3d14f7eb49fc05424954474f0203eb3a656215fa62b1d240096f695e3184eec77ee08fbbc5ced600b5af875950941d4f08c91c860545e970325543b283f8ebb036877337a2c96907ebd7cd2f03544202fb658ed2694609fa7a331c7234d22582c6065cb07a8e42ea006b2f0871ba2831038609d4767745bee45c2d9e7a649bef7eb9edf117c775c22a11085471006f9d5208fc05424954474f051e492063616e2062656c69657665206974206973206e6f74206275747465720001008000000000010000000000000000000000000000000000000000000000000000000000000000ffffffff220020e816464a0141176fb1b582b012c5b2a7b3ab3f708465b5cf9cf81999f4e6aff100000000010000000000000000225120bdc70773283923be0e6d17777d35cf901e02822a445637ad80ae694b557ab93d0000000001012b0000000000000000225120bdc70773283923be0e6d17777d35cf901e02822a445637ad80ae694b557ab93d2116f5370f52856166f7ec086a3e95cd7f1e848301b90681a7d0146ecb9ac5e1c59d1500f2ef0389000000000000000029000000090000002116f6b36d425bd445717706f8a1dbe4dd813fb0c50339fbec367eb0ed8c39b3d9dc1500f65cd86900000000000000002900000009000000011720c4a6939e74c11c496fdcee803e3292117760a19fe42a3dd089ad801e9f2cb0430118200da8eab7f651495d404cf9c51e15747719a42e3b4078ca87c5412e84563bd47b48fc05424954474f01bdc70773283923be0e6d17777d35cf901e02822a445637ad80ae694b557ab93dc4a6939e74c11c496fdcee803e3292117760a19fe42a3dd089ad801e9f2cb0434202f5370f52856166f7ec086a3e95cd7f1e848301b90681a7d0146ecb9ac5e1c59d02f6b36d425bd445717706f8a1dbe4dd813fb0c50339fbec367eb0ed8c39b3d9dc49fc05424954474f0202f5370f52856166f7ec086a3e95cd7f1e848301b90681a7d0146ecb9ac5e1c59dbdc70773283923be0e6d17777d35cf901e02822a445637ad80ae694b557ab93d42039cf40d4e38434812b1504189ec66a861a47f7e2930c209c9f51dc3b7db5edd98029e8aa6d757c6ad481402c2f2130c430a84e117e9c30e216651595a47ff10d21f49fc05424954474f0202f6b36d425bd445717706f8a1dbe4dd813fb0c50339fbec367eb0ed8c39b3d9dcbdc70773283923be0e6d17777d35cf901e02822a445637ad80ae694b557ab93d420203713b23f25f1db1be5fda7cebc1ef158c2162b884b29e02ffeff12a57044a3c03581c5f2a00d83cf087aeb5685c6c4f9a05ac84ee0ce53e0427591ed23fce47d008fc05424954474f051e492063616e2062656c69657665206974206973206e6f74206275747465720000', - userSignature: - '70736274ff0100fdae01000000000a97aac049b2b52216aecb2bc78c8af46201212ee96e20e914df2e36d69a543c5a0000000000000000000cdca00b194e8ca29eb3bf17c78bbeeefa318b6d84c315359f7d19106615681b000000000000000000c8f6693d34c4e98ef1fee4970e8342a12e67f81a4e74f7c5f5203c7fd6bcf2b90000000000000000009f29963b1fa0a10e5fd6d35893ed4342755c5446055a3bb3f11db5dbfb50c4e2000000000000000000f76fd6f563a17407845ad8f4e5654a520db5ebe1248b05a7e0926037cce94057000000000000000000764f19f344db53359718284d4bb03bb0bbb83ae4125d45047faf3bf26a97ca95000000000000000000e2b53078c15bebe0bc8152af38bb355d08adeae9b5b86ec6770ce66d92e9d969000000000000000000a2dcc6006e599cb473166667035ba9b6c662cfed865daebb425a89d40af2dffe00000000000000000047b378408d0f5a9be521ce8e37113a0202b0e0a8c8d3409e3e1e4c27038c29560000000000000000005019def787deba1bac0c861ecea5bdbd6269f373d9daeaa31396350b2c84d2ca000000000000000000010000000000000000016a000000004f010488b21e0000000000000000003a922e29f0c8eb0db2a60484cbdcb631f6b107c9caae3ffdcf3e7d2ec1f6bcd00312148715f361dab685a669d42431e5d6d3f973404dab9c9fd1b950b279ad763404cc18ae084f010488b21e0000000000000000006d1d656d3ddd91c194c04565a3603702a21016ced14a265f38982d6275e67b6403d3bac2313a7c6b21cbb11b14b0d10341f922c0a403a8bd8c87f0dc820f35af6e04f65cd8694f010488b21e000000000000000000cb04fd63ab34d90fe6466b880e2a02ccf8a863374312991af8911b1aaab443340336ef228ffe9b8efffba052c32d334660dd1f8366cf8fe44ae5aa672b6b62909504f2ef03890001007500000000010000000000000000000000000000000000000000000000000000000000000000ffffffff220020e816464a0141176fb1b582b012c5b2a7b3ab3f708465b5cf9cf81999f4e6aff10000000001000000000000000017a914d909474404c124a3d04c3fbff61faa49cf43c58b8700000000010120000000000000000017a914d909474404c124a3d04c3fbff61faa49cf43c58b87220202cc4d0fa411cac244486f8eb2c08e035ff7410f460a359ca7f8810991bd3b4209473044022013e1c5878f22f12eb699a5eb11f150999f5b169abaf587eb1b14cf0ce7a4f1cc02205fc547f631d8eb384695853f6d981bc1132eeccf5db5243f367166bbd9f2d60201010469522102cc4d0fa411cac244486f8eb2c08e035ff7410f460a359ca7f8810991bd3b42092102d72fd0d0d90293434ad5fca160f278e03c614497aa4e425cf454e2c1330f96ab210344d884136df550202865ffbc6218c7f9c88fe6ce39c945798190badb38a752f153ae220602cc4d0fa411cac244486f8eb2c08e035ff7410f460a359ca7f8810991bd3b420914f2ef038900000000000000000000000000000000220602d72fd0d0d90293434ad5fca160f278e03c614497aa4e425cf454e2c1330f96ab14cc18ae080000000000000000000000000000000022060344d884136df550202865ffbc6218c7f9c88fe6ce39c945798190badb38a752f114f65cd8690000000000000000000000000000000008fc05424954474f051e492063616e2062656c69657665206974206973206e6f74206275747465720001007500000000010000000000000000000000000000000000000000000000000000000000000000ffffffff220020e816464a0141176fb1b582b012c5b2a7b3ab3f708465b5cf9cf81999f4e6aff10000000001000000000000000017a9145768efe6a71468736dce66cba5929c6350609c358700000000010120000000000000000017a9145768efe6a71468736dce66cba5929c6350609c3587220203a0334f9fbb9da9888e9ea3cc458c2a9ef659aec340bfb1d2d6616f01e5e5c4e4473044022030c6314abb04849ef1fd552a6dc7691bbb21540c2bd7aa5a3f23385649db2ae402203dcedb41fff80b2115d736d29e65fa329ce2d175741f1df65b8d4be30f444dea01010469522103a0334f9fbb9da9888e9ea3cc458c2a9ef659aec340bfb1d2d6616f01e5e5c4e421028fd779569e5588cabc672fc3c64fe12501490439a8e1990e079003a1a66921ea2103761b51c305db18bf65bb7703e47bb31e40372bf58a06b8970d3a3442f867741e53ae2206028fd779569e5588cabc672fc3c64fe12501490439a8e1990e079003a1a66921ea14cc18ae0800000000000000000100000001000000220603761b51c305db18bf65bb7703e47bb31e40372bf58a06b8970d3a3442f867741e14f65cd86900000000000000000100000001000000220603a0334f9fbb9da9888e9ea3cc458c2a9ef659aec340bfb1d2d6616f01e5e5c4e414f2ef03890000000000000000010000000100000008fc05424954474f051e492063616e2062656c69657665206974206973206e6f74206275747465720001007500000000010000000000000000000000000000000000000000000000000000000000000000ffffffff220020e816464a0141176fb1b582b012c5b2a7b3ab3f708465b5cf9cf81999f4e6aff10000000001000000000000000017a914836e0b75e731521db07aa2dfc5a15c9b7e279ab38700000000010120000000000000000017a914836e0b75e731521db07aa2dfc5a15c9b7e279ab387220202d5878a4d1c23ea735d7146ef2e1094f08d118bf0e20ea09366077c6b086c49d64730440220126fd17b31a4cb2402592bd2f1bcda4d3b90ce8a49012755e2006800a9e0a42302202e1aeecbb936a4ca0afd576754b525824bb15e8254ed07ce6cb7174ef0ab5b790101042200208224f2e5de25d91de7ae08f8744f98ac30a7c86e9c0fcf169279e520c2b8313a010569522102d5878a4d1c23ea735d7146ef2e1094f08d118bf0e20ea09366077c6b086c49d62102804dae97fd1acbb46d2816f530f83119dccdcaf3f5dd4c4dabb8edccd04bc65c21022ddee7d0b3f7f894fca44650a72137908eb843432e6874cb873cc0f2d27a54d653ae2206022ddee7d0b3f7f894fca44650a72137908eb843432e6874cb873cc0f2d27a54d614f65cd86900000000000000000a00000002000000220602804dae97fd1acbb46d2816f530f83119dccdcaf3f5dd4c4dabb8edccd04bc65c14cc18ae0800000000000000000a00000002000000220602d5878a4d1c23ea735d7146ef2e1094f08d118bf0e20ea09366077c6b086c49d614f2ef038900000000000000000a0000000200000008fc05424954474f051e492063616e2062656c69657665206974206973206e6f74206275747465720001007500000000010000000000000000000000000000000000000000000000000000000000000000ffffffff220020e816464a0141176fb1b582b012c5b2a7b3ab3f708465b5cf9cf81999f4e6aff10000000001000000000000000017a914a7621ee3549de756e9048183171d02a48e1f18a28700000000010120000000000000000017a914a7621ee3549de756e9048183171d02a48e1f18a2872202034f0c87cd48157c37d26c296937b816218f6093d038ae6c3deba6b9f5776abfb3473044022007a80b56b6d628a7be48b825bf964269de3101598e1b83dce67996ad14af21a902205eab963b093c0a871d8e6d63cf9c008fa7a70c6cc3cb5f78f951399eab283406010104220020fff6d24d943ecfea3b2cab30a1db70ef66b765be13b13e4e1758410217edf5710105695221034f0c87cd48157c37d26c296937b816218f6093d038ae6c3deba6b9f5776abfb32103eebab40c070ebe90e2806af5a4e365116c013a4d81ee7c167663649ef7a91d5321031b6cad2325e074b6f7419e7d1bfe37f317b309707630c0c313f89221cc8113c753ae2206031b6cad2325e074b6f7419e7d1bfe37f317b309707630c0c313f89221cc8113c714f65cd86900000000000000000b000000030000002206034f0c87cd48157c37d26c296937b816218f6093d038ae6c3deba6b9f5776abfb314f2ef038900000000000000000b00000003000000220603eebab40c070ebe90e2806af5a4e365116c013a4d81ee7c167663649ef7a91d5314cc18ae0800000000000000000b0000000300000008fc05424954474f051e492063616e2062656c69657665206974206973206e6f74206275747465720001008000000000010000000000000000000000000000000000000000000000000000000000000000ffffffff220020e816464a0141176fb1b582b012c5b2a7b3ab3f708465b5cf9cf81999f4e6aff10000000001000000000000000022002080ebe4e2d2d33e7898cb0ddabb1a6dbc2466d6333820886fb6842b49e2ac76ba0000000001012b000000000000000022002080ebe4e2d2d33e7898cb0ddabb1a6dbc2466d6333820886fb6842b49e2ac76ba22020383a7be358204e1f8dfba8dc7cdc0dca66a00b14eaf679d91b1a92f656c26bf8447304402202847ad6c0dafc791fe269d346e95c8ccba12efa90be9bc121af181b31500f26502201c0193ae7152d4a810a30cab134e5dff4bea538174de2fab1c903a00a543a5e30101056952210383a7be358204e1f8dfba8dc7cdc0dca66a00b14eaf679d91b1a92f656c26bf8421027c8c55f1c02918ebf0daed6dc58f5e0b1074e46d8310d162570044084a0967692103623dc211dc28212e83fdf9f808e1dec770e461b0af1ba65458be106222be6a6153ae2206027c8c55f1c02918ebf0daed6dc58f5e0b1074e46d8310d162570044084a09676914cc18ae0800000000000000001400000004000000220603623dc211dc28212e83fdf9f808e1dec770e461b0af1ba65458be106222be6a6114f65cd8690000000000000000140000000400000022060383a7be358204e1f8dfba8dc7cdc0dca66a00b14eaf679d91b1a92f656c26bf8414f2ef03890000000000000000140000000400000008fc05424954474f051e492063616e2062656c69657665206974206973206e6f74206275747465720001008000000000010000000000000000000000000000000000000000000000000000000000000000ffffffff220020e816464a0141176fb1b582b012c5b2a7b3ab3f708465b5cf9cf81999f4e6aff10000000001000000000000000022002096462d2916f4907312fb02c74412b5a8d92f566f5d4f34d744916720aec977be0000000001012b000000000000000022002096462d2916f4907312fb02c74412b5a8d92f566f5d4f34d744916720aec977be220202f1af33db32f4c97b5f9381129f47e9f622a427144518eae3f8eca814741241f647304402206c472129bab262d8e15c4d8eae6bae8ffaafad49e0857de0defc705c454ac7c202201d1f95264be08d72bd2dc313e61a7394ea4b4d9e4b01f78684059f6d0068240f01010569522102f1af33db32f4c97b5f9381129f47e9f622a427144518eae3f8eca814741241f62102671f7f94e0adbc5be6893191ab88405b7aaa5cba3ef98ae4fd136316aad4cef32102d71c110eed1ae7e1c51377ac72f94339caa280a6d9dd7a843aad65cf4509288b53ae220602671f7f94e0adbc5be6893191ab88405b7aaa5cba3ef98ae4fd136316aad4cef314cc18ae0800000000000000001500000005000000220602d71c110eed1ae7e1c51377ac72f94339caa280a6d9dd7a843aad65cf4509288b14f65cd86900000000000000001500000005000000220602f1af33db32f4c97b5f9381129f47e9f622a427144518eae3f8eca814741241f614f2ef03890000000000000000150000000500000008fc05424954474f051e492063616e2062656c69657665206974206973206e6f74206275747465720001008000000000010000000000000000000000000000000000000000000000000000000000000000ffffffff220020e816464a0141176fb1b582b012c5b2a7b3ab3f708465b5cf9cf81999f4e6aff100000000010000000000000000225120db284bf5c2d0ba0a03fe29240f29bda603a705aa090584b637474ec2c09f7c290000000001012b0000000000000000225120db284bf5c2d0ba0a03fe29240f29bda603a705aa090584b637474ec2c09f7c294114c69e0b14bfba3101e0f336480a4617964ddb34d14755406529bc3f1f57fa49c842a464371efcc831b425e671f82488821ec1fbd1610cd5017540d3f2b17eb79140b2b2467a458d385aa4afb982c624ea7239720f204c54abaf66f34d1feca154092bb8b060ee3564be1bf4abd33c508c5474e0182a6abb22ef9fad35c929182e3a4215c11ce1dfba2ee2d1cab438a36b7e39f77da38229cc8f220570166a04bf0f29daf10f3d3e320bd977cf4de055594a1b93b6bf3bf35d9e5b8ac3bae37ca23f76f91d4520c69e0b14bfba3101e0f336480a4617964ddb34d14755406529bc3f1f57fa49c8ad202a42e9d0284d5515d404dd84024b3d2eb291e2f971da02262a91551e35ed7e17acc021162a42e9d0284d5515d404dd84024b3d2eb291e2f971da02262a91551e35ed7e17350142a464371efcc831b425e671f82488821ec1fbd1610cd5017540d3f2b17eb791f65cd86900000000000000001e000000060000002116c69e0b14bfba3101e0f336480a4617964ddb34d14755406529bc3f1f57fa49c8350142a464371efcc831b425e671f82488821ec1fbd1610cd5017540d3f2b17eb791f2ef038900000000000000001e0000000600000008fc05424954474f051e492063616e2062656c69657665206974206973206e6f74206275747465720001008000000000010000000000000000000000000000000000000000000000000000000000000000ffffffff220020e816464a0141176fb1b582b012c5b2a7b3ab3f708465b5cf9cf81999f4e6aff1000000000100000000000000002251201a7acb31cf6bbf739dcc38b6d889ed3c2f3c8dfe219bd53347de500e7a421c7d0000000001012b00000000000000002251201a7acb31cf6bbf739dcc38b6d889ed3c2f3c8dfe219bd53347de500e7a421c7d4114e08fd25466b7ddb5239bd54c4823f877c137bda413fb5bd8a12bc5242a18900f4b5521d0235ac6798e0ec20fcab5572e4853cc74b2ac5623d8c8472a9b8c3d744002ecc2125852f5856584d56181d4ef896208b20d95046fea05944b78c5d4978dd8f7013b30cfcb9837aeb8bbe314a6bd74b4ef9c9460fff6a9f04a41342ce4a14215c0a30fa2cbc9c0febc03c43d3d4d5f450ca164039364081cc681cc308388d249947ae56f815f9d25f5c2b33edeaad613c1a7d14fba7a2d48db9523b06c5eea55074520e08fd25466b7ddb5239bd54c4823f877c137bda413fb5bd8a12bc5242a18900fad209b8c27e9f9f279390ab304cc3415e29d59e15f6ba34d463446fc90f8ea3ce315acc021169b8c27e9f9f279390ab304cc3415e29d59e15f6ba34d463446fc90f8ea3ce31535014b5521d0235ac6798e0ec20fcab5572e4853cc74b2ac5623d8c8472a9b8c3d74f65cd86900000000000000001f000000070000002116e08fd25466b7ddb5239bd54c4823f877c137bda413fb5bd8a12bc5242a18900f35014b5521d0235ac6798e0ec20fcab5572e4853cc74b2ac5623d8c8472a9b8c3d74f2ef038900000000000000001f0000000700000008fc05424954474f051e492063616e2062656c69657665206974206973206e6f74206275747465720001008000000000010000000000000000000000000000000000000000000000000000000000000000ffffffff220020e816464a0141176fb1b582b012c5b2a7b3ab3f708465b5cf9cf81999f4e6aff1000000000100000000000000002251201d4f08c91c860545e970325543b283f8ebb036877337a2c96907ebd7cd2f03540000000001012b00000000000000002251201d4f08c91c860545e970325543b283f8ebb036877337a2c96907ebd7cd2f0354211633bc494f3ea86c13a9fba20abfa888593110db9e23c268105c8988d87957b9101500f2ef0389000000000000000028000000080000002116eb3a656215fa62b1d240096f695e3184eec77ee08fbbc5ced600b5af875950941500f65cd869000000000000000028000000080000000117207f5b14ae6f1cb9426136073c6962eef187f4af390aaa7ea11da6be101558cb670118202d5ae45def1d1e5c0e119b107275755f75386d7a86dec79d6f4621b385cf6cd648fc05424954474f011d4f08c91c860545e970325543b283f8ebb036877337a2c96907ebd7cd2f03547f5b14ae6f1cb9426136073c6962eef187f4af390aaa7ea11da6be101558cb67420233bc494f3ea86c13a9fba20abfa888593110db9e23c268105c8988d87957b91003eb3a656215fa62b1d240096f695e3184eec77ee08fbbc5ced600b5af8759509449fc05424954474f020233bc494f3ea86c13a9fba20abfa888593110db9e23c268105c8988d87957b9101d4f08c91c860545e970325543b283f8ebb036877337a2c96907ebd7cd2f0354420214145dfddd77b53e56128ec0b4a4f05ce87d15901d9faf96d1adac8424275bf602be4ea2b65970c459da9a1d518a62c30b0b91bf49da7cffd15ef6d50f3d14f7eb49fc05424954474f0203eb3a656215fa62b1d240096f695e3184eec77ee08fbbc5ced600b5af875950941d4f08c91c860545e970325543b283f8ebb036877337a2c96907ebd7cd2f03544202fb658ed2694609fa7a331c7234d22582c6065cb07a8e42ea006b2f0871ba2831038609d4767745bee45c2d9e7a649bef7eb9edf117c775c22a11085471006f9d5249fc05424954474f030233bc494f3ea86c13a9fba20abfa888593110db9e23c268105c8988d87957b9101d4f08c91c860545e970325543b283f8ebb036877337a2c96907ebd7cd2f0354207a75fe800b86ce6ac43f4b343d6de5cfc641730be25baa876b6e6cb293b717a408fc05424954474f051e492063616e2062656c69657665206974206973206e6f74206275747465720001008000000000010000000000000000000000000000000000000000000000000000000000000000ffffffff220020e816464a0141176fb1b582b012c5b2a7b3ab3f708465b5cf9cf81999f4e6aff100000000010000000000000000225120bdc70773283923be0e6d17777d35cf901e02822a445637ad80ae694b557ab93d0000000001012b0000000000000000225120bdc70773283923be0e6d17777d35cf901e02822a445637ad80ae694b557ab93d2116f5370f52856166f7ec086a3e95cd7f1e848301b90681a7d0146ecb9ac5e1c59d1500f2ef0389000000000000000029000000090000002116f6b36d425bd445717706f8a1dbe4dd813fb0c50339fbec367eb0ed8c39b3d9dc1500f65cd86900000000000000002900000009000000011720c4a6939e74c11c496fdcee803e3292117760a19fe42a3dd089ad801e9f2cb0430118200da8eab7f651495d404cf9c51e15747719a42e3b4078ca87c5412e84563bd47b48fc05424954474f01bdc70773283923be0e6d17777d35cf901e02822a445637ad80ae694b557ab93dc4a6939e74c11c496fdcee803e3292117760a19fe42a3dd089ad801e9f2cb0434202f5370f52856166f7ec086a3e95cd7f1e848301b90681a7d0146ecb9ac5e1c59d02f6b36d425bd445717706f8a1dbe4dd813fb0c50339fbec367eb0ed8c39b3d9dc49fc05424954474f0202f5370f52856166f7ec086a3e95cd7f1e848301b90681a7d0146ecb9ac5e1c59dbdc70773283923be0e6d17777d35cf901e02822a445637ad80ae694b557ab93d42039cf40d4e38434812b1504189ec66a861a47f7e2930c209c9f51dc3b7db5edd98029e8aa6d757c6ad481402c2f2130c430a84e117e9c30e216651595a47ff10d21f49fc05424954474f0202f6b36d425bd445717706f8a1dbe4dd813fb0c50339fbec367eb0ed8c39b3d9dcbdc70773283923be0e6d17777d35cf901e02822a445637ad80ae694b557ab93d420203713b23f25f1db1be5fda7cebc1ef158c2162b884b29e02ffeff12a57044a3c03581c5f2a00d83cf087aeb5685c6c4f9a05ac84ee0ce53e0427591ed23fce47d049fc05424954474f0302f5370f52856166f7ec086a3e95cd7f1e848301b90681a7d0146ecb9ac5e1c59dbdc70773283923be0e6d17777d35cf901e02822a445637ad80ae694b557ab93d200b2163bf3e86e87f599519f425f037d459c776f754cad1e1c908a6731a35dd8208fc05424954474f051e492063616e2062656c69657665206974206973206e6f74206275747465720000', - hsmSignature: - '70736274ff0100fdae01000000000a97aac049b2b52216aecb2bc78c8af46201212ee96e20e914df2e36d69a543c5a0000000000000000000cdca00b194e8ca29eb3bf17c78bbeeefa318b6d84c315359f7d19106615681b000000000000000000c8f6693d34c4e98ef1fee4970e8342a12e67f81a4e74f7c5f5203c7fd6bcf2b90000000000000000009f29963b1fa0a10e5fd6d35893ed4342755c5446055a3bb3f11db5dbfb50c4e2000000000000000000f76fd6f563a17407845ad8f4e5654a520db5ebe1248b05a7e0926037cce94057000000000000000000764f19f344db53359718284d4bb03bb0bbb83ae4125d45047faf3bf26a97ca95000000000000000000e2b53078c15bebe0bc8152af38bb355d08adeae9b5b86ec6770ce66d92e9d969000000000000000000a2dcc6006e599cb473166667035ba9b6c662cfed865daebb425a89d40af2dffe00000000000000000047b378408d0f5a9be521ce8e37113a0202b0e0a8c8d3409e3e1e4c27038c29560000000000000000005019def787deba1bac0c861ecea5bdbd6269f373d9daeaa31396350b2c84d2ca000000000000000000010000000000000000016a000000004f010488b21e0000000000000000003a922e29f0c8eb0db2a60484cbdcb631f6b107c9caae3ffdcf3e7d2ec1f6bcd00312148715f361dab685a669d42431e5d6d3f973404dab9c9fd1b950b279ad763404cc18ae084f010488b21e0000000000000000006d1d656d3ddd91c194c04565a3603702a21016ced14a265f38982d6275e67b6403d3bac2313a7c6b21cbb11b14b0d10341f922c0a403a8bd8c87f0dc820f35af6e04f65cd8694f010488b21e000000000000000000cb04fd63ab34d90fe6466b880e2a02ccf8a863374312991af8911b1aaab443340336ef228ffe9b8efffba052c32d334660dd1f8366cf8fe44ae5aa672b6b62909504f2ef03890001007500000000010000000000000000000000000000000000000000000000000000000000000000ffffffff220020e816464a0141176fb1b582b012c5b2a7b3ab3f708465b5cf9cf81999f4e6aff10000000001000000000000000017a914d909474404c124a3d04c3fbff61faa49cf43c58b8700000000010120000000000000000017a914d909474404c124a3d04c3fbff61faa49cf43c58b87220202cc4d0fa411cac244486f8eb2c08e035ff7410f460a359ca7f8810991bd3b4209473044022013e1c5878f22f12eb699a5eb11f150999f5b169abaf587eb1b14cf0ce7a4f1cc02205fc547f631d8eb384695853f6d981bc1132eeccf5db5243f367166bbd9f2d6020122020344d884136df550202865ffbc6218c7f9c88fe6ce39c945798190badb38a752f1483045022100e128c3734a4ef18097e4d2e9806b3d4a101ebc6fb77c7d5935c57383a695bea3022020773bcc1367d2db4445e3ae7b9218f254a33994df73ac6097ffc8590c566bfc01010469522102cc4d0fa411cac244486f8eb2c08e035ff7410f460a359ca7f8810991bd3b42092102d72fd0d0d90293434ad5fca160f278e03c614497aa4e425cf454e2c1330f96ab210344d884136df550202865ffbc6218c7f9c88fe6ce39c945798190badb38a752f153ae220602cc4d0fa411cac244486f8eb2c08e035ff7410f460a359ca7f8810991bd3b420914f2ef038900000000000000000000000000000000220602d72fd0d0d90293434ad5fca160f278e03c614497aa4e425cf454e2c1330f96ab14cc18ae080000000000000000000000000000000022060344d884136df550202865ffbc6218c7f9c88fe6ce39c945798190badb38a752f114f65cd8690000000000000000000000000000000008fc05424954474f051e492063616e2062656c69657665206974206973206e6f74206275747465720001007500000000010000000000000000000000000000000000000000000000000000000000000000ffffffff220020e816464a0141176fb1b582b012c5b2a7b3ab3f708465b5cf9cf81999f4e6aff10000000001000000000000000017a9145768efe6a71468736dce66cba5929c6350609c358700000000010120000000000000000017a9145768efe6a71468736dce66cba5929c6350609c3587220203761b51c305db18bf65bb7703e47bb31e40372bf58a06b8970d3a3442f867741e47304402203731cee532f9b68c1852df3cfd6b316ee9f3dc09037d69deca233f2e117019d90220339420b35cbbc40a52a2fe440035d1eae1e13146d6650ee48fbaca39fff731df01220203a0334f9fbb9da9888e9ea3cc458c2a9ef659aec340bfb1d2d6616f01e5e5c4e4473044022030c6314abb04849ef1fd552a6dc7691bbb21540c2bd7aa5a3f23385649db2ae402203dcedb41fff80b2115d736d29e65fa329ce2d175741f1df65b8d4be30f444dea01010469522103a0334f9fbb9da9888e9ea3cc458c2a9ef659aec340bfb1d2d6616f01e5e5c4e421028fd779569e5588cabc672fc3c64fe12501490439a8e1990e079003a1a66921ea2103761b51c305db18bf65bb7703e47bb31e40372bf58a06b8970d3a3442f867741e53ae2206028fd779569e5588cabc672fc3c64fe12501490439a8e1990e079003a1a66921ea14cc18ae0800000000000000000100000001000000220603761b51c305db18bf65bb7703e47bb31e40372bf58a06b8970d3a3442f867741e14f65cd86900000000000000000100000001000000220603a0334f9fbb9da9888e9ea3cc458c2a9ef659aec340bfb1d2d6616f01e5e5c4e414f2ef03890000000000000000010000000100000008fc05424954474f051e492063616e2062656c69657665206974206973206e6f74206275747465720001007500000000010000000000000000000000000000000000000000000000000000000000000000ffffffff220020e816464a0141176fb1b582b012c5b2a7b3ab3f708465b5cf9cf81999f4e6aff10000000001000000000000000017a914836e0b75e731521db07aa2dfc5a15c9b7e279ab38700000000010120000000000000000017a914836e0b75e731521db07aa2dfc5a15c9b7e279ab3872202022ddee7d0b3f7f894fca44650a72137908eb843432e6874cb873cc0f2d27a54d647304402202a7ac15cb66799a86f91306abb1c4cf61b6d4582abc5f80ee6a51ef17967b070022056ed845749df7c4923c5b50c8c4b701a4962da0d95dfb0be6df737bfe83ae3d401220202d5878a4d1c23ea735d7146ef2e1094f08d118bf0e20ea09366077c6b086c49d64730440220126fd17b31a4cb2402592bd2f1bcda4d3b90ce8a49012755e2006800a9e0a42302202e1aeecbb936a4ca0afd576754b525824bb15e8254ed07ce6cb7174ef0ab5b790101042200208224f2e5de25d91de7ae08f8744f98ac30a7c86e9c0fcf169279e520c2b8313a010569522102d5878a4d1c23ea735d7146ef2e1094f08d118bf0e20ea09366077c6b086c49d62102804dae97fd1acbb46d2816f530f83119dccdcaf3f5dd4c4dabb8edccd04bc65c21022ddee7d0b3f7f894fca44650a72137908eb843432e6874cb873cc0f2d27a54d653ae2206022ddee7d0b3f7f894fca44650a72137908eb843432e6874cb873cc0f2d27a54d614f65cd86900000000000000000a00000002000000220602804dae97fd1acbb46d2816f530f83119dccdcaf3f5dd4c4dabb8edccd04bc65c14cc18ae0800000000000000000a00000002000000220602d5878a4d1c23ea735d7146ef2e1094f08d118bf0e20ea09366077c6b086c49d614f2ef038900000000000000000a0000000200000008fc05424954474f051e492063616e2062656c69657665206974206973206e6f74206275747465720001007500000000010000000000000000000000000000000000000000000000000000000000000000ffffffff220020e816464a0141176fb1b582b012c5b2a7b3ab3f708465b5cf9cf81999f4e6aff10000000001000000000000000017a914a7621ee3549de756e9048183171d02a48e1f18a28700000000010120000000000000000017a914a7621ee3549de756e9048183171d02a48e1f18a2872202031b6cad2325e074b6f7419e7d1bfe37f317b309707630c0c313f89221cc8113c7473044022074a13727a3e5736aba2005a006ea1af0e83595de1eb43b6d07468e7909ba9a24022006d94d75266d390e4134dc7bbe544fd76005e2852a88ed9db57c3c02a52f94c8012202034f0c87cd48157c37d26c296937b816218f6093d038ae6c3deba6b9f5776abfb3473044022007a80b56b6d628a7be48b825bf964269de3101598e1b83dce67996ad14af21a902205eab963b093c0a871d8e6d63cf9c008fa7a70c6cc3cb5f78f951399eab283406010104220020fff6d24d943ecfea3b2cab30a1db70ef66b765be13b13e4e1758410217edf5710105695221034f0c87cd48157c37d26c296937b816218f6093d038ae6c3deba6b9f5776abfb32103eebab40c070ebe90e2806af5a4e365116c013a4d81ee7c167663649ef7a91d5321031b6cad2325e074b6f7419e7d1bfe37f317b309707630c0c313f89221cc8113c753ae2206031b6cad2325e074b6f7419e7d1bfe37f317b309707630c0c313f89221cc8113c714f65cd86900000000000000000b000000030000002206034f0c87cd48157c37d26c296937b816218f6093d038ae6c3deba6b9f5776abfb314f2ef038900000000000000000b00000003000000220603eebab40c070ebe90e2806af5a4e365116c013a4d81ee7c167663649ef7a91d5314cc18ae0800000000000000000b0000000300000008fc05424954474f051e492063616e2062656c69657665206974206973206e6f74206275747465720001008000000000010000000000000000000000000000000000000000000000000000000000000000ffffffff220020e816464a0141176fb1b582b012c5b2a7b3ab3f708465b5cf9cf81999f4e6aff10000000001000000000000000022002080ebe4e2d2d33e7898cb0ddabb1a6dbc2466d6333820886fb6842b49e2ac76ba0000000001012b000000000000000022002080ebe4e2d2d33e7898cb0ddabb1a6dbc2466d6333820886fb6842b49e2ac76ba220203623dc211dc28212e83fdf9f808e1dec770e461b0af1ba65458be106222be6a61483045022100866ab759aa3e17fc4b9128bc7bcb871c153a83ad62fe600cc631bb16eeb27f9d0220470d8f3a0a908e55b8c424d384d1ab50d7f77023860ed6fe68a0e3f3db0e7a4d0122020383a7be358204e1f8dfba8dc7cdc0dca66a00b14eaf679d91b1a92f656c26bf8447304402202847ad6c0dafc791fe269d346e95c8ccba12efa90be9bc121af181b31500f26502201c0193ae7152d4a810a30cab134e5dff4bea538174de2fab1c903a00a543a5e30101056952210383a7be358204e1f8dfba8dc7cdc0dca66a00b14eaf679d91b1a92f656c26bf8421027c8c55f1c02918ebf0daed6dc58f5e0b1074e46d8310d162570044084a0967692103623dc211dc28212e83fdf9f808e1dec770e461b0af1ba65458be106222be6a6153ae2206027c8c55f1c02918ebf0daed6dc58f5e0b1074e46d8310d162570044084a09676914cc18ae0800000000000000001400000004000000220603623dc211dc28212e83fdf9f808e1dec770e461b0af1ba65458be106222be6a6114f65cd8690000000000000000140000000400000022060383a7be358204e1f8dfba8dc7cdc0dca66a00b14eaf679d91b1a92f656c26bf8414f2ef03890000000000000000140000000400000008fc05424954474f051e492063616e2062656c69657665206974206973206e6f74206275747465720001008000000000010000000000000000000000000000000000000000000000000000000000000000ffffffff220020e816464a0141176fb1b582b012c5b2a7b3ab3f708465b5cf9cf81999f4e6aff10000000001000000000000000022002096462d2916f4907312fb02c74412b5a8d92f566f5d4f34d744916720aec977be0000000001012b000000000000000022002096462d2916f4907312fb02c74412b5a8d92f566f5d4f34d744916720aec977be220202d71c110eed1ae7e1c51377ac72f94339caa280a6d9dd7a843aad65cf4509288b473044022074a44626578abb12b29db5aa2a57dddbf1548c8b295e382f74711b7cc45b309c0220725bde51555e81c54dc73be0c4928fec6a1e100f30aae7b7ce99ff67a06b0d7d01220202f1af33db32f4c97b5f9381129f47e9f622a427144518eae3f8eca814741241f647304402206c472129bab262d8e15c4d8eae6bae8ffaafad49e0857de0defc705c454ac7c202201d1f95264be08d72bd2dc313e61a7394ea4b4d9e4b01f78684059f6d0068240f01010569522102f1af33db32f4c97b5f9381129f47e9f622a427144518eae3f8eca814741241f62102671f7f94e0adbc5be6893191ab88405b7aaa5cba3ef98ae4fd136316aad4cef32102d71c110eed1ae7e1c51377ac72f94339caa280a6d9dd7a843aad65cf4509288b53ae220602671f7f94e0adbc5be6893191ab88405b7aaa5cba3ef98ae4fd136316aad4cef314cc18ae0800000000000000001500000005000000220602d71c110eed1ae7e1c51377ac72f94339caa280a6d9dd7a843aad65cf4509288b14f65cd86900000000000000001500000005000000220602f1af33db32f4c97b5f9381129f47e9f622a427144518eae3f8eca814741241f614f2ef03890000000000000000150000000500000008fc05424954474f051e492063616e2062656c69657665206974206973206e6f74206275747465720001008000000000010000000000000000000000000000000000000000000000000000000000000000ffffffff220020e816464a0141176fb1b582b012c5b2a7b3ab3f708465b5cf9cf81999f4e6aff100000000010000000000000000225120db284bf5c2d0ba0a03fe29240f29bda603a705aa090584b637474ec2c09f7c290000000001012b0000000000000000225120db284bf5c2d0ba0a03fe29240f29bda603a705aa090584b637474ec2c09f7c2941142a42e9d0284d5515d404dd84024b3d2eb291e2f971da02262a91551e35ed7e1742a464371efcc831b425e671f82488821ec1fbd1610cd5017540d3f2b17eb79140f419ebe7ba0487a4c179c4f010cc92b893149ed12ccf504d106f95394639a707b8c9e51c3b38c5465861c1b178377ad46c8f220442863957fb7bc248c5dd33e24114c69e0b14bfba3101e0f336480a4617964ddb34d14755406529bc3f1f57fa49c842a464371efcc831b425e671f82488821ec1fbd1610cd5017540d3f2b17eb79140b2b2467a458d385aa4afb982c624ea7239720f204c54abaf66f34d1feca154092bb8b060ee3564be1bf4abd33c508c5474e0182a6abb22ef9fad35c929182e3a4215c11ce1dfba2ee2d1cab438a36b7e39f77da38229cc8f220570166a04bf0f29daf10f3d3e320bd977cf4de055594a1b93b6bf3bf35d9e5b8ac3bae37ca23f76f91d4520c69e0b14bfba3101e0f336480a4617964ddb34d14755406529bc3f1f57fa49c8ad202a42e9d0284d5515d404dd84024b3d2eb291e2f971da02262a91551e35ed7e17acc021162a42e9d0284d5515d404dd84024b3d2eb291e2f971da02262a91551e35ed7e17350142a464371efcc831b425e671f82488821ec1fbd1610cd5017540d3f2b17eb791f65cd86900000000000000001e000000060000002116c69e0b14bfba3101e0f336480a4617964ddb34d14755406529bc3f1f57fa49c8350142a464371efcc831b425e671f82488821ec1fbd1610cd5017540d3f2b17eb791f2ef038900000000000000001e0000000600000008fc05424954474f051e492063616e2062656c69657665206974206973206e6f74206275747465720001008000000000010000000000000000000000000000000000000000000000000000000000000000ffffffff220020e816464a0141176fb1b582b012c5b2a7b3ab3f708465b5cf9cf81999f4e6aff1000000000100000000000000002251201a7acb31cf6bbf739dcc38b6d889ed3c2f3c8dfe219bd53347de500e7a421c7d0000000001012b00000000000000002251201a7acb31cf6bbf739dcc38b6d889ed3c2f3c8dfe219bd53347de500e7a421c7d41149b8c27e9f9f279390ab304cc3415e29d59e15f6ba34d463446fc90f8ea3ce3154b5521d0235ac6798e0ec20fcab5572e4853cc74b2ac5623d8c8472a9b8c3d7440e831e3e0219552cfdd87c6f9af3854c6447a09265854d0c4d3c160ac0557498aeab1160c6f7d62422db67af3776f06ebd76c2d35e514125f5ddedc435ba277d84114e08fd25466b7ddb5239bd54c4823f877c137bda413fb5bd8a12bc5242a18900f4b5521d0235ac6798e0ec20fcab5572e4853cc74b2ac5623d8c8472a9b8c3d744002ecc2125852f5856584d56181d4ef896208b20d95046fea05944b78c5d4978dd8f7013b30cfcb9837aeb8bbe314a6bd74b4ef9c9460fff6a9f04a41342ce4a14215c0a30fa2cbc9c0febc03c43d3d4d5f450ca164039364081cc681cc308388d249947ae56f815f9d25f5c2b33edeaad613c1a7d14fba7a2d48db9523b06c5eea55074520e08fd25466b7ddb5239bd54c4823f877c137bda413fb5bd8a12bc5242a18900fad209b8c27e9f9f279390ab304cc3415e29d59e15f6ba34d463446fc90f8ea3ce315acc021169b8c27e9f9f279390ab304cc3415e29d59e15f6ba34d463446fc90f8ea3ce31535014b5521d0235ac6798e0ec20fcab5572e4853cc74b2ac5623d8c8472a9b8c3d74f65cd86900000000000000001f000000070000002116e08fd25466b7ddb5239bd54c4823f877c137bda413fb5bd8a12bc5242a18900f35014b5521d0235ac6798e0ec20fcab5572e4853cc74b2ac5623d8c8472a9b8c3d74f2ef038900000000000000001f0000000700000008fc05424954474f051e492063616e2062656c69657665206974206973206e6f74206275747465720001008000000000010000000000000000000000000000000000000000000000000000000000000000ffffffff220020e816464a0141176fb1b582b012c5b2a7b3ab3f708465b5cf9cf81999f4e6aff1000000000100000000000000002251201d4f08c91c860545e970325543b283f8ebb036877337a2c96907ebd7cd2f03540000000001012b00000000000000002251201d4f08c91c860545e970325543b283f8ebb036877337a2c96907ebd7cd2f0354211633bc494f3ea86c13a9fba20abfa888593110db9e23c268105c8988d87957b9101500f2ef0389000000000000000028000000080000002116eb3a656215fa62b1d240096f695e3184eec77ee08fbbc5ced600b5af875950941500f65cd869000000000000000028000000080000000117207f5b14ae6f1cb9426136073c6962eef187f4af390aaa7ea11da6be101558cb670118202d5ae45def1d1e5c0e119b107275755f75386d7a86dec79d6f4621b385cf6cd648fc05424954474f011d4f08c91c860545e970325543b283f8ebb036877337a2c96907ebd7cd2f03547f5b14ae6f1cb9426136073c6962eef187f4af390aaa7ea11da6be101558cb67420233bc494f3ea86c13a9fba20abfa888593110db9e23c268105c8988d87957b91003eb3a656215fa62b1d240096f695e3184eec77ee08fbbc5ced600b5af8759509449fc05424954474f020233bc494f3ea86c13a9fba20abfa888593110db9e23c268105c8988d87957b9101d4f08c91c860545e970325543b283f8ebb036877337a2c96907ebd7cd2f0354420214145dfddd77b53e56128ec0b4a4f05ce87d15901d9faf96d1adac8424275bf602be4ea2b65970c459da9a1d518a62c30b0b91bf49da7cffd15ef6d50f3d14f7eb49fc05424954474f0203eb3a656215fa62b1d240096f695e3184eec77ee08fbbc5ced600b5af875950941d4f08c91c860545e970325543b283f8ebb036877337a2c96907ebd7cd2f03544202fb658ed2694609fa7a331c7234d22582c6065cb07a8e42ea006b2f0871ba2831038609d4767745bee45c2d9e7a649bef7eb9edf117c775c22a11085471006f9d5249fc05424954474f030233bc494f3ea86c13a9fba20abfa888593110db9e23c268105c8988d87957b9101d4f08c91c860545e970325543b283f8ebb036877337a2c96907ebd7cd2f0354207a75fe800b86ce6ac43f4b343d6de5cfc641730be25baa876b6e6cb293b717a449fc05424954474f0303eb3a656215fa62b1d240096f695e3184eec77ee08fbbc5ced600b5af875950941d4f08c91c860545e970325543b283f8ebb036877337a2c96907ebd7cd2f0354200ec1bc22ba2af402999aaf84f0f5d0bcbccef9aca37e1b0a697f187bca1d9ced08fc05424954474f051e492063616e2062656c69657665206974206973206e6f74206275747465720001008000000000010000000000000000000000000000000000000000000000000000000000000000ffffffff220020e816464a0141176fb1b582b012c5b2a7b3ab3f708465b5cf9cf81999f4e6aff100000000010000000000000000225120bdc70773283923be0e6d17777d35cf901e02822a445637ad80ae694b557ab93d0000000001012b0000000000000000225120bdc70773283923be0e6d17777d35cf901e02822a445637ad80ae694b557ab93d2116f5370f52856166f7ec086a3e95cd7f1e848301b90681a7d0146ecb9ac5e1c59d1500f2ef0389000000000000000029000000090000002116f6b36d425bd445717706f8a1dbe4dd813fb0c50339fbec367eb0ed8c39b3d9dc1500f65cd86900000000000000002900000009000000011720c4a6939e74c11c496fdcee803e3292117760a19fe42a3dd089ad801e9f2cb0430118200da8eab7f651495d404cf9c51e15747719a42e3b4078ca87c5412e84563bd47b48fc05424954474f01bdc70773283923be0e6d17777d35cf901e02822a445637ad80ae694b557ab93dc4a6939e74c11c496fdcee803e3292117760a19fe42a3dd089ad801e9f2cb0434202f5370f52856166f7ec086a3e95cd7f1e848301b90681a7d0146ecb9ac5e1c59d02f6b36d425bd445717706f8a1dbe4dd813fb0c50339fbec367eb0ed8c39b3d9dc49fc05424954474f0202f5370f52856166f7ec086a3e95cd7f1e848301b90681a7d0146ecb9ac5e1c59dbdc70773283923be0e6d17777d35cf901e02822a445637ad80ae694b557ab93d42039cf40d4e38434812b1504189ec66a861a47f7e2930c209c9f51dc3b7db5edd98029e8aa6d757c6ad481402c2f2130c430a84e117e9c30e216651595a47ff10d21f49fc05424954474f0202f6b36d425bd445717706f8a1dbe4dd813fb0c50339fbec367eb0ed8c39b3d9dcbdc70773283923be0e6d17777d35cf901e02822a445637ad80ae694b557ab93d420203713b23f25f1db1be5fda7cebc1ef158c2162b884b29e02ffeff12a57044a3c03581c5f2a00d83cf087aeb5685c6c4f9a05ac84ee0ce53e0427591ed23fce47d049fc05424954474f0302f5370f52856166f7ec086a3e95cd7f1e848301b90681a7d0146ecb9ac5e1c59dbdc70773283923be0e6d17777d35cf901e02822a445637ad80ae694b557ab93d200b2163bf3e86e87f599519f425f037d459c776f754cad1e1c908a6731a35dd8249fc05424954474f0302f6b36d425bd445717706f8a1dbe4dd813fb0c50339fbec367eb0ed8c39b3d9dcbdc70773283923be0e6d17777d35cf901e02822a445637ad80ae694b557ab93d200c881abd2dd7729ff2b2078c52f8847103a6fc445f7af243a148dc186457efd008fc05424954474f051e492063616e2062656c69657665206974206973206e6f74206275747465720000', - fullSigned: - '70736274ff0100fdae01000000000a97aac049b2b52216aecb2bc78c8af46201212ee96e20e914df2e36d69a543c5a0000000000000000000cdca00b194e8ca29eb3bf17c78bbeeefa318b6d84c315359f7d19106615681b000000000000000000c8f6693d34c4e98ef1fee4970e8342a12e67f81a4e74f7c5f5203c7fd6bcf2b90000000000000000009f29963b1fa0a10e5fd6d35893ed4342755c5446055a3bb3f11db5dbfb50c4e2000000000000000000f76fd6f563a17407845ad8f4e5654a520db5ebe1248b05a7e0926037cce94057000000000000000000764f19f344db53359718284d4bb03bb0bbb83ae4125d45047faf3bf26a97ca95000000000000000000e2b53078c15bebe0bc8152af38bb355d08adeae9b5b86ec6770ce66d92e9d969000000000000000000a2dcc6006e599cb473166667035ba9b6c662cfed865daebb425a89d40af2dffe00000000000000000047b378408d0f5a9be521ce8e37113a0202b0e0a8c8d3409e3e1e4c27038c29560000000000000000005019def787deba1bac0c861ecea5bdbd6269f373d9daeaa31396350b2c84d2ca000000000000000000010000000000000000016a000000004f010488b21e0000000000000000003a922e29f0c8eb0db2a60484cbdcb631f6b107c9caae3ffdcf3e7d2ec1f6bcd00312148715f361dab685a669d42431e5d6d3f973404dab9c9fd1b950b279ad763404cc18ae084f010488b21e0000000000000000006d1d656d3ddd91c194c04565a3603702a21016ced14a265f38982d6275e67b6403d3bac2313a7c6b21cbb11b14b0d10341f922c0a403a8bd8c87f0dc820f35af6e04f65cd8694f010488b21e000000000000000000cb04fd63ab34d90fe6466b880e2a02ccf8a863374312991af8911b1aaab443340336ef228ffe9b8efffba052c32d334660dd1f8366cf8fe44ae5aa672b6b62909504f2ef03890001007500000000010000000000000000000000000000000000000000000000000000000000000000ffffffff220020e816464a0141176fb1b582b012c5b2a7b3ab3f708465b5cf9cf81999f4e6aff10000000001000000000000000017a914d909474404c124a3d04c3fbff61faa49cf43c58b8700000000010120000000000000000017a914d909474404c124a3d04c3fbff61faa49cf43c58b87220202cc4d0fa411cac244486f8eb2c08e035ff7410f460a359ca7f8810991bd3b4209473044022013e1c5878f22f12eb699a5eb11f150999f5b169abaf587eb1b14cf0ce7a4f1cc02205fc547f631d8eb384695853f6d981bc1132eeccf5db5243f367166bbd9f2d6020122020344d884136df550202865ffbc6218c7f9c88fe6ce39c945798190badb38a752f1483045022100e128c3734a4ef18097e4d2e9806b3d4a101ebc6fb77c7d5935c57383a695bea3022020773bcc1367d2db4445e3ae7b9218f254a33994df73ac6097ffc8590c566bfc01010469522102cc4d0fa411cac244486f8eb2c08e035ff7410f460a359ca7f8810991bd3b42092102d72fd0d0d90293434ad5fca160f278e03c614497aa4e425cf454e2c1330f96ab210344d884136df550202865ffbc6218c7f9c88fe6ce39c945798190badb38a752f153ae220602cc4d0fa411cac244486f8eb2c08e035ff7410f460a359ca7f8810991bd3b420914f2ef038900000000000000000000000000000000220602d72fd0d0d90293434ad5fca160f278e03c614497aa4e425cf454e2c1330f96ab14cc18ae080000000000000000000000000000000022060344d884136df550202865ffbc6218c7f9c88fe6ce39c945798190badb38a752f114f65cd8690000000000000000000000000000000008fc05424954474f051e492063616e2062656c69657665206974206973206e6f74206275747465720001007500000000010000000000000000000000000000000000000000000000000000000000000000ffffffff220020e816464a0141176fb1b582b012c5b2a7b3ab3f708465b5cf9cf81999f4e6aff10000000001000000000000000017a9145768efe6a71468736dce66cba5929c6350609c358700000000010120000000000000000017a9145768efe6a71468736dce66cba5929c6350609c3587220203761b51c305db18bf65bb7703e47bb31e40372bf58a06b8970d3a3442f867741e47304402203731cee532f9b68c1852df3cfd6b316ee9f3dc09037d69deca233f2e117019d90220339420b35cbbc40a52a2fe440035d1eae1e13146d6650ee48fbaca39fff731df01220203a0334f9fbb9da9888e9ea3cc458c2a9ef659aec340bfb1d2d6616f01e5e5c4e4473044022030c6314abb04849ef1fd552a6dc7691bbb21540c2bd7aa5a3f23385649db2ae402203dcedb41fff80b2115d736d29e65fa329ce2d175741f1df65b8d4be30f444dea01010469522103a0334f9fbb9da9888e9ea3cc458c2a9ef659aec340bfb1d2d6616f01e5e5c4e421028fd779569e5588cabc672fc3c64fe12501490439a8e1990e079003a1a66921ea2103761b51c305db18bf65bb7703e47bb31e40372bf58a06b8970d3a3442f867741e53ae2206028fd779569e5588cabc672fc3c64fe12501490439a8e1990e079003a1a66921ea14cc18ae0800000000000000000100000001000000220603761b51c305db18bf65bb7703e47bb31e40372bf58a06b8970d3a3442f867741e14f65cd86900000000000000000100000001000000220603a0334f9fbb9da9888e9ea3cc458c2a9ef659aec340bfb1d2d6616f01e5e5c4e414f2ef03890000000000000000010000000100000008fc05424954474f051e492063616e2062656c69657665206974206973206e6f74206275747465720001007500000000010000000000000000000000000000000000000000000000000000000000000000ffffffff220020e816464a0141176fb1b582b012c5b2a7b3ab3f708465b5cf9cf81999f4e6aff10000000001000000000000000017a914836e0b75e731521db07aa2dfc5a15c9b7e279ab38700000000010120000000000000000017a914836e0b75e731521db07aa2dfc5a15c9b7e279ab3872202022ddee7d0b3f7f894fca44650a72137908eb843432e6874cb873cc0f2d27a54d647304402202a7ac15cb66799a86f91306abb1c4cf61b6d4582abc5f80ee6a51ef17967b070022056ed845749df7c4923c5b50c8c4b701a4962da0d95dfb0be6df737bfe83ae3d401220202d5878a4d1c23ea735d7146ef2e1094f08d118bf0e20ea09366077c6b086c49d64730440220126fd17b31a4cb2402592bd2f1bcda4d3b90ce8a49012755e2006800a9e0a42302202e1aeecbb936a4ca0afd576754b525824bb15e8254ed07ce6cb7174ef0ab5b790101042200208224f2e5de25d91de7ae08f8744f98ac30a7c86e9c0fcf169279e520c2b8313a010569522102d5878a4d1c23ea735d7146ef2e1094f08d118bf0e20ea09366077c6b086c49d62102804dae97fd1acbb46d2816f530f83119dccdcaf3f5dd4c4dabb8edccd04bc65c21022ddee7d0b3f7f894fca44650a72137908eb843432e6874cb873cc0f2d27a54d653ae2206022ddee7d0b3f7f894fca44650a72137908eb843432e6874cb873cc0f2d27a54d614f65cd86900000000000000000a00000002000000220602804dae97fd1acbb46d2816f530f83119dccdcaf3f5dd4c4dabb8edccd04bc65c14cc18ae0800000000000000000a00000002000000220602d5878a4d1c23ea735d7146ef2e1094f08d118bf0e20ea09366077c6b086c49d614f2ef038900000000000000000a0000000200000008fc05424954474f051e492063616e2062656c69657665206974206973206e6f74206275747465720001007500000000010000000000000000000000000000000000000000000000000000000000000000ffffffff220020e816464a0141176fb1b582b012c5b2a7b3ab3f708465b5cf9cf81999f4e6aff10000000001000000000000000017a914a7621ee3549de756e9048183171d02a48e1f18a28700000000010120000000000000000017a914a7621ee3549de756e9048183171d02a48e1f18a2872202031b6cad2325e074b6f7419e7d1bfe37f317b309707630c0c313f89221cc8113c7473044022074a13727a3e5736aba2005a006ea1af0e83595de1eb43b6d07468e7909ba9a24022006d94d75266d390e4134dc7bbe544fd76005e2852a88ed9db57c3c02a52f94c8012202034f0c87cd48157c37d26c296937b816218f6093d038ae6c3deba6b9f5776abfb3473044022007a80b56b6d628a7be48b825bf964269de3101598e1b83dce67996ad14af21a902205eab963b093c0a871d8e6d63cf9c008fa7a70c6cc3cb5f78f951399eab283406010104220020fff6d24d943ecfea3b2cab30a1db70ef66b765be13b13e4e1758410217edf5710105695221034f0c87cd48157c37d26c296937b816218f6093d038ae6c3deba6b9f5776abfb32103eebab40c070ebe90e2806af5a4e365116c013a4d81ee7c167663649ef7a91d5321031b6cad2325e074b6f7419e7d1bfe37f317b309707630c0c313f89221cc8113c753ae2206031b6cad2325e074b6f7419e7d1bfe37f317b309707630c0c313f89221cc8113c714f65cd86900000000000000000b000000030000002206034f0c87cd48157c37d26c296937b816218f6093d038ae6c3deba6b9f5776abfb314f2ef038900000000000000000b00000003000000220603eebab40c070ebe90e2806af5a4e365116c013a4d81ee7c167663649ef7a91d5314cc18ae0800000000000000000b0000000300000008fc05424954474f051e492063616e2062656c69657665206974206973206e6f74206275747465720001008000000000010000000000000000000000000000000000000000000000000000000000000000ffffffff220020e816464a0141176fb1b582b012c5b2a7b3ab3f708465b5cf9cf81999f4e6aff10000000001000000000000000022002080ebe4e2d2d33e7898cb0ddabb1a6dbc2466d6333820886fb6842b49e2ac76ba0000000001012b000000000000000022002080ebe4e2d2d33e7898cb0ddabb1a6dbc2466d6333820886fb6842b49e2ac76ba220203623dc211dc28212e83fdf9f808e1dec770e461b0af1ba65458be106222be6a61483045022100866ab759aa3e17fc4b9128bc7bcb871c153a83ad62fe600cc631bb16eeb27f9d0220470d8f3a0a908e55b8c424d384d1ab50d7f77023860ed6fe68a0e3f3db0e7a4d0122020383a7be358204e1f8dfba8dc7cdc0dca66a00b14eaf679d91b1a92f656c26bf8447304402202847ad6c0dafc791fe269d346e95c8ccba12efa90be9bc121af181b31500f26502201c0193ae7152d4a810a30cab134e5dff4bea538174de2fab1c903a00a543a5e30101056952210383a7be358204e1f8dfba8dc7cdc0dca66a00b14eaf679d91b1a92f656c26bf8421027c8c55f1c02918ebf0daed6dc58f5e0b1074e46d8310d162570044084a0967692103623dc211dc28212e83fdf9f808e1dec770e461b0af1ba65458be106222be6a6153ae2206027c8c55f1c02918ebf0daed6dc58f5e0b1074e46d8310d162570044084a09676914cc18ae0800000000000000001400000004000000220603623dc211dc28212e83fdf9f808e1dec770e461b0af1ba65458be106222be6a6114f65cd8690000000000000000140000000400000022060383a7be358204e1f8dfba8dc7cdc0dca66a00b14eaf679d91b1a92f656c26bf8414f2ef03890000000000000000140000000400000008fc05424954474f051e492063616e2062656c69657665206974206973206e6f74206275747465720001008000000000010000000000000000000000000000000000000000000000000000000000000000ffffffff220020e816464a0141176fb1b582b012c5b2a7b3ab3f708465b5cf9cf81999f4e6aff10000000001000000000000000022002096462d2916f4907312fb02c74412b5a8d92f566f5d4f34d744916720aec977be0000000001012b000000000000000022002096462d2916f4907312fb02c74412b5a8d92f566f5d4f34d744916720aec977be220202d71c110eed1ae7e1c51377ac72f94339caa280a6d9dd7a843aad65cf4509288b473044022074a44626578abb12b29db5aa2a57dddbf1548c8b295e382f74711b7cc45b309c0220725bde51555e81c54dc73be0c4928fec6a1e100f30aae7b7ce99ff67a06b0d7d01220202f1af33db32f4c97b5f9381129f47e9f622a427144518eae3f8eca814741241f647304402206c472129bab262d8e15c4d8eae6bae8ffaafad49e0857de0defc705c454ac7c202201d1f95264be08d72bd2dc313e61a7394ea4b4d9e4b01f78684059f6d0068240f01010569522102f1af33db32f4c97b5f9381129f47e9f622a427144518eae3f8eca814741241f62102671f7f94e0adbc5be6893191ab88405b7aaa5cba3ef98ae4fd136316aad4cef32102d71c110eed1ae7e1c51377ac72f94339caa280a6d9dd7a843aad65cf4509288b53ae220602671f7f94e0adbc5be6893191ab88405b7aaa5cba3ef98ae4fd136316aad4cef314cc18ae0800000000000000001500000005000000220602d71c110eed1ae7e1c51377ac72f94339caa280a6d9dd7a843aad65cf4509288b14f65cd86900000000000000001500000005000000220602f1af33db32f4c97b5f9381129f47e9f622a427144518eae3f8eca814741241f614f2ef03890000000000000000150000000500000008fc05424954474f051e492063616e2062656c69657665206974206973206e6f74206275747465720001008000000000010000000000000000000000000000000000000000000000000000000000000000ffffffff220020e816464a0141176fb1b582b012c5b2a7b3ab3f708465b5cf9cf81999f4e6aff100000000010000000000000000225120db284bf5c2d0ba0a03fe29240f29bda603a705aa090584b637474ec2c09f7c290000000001012b0000000000000000225120db284bf5c2d0ba0a03fe29240f29bda603a705aa090584b637474ec2c09f7c2941142a42e9d0284d5515d404dd84024b3d2eb291e2f971da02262a91551e35ed7e1742a464371efcc831b425e671f82488821ec1fbd1610cd5017540d3f2b17eb79140f419ebe7ba0487a4c179c4f010cc92b893149ed12ccf504d106f95394639a707b8c9e51c3b38c5465861c1b178377ad46c8f220442863957fb7bc248c5dd33e24114c69e0b14bfba3101e0f336480a4617964ddb34d14755406529bc3f1f57fa49c842a464371efcc831b425e671f82488821ec1fbd1610cd5017540d3f2b17eb79140b2b2467a458d385aa4afb982c624ea7239720f204c54abaf66f34d1feca154092bb8b060ee3564be1bf4abd33c508c5474e0182a6abb22ef9fad35c929182e3a4215c11ce1dfba2ee2d1cab438a36b7e39f77da38229cc8f220570166a04bf0f29daf10f3d3e320bd977cf4de055594a1b93b6bf3bf35d9e5b8ac3bae37ca23f76f91d4520c69e0b14bfba3101e0f336480a4617964ddb34d14755406529bc3f1f57fa49c8ad202a42e9d0284d5515d404dd84024b3d2eb291e2f971da02262a91551e35ed7e17acc021162a42e9d0284d5515d404dd84024b3d2eb291e2f971da02262a91551e35ed7e17350142a464371efcc831b425e671f82488821ec1fbd1610cd5017540d3f2b17eb791f65cd86900000000000000001e000000060000002116c69e0b14bfba3101e0f336480a4617964ddb34d14755406529bc3f1f57fa49c8350142a464371efcc831b425e671f82488821ec1fbd1610cd5017540d3f2b17eb791f2ef038900000000000000001e0000000600000008fc05424954474f051e492063616e2062656c69657665206974206973206e6f74206275747465720001008000000000010000000000000000000000000000000000000000000000000000000000000000ffffffff220020e816464a0141176fb1b582b012c5b2a7b3ab3f708465b5cf9cf81999f4e6aff1000000000100000000000000002251201a7acb31cf6bbf739dcc38b6d889ed3c2f3c8dfe219bd53347de500e7a421c7d0000000001012b00000000000000002251201a7acb31cf6bbf739dcc38b6d889ed3c2f3c8dfe219bd53347de500e7a421c7d41149b8c27e9f9f279390ab304cc3415e29d59e15f6ba34d463446fc90f8ea3ce3154b5521d0235ac6798e0ec20fcab5572e4853cc74b2ac5623d8c8472a9b8c3d7440e831e3e0219552cfdd87c6f9af3854c6447a09265854d0c4d3c160ac0557498aeab1160c6f7d62422db67af3776f06ebd76c2d35e514125f5ddedc435ba277d84114e08fd25466b7ddb5239bd54c4823f877c137bda413fb5bd8a12bc5242a18900f4b5521d0235ac6798e0ec20fcab5572e4853cc74b2ac5623d8c8472a9b8c3d744002ecc2125852f5856584d56181d4ef896208b20d95046fea05944b78c5d4978dd8f7013b30cfcb9837aeb8bbe314a6bd74b4ef9c9460fff6a9f04a41342ce4a14215c0a30fa2cbc9c0febc03c43d3d4d5f450ca164039364081cc681cc308388d249947ae56f815f9d25f5c2b33edeaad613c1a7d14fba7a2d48db9523b06c5eea55074520e08fd25466b7ddb5239bd54c4823f877c137bda413fb5bd8a12bc5242a18900fad209b8c27e9f9f279390ab304cc3415e29d59e15f6ba34d463446fc90f8ea3ce315acc021169b8c27e9f9f279390ab304cc3415e29d59e15f6ba34d463446fc90f8ea3ce31535014b5521d0235ac6798e0ec20fcab5572e4853cc74b2ac5623d8c8472a9b8c3d74f65cd86900000000000000001f000000070000002116e08fd25466b7ddb5239bd54c4823f877c137bda413fb5bd8a12bc5242a18900f35014b5521d0235ac6798e0ec20fcab5572e4853cc74b2ac5623d8c8472a9b8c3d74f2ef038900000000000000001f0000000700000008fc05424954474f051e492063616e2062656c69657665206974206973206e6f74206275747465720001008000000000010000000000000000000000000000000000000000000000000000000000000000ffffffff220020e816464a0141176fb1b582b012c5b2a7b3ab3f708465b5cf9cf81999f4e6aff1000000000100000000000000002251201d4f08c91c860545e970325543b283f8ebb036877337a2c96907ebd7cd2f03540000000001012b00000000000000002251201d4f08c91c860545e970325543b283f8ebb036877337a2c96907ebd7cd2f0354211633bc494f3ea86c13a9fba20abfa888593110db9e23c268105c8988d87957b9101500f2ef0389000000000000000028000000080000002116eb3a656215fa62b1d240096f695e3184eec77ee08fbbc5ced600b5af875950941500f65cd869000000000000000028000000080000000117207f5b14ae6f1cb9426136073c6962eef187f4af390aaa7ea11da6be101558cb670118202d5ae45def1d1e5c0e119b107275755f75386d7a86dec79d6f4621b385cf6cd648fc05424954474f011d4f08c91c860545e970325543b283f8ebb036877337a2c96907ebd7cd2f03547f5b14ae6f1cb9426136073c6962eef187f4af390aaa7ea11da6be101558cb67420233bc494f3ea86c13a9fba20abfa888593110db9e23c268105c8988d87957b91003eb3a656215fa62b1d240096f695e3184eec77ee08fbbc5ced600b5af8759509449fc05424954474f020233bc494f3ea86c13a9fba20abfa888593110db9e23c268105c8988d87957b9101d4f08c91c860545e970325543b283f8ebb036877337a2c96907ebd7cd2f03544203f1d04082e975a8b42c2475a00924e3479bb64336dae4e2fd07d86efa49eeb5bf03a812a9ce506da15c2bcb1b3984fec825672310274dc5367f8989dea78dc0e76149fc05424954474f0203eb3a656215fa62b1d240096f695e3184eec77ee08fbbc5ced600b5af875950941d4f08c91c860545e970325543b283f8ebb036877337a2c96907ebd7cd2f03544203e871015c33cd9f36aa0b9d93e1f59375a9e70151cd5b65d6e2a3de682d0884f7036b5f179e608c2a672971cdf8195a8e7856380564df63289fab938b41d77c7c8f49fc05424954474f030233bc494f3ea86c13a9fba20abfa888593110db9e23c268105c8988d87957b9101d4f08c91c860545e970325543b283f8ebb036877337a2c96907ebd7cd2f0354201e7040844aec63c2fdf1faa44a22e2359524d256b0f37206ce3e6985bd33819949fc05424954474f0303eb3a656215fa62b1d240096f695e3184eec77ee08fbbc5ced600b5af875950941d4f08c91c860545e970325543b283f8ebb036877337a2c96907ebd7cd2f035420b6b193e3242edd703c964aa16e8c997febe8ee18eda50bcdd9490b3a4054aab408fc05424954474f051e492063616e2062656c69657665206974206973206e6f74206275747465720001008000000000010000000000000000000000000000000000000000000000000000000000000000ffffffff220020e816464a0141176fb1b582b012c5b2a7b3ab3f708465b5cf9cf81999f4e6aff100000000010000000000000000225120bdc70773283923be0e6d17777d35cf901e02822a445637ad80ae694b557ab93d0000000001012b0000000000000000225120bdc70773283923be0e6d17777d35cf901e02822a445637ad80ae694b557ab93d2116f5370f52856166f7ec086a3e95cd7f1e848301b90681a7d0146ecb9ac5e1c59d1500f2ef0389000000000000000029000000090000002116f6b36d425bd445717706f8a1dbe4dd813fb0c50339fbec367eb0ed8c39b3d9dc1500f65cd86900000000000000002900000009000000011720c4a6939e74c11c496fdcee803e3292117760a19fe42a3dd089ad801e9f2cb0430118200da8eab7f651495d404cf9c51e15747719a42e3b4078ca87c5412e84563bd47b48fc05424954474f01bdc70773283923be0e6d17777d35cf901e02822a445637ad80ae694b557ab93dc4a6939e74c11c496fdcee803e3292117760a19fe42a3dd089ad801e9f2cb0434202f5370f52856166f7ec086a3e95cd7f1e848301b90681a7d0146ecb9ac5e1c59d02f6b36d425bd445717706f8a1dbe4dd813fb0c50339fbec367eb0ed8c39b3d9dc49fc05424954474f0202f5370f52856166f7ec086a3e95cd7f1e848301b90681a7d0146ecb9ac5e1c59dbdc70773283923be0e6d17777d35cf901e02822a445637ad80ae694b557ab93d4202d5ae6b61ab16b114de105f6bc7ca9c1f247f25b7c37409a69a55f1ea626ae5c5023c8a1d61c3c9ceab2f8bbeb12c1fde0c3a54d5b8f03187da0ce6d0f793e44fcb49fc05424954474f0202f6b36d425bd445717706f8a1dbe4dd813fb0c50339fbec367eb0ed8c39b3d9dcbdc70773283923be0e6d17777d35cf901e02822a445637ad80ae694b557ab93d420266cdac5067019d12c22fbce70b37211ab204e7fb7d0fde90799c66609a9ba88f0239c65cb555aac728ce31a441e216407b4b9a3824a024d987268a1fc7c826ecf049fc05424954474f0302f5370f52856166f7ec086a3e95cd7f1e848301b90681a7d0146ecb9ac5e1c59dbdc70773283923be0e6d17777d35cf901e02822a445637ad80ae694b557ab93d204f3fe6ef46b24401f71fc0e33cf9fbbf7f6ec86527b0dfc30240dce85487f8a149fc05424954474f0302f6b36d425bd445717706f8a1dbe4dd813fb0c50339fbec367eb0ed8c39b3d9dcbdc70773283923be0e6d17777d35cf901e02822a445637ad80ae694b557ab93d204eebcd450801725ee417f647de7888bf42a5b12130b309610cdc05e8fac15ba208fc05424954474f051e492063616e2062656c69657665206974206973206e6f74206275747465720000', - extractedTx: - '0000000000010a97aac049b2b52216aecb2bc78c8af46201212ee96e20e914df2e36d69a543c5a00000000fdfd0000473044022013e1c5878f22f12eb699a5eb11f150999f5b169abaf587eb1b14cf0ce7a4f1cc02205fc547f631d8eb384695853f6d981bc1132eeccf5db5243f367166bbd9f2d60201483045022100e128c3734a4ef18097e4d2e9806b3d4a101ebc6fb77c7d5935c57383a695bea3022020773bcc1367d2db4445e3ae7b9218f254a33994df73ac6097ffc8590c566bfc014c69522102cc4d0fa411cac244486f8eb2c08e035ff7410f460a359ca7f8810991bd3b42092102d72fd0d0d90293434ad5fca160f278e03c614497aa4e425cf454e2c1330f96ab210344d884136df550202865ffbc6218c7f9c88fe6ce39c945798190badb38a752f153ae000000000cdca00b194e8ca29eb3bf17c78bbeeefa318b6d84c315359f7d19106615681b00000000fc00473044022030c6314abb04849ef1fd552a6dc7691bbb21540c2bd7aa5a3f23385649db2ae402203dcedb41fff80b2115d736d29e65fa329ce2d175741f1df65b8d4be30f444dea0147304402203731cee532f9b68c1852df3cfd6b316ee9f3dc09037d69deca233f2e117019d90220339420b35cbbc40a52a2fe440035d1eae1e13146d6650ee48fbaca39fff731df014c69522103a0334f9fbb9da9888e9ea3cc458c2a9ef659aec340bfb1d2d6616f01e5e5c4e421028fd779569e5588cabc672fc3c64fe12501490439a8e1990e079003a1a66921ea2103761b51c305db18bf65bb7703e47bb31e40372bf58a06b8970d3a3442f867741e53ae00000000c8f6693d34c4e98ef1fee4970e8342a12e67f81a4e74f7c5f5203c7fd6bcf2b900000000232200208224f2e5de25d91de7ae08f8744f98ac30a7c86e9c0fcf169279e520c2b8313a000000009f29963b1fa0a10e5fd6d35893ed4342755c5446055a3bb3f11db5dbfb50c4e20000000023220020fff6d24d943ecfea3b2cab30a1db70ef66b765be13b13e4e1758410217edf57100000000f76fd6f563a17407845ad8f4e5654a520db5ebe1248b05a7e0926037cce94057000000000000000000764f19f344db53359718284d4bb03bb0bbb83ae4125d45047faf3bf26a97ca95000000000000000000e2b53078c15bebe0bc8152af38bb355d08adeae9b5b86ec6770ce66d92e9d969000000000000000000a2dcc6006e599cb473166667035ba9b6c662cfed865daebb425a89d40af2dffe00000000000000000047b378408d0f5a9be521ce8e37113a0202b0e0a8c8d3409e3e1e4c27038c29560000000000000000005019def787deba1bac0c861ecea5bdbd6269f373d9daeaa31396350b2c84d2ca000000000000000000010000000000000000016a000004004730440220126fd17b31a4cb2402592bd2f1bcda4d3b90ce8a49012755e2006800a9e0a42302202e1aeecbb936a4ca0afd576754b525824bb15e8254ed07ce6cb7174ef0ab5b790147304402202a7ac15cb66799a86f91306abb1c4cf61b6d4582abc5f80ee6a51ef17967b070022056ed845749df7c4923c5b50c8c4b701a4962da0d95dfb0be6df737bfe83ae3d40169522102d5878a4d1c23ea735d7146ef2e1094f08d118bf0e20ea09366077c6b086c49d62102804dae97fd1acbb46d2816f530f83119dccdcaf3f5dd4c4dabb8edccd04bc65c21022ddee7d0b3f7f894fca44650a72137908eb843432e6874cb873cc0f2d27a54d653ae0400473044022007a80b56b6d628a7be48b825bf964269de3101598e1b83dce67996ad14af21a902205eab963b093c0a871d8e6d63cf9c008fa7a70c6cc3cb5f78f951399eab28340601473044022074a13727a3e5736aba2005a006ea1af0e83595de1eb43b6d07468e7909ba9a24022006d94d75266d390e4134dc7bbe544fd76005e2852a88ed9db57c3c02a52f94c801695221034f0c87cd48157c37d26c296937b816218f6093d038ae6c3deba6b9f5776abfb32103eebab40c070ebe90e2806af5a4e365116c013a4d81ee7c167663649ef7a91d5321031b6cad2325e074b6f7419e7d1bfe37f317b309707630c0c313f89221cc8113c753ae040047304402202847ad6c0dafc791fe269d346e95c8ccba12efa90be9bc121af181b31500f26502201c0193ae7152d4a810a30cab134e5dff4bea538174de2fab1c903a00a543a5e301483045022100866ab759aa3e17fc4b9128bc7bcb871c153a83ad62fe600cc631bb16eeb27f9d0220470d8f3a0a908e55b8c424d384d1ab50d7f77023860ed6fe68a0e3f3db0e7a4d016952210383a7be358204e1f8dfba8dc7cdc0dca66a00b14eaf679d91b1a92f656c26bf8421027c8c55f1c02918ebf0daed6dc58f5e0b1074e46d8310d162570044084a0967692103623dc211dc28212e83fdf9f808e1dec770e461b0af1ba65458be106222be6a6153ae040047304402206c472129bab262d8e15c4d8eae6bae8ffaafad49e0857de0defc705c454ac7c202201d1f95264be08d72bd2dc313e61a7394ea4b4d9e4b01f78684059f6d0068240f01473044022074a44626578abb12b29db5aa2a57dddbf1548c8b295e382f74711b7cc45b309c0220725bde51555e81c54dc73be0c4928fec6a1e100f30aae7b7ce99ff67a06b0d7d0169522102f1af33db32f4c97b5f9381129f47e9f622a427144518eae3f8eca814741241f62102671f7f94e0adbc5be6893191ab88405b7aaa5cba3ef98ae4fd136316aad4cef32102d71c110eed1ae7e1c51377ac72f94339caa280a6d9dd7a843aad65cf4509288b53ae0440f419ebe7ba0487a4c179c4f010cc92b893149ed12ccf504d106f95394639a707b8c9e51c3b38c5465861c1b178377ad46c8f220442863957fb7bc248c5dd33e240b2b2467a458d385aa4afb982c624ea7239720f204c54abaf66f34d1feca154092bb8b060ee3564be1bf4abd33c508c5474e0182a6abb22ef9fad35c929182e3a4420c69e0b14bfba3101e0f336480a4617964ddb34d14755406529bc3f1f57fa49c8ad202a42e9d0284d5515d404dd84024b3d2eb291e2f971da02262a91551e35ed7e17ac41c11ce1dfba2ee2d1cab438a36b7e39f77da38229cc8f220570166a04bf0f29daf10f3d3e320bd977cf4de055594a1b93b6bf3bf35d9e5b8ac3bae37ca23f76f91d0440e831e3e0219552cfdd87c6f9af3854c6447a09265854d0c4d3c160ac0557498aeab1160c6f7d62422db67af3776f06ebd76c2d35e514125f5ddedc435ba277d84002ecc2125852f5856584d56181d4ef896208b20d95046fea05944b78c5d4978dd8f7013b30cfcb9837aeb8bbe314a6bd74b4ef9c9460fff6a9f04a41342ce4a14420e08fd25466b7ddb5239bd54c4823f877c137bda413fb5bd8a12bc5242a18900fad209b8c27e9f9f279390ab304cc3415e29d59e15f6ba34d463446fc90f8ea3ce315ac41c0a30fa2cbc9c0febc03c43d3d4d5f450ca164039364081cc681cc308388d249947ae56f815f9d25f5c2b33edeaad613c1a7d14fba7a2d48db9523b06c5eea5507014031c1b3b7b21d9d470772b556159a6e6761cc07058351f9228a7451dba41767cee17c30dd48161afdf8e441a480b60a4ddb4209cf2df15f984ff86e877d4a1ff60140d3e7ef0dd3ecae81237abddc1b5365ef81d89439f0a47a4484ab6e0e6fb99c55af9c089f37d4d1512a1f020c200d922dd527a310a34cdc1354e7afdae9bd42ea00000000', - }, -}; diff --git a/modules/abstract-utxo/test/unit/transaction/fixedScript/explainPsbt.ts b/modules/abstract-utxo/test/unit/transaction/fixedScript/explainPsbt.ts index cacbbe2d6c..fe19d2a92a 100644 --- a/modules/abstract-utxo/test/unit/transaction/fixedScript/explainPsbt.ts +++ b/modules/abstract-utxo/test/unit/transaction/fixedScript/explainPsbt.ts @@ -2,7 +2,7 @@ import assert from 'node:assert/strict'; import * as utxolib from '@bitgo/utxo-lib'; import { testutil } from '@bitgo/utxo-lib'; -import { fixedScriptWallet, Triple } from '@bitgo/wasm-utxo'; +import { fixedScriptWallet } from '@bitgo/wasm-utxo'; import type { TransactionExplanation } from '../../../../src/transaction/fixedScript/explainTransaction'; import { @@ -18,8 +18,8 @@ function describeTransactionWith(acidTest: testutil.AcidTest) { describe(`${acidTest.name}`, function () { let psbt: utxolib.bitgo.UtxoPsbt; let psbtBytes: Buffer; - let walletXpubs: Triple; - let customChangeWalletXpubs: Triple | undefined; + let walletXpubs: fixedScriptWallet.RootWalletKeys; + let customChangeWalletXpubs: fixedScriptWallet.RootWalletKeys | undefined; let wasmPsbt: fixedScriptWallet.BitGoPsbt; let refExplanation: TransactionExplanation; before('prepare', function () { @@ -31,8 +31,8 @@ function describeTransactionWith(acidTest: testutil.AcidTest) { psbtBytes = psbt.toBuffer(); const networkName = utxolib.getNetworkName(acidTest.network); assert(networkName); - walletXpubs = acidTest.rootWalletKeys.triple.map((k) => k.neutered().toBase58()) as Triple; - customChangeWalletXpubs = acidTest.otherWalletKeys.triple.map((k) => k.neutered().toBase58()) as Triple; + walletXpubs = fixedScriptWallet.RootWalletKeys.from(acidTest.rootWalletKeys); + customChangeWalletXpubs = fixedScriptWallet.RootWalletKeys.from(acidTest.otherWalletKeys); wasmPsbt = fixedScriptWallet.BitGoPsbt.fromBytes(psbtBytes, networkName); }); @@ -154,7 +154,7 @@ describe('aggregateTransactionExplanations', function () { const networkName = utxolib.getNetworkName(acidTest.network); assert(networkName); const wasmPsbt = fixedScriptWallet.BitGoPsbt.fromBytes(psbtBytes, networkName); - const walletXpubs = acidTest.rootWalletKeys.triple.map((k) => k.neutered().toBase58()) as Triple; + const walletXpubs = fixedScriptWallet.RootWalletKeys.from(acidTest.rootWalletKeys); exp = explainPsbtWasmBigInt(wasmPsbt, walletXpubs, { replayProtection: { publicKeys: [acidTest.getReplayProtectionPublicKey()] }, }); From 9d90a6dd260e8ae7f23a194a4c8325c678711b9e Mon Sep 17 00:00:00 2001 From: Claude Agent Date: Sun, 19 Apr 2026 03:52:51 +0000 Subject: [PATCH 12/13] ci(root): upgrade aws-actions/configure-aws-credentials to v6 Node.js 20 actions are deprecated; forced migration to Node.js 24 on June 2, 2026. Ticket: DX-496 --- .github/workflows/claude-pr.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/claude-pr.yml b/.github/workflows/claude-pr.yml index b8c906ecae..2cab25dffa 100644 --- a/.github/workflows/claude-pr.yml +++ b/.github/workflows/claude-pr.yml @@ -29,7 +29,7 @@ jobs: uses: actions/checkout@v6 - name: Configure AWS Credentials (OIDC) - uses: aws-actions/configure-aws-credentials@v5 + uses: aws-actions/configure-aws-credentials@v6 with: role-to-assume: arn:aws:iam::199765120567:role/${{ github.event.repository.name }}-iam-protected aws-region: us-west-2 From 8542b17590752d1bb547e3dd58fec1356b6318e7 Mon Sep 17 00:00:00 2001 From: Alex Tse Date: Wed, 29 Apr 2026 14:13:50 -0400 Subject: [PATCH 13/13] Revert "feat: implement BitGo signing in SDK" --- modules/express/src/clientRoutes.ts | 13 +- .../test/unit/typedRoutes/ofcSignPayload.ts | 93 ----------- .../src/bitgo/trading/iTradingAccount.ts | 7 +- .../src/bitgo/trading/network/network.ts | 7 +- .../src/bitgo/trading/network/types.ts | 2 +- .../src/bitgo/trading/tradingAccount.ts | 40 +---- modules/sdk-core/src/bitgo/wallet/iWallet.ts | 1 - modules/sdk-core/src/coins/ofc.ts | 21 --- modules/sdk-core/src/coins/ofcToken.ts | 20 +-- .../test/unit/bitgo/trading/tradingAccount.ts | 134 ---------------- .../bitgo/wallet/ofcWalletSignTransaction.ts | 79 ---------- modules/sdk-core/test/unit/coins/ofc.ts | 146 ------------------ 12 files changed, 13 insertions(+), 550 deletions(-) delete mode 100644 modules/sdk-core/test/unit/bitgo/trading/tradingAccount.ts delete mode 100644 modules/sdk-core/test/unit/bitgo/wallet/ofcWalletSignTransaction.ts delete mode 100644 modules/sdk-core/test/unit/coins/ofc.ts diff --git a/modules/express/src/clientRoutes.ts b/modules/express/src/clientRoutes.ts index dd39f1cece..0ae3d95021 100755 --- a/modules/express/src/clientRoutes.ts +++ b/modules/express/src/clientRoutes.ts @@ -405,15 +405,6 @@ function getWalletPwFromEnv(walletId: string): string { return walletPw; } -/** - * Returns the wallet passphrase from the environment, or undefined if not set. - * Unlike getWalletPwFromEnv, this does not throw when the env variable is absent. - * Use this when the passphrase is optional (e.g. KMS-backed wallets). - */ -function findWalletPwFromEnv(walletId: string): string | undefined { - return process.env[`WALLET_${walletId}_PASSPHRASE`]; -} - async function getEncryptedPrivKey(path: string, walletId: string): Promise { const privKeyFile = await fs.readFile(path, { encoding: 'utf8' }); const encryptedPrivKey = JSON.parse(privKeyFile); @@ -640,9 +631,7 @@ export async function handleV2OFCSignPayload( throw new ApiResponseError(`Could not find OFC wallet ${walletId}`, 404); } - // Prefer the passphrase from the request body; fall back to the env var. - // If neither is present, pass undefined — signPayload() routes to KMS internally. - const walletPassphrase = bodyWalletPassphrase ?? findWalletPwFromEnv(wallet.id()); + const walletPassphrase = bodyWalletPassphrase || getWalletPwFromEnv(wallet.id()); const tradingAccount = wallet.toTradingAccount(); const stringifiedPayload = typeof payload === 'string' ? payload : JSON.stringify(payload); const signature = await tradingAccount.signPayload({ diff --git a/modules/express/test/unit/typedRoutes/ofcSignPayload.ts b/modules/express/test/unit/typedRoutes/ofcSignPayload.ts index 9b0685183d..59cdd54f4d 100644 --- a/modules/express/test/unit/typedRoutes/ofcSignPayload.ts +++ b/modules/express/test/unit/typedRoutes/ofcSignPayload.ts @@ -223,103 +223,10 @@ describe('OfcSignPayload codec tests', function () { const decodedResponse = assertDecode(OfcSignPayloadResponse200, result.body); assert.strictEqual(decodedResponse.signature, mockSignPayloadResponse.signature); - // Verify env passphrase was forwarded to signPayload - const signCall = mockTradingAccount.signPayload.getCall(0); - assert.ok(signCall, 'tradingAccount.signPayload should have been called'); - assert.strictEqual(signCall.args[0].walletPassphrase, 'env_passphrase', 'env passphrase should be forwarded'); - // Cleanup environment variable delete process.env['WALLET_ofc-wallet-id-123_PASSPHRASE']; }); - it('should pass undefined walletPassphrase to signPayload when no passphrase in body or env (KMS path)', async function () { - const requestBody = { - walletId: 'ofc-wallet-id-no-passphrase', - payload: { amount: '1000000', currency: 'USD' }, - // no walletPassphrase - }; - - // Ensure no env var is set for this wallet - delete process.env['WALLET_ofc-wallet-id-no-passphrase_PASSPHRASE']; - - const mockTradingAccount = { - signPayload: sinon.stub().resolves(mockSignPayloadResponse.signature), - }; - - const mockWallet = { - id: () => requestBody.walletId, - toTradingAccount: sinon.stub().returns(mockTradingAccount), - }; - - const walletsGetStub = sinon.stub().resolves(mockWallet); - const mockWallets = { get: walletsGetStub }; - const mockCoin = { wallets: sinon.stub().returns(mockWallets) }; - sinon.stub(BitGo.prototype, 'coin').returns(mockCoin as any); - - const result = await agent - .post('/api/v2/ofc/signPayload') - .set('Authorization', 'Bearer test_access_token_12345') - .set('Content-Type', 'application/json') - .send(requestBody); - - assert.strictEqual(result.status, 200); - const decodedResponse = assertDecode(OfcSignPayloadResponse200, result.body); - assert.strictEqual(decodedResponse.signature, mockSignPayloadResponse.signature); - - // signPayload must be called with walletPassphrase=undefined so the SDK routes to KMS - const signCall = mockTradingAccount.signPayload.getCall(0); - assert.ok(signCall, 'tradingAccount.signPayload should have been called'); - assert.strictEqual( - signCall.args[0].walletPassphrase, - undefined, - 'walletPassphrase should be undefined to trigger KMS signing' - ); - }); - - it('should prefer body walletPassphrase over env passphrase', async function () { - const requestBody = { - walletId: 'ofc-wallet-id-123', - payload: { amount: '500' }, - walletPassphrase: 'body_passphrase', - }; - - // Set a different env passphrase — body should win - process.env['WALLET_ofc-wallet-id-123_PASSPHRASE'] = 'env_passphrase'; - - const mockTradingAccount = { - signPayload: sinon.stub().resolves(mockSignPayloadResponse.signature), - }; - - const mockWallet = { - id: () => requestBody.walletId, - toTradingAccount: sinon.stub().returns(mockTradingAccount), - }; - - const walletsGetStub = sinon.stub().resolves(mockWallet); - const mockWallets = { get: walletsGetStub }; - const mockCoin = { wallets: sinon.stub().returns(mockWallets) }; - sinon.stub(BitGo.prototype, 'coin').returns(mockCoin as any); - - const result = await agent - .post('/api/v2/ofc/signPayload') - .set('Authorization', 'Bearer test_access_token_12345') - .set('Content-Type', 'application/json') - .send(requestBody); - - assert.strictEqual(result.status, 200); - - // body passphrase should take precedence - const signCall = mockTradingAccount.signPayload.getCall(0); - assert.ok(signCall, 'tradingAccount.signPayload should have been called'); - assert.strictEqual( - signCall.args[0].walletPassphrase, - 'body_passphrase', - 'body passphrase should take precedence over env' - ); - - delete process.env['WALLET_ofc-wallet-id-123_PASSPHRASE']; - }); - it('should successfully sign complex nested JSON payload', async function () { const requestBody = { walletId: 'ofc-wallet-id-123', diff --git a/modules/sdk-core/src/bitgo/trading/iTradingAccount.ts b/modules/sdk-core/src/bitgo/trading/iTradingAccount.ts index b99dfd8596..462c7e25ba 100644 --- a/modules/sdk-core/src/bitgo/trading/iTradingAccount.ts +++ b/modules/sdk-core/src/bitgo/trading/iTradingAccount.ts @@ -1,13 +1,8 @@ import { ITradingNetwork } from './network'; -/** - * Parameters for the signing a payload from the trading account - * @param payload - The payload to sign - * @param walletPassphrase - The passphrase of the wallet that will be used to decrypt the user key and sign the payload. If not provided, the BitGo key will be used. - */ export interface SignPayloadParameters { payload: string | Record; - walletPassphrase?: string; + walletPassphrase: string; } export interface ITradingAccount { diff --git a/modules/sdk-core/src/bitgo/trading/network/network.ts b/modules/sdk-core/src/bitgo/trading/network/network.ts index 90902cac32..ed8698d721 100644 --- a/modules/sdk-core/src/bitgo/trading/network/network.ts +++ b/modules/sdk-core/src/bitgo/trading/network/network.ts @@ -109,7 +109,7 @@ export class TradingNetwork implements ITradingNetwork { /** * Prepare an allocation for submission - * @param {string} walletPassphrase ofc wallet passphrase - required only when signing via user key + * @param {string} walletPassphrase ofc wallet passphrase * @param {string} connectionId connection to whom to make the allocation or deallocation * @param {string=} clientExternalId one time generated uuid v4 * @param {string} currency currency for which the allocation should be made. e.g. btc / tbtc @@ -130,7 +130,10 @@ export class TradingNetwork implements ITradingNetwork { } const payload = JSON.stringify(body); - const signature = await this.wallet.toTradingAccount().signPayload({ payload, walletPassphrase }); + + const prv = await this.wallet.getPrv({ walletPassphrase }); + const signedBuffer: Buffer = await this.wallet.baseCoin.signMessage({ prv }, payload); + const signature = signedBuffer.toString('hex'); return { ...body, diff --git a/modules/sdk-core/src/bitgo/trading/network/types.ts b/modules/sdk-core/src/bitgo/trading/network/types.ts index 0d120fa07c..e6da5283f4 100644 --- a/modules/sdk-core/src/bitgo/trading/network/types.ts +++ b/modules/sdk-core/src/bitgo/trading/network/types.ts @@ -125,7 +125,7 @@ export type GetNetworkAllocationByIdResponse = { }; export type PrepareNetworkAllocationParams = Omit & { - walletPassphrase?: string; + walletPassphrase: string; clientExternalId?: string; nonce?: string; }; diff --git a/modules/sdk-core/src/bitgo/trading/tradingAccount.ts b/modules/sdk-core/src/bitgo/trading/tradingAccount.ts index 19eaaa4f66..40489647ae 100644 --- a/modules/sdk-core/src/bitgo/trading/tradingAccount.ts +++ b/modules/sdk-core/src/bitgo/trading/tradingAccount.ts @@ -23,51 +23,13 @@ export class TradingAccount implements ITradingAccount { } /** - * Signs an arbitrary payload. Use the user key if passphrase is provided, or the BitGo key if not. + * Signs an arbitrary payload with the user key on this trading account * @param params * @param params.payload arbitrary payload object (string | Record) * @param params.walletPassphrase passphrase on this trading account, used to unlock the account user key * @returns hex-encoded signature of the payload */ async signPayload(params: SignPayloadParameters): Promise { - // if no passphrase is provided, attempt to sign using the wallet's bitgo key remotely - if (!params.walletPassphrase) { - return this.signPayloadByBitGoKey(params); - } - // if a passphrase is provided, we must be trying to sign using the user private key - decrypt and sign locally - return this.signPayloadByUserKey(params); - } - - /** - * Signs the payload of a trading account via the trading account BitGo key - * @param params - * @private - */ - private async signPayloadByBitGoKey(params: Omit): Promise { - const walletData = this.wallet.toJSON(); - if (walletData.userKeySigningRequired) { - throw new Error( - 'Wallet must use user key to sign ofc transaction, please provide the wallet passphrase or visit your wallet settings page to configure one.' - ); - } - if (walletData.keys.length < 2) { - throw new Error( - 'Wallet does not support BitGo signing. Please reach out to support@bitgo.com to resolve this issue.' - ); - } - - const url = this.wallet.url('/tx/sign'); - const { signature } = await this.wallet.bitgo.post(url).send(params.payload).result(); - - return signature; - } - - /** - * Signs the payload of a trading account locally by fetching the user's encrypted private key and decrypt using passphrase - * @param params - * @private - */ - private async signPayloadByUserKey(params: SignPayloadParameters): Promise { const key = (await this.wallet.baseCoin.keychains().get({ id: this.wallet.keyIds()[0] })) as any; const prv = this.wallet.bitgo.decrypt({ input: key.encryptedPrv, diff --git a/modules/sdk-core/src/bitgo/wallet/iWallet.ts b/modules/sdk-core/src/bitgo/wallet/iWallet.ts index 62d790d9c0..1f65b5a977 100644 --- a/modules/sdk-core/src/bitgo/wallet/iWallet.ts +++ b/modules/sdk-core/src/bitgo/wallet/iWallet.ts @@ -909,7 +909,6 @@ export interface WalletData { evmKeyRingReferenceWalletId?: string; isParent?: boolean; enabledChildChains?: string[]; - userKeySigningRequired?: string; } export interface RecoverTokenOptions { diff --git a/modules/sdk-core/src/coins/ofc.ts b/modules/sdk-core/src/coins/ofc.ts index 18893123f6..4e089ba5a8 100644 --- a/modules/sdk-core/src/coins/ofc.ts +++ b/modules/sdk-core/src/coins/ofc.ts @@ -15,7 +15,6 @@ import { SignTransactionOptions, VerifyAddressOptions, VerifyTransactionOptions, - Wallet, } from '../'; export class Ofc extends BaseCoin { @@ -105,26 +104,6 @@ export class Ofc extends BaseCoin { throw new MethodNotImplementedError(); } - /** - * Signs a message using a trading wallet's BitGo Key - * @param wallet - uses the BitGo key of this trading wallet to sign the message remotely in a KMS - * @param message - */ - async signMessage(wallet: Wallet, message: string): Promise; - /** - * Signs a message using the private key - * @param key - uses the private key to sign the message - * @param message - */ - async signMessage(key: { prv: string }, message: string): Promise; - async signMessage(keyOrWallet: { prv: string } | Wallet, message: string): Promise { - if (!(keyOrWallet instanceof Wallet)) { - return super.signMessage(keyOrWallet, message); - } - const signatureHexString = await keyOrWallet.toTradingAccount().signPayload({ payload: message }); - return Buffer.from(signatureHexString, 'hex'); - } - /** @inheritDoc */ auditDecryptedKey(params: AuditDecryptedKeyParams) { throw new MethodNotImplementedError(); diff --git a/modules/sdk-core/src/coins/ofcToken.ts b/modules/sdk-core/src/coins/ofcToken.ts index e100285443..42cef18e75 100644 --- a/modules/sdk-core/src/coins/ofcToken.ts +++ b/modules/sdk-core/src/coins/ofcToken.ts @@ -9,7 +9,6 @@ import { SignTransactionOptions as BaseSignTransactionOptions, SignedTransaction, ITransactionRecipient, - Wallet, } from '../'; import { isBolt11Invoice } from '../lightning'; @@ -19,8 +18,7 @@ export interface SignTransactionOptions extends BaseSignTransactionOptions { txPrebuild: { payload: string; }; - prv?: string; - wallet?: Wallet; + prv: string; } export { OfcTokenConfig }; @@ -109,25 +107,15 @@ export class OfcToken extends Ofc { } /** - * Signs a half-signed OFC transaction. - * Signs the transaction remotely using the BitGo key if prv is not provided. + * Assemble keychain and half-sign prebuilt transaction * @param params * @returns {Promise} */ async signTransaction(params: SignTransactionOptions): Promise { const txPrebuild = params.txPrebuild; const payload = txPrebuild.payload; - - let signature: string; - if (params.wallet) { - signature = await params.wallet.toTradingAccount().signPayload({ payload, walletPassphrase: params.prv }); - } else if (params.prv) { - const signatureBuffer = (await this.signMessage({ prv: params.prv }, payload)) as any; - signature = signatureBuffer.toString('hex'); - } else { - throw new Error('You must pass in either one of wallet or prv'); - } - + const signatureBuffer = (await this.signMessage(params, payload)) as any; + const signature: string = signatureBuffer.toString('hex'); return { halfSigned: { payload, signature } } as any; } diff --git a/modules/sdk-core/test/unit/bitgo/trading/tradingAccount.ts b/modules/sdk-core/test/unit/bitgo/trading/tradingAccount.ts deleted file mode 100644 index f05262effa..0000000000 --- a/modules/sdk-core/test/unit/bitgo/trading/tradingAccount.ts +++ /dev/null @@ -1,134 +0,0 @@ -/** - * @prettier - */ -import sinon from 'sinon'; -import 'should'; -import { TradingAccount } from '../../../../src/bitgo/trading/tradingAccount'; - -describe('TradingAccount', function () { - let tradingAccount: TradingAccount; - let mockBitGo: any; - let mockWallet: any; - let mockBaseCoin: any; - - const enterpriseId = 'test-enterprise-id'; - const walletPassphrase = 'test-passphrase'; - const encryptedPrv = 'encrypted-prv'; - const decryptedPrv = 'decrypted-prv'; - const signature = 'aabbccdd'; - - beforeEach(function () { - const postStub = sinon.stub(); - postStub.returns({ - send: sinon.stub().returns({ - result: sinon.stub().resolves({ signature }), - }), - }); - - mockBitGo = { - post: postStub, - decrypt: sinon.stub().returns(decryptedPrv), - }; - - mockBaseCoin = { - keychains: sinon.stub().returns({ - get: sinon.stub().resolves({ encryptedPrv }), - }), - signMessage: sinon.stub().resolves(Buffer.from(signature, 'hex')), - }; - - mockWallet = { - id: sinon.stub().returns('test-wallet-id'), - keyIds: sinon.stub().returns(['user-key-id', 'backup-key-id', 'bitgo-key-id']), - url: sinon.stub().returns('https://example.com/wallet/test-wallet-id/tx/sign'), - toJSON: sinon.stub().returns({ - id: 'test-wallet-id', - keys: ['user-key-id', 'backup-key-id', 'bitgo-key-id'], - userKeySigningRequired: undefined, - }), - baseCoin: mockBaseCoin, - bitgo: mockBitGo, - }; - - tradingAccount = new TradingAccount(enterpriseId, mockWallet, mockBitGo); - }); - - afterEach(function () { - sinon.restore(); - }); - - describe('signPayload', function () { - const payload = { data: 'test-payload' }; - const payloadString = 'test-payload-string'; - - describe('without walletPassphrase (BitGo remote signing)', function () { - it('should sign using the BitGo key remotely when no passphrase is provided', async function () { - const result = await tradingAccount.signPayload({ payload }); - - mockWallet.toJSON.calledOnce.should.be.true(); - mockWallet.url.calledWith('/tx/sign').should.be.true(); - mockBitGo.post.calledOnce.should.be.true(); - result.should.equal(signature); - }); - - it('should sign a string payload remotely when no passphrase is provided', async function () { - const result = await tradingAccount.signPayload({ payload: payloadString }); - - result.should.equal(signature); - }); - - it('should throw if userKeySigningRequired is set and no passphrase is provided', async function () { - mockWallet.toJSON.returns({ - id: 'test-wallet-id', - keys: ['user-key-id', 'backup-key-id', 'bitgo-key-id'], - userKeySigningRequired: 'true', - }); - - await tradingAccount - .signPayload({ payload }) - .should.be.rejectedWith( - 'Wallet must use user key to sign ofc transaction, please provide the wallet passphrase or visit your wallet settings page to configure one.' - ); - }); - - it('should throw if wallet has fewer than 2 keys and no passphrase is provided', async function () { - mockWallet.toJSON.returns({ - id: 'test-wallet-id', - keys: ['user-key-id'], - userKeySigningRequired: undefined, - }); - - await tradingAccount - .signPayload({ payload }) - .should.be.rejectedWith( - 'Wallet does not support BitGo signing. Please reach out to support@bitgo.com to resolve this issue.' - ); - }); - }); - - describe('with walletPassphrase (local user key signing)', function () { - it('should decrypt the user key and sign the payload locally', async function () { - const result = await tradingAccount.signPayload({ payload, walletPassphrase }); - - mockBaseCoin.keychains().get.calledWith({ id: 'user-key-id' }).should.be.true(); - mockBitGo.decrypt.calledWith({ input: encryptedPrv, password: walletPassphrase }).should.be.true(); - mockBaseCoin.signMessage.calledOnce.should.be.true(); - result.should.equal(Buffer.from(signature, 'hex').toString('hex')); - }); - - it('should stringify a Record payload before signing locally', async function () { - await tradingAccount.signPayload({ payload, walletPassphrase }); - - const signMessageCall = mockBaseCoin.signMessage.getCall(0); - signMessageCall.args[1].should.equal(JSON.stringify(payload)); - }); - - it('should pass a string payload directly to signMessage', async function () { - await tradingAccount.signPayload({ payload: payloadString, walletPassphrase }); - - const signMessageCall = mockBaseCoin.signMessage.getCall(0); - signMessageCall.args[1].should.equal(payloadString); - }); - }); - }); -}); diff --git a/modules/sdk-core/test/unit/bitgo/wallet/ofcWalletSignTransaction.ts b/modules/sdk-core/test/unit/bitgo/wallet/ofcWalletSignTransaction.ts deleted file mode 100644 index 4f0d26bbe4..0000000000 --- a/modules/sdk-core/test/unit/bitgo/wallet/ofcWalletSignTransaction.ts +++ /dev/null @@ -1,79 +0,0 @@ -/** - * @prettier - */ -import sinon from 'sinon'; -import 'should'; -import { Wallet } from '../../../../src'; - -describe('Wallet - OFC signTransaction', function () { - let wallet: Wallet; - let mockBitGo: any; - let mockBaseCoin: any; - let mockWalletData: any; - - beforeEach(function () { - mockBitGo = { - url: sinon.stub().returns('https://test.bitgo.com'), - post: sinon.stub(), - get: sinon.stub(), - setRequestTracer: sinon.stub(), - }; - - mockBaseCoin = { - getFamily: sinon.stub().returns('ofc'), - url: sinon.stub().returns('https://test.bitgo.com/wallet'), - keychains: sinon.stub(), - supportsTss: sinon.stub().returns(false), - getMPCAlgorithm: sinon.stub(), - presignTransaction: sinon.stub().resolvesArg(0), - keyIdsForSigning: sinon.stub().returns([0]), - signTransaction: sinon.stub().resolves({ halfSigned: { payload: 'test', signature: 'aabbcc' } }), - }; - - mockWalletData = { - id: 'test-wallet-id', - coin: 'ofcusdt', - keys: ['user-key', 'backup-key', 'bitgo-key'], - multisigType: 'onchain', - enterprise: 'ent-id', - }; - - wallet = new Wallet(mockBitGo, mockBaseCoin, mockWalletData); - }); - - afterEach(function () { - sinon.restore(); - }); - - it('should pass wallet instance to baseCoin.signTransaction', async function () { - const txPrebuild = { txInfo: { payload: '{"amount":"100"}' } } as any; - const prv = 'test-prv'; - - await wallet.signTransaction({ txPrebuild, prv }); - - mockBaseCoin.signTransaction.calledOnce.should.be.true(); - const callArgs = mockBaseCoin.signTransaction.getCall(0).args[0]; - callArgs.wallet.should.equal(wallet); - }); - - it('should pass prv to baseCoin.signTransaction when provided directly', async function () { - const txPrebuild = { txInfo: { payload: '{"amount":"100"}' } } as any; - const prv = 'test-prv'; - - await wallet.signTransaction({ txPrebuild, prv }); - - const callArgs = mockBaseCoin.signTransaction.getCall(0).args[0]; - callArgs.prv.should.equal(prv); - }); - - it('should pass wallet instance to baseCoin.signTransaction even when no prv is available', async function () { - sinon.stub(wallet, 'getUserPrv').returns(undefined as any); - const txPrebuild = { txInfo: { payload: '{"amount":"100"}' } } as any; - - await wallet.signTransaction({ txPrebuild }); - - mockBaseCoin.signTransaction.calledOnce.should.be.true(); - const callArgs = mockBaseCoin.signTransaction.getCall(0).args[0]; - callArgs.wallet.should.equal(wallet); - }); -}); diff --git a/modules/sdk-core/test/unit/coins/ofc.ts b/modules/sdk-core/test/unit/coins/ofc.ts deleted file mode 100644 index 780c288645..0000000000 --- a/modules/sdk-core/test/unit/coins/ofc.ts +++ /dev/null @@ -1,146 +0,0 @@ -/** - * @prettier - */ -import sinon from 'sinon'; -import 'should'; -import { Ofc } from '../../../src/coins/ofc'; -import { OfcToken } from '../../../src/coins/ofcToken'; -import { BaseCoin } from '../../../src/bitgo/baseCoin/baseCoin'; -import { Wallet } from '../../../src'; - -const TEST_TOKEN_CONFIG = { - coin: 'ofcusdt', - decimalPlaces: 6, - name: 'OFCUSDT', - type: 'ofcusdt', - backingCoin: 'usdt', - isFiat: false, -}; - -describe('Ofc / OfcToken', function () { - let mockBitGo: any; - - beforeEach(function () { - mockBitGo = { url: sinon.stub().returns('https://test.bitgo.com') }; - }); - - afterEach(function () { - sinon.restore(); - }); - - describe('signMessage', function () { - let ofc: Ofc; - - beforeEach(function () { - ofc = new Ofc(mockBitGo); - }); - - describe('with a Wallet instance', function () { - it('should delegate to wallet.toTradingAccount().signPayload() and return a Buffer', async function () { - const hexSignature = 'deadbeef'; - const signPayloadStub = sinon.stub().resolves(hexSignature); - - const mockBaseCoin = { supportsTss: sinon.stub().returns(false), getMPCAlgorithm: sinon.stub() }; - const walletData = { - id: 'wallet-id', - keys: ['key1', 'key2', 'key3'], - multisigType: 'onchain', - enterprise: 'ent-id', - }; - const wallet = new Wallet(mockBitGo, mockBaseCoin as any, walletData); - sinon.stub(wallet, 'toTradingAccount').returns({ signPayload: signPayloadStub } as any); - - const message = 'test message'; - const result = await ofc.signMessage(wallet, message); - - signPayloadStub.calledOnceWith({ payload: message }).should.be.true(); - result.should.deepEqual(Buffer.from(hexSignature, 'hex')); - }); - }); - - describe('with a prv key', function () { - it('should delegate to the base class signMessage', async function () { - const expectedResult = Buffer.from('basesignature', 'hex'); - const superSignMessageStub = sinon.stub(BaseCoin.prototype, 'signMessage').resolves(expectedResult); - - const key = { - prv: 'xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqhuCo36EkzGH6qiT9mJHBvuPKtLRYD4NxFb5hgXMQBB2LLT6mxLDHHo', - }; - const message = 'test message'; - const result = await ofc.signMessage(key, message); - - superSignMessageStub.calledOnceWith(key, message).should.be.true(); - result.should.equal(expectedResult); - }); - }); - }); - - describe('signTransaction (OfcToken)', function () { - let ofcToken: OfcToken; - const payload = '{"amount":"100","from":"alice","to":"bob"}'; - - beforeEach(function () { - ofcToken = new OfcToken(mockBitGo, TEST_TOKEN_CONFIG); - }); - - describe('with wallet and no prv (BitGo remote signing)', function () { - it('should call wallet.toTradingAccount().signPayload() without a passphrase', async function () { - const hexSignature = 'aabbccdd'; - const signPayloadStub = sinon.stub().resolves(hexSignature); - const mockWallet = { toTradingAccount: sinon.stub().returns({ signPayload: signPayloadStub }) }; - - const result = await ofcToken.signTransaction({ txPrebuild: { payload }, wallet: mockWallet as any }); - - signPayloadStub.calledOnceWith({ payload, walletPassphrase: undefined }).should.be.true(); - result.should.deepEqual({ halfSigned: { payload, signature: hexSignature } }); - }); - }); - - describe('with wallet and prv (local signing routed through wallet)', function () { - it('should call wallet.toTradingAccount().signPayload() with the wallet passphrase', async function () { - const hexSignature = 'aabbccdd'; - const passphrase = 'test-passphrase'; - const signPayloadStub = sinon.stub().resolves(hexSignature); - const mockWallet = { toTradingAccount: sinon.stub().returns({ signPayload: signPayloadStub }) }; - - const result = await ofcToken.signTransaction({ - txPrebuild: { payload }, - wallet: mockWallet as any, - prv: passphrase, - }); - - signPayloadStub.calledOnceWith({ payload, walletPassphrase: passphrase }).should.be.true(); - result.should.deepEqual({ halfSigned: { payload, signature: hexSignature } }); - }); - }); - - describe('with prv only (local signing without wallet)', function () { - it('should sign locally and return the correct halfSigned result', async function () { - const signatureBytes = Buffer.from('ccddee', 'hex'); - sinon.stub(BaseCoin.prototype, 'signMessage').resolves(signatureBytes); - - const result = await ofcToken.signTransaction({ txPrebuild: { payload }, prv: 'test-prv' }); - - result.should.deepEqual({ halfSigned: { payload, signature: signatureBytes.toString('hex') } }); - }); - - it('should pass the prv to signMessage', async function () { - const signatureBytes = Buffer.from('ccddee', 'hex'); - const superSignMessageStub = sinon.stub(BaseCoin.prototype, 'signMessage').resolves(signatureBytes); - const prv = 'test-prv'; - - await ofcToken.signTransaction({ txPrebuild: { payload }, prv }); - - superSignMessageStub.calledOnceWith({ prv }, payload).should.be.true(); - }); - }); - - describe('with neither wallet nor prv', function () { - it('should throw an error', async function () { - await ofcToken - .signTransaction({ txPrebuild: { payload } }) - .should.be.rejectedWith('You must pass in either one of wallet or prv'); - }); - }); - }); -});