Skip to content

feat(signing-bitgo): add BitGo TSS MPC signing driver#2163

Open
ravibitgo wants to merge 1 commit into
canton-network:mainfrom
ravibitgo:signing-bitgo
Open

feat(signing-bitgo): add BitGo TSS MPC signing driver#2163
ravibitgo wants to merge 1 commit into
canton-network:mainfrom
ravibitgo:signing-bitgo

Conversation

@ravibitgo

Copy link
Copy Markdown

Summary

Adds a new signing driver (@canton-network/core-signing-bitgo) that integrates BitGo's TSS MPC custodial wallets as a Canton signing provider.

  • Key model: A BitGo custodial wallet maps 1:1 to a Canton key. walletId is used as the stable key identifier (publicKey) throughout — wallet creation is the createKey flow, and the walletId is passed as keyIdentifier.id on every signing request.
  • Async signing: signTransaction submits a message signing request (POST /msgrequests) and returns immediately with status: pending. The gateway polls getTransaction until messages[0].state === 'signed', at which point the Ed25519 signature and Canton signer fingerprint are extracted from the signed txHash (a hex-encoded JSON blob).
  • Restart resilience: An in-memory txRequestId → walletId map is populated at sign time for fast lookups. On process restart, the enterprise txrequests endpoint (GET /api/v2/enterprise/{id}/txrequests) serves as a fallback — no Redis required, consistent with other drivers.

Changes

  • core/signing-bitgo/ — new package: BitGoHandler (API client) + BitGoSigningDriver (implements SigningDriverInterface), 61 tests, test scripts, README
  • core/signing-lib/src/config/schema.ts — adds BITGO = 'bitgo' to SigningProvider enum
  • wallet-gateway/remote/src/env.ts + init.ts — wires up BitGo env vars (BITGO_ACCESS_TOKEN, BITGO_API_URL, BITGO_ENTERPRISE_ID, BITGO_COIN) and registers the driver

Design notes for reviewers

  • publicKey === walletId is intentional. Unlike Fireblocks (which returns a real Ed25519 public key), BitGo's MPC model doesn't expose a single public key upfront. The walletId is the stable identifier used to route signing requests.
  • apiVersion=full and latest=true are both required on txrequest fetches. Without full, messages[0].state is absent; without latest, you get a stale version of the txRequest.
  • Message-level state takes precedence: Canton signing is complete when messages[0].state === 'signed', even if the txRequest itself is still in pendingDelivery. This was discovered empirically during integration testing.

Known limitations

  • getTransactions with publicKeys fetches only the first page of txrequests per wallet (no cursor/pagination yet). Fine for current usage; can be followed up.

Test plan

  • yarn test in core/signing-bitgo — 61 tests pass
  • yarn build in core/signing-bitgo
  • Manual connectivity: BITGO_ACCESS_TOKEN= BITGO_ENTERPRISE_ID= npx tsx scripts/test-connectivity.ts
  • Manual sign round-trip: npx tsx scripts/test-sign.ts (submit mode, then check mode)

@ravibitgo
ravibitgo requested a review from a team as a code owner July 17, 2026 10:21
@ravibitgo
ravibitgo force-pushed the signing-bitgo branch 2 times, most recently from 57bc6a9 to c990ec3 Compare July 17, 2026 10:55
Copilot AI review requested due to automatic review settings July 21, 2026 03:40

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new BitGo-backed signing provider to the Wallet Gateway, enabling asynchronous Canton transaction signing via BitGo TSS MPC custodial wallets, and wires it into the existing signing-provider framework.

Changes:

  • Introduces @canton-network/core-signing-bitgo (BitGo API handler + SigningDriverInterface implementation) with tests, scripts, and README.
  • Extends signing-provider configuration to include SigningProvider.BITGO.
  • Wires BitGo env vars and driver registration into wallet-gateway/remote.

Reviewed changes

