Wallets
Proof of Reserves
Overview
You can take steps to provide assurances and transparency to your clients and end users by verifying proof of reserves (PoR). Providing PoR is especially important for cryptocurrency exchanges and trading platforms, as it enables you to prove liquidity and solvency.
BitGo offers multiple REST APIs that you can use to return your balances and other wallet details. In its most basic form, PoR entails sharing your balances and public addresses. Clients and end users can then confirm your balances on their respective blockchains. If you require more in depth PoR, you can partner with third-party auditors. Using a Merkle tree, auditors can anonymize data and confirm ownership of individuals' assets within your wallets.
You can verify PoR for custody and self-custody wallets. However, you can't verify PoR for Go Accounts, due to the off-chain ledger they use.
Prerequisites
Steps
1. List Wallets
Get a list of your wallets, and determine which among these constitute your reserves. BitGo recommends that you use custody wallets for your reserves, as these provide maximum security.
Endpoint: List wallets
export ACCESS_TOKEN="<YOUR_ACCESS_TOKEN>"
curl -X GET \
https://app.bitgo-test.com/api/v2/wallets \
-H 'Content-Type: application/json' \
-H "Authorization: Bearer $ACCESS_TOKEN"
Step Result
Save the id for the wallets that make up your reserves.
{
"wallets": [
{
"id": "62f0418088859b0007ad2c1337e402f0",
"users": [
{
"user": "62ab90e06dfda30007974f0a52a12995",
"permissions": [
"admin",
"view",
"spend"
]
}
],
"coin": "btc",
"label": "Cold BTC Wallet",
"m": 2,
"n": 3,
"keys": [
"62f04084d540700007a1da45e6a4d28e",
"62f040cce56ef50007af49c09cafaf90",
"62f040e05eec400007bd55f532d86fac"
],
"keySignatures": {},
"enterprise": "62c5ae8174ac860007aff138a2d74df7",
"tags": [
"62f0418088859b0007ad2c1337e402f0",
"62c5ae8174ac860007aff138a2d74df7"
],
"disableTransactionNotifications": false,
"freeze": {},
"deleted": false,
"approvalsRequired": 1,
"isCold": true,
"coinSpecific": {},
"admin": {},
"clientFlags": [],
"walletFlags": [],
"allowBackupKeySigning": false,
"startDate": "2022-08-07T22:49:36.000Z",
"type": "cold",
"buildDefaults": {},
"customChangeKeySignatures": {},
"hasLargeNumberOfAddresses": false,
"multisigType": "onchain",
"config": {},
"balance": 1256709,
"confirmedBalance": 1256709,
"spendableBalance": 1256709,
"balanceString": "1256709",
"confirmedBalanceString": "1256709",
"spendableBalanceString": "1256709"
},
{
"id": "62f03dee8d13be0007894b3dbc21be17",
"users": [
{
"user": "62ab90e06dfda30007974f0a52a12995",
"permissions": [
"admin",
"view",
"spend"
]
}
],
"coin": "dot",
"label": "MPC DOT Wallet",
"m": 2,
"n": 3,
"keys": [
"62f03dee8d13be0007894b41f63bc504",
"62f03dee8d13be0007894b43cb0f65d0",
"62f03dee8d13be0007894b3f8024e0e8"
],
"keySignatures": {},
"enterprise": "62c5ae8174ac860007aff138a2d74df7",
"tags": [
"62f03dee8d13be0007894b3dbc21be17",
"62c5ae8174ac860007aff138a2d74df7"
],
"disableTransactionNotifications": false,
"freeze": {},
"deleted": false,
"approvalsRequired": 1,
"isCold": true,
"coinSpecific": {
"pendingChainInitialization": false,
"rootAddress": "5G4oQ1xMu5V4y9Cdp9zPnVR57Nh8vsbYRRTei7MmQU6e1DHv",
"lastNonce": 0,
"lastChainIndex": {
"0": 1,
"1": -1
}
},
"admin": {},
"clientFlags": [],
"walletFlags": [],
"allowBackupKeySigning": false,
"startDate": "2022-08-07T22:34:22.000Z",
"type": "custodial",
"buildDefaults": {},
"customChangeKeySignatures": {},
"hasLargeNumberOfAddresses": false,
"multisigType": "MPC",
"config": {},
"balanceString": "2000000000000",
"confirmedBalanceString": "2000000000000",
"spendableBalanceString": "0"
},
{
"id": "62c75ed042852800082440d8d63a4319",
"users": [
{
"user": "62ab90e06dfda30007974f0a52a12995",
"permissions": [
"admin",
"view",
"spend"
]
}
],
"coin": "btc",
"label": "Hot BTC Wallet",
"m": 2,
"n": 3,
"keys": [
"62c75ecf871f420008f23497a2ea1174",
"62c75ecfd2d04f000858d79f4ee278ae",
"62c75ecf3f7ede0007f9b398aafdf07b"
],
"keySignatures": {
"backupPub": "209faade005392cf930a2325db3dfba92cfa90fb27971c2b842768059f9caf653a1b76d893c3442b83549c82b8c97580948d3208d4b6e81e49c02dbb7648db1d99",
"bitgoPub": "2089747b9eee580371691db42a85a7f3b93fd1b8be30dacdcfe537e5b48291b1462ea34afbbbc26ba11dbb5f2077843915ea812c1a6268b89d9977511563be241e"
},
"enterprise": "62c5ae8174ac860007aff138a2d74df7",
"tags": [
"62c75ed042852800082440d8d63a4319",
"62c5ae8174ac860007aff138a2d74df7"
],
"disableTransactionNotifications": false,
"freeze": {},
"deleted": false,
"approvalsRequired": 1,
"isCold": false,
"coinSpecific": {},
"admin": {},
"clientFlags": [],
"walletFlags": [],
"allowBackupKeySigning": false,
"startDate": "2022-07-07T22:31:44.000Z",
"type": "hot",
"buildDefaults": {},
"customChangeKeySignatures": {},
"hasLargeNumberOfAddresses": false,
"multisigType": "onchain",
"config": {},
"balance": 232687,
"confirmedBalance": 232687,
"spendableBalance": 232687,
"balanceString": "232687",
"confirmedBalanceString": "232687",
"spendableBalanceString": "232687"
},
{
"id": "62c60db8b19ca800088f8ed53eb2e628",
"users": [
{
"user": "62ab90e06dfda30007974f0a52a12995",
"permissions": [
"admin",
"view",
"spend"
]
}
],
"coin": "xrp",
"label": "Cold XRP Wallet",
"m": 2,
"n": 3,
"keys": [
"62c60db8b19ca800088f8edd5ae4d4e7",
"62c60db8b19ca800088f8ee378424d5f",
"62c60db9b19ca800088f8f5f7ec220db"
],
"keySignatures": {},
"enterprise": "62c5ae8174ac860007aff138a2d74df7",
"tags": [
"62c60db8b19ca800088f8ed53eb2e628",
"62c5ae8174ac860007aff138a2d74df7"
],
"disableTransactionNotifications": false,
"freeze": {},
"deleted": false,
"approvalsRequired": 1,
"isCold": true,
"coinSpecific": {
"rootAddress": "rM2ESyPZwycfRwQvNo8MHKi5hAXZgjpj1X",
"pendingChainInitialization": false,
"creationFailure": []
},
"admin": {},
"clientFlags": [],
"walletFlags": [],
"allowBackupKeySigning": false,
"startDate": "2022-07-06T22:33:28.000Z",
"type": "custodial",
"buildDefaults": {},
"customChangeKeySignatures": {},
"hasLargeNumberOfAddresses": false,
"multisigType": "onchain",
"config": {},
"balanceString": "12000000",
"confirmedBalanceString": "12000000",
"spendableBalanceString": "0"
},
{
"id": "62c5b5c9d0ece30007cec9616ff29edc",
"users": [
{
"user": "62ab90e06dfda30007974f0a52a12995",
"permissions": [
"admin",
"view",
"spend"
]
}
],
"coin": "btc",
"label": "My Demo BTC Wallet",
"m": 2,
"n": 3,
"keys": [
"62c5b54a8ec0290007324c0fab5337f6",
"62c5b54a8a0c5200071e49a857e50b0e",
"62c5b54bd0ece30007ce8377f23f264f"
],
"keySignatures": {},
"enterprise": "62c5ae8174ac860007aff138a2d74df7",
"tags": [
"62c5b5c9d0ece30007cec9616ff29edc",
"62c5ae8174ac860007aff138a2d74df7"
],
"disableTransactionNotifications": false,
"freeze": {},
"deleted": false,
"approvalsRequired": 1,
"isCold": false,
"coinSpecific": {},
"admin": {},
"clientFlags": [],
"walletFlags": [],
"allowBackupKeySigning": false,
"startDate": "2022-07-06T16:18:17.000Z",
"type": "hot",
"buildDefaults": {},
"customChangeKeySignatures": {},
"hasLargeNumberOfAddresses": false,
"multisigType": "onchain",
"config": {},
"balance": 1089773,
"confirmedBalance": 1089773,
"spendableBalance": 1089773,
"balanceString": "1089773",
"confirmedBalanceString": "1089773",
"spendableBalanceString": "1089773"
},
]
}
2. List Addresses
Using the wallet ids, get all the addresses for your reserve wallets.
Endpoint: List addresses
export COIN="<ASSET_ID>"
export WALLET_ID="<YOUR_WALLET_ID>"
export ACCESS_TOKEN="<YOUR_ACCESS_TOKEN>"
curl -X GET \
"https://app.bitgo-test.com/api/v2/$COIN/wallet/$WALLET_ID/addresses?includeBalances=true" \
-H 'Content-Type: application/json' \
-H "Authorization: Bearer $ACCESS_TOKEN"
Step Result
Save each address that contains a balance. Anyone can search for these public addresses on their respective blockchains.
{
"coin": "btc",
"totalAddressCount": 3,
"addresses": [
{
"id": "62f0418088859b0007ad2c23dd2a316e",
"address": "2MxNxcyy4Jt4NQGkwR8bVD6pYPDSMhQqkmr",
"chain": 10,
"index": 1,
"coin": "btc",
"wallet": "62f0418088859b0007ad2c1337e402f0",
"coinSpecific": {
"redeemScript": "00201ca0444be0e22067cf8ed6cbc81268ade365e0559844d00eedf3b0af22e9bfc0",
"witnessScript": "5221020292b2ee9c27e3d1425b8516ebd43351b2d163b7dfb839a36b9c73e40afe043021029b387b9a41acbcf0f6c8c0fd72247a0670dcc7bcf0e0c19d466253b6e8b6cbb821023f3e4bec6ff7d805f168e7ce80a813c3f0df26f4b29e1dea3bbeda90291d1e8f53ae"
},
"balance": {
"updated": "2022-08-31T16:07:28.000Z",
"balance": 1256709,
"balanceString": "1256709",
"totalReceived": 1256709,
"totalSent": 0,
"confirmedBalanceString": "1256709",
"spendableBalanceString": "1256709",
"tokens": {}
}
},
{
"id": "630f8734f2d4160007837e4c34a1a9b2",
"address": "2N3oMXAzeU6XBBBFmWJiFDbwtVxRiUprsv6",
"chain": 10,
"index": 2,
"coin": "btc",
"wallet": "62f0418088859b0007ad2c1337e402f0",
"coinSpecific": {
"redeemScript": "0020a9f3c4d917a9094193fe8baaf88844ff42ea96de1a1fee92fdc9e84093794395",
"witnessScript": "5221022c9f415e4c7491fb6f87dad25294f6e083bd9511bf50f94e031e0d387740f7c121035da85278e3d4dc17100b4fb1f591d552d64e74501a7b9927f69e9385f82946fe2103b569f313c45d577f56f38efb163d4d86d32b2281f4d605a2eb4e6102cc118fbc53ae"
},
"balance": {
"balance": 0,
"balanceString": "0",
"totalReceived": 0,
"totalSent": 0,
"confirmedBalanceString": "0",
"spendableBalanceString": "0",
"tokens": {}
}
},
{
"id": "630fc9fe7db3fd0007157e7b0867d82b",
"address": "tb1qkzkvluehktkcrx6y85yjlqyal558j9nk4garzghyfjtkwyw73prsvvzcwf",
"chain": 21,
"index": 1,
"coin": "btc",
"wallet": "62f0418088859b0007ad2c1337e402f0",
"coinSpecific": {
"witnessScript": "52210319a0ad3dda87215d90a195e074f0f053d393cc55dd43c742e2d59bca9c8f495921033ebb11af652e887e17be2b8e029b792318426e136439d0cbb0a86d3645617620210319128393716221430d05a29930e75cabaf9fae741f0c0882030fb26b2e42c2b953ae"
},
"balance": {
"balance": 0,
"balanceString": "0",
"totalReceived": 0,
"totalSent": 0,
"confirmedBalanceString": "0",
"spendableBalanceString": "0",
"tokens": {}
}
}
],
"count": 3
}
Repeat this step for each wallet in your reserve.
3. Confirm Address Ownership
For multisignature wallets ("multisigType": "onchain"), you can verify address ownership by proving your keys belong to the address. There are two different methods for verifying address ownership. Choose the method you prefer.
Re-derive the wallet address from the public keys of the wallet and confirm it matches the address BitGo returned. Because the derivation uses all three wallet keys (user, backup, and BitGo), a successful match proves those keys constructed the address and that you own it.
Send the public keys of the wallet in the keychains array, along with the address and the derivation index from the Step 2 address record. You can get each public key with Get Key, using the key IDs from Step 1. This method doesn't support custodial wallets.
Endpoint: Verify if an address belongs to a wallet
export BITGO_EXPRESS_HOST="<YOUR_LOCAL_HOST>"
export COIN="<ASSET_ID>"
export WALLET_ID="<YOUR_WALLET_ID>"
export ACCESS_TOKEN="<YOUR_ACCESS_TOKEN>"
export ADDRESS="<ADDRESS_TO_VERIFY>"
export INDEX="<ADDRESS_INDEX>"
curl -X POST \
http://$BITGO_EXPRESS_HOST/api/v2/$COIN/wallet/$WALLET_ID/iswalletaddress \
-H 'Content-Type: application/json' \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-d '{
"address": "'"$ADDRESS"'",
"index": '"$INDEX"',
"keychains": [
{ "pub": "<USER_PUBLIC_KEY>" },
{ "pub": "<BACKUP_PUBLIC_KEY>" },
{ "pub": "<BITGO_PUBLIC_KEY>" }
]
}'
const BitGoJS = require('bitgo');
const bitgo = new BitGoJS.BitGo({ env: 'test' });
const coin = 'tbtc4';
// Set your wallet id
const walletId = '';
// Set the address and the derivation index from the Step 2 address record
const address = '';
const index = 0;
// Set your access token here
const accessToken = '';
bitgo.authenticateWithAccessToken({ accessToken });
const baseCoin = bitgo.coin(coin);
const wallet = await baseCoin.wallets().get({ id: walletId });
const keychains = await baseCoin.keychains().getKeysForSigning({ wallet });
const isWalletAddress = await baseCoin.isWalletAddress({ keychains, address, index });
console.dir(isWalletAddress);
Step Result
The endpoint returns true when the public keys of the wallet derive the address, which proves you own it. If it returns false, confirm that the index matches the Step 2 address record and that all three public keys are correct.
true
Note: Forwarder version 0 addresses always return true, because the architecture prevents a cryptographic check.
When you spend from a Bitcoin P2SH or P2WSH address, the blockchain reveals the full multisig script, which contains the public keys that constructed the address.
This method applies to Bitcoin and other UTXO blockchains that use P2SH or P2WSH addresses. It doesn't apply to account-based blockchains, such as ETH, XRP, and DOT, to Bitcoin Taproot (P2TR) addresses, or to any address you haven't spent from. For those addresses, use the Address Re-Derivation method.
Find the spending transaction for the address in a block explorer, then decode the script with bitcoin-cli. For a P2SH address, which starts with 2 or 3, the redeem script is in the scriptSig field of the spending input. For a native SegWit P2WSH address, which starts with bc1q or tb1q, the witness script is the last element in the witness stack of the spending input.
export TXID="<SPENDING_TRANSACTION_ID>"
export SCRIPT_HEX="<REDEEM_OR_WITNESS_SCRIPT_HEX>"
# Get the spending transaction, then read the script from the input that spends your address
bitcoin-cli getrawtransaction $TXID true
# Decode the redeem script for P2SH, or the witness script for P2WSH
bitcoin-cli decodescript $SCRIPT_HEX
Step Result
The decoded script lists the public keys of the multisig. Derive the public key of your extended public key at the chain and index values from the Step 2 address record, then confirm the script contains it. A match proves your key constructed the address, which proves you own it.
{
"asm": "2 022c9f415e4c7491fb6f87dad25294f6e083bd9511bf50f94e031e0d387740f7c1 035da85278e3d4dc17100b4fb1f591d552d64e74501a7b9927f69e9385f82946fe 03b569f313c45d577f56f38efb163d4d86d32b2281f4d605a2eb4e6102cc118fbc 3 OP_CHECKMULTISIG",
...
"type": "multisig"
}
4. Aggregate Balances
Write a script that aggregates the balances for your coin or tokens. Ensure that you're adding the balances of every address in your reserve wallets.
5. Share Balances
Once you aggregate your balances, you can share your reserves, along with the public addresses. Your clients and end users can search for the public addresses on their respective blockchains and confirm their balances.
If you need to confirm individuals' assets within your reserves, you can share your PoR with a third-party auditor.