feat: enhance multisig recovery with split AWM support#251
Conversation
|
@claude review and get_issue the ticket on linear for more context |
|
Claude finished @danielpeng1's task —— View job Code Review for Split AWM Recovery Enhancement
SummaryThis PR implements a significant enhancement to the multisig recovery system by adding split AWM support - enabling two-phase recovery where user keys and backup keys can be managed by separate AWM instances. The implementation is well-architected and addresses both UTXO and EVM recovery flows with proper error handling and comprehensive testing. Key Strengths1. Excellent Architecture Design
2. Robust Error Handling
3. Comprehensive Test Coverage
Technical AnalysisAWM Recovery Handler (
|
| const logPrefix = '[asyncJobWorker:handleMultisigRecoveryOperation]'; | ||
| const { jobId, version } = job; | ||
|
|
||
| const isSplitRecovery = job.awmBackupResponse !== undefined; |
There was a problem hiding this comment.
if we are not implementing split async recovery right now, wouldn't this be always undefined?
There was a problem hiding this comment.
yeah this is just some currently dead forward compatible scaffolding for the async recovery i put in since I already had it; it's probably more confusing than not having it so i'll just remove it.
| awmClient: AdvancedWalletManagerClient, | ||
| recoveryBody: MultisigRecoveryBody, | ||
| ): Promise<SignedTransaction | AsyncJobResponse> { | ||
| const userClient = req.awmUserClient; |
There was a problem hiding this comment.
we probably should add a validation during setup that awmBackupUrl is mutually exclusive with aysncMode
There was a problem hiding this comment.
Ah that's true I didn't add this here, let me do this in a followup.
| // (halfSigned.txHex nested), so reject backup signing with such a payload as misconfiguration | ||
| // rather than silently passing undefined into the key provider. | ||
| if (keyToSign === 'backup' && baseCoin.isEVM()) { | ||
| if ( |
There was a problem hiding this comment.
can we not group these ifs together?
| @@ -76,13 +108,54 @@ export async function recoveryMultisigTransaction( | |||
| DEFAULT_MUSIG_ETH_GAS_PARAMS; | |||
There was a problem hiding this comment.
I feel like this can be refactored now that I look at it:
if (!isEthLikeCoin(baseCoin)) { ... throw Error}
| const { halfSigned } = halfSignedTx; | ||
| // Cast to BaseCoin for the loose SignTransactionOptions signature; the | ||
| // AbstractEthLikeNewCoins overload's stricter txPrebuild types don't fit recovery. | ||
| return await (baseCoin as BaseCoin).signTransaction({ |
There was a problem hiding this comment.
there is some duplicate code with the non-split flow. The non-split flow needs to produce the half signed transaction then sign that half-tx. The code in here with just signing the half-tx can be refactored with the second half of the non-split flow.
I would create a helper funciton to create a half signed tx and another to create a full tx, and have the handler call them.
| @@ -164,34 +242,66 @@ export async function recoveryMultisigTransaction( | |||
| throw new Error(errorMsg); | |||
| } | |||
| } else if (isUtxoCoin(baseCoin)) { | |||
There was a problem hiding this comment.
these should be separate helper functions. 100 lines of if blocks are just hard to read.
There was a problem hiding this comment.
done, evm and utxo logic now live in their own top-lvl functions
| // Wraps a key-provider signing failure so the response handler preserves upstream status/message. | ||
| function signingError(error: unknown, keySource: KeySource): BitgoApiResponseError { | ||
| const status = | ||
| error && |
There was a problem hiding this comment.
there should be a ApiError class
There was a problem hiding this comment.
done; using BitgoApiResponseError now, and fixed a bug with the constructor not setting this.name (so responseHandler.ts's error.name === 'ApiResponseError' check never matched)
| // The backup path reads halfSigned.txHex and optionally expireTime/backupKeyNonce/recipients, | ||
| // so require halfSigned to be a non-null object with a string txHex. | ||
| return ( | ||
| typeof halfSigned === 'object' && |
There was a problem hiding this comment.
nit: these probably can be codecs.
pranavjain97
left a comment
There was a problem hiding this comment.
lgtm mostly, some type issues
| txHex: t.string, | ||
| }), // the full signed tx | ||
| 200: t.any, | ||
| ...ErrorResponses, |
There was a problem hiding this comment.
200: t.any removes the only runtime type check on the recovery response shape. a caller getting back a malformed body won't be caught at the boundary.
There was a problem hiding this comment.
i've tightened this now
| logger.info('Recovering multisig (user half-sign) for coin: %s', this.coin); | ||
| const res = await request.decodeExpecting(200); | ||
|
|
||
| return res.body as SignedTransaction; |
There was a problem hiding this comment.
res.body as SignedTransaction is unchecked since the response codec is t.any. if the AWM returns a wrong shape it silently passes through.
| const backupClient = req.awmBackupClient; | ||
|
|
||
| // Split AWM stays sync: the OSO bridge can't sequence a user half-sign then backup full-sign. | ||
| if (userClient !== backupClient) { |
There was a problem hiding this comment.
wouldn't a dedicated hasSeparateBackupAwm boolean be clearer here? reference equality breaks silently if clients are ever re-constructed.
0371887 to
6f14fe1
Compare
|
@alextse-bg @pranavjain97 I've implemented the requested changes, and added some tests for There's a couple things that I'd like to do in a followup, like this, recovery splitAwm integ test, as well as some fixes regarding error handling, but this PR is already getting pretty huge so these can come next |
|
Claude finished @pranavjain97's task —— View job Code Review for Split AWM Recovery Enhancement
SummaryThis PR successfully implements split AWM (Advanced Wallet Manager) recovery support, enabling two-phase recovery operations where user and backup keys can be managed by separate AWM instances. The implementation demonstrates excellent engineering practices with comprehensive error handling, robust validation, and thorough test coverage. Key Strengths1. Excellent Architecture & Design
2. Outstanding Error HandlingThe error handling improvements are particularly noteworthy:
3. Comprehensive Test CoverageThe test suite is exceptionally thorough:
4. Smart Implementation PatternsOptimized key retrieval ( const userPrv = keyToSign === 'backup' ? undefined : await retrieveKeyProviderPrvKey(...);
const backupPrv = keyToSign === 'user' ? undefined : await retrieveKeyProviderPrvKey(...);Clean two-phase orchestration ( const hasSeparateBackupAwm = userClient !== backupClient;
if (hasSeparateBackupAwm) {
const halfSignedTx = await userClient.recoveryMultisigUserHalfSign(recoveryBody);
return backupClient.recoveryMultisig({
...recoveryBody,
keyToSign: 'backup',
halfSignedTransaction: halfSignedTx,
});
}Review of Addressed FeedbackThe author has successfully addressed most of the previous review feedback: ✅ Refactored large conditional blocks: The massive if/else logic has been properly extracted into separate functions ( ✅ Improved error handling: Now uses ✅ Tightened API response types: The response codec is now properly typed instead of ✅ Removed forward-compatible scaffolding: Dead code for async job handling has been cleaned up Technical AnalysisAWM Recovery Handler (
|
pranavjain97
left a comment
There was a problem hiding this comment.
some of the claude suggestions can be done in followup
Adds two-phase split-AWM recovery (user half-sign -> backup full-sign) for
recoveryandrecoveryConsolidations, for UTXO + EVM, local + external signing.multisigRecovery.ts: implementskeyToSign(user/backup) with runtime guards on the half-signed tx shape; external signing failures now throwBitgoApiResponseError(preserves upstream status) and reject EVM backup with a richhalfSignedobject (requires flattxHex).submitMultisigRecoveryJobgains asourcesparam; worker prefersawmBackupResponsewhen present, this is forward-compatible scaffolding.Tests
keyToSignisolation, half-signed-tx guards (400s).asyncEnabled(bridge not called).awmBackupResponsepreferred; malformed rejected. Utils:sourcesparam.testUtils:X-OSO-Sourceheader nock.Ticket: WCN-1082