Copilot reviewed 15 out of 16 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
yarn.lock Adds BitGo SDK (and related) dependencies and bumps various packages.
wallet-gateway/remote/src/init.ts Registers BitGo signing driver when BITGO_ACCESS_TOKEN is set.
wallet-gateway/remote/src/env.ts Adds BitGo-related environment variable accessors.
wallet-gateway/remote/package.json Adds workspace dependency on @canton-network/core-signing-bitgo.
core/signing-lib/src/config/schema.ts Adds BITGO = 'bitgo' to SigningProvider enum.
core/signing-bitgo/vitest.config.ts Adds Vitest config + coverage thresholds for the new package.
core/signing-bitgo/tsup.config.ts Adds tsup build config for the new package.
core/signing-bitgo/tsconfig.json Adds TypeScript config for the new package.
core/signing-bitgo/src/index.ts Implements BitGoSigningDriver (controller methods, config get/set).
core/signing-bitgo/src/index.test.ts Adds unit tests for BitGoSigningDriver.
core/signing-bitgo/src/bitgo.ts Implements BitGoHandler (BitGo REST calls, tx request formatting).
core/signing-bitgo/src/bitgo.test.ts Adds unit tests for BitGoHandler including state/signature extraction.
core/signing-bitgo/scripts/test-sign.ts Adds a manual submit/check script for signing requests.
core/signing-bitgo/scripts/test-connectivity.ts Adds a manual connectivity script (getKeys/createKey).
core/signing-bitgo/README.md Documents usage, configuration, and operational behavior.
core/signing-bitgo/package.json Defines the new package’s build/test setup and dependencies.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread core/signing-bitgo/src/bitgo.ts
Comment thread core/signing-bitgo/src/bitgo.ts Outdated
Comment thread core/signing-bitgo/src/index.ts
Copilot AI review requested due to automatic review settings July 22, 2026 05:43

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 15 out of 16 changed files in this pull request and generated 3 comments.

Comment thread core/signing-bitgo/src/bitgo.ts Outdated
Comment thread core/signing-bitgo/src/bitgo.ts Outdated
Comment thread core/signing-bitgo/README.md Outdated
Copilot AI review requested due to automatic review settings July 22, 2026 05:48

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 15 out of 16 changed files in this pull request and generated 3 comments.

Comments suppressed due to low confidence (1)

core/signing-bitgo/README.md:26

  • The README’s coin auto-detection description doesn’t match the implementation (bitgo.ts defaults to tcanton only when baseUrl includes "bitgo-test.com", otherwise canton). This can mislead users configuring a proxy/non-test URL.
| `coin`         | No       | Canton coin identifier. Auto-detected: `canton` for `*.bitgo.com`, `tcanton` otherwise.                                               |

Comment thread core/signing-bitgo/src/index.ts Outdated
Comment thread core/signing-bitgo/src/bitgo.ts
Comment thread wallet-gateway/remote/src/init.ts
Copilot AI review requested due to automatic review settings July 22, 2026 05:55

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 15 out of 16 changed files in this pull request and generated 1 comment.

Comments suppressed due to low confidence (1)

core/signing-bitgo/src/bitgo.ts:273

  • formatTxRequest can return status='signed' without a signature when txReq.state maps to signed (e.g. delivered/signed) but messages[0].txHash is absent/undefined. This is inconsistent with the driver’s own intent to avoid "signed-without-signature" and can cause the gateway to stop polling without a usable signature. Consider always attempting signature extraction whenever the mapped status is 'signed', and downgrade to 'pending' if extraction fails.
        const mappedStatus: SigningStatus =
            messageState === 'signed'
                ? 'signed'
                : (BITGO_STATE_TO_CANTON[txReq.state] ?? 'pending')
        // Only extract when signed status comes from message-level state (txRequest still in

Comment thread core/signing-bitgo/tsup.config.ts Outdated
Copilot AI review requested due to automatic review settings July 22, 2026 06:11

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 15 out of 16 changed files in this pull request and generated 1 comment.

Comments suppressed due to low confidence (1)

core/signing-bitgo/src/bitgo.ts:275

  • formatTxRequest can return status 'signed' without a signature when txReq.state maps to 'signed' (e.g. delivered/signed) but messages[0].txHash is missing/empty. The current fallback-to-pending guard only triggers when messageState === 'signed', so callers may observe a signed tx with no signature.
        // Prefer message-level state: for Canton message signing, the crypto is complete once
        // messages[0].state === 'signed', even if the txRequest is still in 'pendingDelivery'.
        const messageState = txReq.messages?.[0]?.state
        const mappedStatus: SigningStatus =
            messageState === 'signed'

Comment thread core/signing-bitgo/src/bitgo.ts Outdated
Copilot AI review requested due to automatic review settings July 22, 2026 06:20

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 15 out of 16 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (2)

core/signing-bitgo/src/bitgo.ts:284

  • formatTxRequest can return status: 'signed' without a signature when BitGo reports a terminal txRequest state (delivered/signed) but messages[0].txHash is missing or cannot be parsed. This contradicts the nearby intent comment (“prevents signed-without-signature from reaching the gateway”) and will cause runtime errors in the gateway (it treats signed-without-signature as fatal).
        // Only extract when signed status comes from message-level state (txRequest still in
        // pendingDelivery). When the txRequest itself is terminal (delivered/signed), trust it.
        const signedFromMessage =
            messageState === 'signed' && mappedStatus === 'signed'
        const signedData = signedFromMessage

