Skip to content

Commit 8fddf8a

Browse files
committed
feat(sdk-core): add externalSigner createOfflineRound3Share handler
Add the EdDSA MPCv2 offline round-3 handler for external signer flows. It restores the encrypted round-2 DSG session and emits the final user signature share without persisting further signing state. - Restore DSG state from the encrypted round-2 session payload. - Verify BitGo round-2 output from txRequest signature shares before advancing DSG. - Generate the final EdDSA MPCv2 round-3 signature share for BitGo. - Cover happy path, v2 envelopes, adata tampering, missing BitGo share, and transaction guard cases in unit tests. Ticket: WCI-478
1 parent c19a442 commit 8fddf8a

2 files changed

Lines changed: 447 additions & 2 deletions

File tree

modules/sdk-core/src/bitgo/utils/tss/eddsa/eddsaMPCv2.ts

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -707,6 +707,100 @@ export class EddsaMPCv2Utils extends BaseEddsaUtils {
707707
}
708708
// #endregion
709709

710+
// #region Round3Share
711+
async createOfflineRound3Share(params: {
712+
txRequest: TxRequest;
713+
walletPassphrase: string;
714+
bitgoPublicGpgKey: string;
715+
encryptedUserGpgPrvKey: string;
716+
encryptedRound2Session: string;
717+
}): Promise<{
718+
signatureShareRound3: SignatureShareRecord;
719+
}> {
720+
const { walletPassphrase, encryptedUserGpgPrvKey, encryptedRound2Session, bitgoPublicGpgKey, txRequest } = params;
721+
722+
const { signableHex, derivationPath } = this.getSignableHexAndDerivationPath(
723+
txRequest,
724+
'Unable to find transactions in txRequest'
725+
);
726+
const adata = `${signableHex}:${derivationPath}`;
727+
728+
const useV2 = isV2Envelope(encryptedRound2Session);
729+
730+
const { bitgoGpgKey, userGpgPrvKey } = await this.getBitgoAndUserGpgKeys(
731+
bitgoPublicGpgKey,
732+
encryptedUserGpgPrvKey,
733+
walletPassphrase,
734+
adata,
735+
EddsaMPCv2Utils.MPS_DSG_SIGNING_USER_GPG_KEY
736+
);
737+
738+
const transactions = txRequest.transactions;
739+
assert(Array.isArray(transactions) && transactions.length === 1, 'txRequest must have exactly one transaction');
740+
const signatureShares = transactions[0].signatureShares;
741+
assert(signatureShares, 'Missing signature shares in round 2 txRequest');
742+
743+
const bitgoShareRoundTwo = [...signatureShares].reverse().find((share) => {
744+
if (share.from !== SignatureShareType.BITGO || share.to !== SignatureShareType.USER) {
745+
return false;
746+
}
747+
748+
try {
749+
return JSON.parse(share.share).type === 'round2Output';
750+
} catch {
751+
return false;
752+
}
753+
});
754+
assert(bitgoShareRoundTwo, 'Missing BitGo round 2 signature share');
755+
756+
const parsedBitGoToUserSigShareRoundTwo = decodeWithCodec(
757+
EddsaMPCv2SignatureShareRound2Output,
758+
JSON.parse(bitgoShareRoundTwo.share),
759+
'Unexpected signature share response. Unable to parse data.'
760+
);
761+
762+
if (parsedBitGoToUserSigShareRoundTwo.type !== 'round2Output') {
763+
throw new Error('Unexpected signature share response. Unable to parse data.');
764+
}
765+
766+
const bitgoDeserializedMsg2 = await verifyPeerMessageRoundTwo(parsedBitGoToUserSigShareRoundTwo, bitgoGpgKey);
767+
768+
this.validateAdata(adata, encryptedRound2Session, EddsaMPCv2Utils.MPS_DSG_SIGNING_ROUND2_STATE);
769+
770+
let decryptedRound2Session: string;
771+
if (useV2) {
772+
decryptedRound2Session = await this.bitgo.decryptAsync({
773+
input: encryptedRound2Session,
774+
password: walletPassphrase,
775+
});
776+
} else {
777+
decryptedRound2Session = this.bitgo.decrypt({
778+
input: encryptedRound2Session,
779+
password: walletPassphrase,
780+
});
781+
}
782+
783+
const { dsgSession, userMsgPayload } = JSON.parse(decryptedRound2Session) as {
784+
dsgSession: string;
785+
userMsgPayload: string;
786+
};
787+
788+
const userDsg = new EddsaMPSDsg.DSG(MPCv2PartiesEnum.USER);
789+
userDsg.restoreSession(dsgSession);
790+
const userMsg2: MPSTypes.DeserializedMessage = {
791+
from: MPCv2PartiesEnum.USER,
792+
payload: new Uint8Array(Buffer.from(userMsgPayload, 'base64')),
793+
};
794+
795+
const [userMsg3] = userDsg.handleIncomingMessages([userMsg2, bitgoDeserializedMsg2]);
796+
assert(userMsg3, 'DSG handleIncomingMessages produced no round-3 output');
797+
798+
const signatureShareRound3 = await getSignatureShareRoundThree(userMsg3, userGpgPrvKey);
799+
800+
return { signatureShareRound3 };
801+
}
802+
// #endregion
803+
710804
/** @inheritdoc */
711805
async signEddsaMPCv2TssUsingExternalSigner(
712806
params: TSSParams | TSSParamsForMessage,

0 commit comments

Comments
 (0)