Overview
This is a high level overview and design doc for hashi the Sui native Bitcoin
orchestrator. This is intended to be a living document that should be updated
as new decisions and features are made with the goal of this being a canonical
description for how hashi is designed and operates.
At a high level hashi is a protocol for securing and managing BTC for use on
the Sui blockchain leveraging threshold cryptography.
The first feature that hashi supports is the ability to deposit and withdrawal
BTC to a managed pool with ownership represented as a fungible Coin<BTC> on
Sui.
User Flows
There are two main user flows for interacting with hashi, deposits and
withdrawals.
Deposit Flow
In order for a user to leverage their BTC on Sui (e.g. as collateral for a
loan), they’ll need to deposit the BTC they want to leverage to a hashi mpc
controlled bitcoin address.
---
title: Deposit Flow
config:
sequence:
diagramMarginX: 0
---
sequenceDiagram
autonumber
participant User
participant Hashi as Sui / Hashi
participant Bitcoin
User ->> Bitcoin: Deposit native BTC
User ->> Hashi: Notify hashi of deposit
Hashi ->> Bitcoin: Query for deposit
Hashi ->> Hashi: Quorum agreement on deposit
Hashi ->> User: hBTC sent to User
BTC deposit address
Every Sui address has its own unique Hashi Bitcoin deposit address. See address scheme for the full derivation details.
Deposit
Once a user’s deposit address has been determined, they can initiate a deposit to hashi.
- Broadcast a Bitcoin transaction depositing
BTCinto the user’s unique deposit address. - Notify hashi of the deposit by submitting a transaction to Sui including the deposit transaction id.
- Hashi nodes will query Bitcoin and watch for confirmation of the deposit transaction.
- Hashi nodes communicate, waiting till a quorum has confirmed the deposit (after X block confirmations).
- Hashi confirms the deposit on chain, minting the equivalent amount of
hBTCand transferring it to the user’s Sui address. The user can then immediately use thehBTCto interact with a defi protocol to, for example, leverage thehBTCas collateral for a loan inUSDC.
For a detailed breakdown of each phase, see deposit.
Withdraw Flow
Once a user has decided they want their BTC back on Bitcoin (e.g. they’ve paid
off their loan) they can initiate a withdrawal.
---
title: Withdraw Flow
config:
sequence:
diagramMarginX: 0
---
sequenceDiagram
autonumber
participant User
participant Hashi as Sui / Hashi
participant Bitcoin
User ->> Hashi: Request withdrawal
Hashi ->> Hashi: Craft and sign Bitcoin transaction using MPC
Hashi ->> Bitcoin: Broadcast transaction
Withdraw
- User sends a transaction to Sui with the amount of
hBTCthey would like to withdraw and the Bitcoin address they want to withdraw to. - Hashi will pick up the withdrawal request and will craft a bitcoin
transaction that sends the requested
BTC(minus fees) to the provided Bitcoin address and uses MPC to sign the transaction. - The transaction is broadcast to the Bitcoin network.
For a detailed breakdown of each phase, see withdraw.
Committee
Hashi is intended to be “native”, meaning the expectation is that the members of the hashi committee are a subset of the Sui validators. Being a member of the hashi committee is restricted to members of Sui’s validator set but is essentially optional as it requires a separate on-chain registration and running extra services. In practice we expect the % of Sui validators who are members of the hashi committee to be >90%.
Registration Info
Each Sui validator will need to register themselves before they’ll be able to join the hashi committee. Each committee member will need to provide the following additional information:
#![allow(unused)]
fn main() {
struct HashiNodeInfo {
/// Sui Validator Address of this node
validator_address: address,
/// Sui Address of an operations account
operator_address: address,
/// bls12381 public key to be used in the next epoch.
///
/// This public key can be rotated but will only take effect at the
/// beginning of the next epoch.
next_epoch_public_key: Element<UncompressedG1>,
/// The publicly reachable URL where the `hashi` service for this validator
/// can be reached.
///
/// This URL can be rotated and any such updates will take effect
/// immediately.
endpoint_url: String,
/// ed25519 public key used to verify TLS self-signed x509 certs
///
/// This public key can be rotated and any such updates will take effect
/// immediately.
tls_public_key: vector<u8>,
}
}
The voting weight each validator possesses will be mirrored from the
SuiSystemState.
Why is the committee not exactly the set of Sui Validators?
Above it’s mentioned that the hashi committee is a subset of the Sui Validators instead of being strictly the same set. There are a few challenges with forcing these sets to be identical:
- Being a member of the committee is strictly optional since hashi’s system
state is separate from sui’s system state. When someone registers to become a
Sui Validator the set of metadata (public keys, network addresses, etc) they
are required to submit only includes information necessary for running the
sui-nodevalidator service. Without changes, there is no way of preventing a new validator from becoming a validator without also registering to join the hashi committee. - If we enforce tight coupling we’d likely need to change sui’s epoch
change/reconfiguration process in a few ways:
- Given the mpc hand-off protocol takes non-trivial amount of time to execute, the new set of validators would need to be locked-in some time period before the closing of the epoch to give the mpc committee time to reconfigure and
- We’d need to block Sui’s epoch change and reconfiguration on successful reconfiguration of the mpc committee.
Addressing any of the above would require deep changes to sui’s reconfiguration process some of which would be directly opposed by the core team and regardless would take a significant amount of time itself to implement correctly.
The one downside of not having tight coupling is needing to handle the hand-off from an old committee to a newer committee as it would require that 2f+1 stake-weighted members of the old committee are alive and willing to participate in the hand-off protocol. This design makes this assumption given the challenges we’d need to overcome to enforce tight coupling and we can likely find some other economic mechanism for motivating older committee members in participating in the hand-off process.
Governance Actions
Governance actions are all defined by their own unique Proposal<T> type.
Proposals are used to adjust protocol parameters, pause or unpause operations,
or perform sensitive operations like package upgrades. Only members of the
current hashi committee are able to create proposals. Each proposal type will
have its own threshold which will need to be reached by a quorum of validators
voting in support of the proposal.
The following is the current set of available proposal types:
Upgrade
Authorizes a package upgrade.
EnableVersion
Re-enables a previously disabled package version, allowing it to be used by the protocol again.
DisableVersion
Disables a package version, preventing it from being used. The currently active version cannot be disabled to avoid bricking the protocol.
UpdateConfig
Updates a protocol configuration parameter by key. Supports any config key-value pair (e.g. deposit fee, rate limits).
Handling Sanctioned Addresses
The decision to facilitate a transaction from/to bitcoin must take into account sanctioned addresses.
Checking if an Address is Sanctioned
Each member of the committee may have different risk tolerances or policies for which set of addresses they don’t want to serve. In order to accommodate different validator preferences the hashi node software will have a configurable mechanism for it determining if servicing a particular address should be denied.
In order to enable custom policies, the hashi node software supports
configuration of a transaction screening endpoint defined by the gRPC service
in
screener_service.proto
One benefit of this interface is the ability for the service to be arbitrarily simple, by checking a predefined sanctions list like this one here, or allow for making calls to third-party risk services like TRM labs or chainalysis.
When are sanctions checks applied
Deposits: When a user submits a Deposit request, their request sits
in a queue or waiting room till the validators vote on accepting that deposit
and minting an appropriate amount of hBTC. Sanctions checking will happen at
the time a validator is deciding to vote for accepting a deposit. If a
validator decides that it doesn’t want to service that deposit, it will not
vote for it and will simply ignore that the deposit exists. If a quorum decides
to accept a deposit that a particular validator did not want to accept, per
protocol it will need to recognize (and subsequently make use of in the future)
the deposited BTC.
Withdrawals: When a user submits a Withdraw request, their request sits in a queue or waiting room till the validators picks it up for processing. Before selecting a request for processing the validators will vote on approving the request. One of the required checks as a part of voting for approval is performing sanctions checking. Once a quorum of validators has voted to approve a request, it can be picked up for processing. If a quorum decides to approve a request for processing, per protocol all validators will be required to assist in driving the request to completion.
Tainted UTXOs
While we intend to have a rigorous implementation of sanctions enforcement, it is ultimately best-effort. A quorum of validators may accept a deposit that one validator may have preferred to not accept, or a previous committee accepted a deposit that the current committee may have rejected. In either case, once a UTXO has been accepted into hashi’s pool, the protocol treats it as its own and it must be able to be used during coin selection to process withdraw requests.
Service
Every committee member will be responsible for running a hashi node service.
Each hashi node will expose an http service, secured by TLS leveraging a
self-signed cert (ed25519 public key can be found in the Hashi System State
object) which will serve a gRPC HashiService.
Sui Contracts
- The hashi move package(s) will be published as normal packages. In other words, the hashi packages will not be system packages and will not be a part of sui’s framework.
Stateless
A main goal of this design is to make the hashi service as stateless as possible. Outside of any cryptographic material required for participating in the protocol, any state critical for the functioning of the service must be stored on Sui as a part of the live object set and knowledge of any historical transactions or events previously emitted must not be needed for correct operations of the service.
The set of data structures and state that are kept on chain are as follows:
block-beta
columns 1
block
committee
config
end
pool["UTXO pool"]
block
gov["Governance Requests"]
deposits["Deposit Request Queue"]
withdrawals["Withdrawal Request Queue"]
end
broadcast["Ordered broadcast channel"]
MPC Protocol
Sui validators use several MPC protocols for realizing a threshold schnorr
signer: Distributed Key Generation for generating a key, Key Rotation for
redistributing the key on committee changes, and a distributed signing protocol
for signing transactions. Those protocols are parametrized by two parameters,
f and t, such that hashi can operate as long as <f of the staking power
is not responsive (“liveness”), and is secure as long as <t of the staking
power is colluding. In the first version of hashi we expect t to be in the
range of 33%-50% and f in the range of 20%-33%. Those values may increase in
future versions of hashi. The protocols are based on prior published work,
modified and improved by our crypto team.
In addition to the MPC signer, hashi will also use a second signer implemented with a cloud enclave that enforces policies independently of the MPC protocols, reducing the risk of collusion or supply chain attacks.
Guardian
In order to protect against vulnerabilities as well as against malicious past committees, hashi will make use of a withdrawal guardian, which is a second signatory on the managed Bitcoin deposits. All deposits will only be spendable with a 2-of-2 multisig with the guardian as one party and the hashi MPC committee as the other.
Additional details around the guardian integration will be added before testnet.
Bitcoin address scheme
Every Sui address has its own unique Hashi Bitcoin deposit address, this allows
hashi a lightweight way to know which address on sui to credit for a deposit.
All Hashi deposit addresses are P2TR (Pay-to-Taproot) where the 2-of-2 multisig
script between Hashi and the Guardian is encoded as the sole leaf in the
Taproot tree.
The exact descriptor is:
tr({i}, multi_a(2, {g}, {h}))
where:
His the base Hashi MPC public key and can be found on-chain.h = derive(H, d)– the child public key derived fromHusing derivation pathd(the depositor’s Sui address)gis the guardian’s fixed public keyiis the NUMS (nothing-up-my-sleeve) internal key defined in BIP-341 (50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0) with no known private key, ensuring all spends occur via the script path
The key derivation is not BIP-32. It is a purpose-built unhardened derivation over secp256k1, keyed by the Sui address, giving each depositor a unique Bitcoin address while the master signing key remains shared across the MPC committee.
Note: for
devnetthe deposit address omits the guardian key and uses a single-key script path:tr({i}, pk({h}))
Rate Limiting Withdrawals
In order to protect against vulnerabilities or other exceptional scenarios, hashi will implement a Rate Limiter on out flows via the Guardian.
The limit will be a configurable value denominated in BTC and implemented as
a token bucket rate limiter, that is capacity will be replenished continuously
over a fixed duration.
When a user wishes to withdraw their BTC back to Bitcoin, they initiate a
withdraw request. All withdraw requests are tagged with a timestamp of when
the request was made and placed in a queue to wait for hashi to process the
withdrawal.
In order to process a withdrawal request hashi will select a request from the queue and perform a number of checks, one of which is communicating with the Guardian to ensure there is sufficient capacity for the request. If all checks are satisfied and there is capacity, hashi will work with the Guardian to sign and broadcast a Bitcoin transaction to satisfy the request.
When a withdraw request comes in and it would exceed the rate limit, hashi will wait to process it until sufficient capacity is replenished.
Withdrawals will generally be processed in FIFO order, but this is not a strict requirement and there are some scenarios where they may be processed out of order.
User withdrawal requests can be canceled, by the Sui address that initiated them, anytime prior to hashi selecting a request for processing.
Fees
Hashi charges two kinds of fees: a flat deposit fee paid in SUI, and
a withdrawal fee paid in BTC. Both are governance-configurable. In
addition to the protocol fee, every withdrawal absorbs the Bitcoin
miner fee required to get the transaction confirmed on-chain.
Deposit fee
Deposits pay a flat SUI fee at request time (deposit_fee config key,
initially 0 SUI). The fee must match exactly; it is transferred to
the Hashi balance on Sui. Deposits must also meet the dust minimum
(546 sats) to avoid creating unspendable UTXOs on Bitcoin.
Withdrawal fees
Withdrawal fees have two components that serve different purposes:
-
Protocol fee (
withdrawal_fee_btc, initially546 sats) – a flatBTCamount deducted upfront when the user submits a withdrawal request. This fee is non-refundable and goes to the Hashi treasury. It covers protocol operating costs and deters spam. The floor is the dust relay minimum (546 sats) to prevent misconfiguration. -
Miner fee – the actual Bitcoin transaction fee required for on-chain confirmation. This is not a fixed value; it depends on the current network fee rate and the transaction’s weight. The user pays this fee through a reduction in their withdrawal output amount.
Why the user pays the miner fee
The UTXO pool belongs to the protocol. If the pool absorbed miner fees,
every withdrawal would shrink the pool by more than the withdrawn
amount, effectively socializing costs across all future users. Shifting
the miner fee to the withdrawing user keeps the pool whole: the
invariant input_total = user_output + change holds, so the change
output that returns to the pool is undiminished.
Withdrawal minimum
The protocol enforces a minimum withdrawal amount to guarantee that every request can produce a valid Bitcoin transaction even under worst-case fee conditions:
withdrawal_minimum = withdrawal_fee_btc
+ worst_case_network_fee
+ DUST_RELAY_MIN_VALUE
withdrawal_fee_btcis the protocol fee deducted upfront.worst_case_network_feeis the maximum miner fee the protocol would ever charge (see below).DUST_RELAY_MIN_VALUE(546 sats) ensures the user’s output remains above Bitcoin’s dust threshold after all deductions.
This means a user who withdraws exactly the minimum will, in the worst
case, receive a 546 sats output. In practice, actual miner fees are
usually well below the worst case, so the user receives more.
Fee rate estimation
Hashi obtains the current fee rate from the connected Bitcoin Core
node via estimatesmartfee, targeting confirmation within 3 blocks
(~30 minutes).
The estimated fee rate is then capped at the governance-configured
max_fee_rate (initially 25 sat/vB). This cap serves two purposes:
- It bounds the miner fee the user can be charged, ensuring it stays within the worst-case budget the Move contract computed at request time.
- It prevents a single fee spike from producing unexpectedly expensive withdrawals.
Worst-case network fee
Every withdrawal is required to cover not just its own on-chain
footprint but also a share of UTXO pool maintenance. At minimum, a
withdrawal must pay for the fixed transaction overhead, its own
recipient output, and a change output back to the pool. On top of
that, the protocol requires each withdrawal to budget for up to
input_budget input weights. This headroom allows the coin selector to
consolidate many small UTXOs into fewer large ones during normal
withdrawal traffic – a form of opportunistic UTXO smashing that keeps
the pool healthy without requiring dedicated consolidation
transactions.
The Move contract and the Rust validator both compute this worst-case miner fee using conservative transaction size estimates:
tx_vbytes = TX_FIXED_VB + (input_budget * INPUT_VB) + (OUTPUT_BUDGET * OUTPUT_VB)
network_fee = max_fee_rate * tx_vbytes
The constants assume a taproot script-path 2-of-2 spend (the heaviest input type Hashi uses):
| Constant | Value | Rationale |
|---|---|---|
TX_FIXED_VB | 11 vB | nVersion (4) + nLockTime (4) + varint counts (3) |
INPUT_VB | 100 vB | 2-of-2 taproot script-path input (398 WU / 4) |
OUTPUT_VB | 43 vB | P2TR output (172 WU / 4) |
OUTPUT_BUDGET | 2 | One recipient output + one change output |
input_budget | 10 | Per-request worst case, governance-configurable |
max_fee_rate | 25 sat/vB | Governance-configurable (initially 25) |
With defaults: (11 + 10*100 + 2*43) * 25 = 27,425 sats.
These estimates are intentionally pessimistic. Most transactions use fewer inputs and pay a lower fee rate, so the actual miner fee is usually a fraction of the worst case. The difference stays in the user’s output – users are only charged for the real transaction weight, not the worst-case budget.
Transaction validation fee bounds
When validators verify a proposed withdrawal transaction, they check the fee from two directions:
- Floor: the fee must be at least
1 sat/vB(the minimum relay fee), or the Bitcoin network will not propagate the transaction. - Ceiling: the fee must not exceed 3x the validator’s own fee estimate for the same transaction weight. This prevents a malicious leader from overpaying fees to extract value from users.
- Per-user cap: the per-user share of the miner fee must not exceed the worst-case network fee computed from the on-chain config. This ensures the Move contract’s upfront minimum calculation was sufficient.
Stuck transactions
Hashi does not attempt to replace stuck transactions with higher-fee replacements (RBF). Instead, if a transaction is not confirmed within a reasonable time, fee bumping relies on CPFP (child pays for parent):
- The withdrawal recipient can spend their output with a high-fee child transaction.
- Hashi can spend the change UTXO that returned to the pool, which also bumps the parent.
Configuration
Hashi maintains a set of on-chain configuration parameters stored in the
Config object. These parameters control protocol behavior for deposits,
withdrawals, fee estimation, and system operations.
All configurable parameters can be updated via the UpdateConfig governance
proposal, which requires 2/3 of committee weight (see
governance actions). Each key is validated against
its expected type on update.
Parameters
deposit_fee
| Type | u64 |
| Default | 0 |
| Unit | SUI (MIST) |
Flat fee in SUI charged to the user when submitting a deposit request.
withdrawal_fee_btc
| Type | u64 |
| Default | 546 |
| Unit | satoshis |
| Floor | 546 (dust relay minimum) |
Flat protocol fee in BTC deducted from the user’s withdrawal amount upfront.
The effective value is always at least 546 sats regardless of what is
configured, preventing misconfiguration from producing unspendable outputs.
max_fee_rate
| Type | u64 |
| Default | 25 |
| Unit | sat/vB |
| Floor | 1 (minimum relay fee rate) |
The worst-case fee rate used to compute the withdrawal minimum and to cap the
actual miner fee charged to users. This should reflect the highest sustained
fee environment the protocol expects to operate in without pausing
withdrawals. The effective value is always at least 1 sat/vB.
input_budget
| Type | u64 |
| Default | 10 |
| Floor | 1 |
The worst-case number of UTXO inputs assumed per individual withdrawal request
for fee estimation purposes. This is not a hard cap on the number of inputs in
a Bitcoin transaction – batched transactions that serve multiple requests may
use more inputs than this value. More inputs means a heavier assumed weight and
a higher worst-case miner fee charged to each user. This headroom also allows
the coin selector to consolidate small UTXOs during normal withdrawal traffic.
The effective value is always at least 1.
bitcoin_confirmation_threshold
| Type | u64 |
| Default | 1 (will be set to 6 before mainnet) |
| Unit | blocks |
The number of Bitcoin block confirmations required before a deposit is considered final. Guards against chain reorganizations.
paused
| Type | bool |
| Default | false |
When true, the protocol pauses processing of deposits and withdrawals.
Requests already in the queue remain queued and will resume processing when the
system is unpaused. Reconfiguration and governance actions are not affected.
withdrawal_cancellation_cooldown_ms
| Type | u64 |
| Default | 3600000 (1 hour) |
| Unit | milliseconds |
The minimum time a withdrawal request must remain in the queue before the user is allowed to cancel it. Prevents users from using rapid submit-cancel cycles to interfere with processing.
Read-only / genesis-only parameters
bitcoin_chain_id
| Type | address |
The 32-byte Bitcoin chain identifier as defined by
BIP-122
(the genesis block hash). Set at genesis and not updatable via the
UpdateConfig proposal.
Derived values
Several values are computed from the configurable parameters above rather than stored directly.
deposit_minimum
deposit_minimum = 546 sats
The minimum deposit amount. Fixed at the dust relay minimum to prevent creating unspendable UTXOs.
worst_case_network_fee
worst_case_network_fee = max_fee_rate * (11 + input_budget * 100 + 2 * 43)
The maximum miner fee the contract will accept for a withdrawal transaction,
assuming the worst-case transaction weight. With defaults: 25 * 1097 = 27,425 sats.
withdrawal_minimum
withdrawal_minimum = withdrawal_fee_btc + worst_case_network_fee + 546
The minimum withdrawal amount, ensuring that even under worst-case fee
conditions the user’s output stays above the dust threshold. With defaults:
546 + 27,425 + 546 = 28,517 sats.
Reconfiguration
One of the most important parts of the hashi protocol is reconfiguration. This is because one of the key parts of reconfiguration is the old committee sharing key shares of the MPC key with the new committee.
The hashi service will monitor the Sui epoch change and will immediately kick off hashi reconfig once Sui’s epoch change completes. During hashi’s reconfig, in progress operations (e.g. processing of withdrawals) will be paused and will be resumed and processed by the new committee upon the completion of reconfiguration.
graph LR
A[Start Reconfig] --> B[DKG or Key Rotation] --> C[End Reconfig]
Start Reconfig
graph LR
A[Start Reconfig]:::active --> B[DKG or Key Rotation] --> C[End Reconfig]
classDef active fill:#f9a825,stroke:#f57f17,color:#000
Each hashi node monitors Sui for epoch changes. When a new Sui epoch is
detected and the hashi epoch has not yet advanced to match, the node knows that
a reconfiguration is needed. A committee member submits an on-chain transaction
by calling hashi::reconfig::start_reconfig to signal that reconfiguration
should begin for the target epoch:
entry fun start_reconfig(
self: &mut Hashi,
sui_system: &SuiSystemState,
ctx: &TxContext,
)
This sets a pending epoch change flag in the on-chain state, which pauses normal operations (deposits, withdrawals) until reconfiguration completes. The new committee membership is determined by the set of validators who have registered with hashi for the new epoch, determining stake-weights from the Sui Validator set stake-weights.
DKG or Key Rotation
graph LR
A[Start Reconfig] --> B[DKG or Key Rotation]:::active --> C[End Reconfig]
classDef active fill:#f9a825,stroke:#f57f17,color:#000
The MPC key protocol runs among the new committee members. Which protocol is used depends on whether this is the first hashi epoch or a subsequent one:
- Initial DKG – if there is no existing MPC public key (i.e. this is the genesis epoch), the committee runs the distributed key generation protocol to produce a fresh master key.
- Key Rotation – if an MPC public key already exists, the old committee’s key shares are redistributed to the new committee. The old committee members act as dealers and the new committee members act as receivers.
In both cases, the output is a DkgOutput containing the new committee’s key
shares and the MPC public key. See MPC protocol for
details.
Each committee member then signs a ReconfigCompletionMessage containing
the target epoch and the MPC public key using their BLS12-381 key. Nodes
collect signatures from each other via RPC until a quorum (2/3 of committee
weight) is reached, producing a BLS aggregate signature certificate. This
ensures that a supermajority of the new committee agrees on the key protocol
output before the epoch transition is finalized on-chain.
End Reconfig
graph LR
A[Start Reconfig] --> B[DKG or Key Rotation] --> C[End Reconfig]:::active
classDef active fill:#f9a825,stroke:#f57f17,color:#000
A committee member submits the aggregate signature certificate on-chain by
calling hashi::reconfig::end_reconfig:
entry fun end_reconfig(
self: &mut Hashi,
mpc_public_key: vector<u8>,
signature: vector<u8>,
signers_bitmap: vector<u8>,
ctx: &TxContext,
)
The on-chain contract verifies the certificate, commits the generated MPC public key if DKG was run or verifies that the key remains unchanged from the previous epoch, advances the hashi epoch, and clears the pending epoch change flag.
Once the new epoch begins the new committee then initializes the signing state for the epoch by running the presigning protocol to generate a batch of presignatures needed for the threshold Schnorr signing protocol (see MPC protocol). Once presignatures are ready, normal operations resume for processing deposits and withdrawals.
Deposit
A deposit moves BTC from a user’s Bitcoin wallet into the Hashi-managed UTXO
pool, minting a corresponding amount of hBTC into the user’s account on Sui.
The process has three phases:
graph LR
A[Request] --> B[Confirm] --> C[Mint]
Request
graph LR
A[Request]:::active --> B[Confirm] --> C[Mint]
classDef active fill:#f9a825,stroke:#f57f17,color:#000
The user creates a Bitcoin transaction that sends BTC to a Hashi deposit
address. Each deposit address is a unique Taproot address derived from the
target destination address on Sui (see address scheme).
The deposit must meet the dust minimum (546 sats) to avoid creating
unspendable UTXOs on Bitcoin.
Once the Bitcoin transaction is broadcast, the user notifies Hashi by
constructing a DepositRequest and calling hashi::deposit::deposit on Sui.
First, the user creates the request by calling hashi::deposit_queue::deposit_request:
public fun deposit_request(
utxo: Utxo,
clock: &Clock,
ctx: &mut TxContext,
): DepositRequest
The Utxo is constructed from the Bitcoin transaction details:
public fun utxo(
utxo_id: UtxoId,
amount: u64,
derivation_path: Option<address>,
): Utxo
public fun utxo_id(
txid: address,
vout: u32,
): UtxoId
txid– the 32-byte Bitcoin transaction hashvout– the output index within that transactionamount– the deposit amount in satoshisderivation_path– the Sui address used to derive the deposit address
The user then submits the request along with the required deposit fee:
public fun deposit(
hashi: &mut Hashi,
request: DepositRequest,
fee: Coin<SUI>,
ctx: &mut TxContext,
)
The function validates that the fee is exact, the deposit meets the dust minimum, and the UTXO has not been previously deposited. The request is then placed in the deposit queue for committee members to begin monitoring for confirmation on Bitcoin.
Confirm
graph LR
A[Request] --> B[Confirm]:::active --> C[Mint]
classDef active fill:#f9a825,stroke:#f57f17,color:#000
Committee members monitor the Bitcoin network for the deposit transaction. The
transaction must reach a sufficient number of block confirmations (see
bitcoin_confirmation_threshold)
before it is considered final. This guards against chain reorganizations where
a confirmed transaction could be reversed. If the transaction is never
confirmed or is invalidated by a reorg, the deposit is ignored.
Once confirmed, each committee member independently screens the deposit’s source address by making a request to its configured sanctions-checking endpoint (see handling sanctions). A member that considers the address sanctioned will not vote to accept the deposit.
Once a node has determined that a deposit request is both confirmed on bitcoin and passes its own screening checks, it will communicate with the other members of the hashi committee and collect signatures from validators who agree that the deposit should be confirmed. If a quorum of validators cannot agree that a deposit should be confirmed, it will either be retried at a later point or ignored if the request is invalid.
Mint
graph LR
A[Request] --> B[Confirm] --> C[Mint]:::active
classDef active fill:#f9a825,stroke:#f57f17,color:#000
Once a quorum of validators have agreed that a deposit should be confirmed,
one validator submits the certificate on-chain by calling hashi::deposit::confirm_deposit:
public fun confirm_deposit(
hashi: &mut Hashi,
request_id: address,
signature: CommitteeSignature,
ctx: &mut TxContext,
)
The function verifies the committee certificate, removes the request from the
deposit queue, mints the corresponding amount of hBTC, and sends it to the
user’s Sui address. The deposited UTXO is added to the Hashi-managed UTXO
pool, making it available for future withdrawal coin selection.
Withdraw
A withdrawal allows a user to redeem their hBTC on Sui for native BTC,
sent to a user-specified address on Bitcoin. The process has four phases:
graph LR
A[Request] --> B[Approve] --> C[Build TX] --> D[Sign] --> E[Broadcast]
Request
graph LR
A[Request]:::active --> B[Approve] --> C[Build TX] --> D[Sign] --> E[Broadcast]
classDef active fill:#f9a825,stroke:#f57f17,color:#000
A user submits an on-chain Sui transaction to request a withdrawal by calling
hashi::withdraw::request_withdrawal:
public fun request_withdrawal(
hashi: &mut Hashi,
clock: &Clock,
btc: Coin<BTC>,
bitcoin_address: vector<u8>,
ctx: &mut TxContext,
)
btc– thehBTCto withdraw. Must be at least thewithdrawal_minimum.bitcoin_address– the destination witness program, eitherP2WPKH(20 bytes) orP2TR(32 bytes).
The protocol enforces a minimum withdrawal amount to guarantee that every request can produce a valid Bitcoin transaction even under worst-case fee conditions. This minimum covers the flat protocol fee, the worst-case miner fee (which pays for both the user’s transaction weight and opportunistic UTXO pool consolidation), and the dust threshold for the user’s output. See fees for the full breakdown.
The flat protocol fee
(withdrawal_fee_btc) is deducted upfront
from the provided hBTC and the remainder is stored with the request. The
request is placed in a queue with a timestamp. The user can cancel their
request at any point before it has been approved, subject to a cooldown period
(withdrawal_cancellation_cooldown_ms).
Approve
graph LR
A[Request] --> B[Approve]:::active --> C[Build TX] --> D[Sign] --> E[Broadcast]
classDef active fill:#f9a825,stroke:#f57f17,color:#000
Before a queued request can advance, the Guardian’s token-bucket rate limiter must have sufficient capacity. If the request would exceed the current limit it remains in the queue until capacity is replenished. Requests are generally processed in FIFO order, but this is not a strict requirement.
Each committee member independently screens the destination address against its configured sanctions-checking endpoint (see handling sanctions). A member that considers the address sanctioned will not vote to approve the request. If a quorum still approves, all members are required to assist in completing the withdrawal.
Committee members communicate with each other to collect signatures to approve
the request. Once a quorum is reached one member will submit the certificate
on-chain by calling hashi::withdraw::approve_request:
entry fun approve_request(
hashi: &mut Hashi,
request_id: address,
epoch: u64,
signature: vector<u8>,
signers_bitmap: vector<u8>,
ctx: &mut TxContext,
)
The function verifies the committee certificate and marks the request as approved, allowing it to be picked up for transaction construction.
Build TX
graph LR
A[Request] --> B[Approve] --> C[Build TX]:::active --> D[Sign] --> E[Broadcast]
classDef active fill:#f9a825,stroke:#f57f17,color:#000
Multiple approved requests are batched into a single Bitcoin transaction to amortize the fixed transaction overhead across users. The leader waits for approved requests to buffer up before constructing a transaction – either until a request has been waiting for roughly 10 minutes, or until a threshold number of approved requests have accumulated, whichever comes first. This balances latency for individual users against efficiency for the protocol.
The leader then constructs an unsigned Bitcoin transaction:
- Coin selection – select UTXOs from the pool to cover the total withdrawal amount (miner fees will be taken out of the user’s withdrawn amount). The coin selector may include extra inputs to opportunistically consolidate small UTXOs (see fees).
- Outputs – one output per withdrawal request to each user’s destination address, plus one change output back to a Hashi-controlled address.
- Fee calculation – the miner fee is estimated via
estimatesmartfee, capped atmax_fee_rate. Each user’s share of the miner fee is deducted from their output. Validators independently verify that the fee is within acceptable bounds (at least1 sat/vB, at most 3x their own estimate).
The resulting unsigned transaction and its txid are committed on-chain by
calling hashi::withdraw::commit_withdrawal_tx:
entry fun commit_withdrawal_tx(
hashi: &mut Hashi,
request_ids: vector<address>,
selected_utxos: vector<vector<u8>>,
outputs: vector<vector<u8>>,
txid: address,
epoch: u64,
signature: vector<u8>,
signers_bitmap: vector<u8>,
clock: &Clock,
r: &Random,
ctx: &mut TxContext,
)
The function verifies the committee certificate, burns the hBTC from each
request, spends the selected UTXOs from the pool, and creates a pending
withdrawal record that all validators can independently verify.
Sign
graph LR
A[Request] --> B[Approve] --> C[Build TX] --> D[Sign]:::active --> E[Broadcast]
classDef active fill:#f9a825,stroke:#f57f17,color:#000
Signing is a two-step process matching the 2-of-2 Taproot script (see address scheme):
- Guardian signature – the unsigned transaction and metadata are sent to the Guardian enclave, which independently validates the request and produces a Schnorr signature for each input.
- MPC signature – the committee runs the threshold Schnorr signing protocol (see MPC protocol) to produce the second signature for each input.
Both signatures are combined into the taproot script-path witness for each input.
Note: for devnet there is no guardian configured. Only the MPC signature is used.
The signed transaction is committed on-chain by calling
hashi::withdraw::sign_withdrawal:
entry fun sign_withdrawal(
hashi: &mut Hashi,
withdrawal_id: address,
request_ids: vector<address>,
signatures: vector<vector<u8>>,
epoch: u64,
signature: vector<u8>,
signers_bitmap: vector<u8>,
ctx: &mut TxContext,
)
The function verifies the committee certificate and attaches the Schnorr signatures to the pending withdrawal.
Broadcast
graph LR
A[Request] --> B[Approve] --> C[Build TX] --> D[Sign] --> E[Broadcast]:::active
classDef active fill:#f9a825,stroke:#f57f17,color:#000
The fully signed transaction is broadcast to the Bitcoin network. The
committee monitors the transaction until it reaches the configured number of
block confirmations (see
bitcoin_confirmation_threshold).
Once confirmed, a committee member calls
hashi::withdraw::confirm_withdrawal:
entry fun confirm_withdrawal(
hashi: &mut Hashi,
withdrawal_id: address,
epoch: u64,
signature: vector<u8>,
signers_bitmap: vector<u8>,
ctx: &mut TxContext,
)
The function verifies the committee certificate and removes the pending withdrawal, returning the spent UTXOs’ change output to the pool.
If a transaction gets stuck, fee bumping is handled via CPFP – either the withdrawal recipient spends their output with a high-fee child, or Hashi spends the change UTXO (see fees – stuck transactions).