Skip to content

Commit c758315

Browse files
authored
Merge pull request #9426 from BitGo/marzooqakather498/wci-1226-sdk-coin-iota-mpcv2-signed-hot-recovery
feat(sdk-coin-iota): add MPCv2 signed hot recovery support
2 parents 40d342b + f883129 commit c758315

3 files changed

Lines changed: 308 additions & 30 deletions

File tree

modules/sdk-coin-iota/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,8 @@
5151
"devDependencies": {
5252
"@bitgo/sdk-api": "^2.3.2",
5353
"@bitgo/sdk-lib-mpc": "^10.17.0",
54-
"@bitgo/sdk-test": "^9.1.68"
54+
"@bitgo/sdk-test": "^9.1.68",
55+
"tweetnacl": "^1.0.3"
5556
},
5657
"files": [
5758
"dist"

modules/sdk-coin-iota/src/iota.ts

Lines changed: 66 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
EDDSAMethods,
66
EDDSAMethodTypes,
77
Environments,
8+
getEddsaSigningMaterial,
89
KeyPair,
910
MPCAlgorithm,
1011
MPCConsolidationRecoveryOptions,
@@ -20,6 +21,7 @@ import {
2021
PopulatedIntent,
2122
PrebuildTransactionWithIntentOptions,
2223
RecoveryTxRequest,
24+
signEddsaMpcV2RecoveryTx,
2325
SignedTransaction,
2426
SignTransactionOptions,
2527
TransactionRecipient,
@@ -303,6 +305,9 @@ export class Iota extends BaseCoin {
303305
const bitgoKey = params.bitgoKey.replace(/\s/g, '');
304306
const MPC = await EDDSAMethods.getInitializedMpcInstance();
305307

308+
// Detect MPCv2 keycard format once up front, to avoid decrypting on every scan iteration.
309+
const isMpcV2 = await this.isMpcv2SigningMaterial(params.userKey, params.backupKey, params.walletPassphrase);
310+
306311
for (let idx = startIdx; idx < endIdx; idx++) {
307312
const derivationPath = (params.seed ? getDerivationPath(params.seed) : 'm') + `/${idx}`;
308313
const derivedPublicKey = MPC.deriveUnhardened(bitgoKey, derivationPath).slice(0, 64);
@@ -337,7 +342,8 @@ export class Iota extends BaseCoin {
337342
derivationPath,
338343
derivedPublicKey,
339344
idx,
340-
bitgoKey
345+
bitgoKey,
346+
isMpcV2
341347
);
342348
} catch (e) {
343349
continue;
@@ -398,7 +404,9 @@ export class Iota extends BaseCoin {
398404
params,
399405
derivationPath,
400406
derivedPublicKey,
401-
unsignedTx
407+
unsignedTx,
408+
isMpcV2,
409+
bitgoKey
402410
);
403411

404412
// Build and return signed transaction
@@ -706,7 +714,8 @@ export class Iota extends BaseCoin {
706714
derivationPath: string,
707715
derivedPublicKey: string,
708716
idx: number,
709-
bitgoKey: string
717+
bitgoKey: string,
718+
isMpcV2: boolean
710719
): Promise<MPCTxs | MPCSweepTxs> {
711720
tokenObjectsWithBalance = tokenObjectsWithBalance.sort((a, b) => (BigInt(b.balance) > BigInt(a.balance) ? 1 : -1));
712721
if (tokenObjectsWithBalance.length > MAX_OBJECT_LIMIT) {
@@ -780,7 +789,9 @@ export class Iota extends BaseCoin {
780789
params,
781790
derivationPath,
782791
derivedPublicKey,
783-
unsignedTx
792+
unsignedTx,
793+
isMpcV2,
794+
bitgoKey
784795
);
785796

786797
const finalTx = (await txBuilder.build()) as TransferTransaction;
@@ -800,12 +811,26 @@ export class Iota extends BaseCoin {
800811
};
801812
}
802813

814+
private async isMpcv2SigningMaterial(
815+
userKey?: string,
816+
backupKey?: string,
817+
walletPassphrase?: string
818+
): Promise<boolean> {
819+
if (!walletPassphrase) return false;
820+
if (!userKey) throw new Error('missing userKey');
821+
if (!backupKey) throw new Error('missing backupKey');
822+
const material = await getEddsaSigningMaterial(userKey.replace(/\s/g, ''), walletPassphrase, this.bitgo);
823+
return material.version === 'v2';
824+
}
825+
803826
private async signRecoveryTransaction(
804827
txBuilder: TransactionBuilder,
805828
params: IotaRecoveryOptions,
806829
derivationPath: string,
807830
derivedPublicKey: string,
808-
unsignedTx: TransferTransaction
831+
unsignedTx: TransferTransaction,
832+
isMpcV2: boolean,
833+
bitgoKey: string
809834
): Promise<string> {
810835
if (!params.userKey) {
811836
throw new Error('missing userKey');
@@ -820,30 +845,44 @@ export class Iota extends BaseCoin {
820845
const userKey = params.userKey.replace(/\s/g, '');
821846
const backupKey = params.backupKey.replace(/\s/g, '');
822847

823-
// Decrypt private keys from KeyCard values
824-
let userPrv: string;
825-
try {
826-
userPrv = await this.bitgo.decrypt({ input: userKey, password: params.walletPassphrase });
827-
} catch (e) {
828-
throw new Error(`Error decrypting user keychain: ${(e as Error).message}`);
829-
}
830-
const userSigningMaterial = JSON.parse(userPrv) as EDDSAMethodTypes.UserSigningMaterial;
848+
let signatureBuffer: Buffer;
831849

832-
let backupPrv: string;
833-
try {
834-
backupPrv = await this.bitgo.decrypt({ input: backupKey, password: params.walletPassphrase });
835-
} catch (e) {
836-
throw new Error(`Error decrypting backup keychain: ${(e as Error).message}`);
837-
}
838-
const backupSigningMaterial = JSON.parse(backupPrv) as EDDSAMethodTypes.BackupSigningMaterial;
850+
if (!isMpcV2) {
851+
// Decrypt private keys from KeyCard values
852+
let userPrv: string;
853+
try {
854+
userPrv = await this.bitgo.decrypt({ input: userKey, password: params.walletPassphrase });
855+
} catch (e) {
856+
throw new Error(`Error decrypting user keychain: ${(e as Error).message}`);
857+
}
858+
const userSigningMaterial = JSON.parse(userPrv) as EDDSAMethodTypes.UserSigningMaterial;
839859

840-
// Generate TSS signature
841-
const signatureBuffer = await EDDSAMethods.getTSSSignature(
842-
userSigningMaterial,
843-
backupSigningMaterial,
844-
derivationPath,
845-
unsignedTx
846-
);
860+
let backupPrv: string;
861+
try {
862+
backupPrv = await this.bitgo.decrypt({ input: backupKey, password: params.walletPassphrase });
863+
} catch (e) {
864+
throw new Error(`Error decrypting backup keychain: ${(e as Error).message}`);
865+
}
866+
const backupSigningMaterial = JSON.parse(backupPrv) as EDDSAMethodTypes.BackupSigningMaterial;
867+
868+
// Generate TSS signature
869+
signatureBuffer = await EDDSAMethods.getTSSSignature(
870+
userSigningMaterial,
871+
backupSigningMaterial,
872+
derivationPath,
873+
unsignedTx
874+
);
875+
} else {
876+
signatureBuffer = await signEddsaMpcV2RecoveryTx({
877+
message: unsignedTx.signablePayload,
878+
userKey,
879+
backupKey,
880+
walletPassphrase: params.walletPassphrase,
881+
bitgoKey,
882+
derivationPath,
883+
bitgo: this.bitgo,
884+
});
885+
}
847886

848887
// Build full signature: scheme_flag (1 byte) + signature (64 bytes) + public_key (32 bytes)
849888
const schemeFlag = Buffer.alloc(1, 0x00); // Ed25519 scheme

0 commit comments

Comments
 (0)