wallet-gateway/remote/src/init.ts:334

  • The BitGo driver is registered here, but the gateway still rejects SigningProvider.BITGO in core flows: TransactionService.sign() has a provider switch that does not include BITGO (wallet-gateway/remote/src/ledger/transaction-service.ts:70-107), and WalletAllocationService similarly lacks BITGO (wallet-gateway/remote/src/ledger/wallet-allocation/wallet-allocation-service.ts:108-173). As a result, wallets configured with signingProviderId: 'bitgo' will throw "Unsupported signing provider" despite the driver being available.
    if (Env.BITGO_ACCESS_TOKEN()) {
        drivers[SigningProvider.BITGO] = new BitGoSigningProvider({
            accessToken: Env.BITGO_ACCESS_TOKEN()!,
            baseUrl: Env.BITGO_API_URL('https://app.bitgo.com'),
            enterpriseId: Env.BITGO_ENTERPRISE_ID(),

Copilot AI review requested due to automatic review settings July 23, 2026 05:14

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 15 out of 16 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (2)

core/signing-bitgo/src/bitgo.ts:286

  • formatTxRequest may return status: 'signed' without a signature when the txRequest is terminal (state: delivered/signed) but messages[0].state/txHash is missing (or not parseable). Since BitGo’s signature is carried in messages[0].txHash (no dedicated signature field), this can surface a signed transaction that the gateway can’t execute. Consider extracting signed data whenever mappedStatus === 'signed', and falling back to pending if extraction fails (same protection you already have for message-level signed).
        const signedFromMessage =
            messageState === 'signed' && mappedStatus === 'signed'
        const signedData = signedFromMessage
            ? this.extractSignedData(txReq.messages?.[0]?.txHash)
            : {}

wallet-gateway/remote/src/init.ts:334

  • The driver is registered here, but core gateway flows still treat SigningProvider.BITGO as unsupported. For example, wallet-gateway/remote/src/ledger/transaction-service.ts throws in the default branch of sign()/execute() when the provider isn’t one of the existing cases (BITGO isn’t included), and wallet-gateway/remote/src/ledger/wallet-allocation/wallet-allocation-service.ts similarly rejects unknown providers in createWallet()/allocateParty(). As-is, selecting bitgo will fail even though the driver is instantiated.
    if (Env.BITGO_ACCESS_TOKEN()) {
        drivers[SigningProvider.BITGO] = new BitGoSigningProvider({
            accessToken: Env.BITGO_ACCESS_TOKEN()!,
            baseUrl: Env.BITGO_API_URL('https://app.bitgo.com'),
            enterpriseId: Env.BITGO_ENTERPRISE_ID(),

Signed-off-by: Ravi Hegde <ravihegde348@bitgo.com>
Copilot AI review requested due to automatic review settings July 24, 2026 03:13

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 15 out of 16 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (2)

wallet-gateway/remote/src/init.ts:336

  • BitGo driver is registered here, but Wallet Gateway logic still treats SigningProvider.BITGO as unsupported in key flows. For example, TransactionService.sign() throws on default for unknown providers (wallet-gateway/remote/src/ledger/transaction-service.ts:70-107) and WalletAllocationService.createWallet()/allocateParty() also throw for unsupported providers (wallet-gateway/remote/src/ledger/wallet-allocation/wallet-allocation-service.ts:108-173, 181-236). As a result, setting a wallet’s signingProviderId to "bitgo" will fail even though the driver is configured.
    if (Env.BITGO_ACCESS_TOKEN()) {
        drivers[SigningProvider.BITGO] = new BitGoSigningProvider({
            accessToken: Env.BITGO_ACCESS_TOKEN()!,
            baseUrl: Env.BITGO_API_URL('https://app.bitgo.com'),
            enterpriseId: Env.BITGO_ENTERPRISE_ID(),
            coin: Env.BITGO_COIN(),
        })

core/signing-bitgo/src/bitgo.ts:280

  • formatTxRequest can return status="signed" without a signature when the txRequest state maps to signed (e.g. delivered/signed) but messages[0].state/txHash is missing. The current signed-without-signature safeguard only applies when messageState === 'signed', not when signed status comes from txReq.state mapping.
        const mappedStatus: SigningStatus =
            messageState === 'signed'
                ? 'signed'
                : (BITGO_STATE_TO_CANTON[txReq.state] ?? 'pending')
        // Only extract when signed status comes from message-level state (txRequest still in

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants