This document is the security document for the zkBTC decentralized bridge, developed by Lightec Labs. Follow us on X.
zkBTC is a ZKP-based bridge that securely bridges Bitcoin to Ethereum and all other major L1s and L2s. The basic workflow are:
- Deposit—The user sends $BTC to a designated operator address. Such a transaction is proved, and the proof is verified in an Ethereum smart contract. After successful verification, the smart contract mints $zkBTC tokens and transfers these tokens to the address supplied along with the deposit (in an
OP_RETURN
output). The smart contract will manage the newly created unspent transaction outputs (UTXOs) from this deposit. The miner who computes the proof and submits it on the user's behalf will receive rewards. - Use—Users can use $zkBTC tokens in any way they wish; they are equivalent to $BTC in the Ethereum ecosystem.
- Redemption—The user calls an Ethereum smart contract function to burn some $zkBTC tokens. The function will also leave some transaction logs specifying which available UTXOs to spend. Specifically, the smart contract will select which UTXOs to spend this time and record related information in the logs. The miner reward is delayed.
- Change and miner reward. Another proof of the redemption transaction in Bitcoin can be provided to the smart contract to bring back the change UTXO and to reward the miner who computes proof for redemption.
For more information and general use, please check out our Gitbook.
We designed zkBTC to be fully decentralized, with no central role in operating on users' assets. To achieve such an ambitious goal, we propose adding an OP_ZKP
opcode to Bitcoin, enabling it to verify zero-knowledge proofs as spending conditions for UTXOs. This way, there are no private keys to be managed at all for the Redemption.
To redeem back to Bitcoin, one must submit proof meeting specific criteria. Finding a ZKP scheme for OP_ZKP
has proven difficult, yet we have a draft here.
Before OP_ZKP
could be realized, we also have an alternative solution, which is to have multiple safe platforms to manage private keys such that:
- Each platform is designed to safe-keep the private key in a way that it is nearly impossible to learn the content of the private key;
- each platform only signs the redemption transaction after a successful ZKP verification;
- We can tolerate one of the platforms being cracked or failing without losing security or assets.
Please turn to later sections for more details.
Recent security incidents reminded us to keep the front end safe, so we are also going to deploy it on-chain.
In both directions, many circuits are working together. Different circuits are combined together via verification key fingerprint binding. The basic idea is to compute a FingerPrint
to verify the key of a circuit as the identifying value. The FingerPrint
is computed as the MiMc
hash of the key components of the in-circuit verifying key. The codes could be found in the common component, and is excerpted below:
// FingerPrint() returns the MiMc hash of the VerifyingKey. It could be used to identify a VerifyingKey
// during recursive verification.
func InCircuitFingerPrint[FR emulated.FieldParams, G1El algebra.G1ElementT, G2El algebra.G2ElementT](
api frontend.API, vk *plonk.VerifyingKey[FR, G1El, G2El]) (frontend.Variable, error) {
var ret frontend.Variable
mimc, err := mimc.NewMiMC(api)
if err != nil {
return ret, err
}
mimc.Write(vk.BaseVerifyingKey.NbPublicVariables)
mimc.Write(vk.CircuitVerifyingKey.Size)
mimc.Write(vk.CircuitVerifyingKey.Generator.Limbs[:]...)
comms := make([]kzg.Commitment[G1El], 0)
comms = append(comms, vk.CircuitVerifyingKey.S[:]...)
comms = append(comms, vk.CircuitVerifyingKey.Ql)
comms = append(comms, vk.CircuitVerifyingKey.Qr)
comms = append(comms, vk.CircuitVerifyingKey.Qm)
comms = append(comms, vk.CircuitVerifyingKey.Qo)
comms = append(comms, vk.CircuitVerifyingKey.Qk)
comms = append(comms, vk.CircuitVerifyingKey.Qcp[:]...)
for _, comm := range comms {
el := comm.G1El
switch r := any(&el).(type) {
case *sw_bls12377.G1Affine:
mimc.Write(r.X)
mimc.Write(r.Y)
case *sw_bls12381.G1Affine:
mimc.Write(r.X.Limbs[:]...)
mimc.Write(r.Y.Limbs[:]...)
case *sw_bls24315.G1Affine:
mimc.Write(r.X)
mimc.Write(r.Y)
case *sw_bw6761.G1Affine:
mimc.Write(r.X.Limbs[:]...)
mimc.Write(r.Y.Limbs[:]...)
case *sw_bn254.G1Affine:
mimc.Write(r.X.Limbs[:]...)
mimc.Write(r.Y.Limbs[:]...)
default:
return ret, fmt.Errorf("unknown parametric type")
}
}
mimc.Write(vk.CircuitVerifyingKey.CommitmentConstraintIndexes[:]...)
result := mimc.Sum()
return result, nil
}
A single-factor defense might attract potential adversaries. Therefore, we designed the system with defense-in-depth in mind. For deposit, we rely on the ICP's Bitcoin integration to sign off on the chain tip
and the CheckPoint
mechanism. For redemption, two of three multi-sig also provide fault tolerance. See related sections for more details.
zkBTC is designed to be fully decentralized, so there is no central role in operating the risk parameters. We must assume the worst-case scenario and defend zkBTC against it. Therefore, the UX must yield in the event of a conflict. For example, we require each Bitcoin deposit transaction to have a confirmation depth of at least twelve instead of six. There are many more examples like this throughout the document.
Third-party services could enhance the user experience (UX) since zkBTC is designed to be fully decentralized. The basic idea is for the third-party service to deposit into the zkBTC system, obtain some zkBTC tokens, and later provide those tokens to users with a better user experience. Of course, third parties are free to define their business models.
zkBTC has completed rigid security audits. We split the code base into three parts, with the smart contracts being audited by two independent auditors and the later one over the newer version covering asset migration. The dual-auditing for the smart contracts is based on its core importance and non-upgradability.
BEOSIN - March 2025 - Bitcoin and common Circuits, Smart Contracts
LeastAuthority - April 2025 - Cryptography
LeastAuthority - June 2025 - Ethereum Circuits and Smart Contracts
We have designed three defenses against potential attacks, layered in depth:
DepositTxCircuit.SigVerif
checks if thechain tip
(the latest block hash) is signed by aDFinity
canister. This is the initial implementation of our long-term security design of decentralized roles signing off thechain tip
.- The
Transaction Depth
check demands that each deposit transaction have a certain confirmation depth. Note that the commonly recommended depth of6
is based on on-chain verification, while in our system, it is essentially an off-chain verification. Therefore, we require at least12
for small deposits and even deeper for large deposits. - The
CheckPoint Depth
check demands that the enclosing block for each transaction is one of the descendants of a particular recognizedCheckPoint
(also a Bitcoin block hash).
Now, assuming a potent attacker with a significant amount of hashing power has already cracked the first defense, the decentralized roles to sign off on the chain tip. It still needs to maintain sufficient checkpoint depth. The attacker won't be able to meet both the Transaction Depth
requirement and the CheckPoint Depth
requirement if we set proper checking rules in the smart contract. Before discussing any specific rules, we must point out the obvious: it is impossible to pass all legit blocks without passing some offending blocks if the adversary indeed masters too much hashing power. Our aim here is not to reject all offending blocks, but to make it as computationally intensive as possible for adversaries, such that attacking us is not economically desirable.
Depth circuits prove that BlockM is the ancestor of BlockN and prove that the depth from BlockM to BlockN is n-m.
note: The minimum depth of tx is 12, and the minimum depth of cp is 72
These circuits include two groups of external circuits:
BlockBulkCircuit
proves that when the depth is 9-24 or 72.RecursiveBulksCircuit
proves that when the depth is > 24. RecursiveBulksCircuit is based on the BlockBulkCircuit proof with a depth of 24 or 72 and then absorbs a depth of 1-9, 18, 36, or 72, repeatedly recursing on itself.
---
title: Depth circuits of code implement
---
classDiagram
class RecursiveBulksCircuit{
+HybridCircuit
+Depth
}
class HybridCircuit["chainark.HybridCircuit"]{
+FirstProof
+SecondComp UnitCore
}
RecursiveBulksCircuit *-- HybridCircuit : Embedding
SecondComp <|-- DepthCore : implements
FirstProof "1" ..> "0..1" BlockBulkCircuit : Dependency
FirstProof "1" ..> "0..1" RecursiveBulksCircuit : Dependency
HybridCircuit *-- SecondComp : Embedding
HybridCircuit *-- FirstProof : Embedding
class BlockBulkCircuit{
+MultiUnit
+BlockHeaders
+Depth
}
BlockBulkCircuit *-- MultiUnit : Embedding
BlockBulkCircuit <|-- DepthCore : implements
class DepthCore{
+BeginHash
+EndHash
+BlockHeaders
+Depth
+GetBeginID()
+GetEndID()
+Define()
}
class MultiUnit["chainark.MultiUnit"]{
+BeginID
+EndID
}
BlockChain
circuits prove that a chain is formed from the genesis block to the blockN, and prove that the difficulty adjustment of every 2016 blocks meets the rules.
BlockChain
circuits are based on chainark. There are three auxiliary circuits:
BaseLevelCircuit
proves that 112 blocks form a chain called a batch.MidLevelCircuit
proves that six batches form a chain, each with 112 blocks. Total is 672 blocks, called a super batch.UpperLevelCircuit
proves that three super batches form a chain. The total is 2016 blocks.
Then there are one BlockChainCircuit
and multiple BlockChainHybridCircuit
:
BlockChainCircuit
combines two existing proofs into one, extending the chain.BlockChainHybridCircuit(n)
recursively verifies one existing proof (ofBlockChainCircuit
orBlockChainHybridCircuit
), plus some more blocks to form a chain, effectively extending the chain proven by the existing proof. There are multiple instances:n := {1, 2, ..., MiniLevel, MiniLevel*2+1, MiniLevel*3+2, MiniLevel*4+3}
. Numbers are chosen to minimize recursive verifications.
The tx circuits prove that the deposit
or redeem
transaction is on-chain and the depth of the tx meets the minimum tx depth
. The chain is formed from the genesis block
to the latest block
, meets the difficulty adjustment rules, and passes through a well-known checkpoint block
. The genesis block is well known, and the latest block is accepted and signed by a designated DFinity
canister.
The tx circuits contain other circuit proofs and circuit components:
BlockChainProof
proves that a consensus chain is formed from the genesis block to the latest block.CpDepthPoof
proves that the chain passes through the well-known checkpoint block, and its depth meets the minimum cp depth.TxInBlockCircuit
andTxDepthPoof
prove that tx is in a block and the depth of the block on the chain meets the minimum tx depth.EcdsaSigVerif
proves that the latest block of this chain was accepted and signed by Dfinity.RedeemInEthProof
proves that the redeem tx in ETH has been finalized on the ETH chain.
The tx circuits contain two external circuits:
DepositTxCircuit
proves that the deposit tx in BTC has been confirmed by the specified minimum depth on the BTC chain.RedeemTxCircuit
proves that the redeem tx in BTC has been confirmed by the specified minimum depth on the BTC chain and proves that its previous ETH redeem tx is also finalized on the ETH chain.
---
title: TxInChain circuits of code implement
---
classDiagram
RedeemTxCircuit *-- DepositTxCircuit : Embedding
RedeemTxCircuit *-- RecursiveVerifierCircuit : Embedding
DepositTxCircuit *-- Verifier : Embedding
DepositTxCircuit *-- EcdsaSigVerif : Embedding
DepositTxCircuit *-- BlockDepthsCircuit : Embedding
DepositTxCircuit *-- TxInBlockCircuit : Embedding
class RecursiveVerifierCircuit{
+Proof
}
RecursiveVerifierCircuit "1" ..> "1" `eth.RedeemCircuit` : Dependency
class Verifier["chainark.Verifier"]{
+Proof
}
Verifier "1" ..> "0..1" BlockChainCircuit : Dependency
Verifier "1" ..> "0..1" BlockChainHybridCircuit : Dependency
class BlockDepthsCircuit{
+TxDepthProof
+CpDepthProof
}
BlockDepthsCircuit "1" ..> "0..2" BlockBulkCircuit : Dependency
BlockDepthsCircuit "1" ..> "0..2" RecursiveBulksCircuit : Dependency
So the circuits are organized as below (FP(Circuit)
is short for FingerPrint(Circuit.Vkey)
):
# for BlockChain circuits
MidLevel.UnitFp = FP(BaseLevel)
UpperLevel.UnitFp = FP(MidLevel)
BlockChainHybridCircuit(n) : {1, ..., 18, 37, 56, 75}
BlockChainFpSet := {
FP(BlockChainCircuit),
FP(BlockChainHybridCircuit(1)), ..., FP(BlockChainHybridCircuit(18)),
FP(BlockChainHybridCircuit(37)), FP(BlockChainHybridCircuit(56)), FP(BlockChainHybridCircuit(75))
}
BlockChainCircuit.UnitFps = {FP(BaseLevel), FP(UpperLevel)}
BlockChainCircuit.SelfFps = BlockChainFpSet
BlockChainHybridCircuit(n).UnitFps = {FP(UpperLevel)}
BlockChainHybridCircuit(n).SelfFps = BlockChainFpSet
# for BlockDepth circuits
BlockBulkCircuit(n) : {9, ..., 24, 72}
RecursiveBulksCircuit(n) : {1, ..., 18, 36, 72}
BlockBulkFpSet := {
FP(BlockBulkCircuit(9)), ..., FP(BlockBulkCircuit(24)),
FP(BlockBulkCircuit(72))
}
RecursiveBulksFpSet := {
FP(RecursiveBulksCircuit(1)), ..., FP(RecursiveBulksCircuit(18)),
FP(RecursiveBulksCircuit(36)), FP(RecursiveBulksCircuit(72)),
}
DepthFpSet := BlockBulkFpSet UNION RecursiveBulksFpSet
RecursiveBulksCircuit(n).UnitFps = {FP(BlockBulkCircuit(24)), FP(BlockBulkCircuit(72))}
RecursiveBulksCircuit(n).SelfFps = RecursiveBulksFpSet
UnitFp(s)
and SelfFps
are concepts from chainark:
UnitFp
orUnitFps
(an array) is used to restrict which inner circuit is acceptable. It is created as a circuit constant to its enclosing circuit, such that changes in the value result in a different circuit.SelfFps
is used to specify a set of circuits, including theself
circuit, so that each circuit in the set could verify proofs from other circuits from the same set. That means each circuit in the set should be able to verify a proof generated by itself (in an earlier invocation). This is how we achieve recursive verification for the entire Bitcoin chain.SelfFps
is public witness to its enclosing circuit.
Finally, DepositTxCircuit
is responsible for generating proofs to be submitted to Ethereum for the purpose of minting zkBTC
tokens. Among other verifications, it recursively verifies:
- one
BlockChainCircuit
orBlockChainHybridCircuit(n)
proof
DepositTxCircuit.BlockChain.UnitFps = BlockChainFpSet
- two proofs from
BlockBulkCircuit(n)
orRecursiveBulksCircuit(n)
, one forTransaction Depth
verification, another forCheckPoint Depth
verification
DepositTxCircuit.BlockDepths.UnitFps = DepthFpSet
The smart contract must estimate the checkpoint depth on its own to determine whether the value presented in the proof is acceptable.
If the transaction is even deeper than the checkpoint or simply rests in the checkpoint block, we simply return true for checkpoint depth checking, as the smart contract trusts the checkpoint. Otherwise, our first attempt is:
estimated_depth = (eth_block.timestamp - tx_block.timestamp) / 600 + (cp_depth - tx_depth);
We assume the Ethereum timestamp as obtained by block.timestamp
is very close to real-time as of the contract execution (ref). However, since the Bitcoin transaction and its enclosing block could be generated by the adversary, its timestamp could not be trusted. As cp_depth
could also be derived from the adversary's fork chain, it is also not reliable.
Alternatively, we could compute the average interval between checkpoint and 'now' and then estimate the depth. However, this computation requires unreliable cp_depth
.
We assume a 10-minute average block interval here. This interval is often not the actual case. A variation of 10% is relatively common and is handled with allowance. See the following sections.
estimated_depth = (eth_block.timestamp - cp_block.timestamp) / 600;
What if the proof submission is purposefully delayed such that eth_block.timestamp
is much later than it should have been? In that case, the estimated_depth
would be larger than the actual value. This does the adversary no good. For the same reason, a valid deposit might be declined if the submission is delayed for too long. Fortunately, this will not happen a lot in our proving-as-mining incentive setting. Every miner will strive to submit proof as soon as possible to earn proving rewards.
Suppose the attacker has 10% of all honest hashing power combined; on average, it must spend at least 1200 minutes (20 hours) to meet the transaction depth requirement for a small deposit, which requires tx_depth
to be at least 12. Meanwhile, around 120 new blocks have been mined in the Bitcoin mainnet. The attacker would find out the checkpoint depth deficit to be 108. If the attacker has 30% of all hashing power, numbers become 400 minutes, 40 new blocks, and 28 blocks of checkpoint depth deficit.
The smart contract, however, needs to address block timestamp drift, proof generation time, block interval variance, and time to broadcast the smart contract invocation transaction and include it in a block, among other considerations. For example, the timestamp of any Bitcoin block might be as early as just a bit later than the median value of its past 11 blocks. That will generate 1 hour, or approximately six blocks more than the actual depth since we use eth_block.timestamp
to estimate the checkpoint depth. The smart contract will have to subtract six or more from its estimated depth so that a legit deposit won't be declined. We could allocate more time for various other factors, ranging from half an hour to one hour. Thus:
required_minimal_depth = estimated_depth - allowance
Specifically, we'd like to reserve 6 for the potential checkpoint timestamp error, 2 for waiting for the chain tip to be available and proof to be generated, and then 1 for every additional 3 confirmation depth requirements covering the variation of hashing power and block time.
allowance = 6 + 2 + ceil((depth - 3)/3) = 7 + ceil(depth/3)
Therefore, for transaction confirmation depth requirements of 12, 18, 24, and 36, which is also the checkpoint candidate depth requirement, the corresponding allowances are 11, 13, 15, and 19.
CheckPoint
is maintained by the Ethereum smart contract without human intervention. The hash of the enclosing block for a deep enough transaction could be a candidate for a new CheckPoint
. When it is time to rotate checkpoints, the smart contract selects a random candidate. Our security does not depend on how random the selection process is. Instead, we impose a depth requirement for each of the candidate such that the adversary won't be able to reach without failing the checkpoint depth test. Our security architecture prioritizes checkpoint safety over individual blocks or transactions. Note that the formula of estimated_depth
already assumes that the adversary starts to mine its own blocks based on a would-be checkpoint block when it is freshly mined. Successful guessing of the next checkpoint grants no additional advantages to adversaries.
Besides the depth requirement for the checkpoint candidate, we also verify that the timestamp of the checkpoint block is not too far in its past or future. According to the Bitcoin consensus rules, the timestamp of the checkpoint block could be at most 2 hours in its future, resulting in some free depth for adversaries. We ran some checks in the circuit to prevent this free depth from becoming too many and proved a flag, which the smart contract could check. On the other hand, if the checkpoint timestamp is in its past, legit deposit might be declined. So, the circuit also checks this case and sets a flag accordingly.
What if the in-circuit check is unreliable enough (for example, the blocks we use to check the timestamp are ALL in their future), and the checkpoint candidate still carries a timestamp in its past or future that is too far away?
Although the in-circuit check is a strong defense, we could still discuss this hypothetical case under the Defense in Depth principle:
estimated_depth = (eth_block.timestamp - (cp_block.true_timestamp + 7200)) / 600
= better_estimation - free_depth;
free_depth = 12;
Note that better_estimation
is unknown to the smart contract and is used here simply to clarify this text.
There is another source of free depth beyond what we have discussed so far. If many new mining powers join the game, the average block interval could be significantly reduced.
Let's discuss a concrete yet hypothetical case: after a checkpoint is selected, only a few blocks after a new mining difficulty has been settled. Immediately after the checkpoint selection, some mining power joins the game solely to reduce the average block time to below 8 minutes, rather than the original 10 minutes. 20 hours later, the mainnet generates 150 new blocks instead of 120. The addition of 30 new blocks gives the adversary a significant boost. The adversary could take a considerable amount of time to mine new blocks and still meet the checkpoint depth requirement.
Our solution is to derive the actual average block interval from the block data and prove it using the ZKP circuit. Then, the smart contract may check the proven block interval carefully and decide that:
- if the proven interval is shorter than 10 minutes, use it to estimate the checkpoint depth;
- Otherwise, keep using the 10-minute interval.
Remember we mentioned earlier that the proof could be generated by the adversary, and the timestamp could be manipulated especially for the block enclosing the transaction or the chain tip (we are talking about defense in depth here so we do not count in the signature to the chain tip). Under this situation, the proven interval could be made very close to 10 minutes. Then, the smart contract uses the 10-minute interval to estimate the checkpoint depth, and free depth is awarded to the adversary. We must use a timestamp that is less likely to be manipulated.
And we do have options. The smart contract imposes a minimal depth requirement for the checkpoint, which is set to 72 as of the time of this writing. We could trace back 24 blocks from the chain tip and calculate the average interval between this point and the checkpoint. We have at least 49 blocks counted in. This provides a good enough estimation except in the case when the mining difficulty adjustment is not included. The adversary could attempt to mine at least 25 blocks, however, this is very computation intensive. We will address this issue at the end of this Checkpoint subsection.
On the other hand and in the worst case, if the mining difficulty adjustment happens right at the beginning of the 24 excluded-from-average blocks, and assuming to the extent that the mainnet starts to generate new blocks every 8 minutes, the adversary could obtain at most 24 - (24 * 8 / 10) = 4.8
free blocks. This is still not directly usable to the adversary, as the mainnet generates these blocks.
We need to determine the threshold of hashing power that an adversary must possess to defeat our checkpoint system. Given:
estimated_depth = (eth_block.timestamp - (cp_block.true_timestamp + 7200)) / 600
= better_estimation - free_depth;
free_depth = 12 + 4.8 ~= 17;
required_minimal_depth = estimated_depth - allowance;
allowance = 7 + ceil(depth/4);
Suppose the transaction depth requirement is D (D = 12 for small deposits, 18 for medium amounts, and 24 and 36 for even larger amounts), and the attacker commands x% hashing power compared to all honest miners combined. At some point on or after the checkpoint block, the attacker must begin to mine its blocks. To meet the transaction depth requirement, the attacker must spend average_attacker_time_for_D_blocks
on average, assuming the average block interval to be 10 minutes:
average_attacker_time_for_D_blocks := D * 600 / (x/100) = D * 60000/x
Meanwhile, the mainnet keeps producing new blocks; the expected average blocks mined are:
expected_average_blocks_mainnet := average_attacker_time_for_D_blocks / 600 = D * 100/x
During this period, the checkpoint depth that the attacker would obtain will lag behind that of the mainnet by cp_depth_diff
:
cp_depth_diff := expected_average_blocks_mainnet - D = D * (100/x - 1)
The attacker needs cp_depth_diff <= allowance
. But since there could be free depth, he needs only cp_depth_diff <= allowance + free_depth
or:
// D * (100/x - 1) <= allowance + free_depth <--> x >= 100*D/(allowance + D + 17)
// (allowance + D + 17) must be positive
Solving the above inequalities for (D, allowance) = (12, 11)
:
// x >= 1200/(12 + 11 + 17) = 30.0
And for (D, allowance) = (18, 13), (24, 15), (36, 19)
. The last one is also the tx_depth
requirement for a checkpoint candidate.
// x >= 1800/(18 + 13 + 17) = 37.5
// x >= 2400/(24 + 15 + 17) = 42.9
// x >= 3600/(36 + 19 + 17) = 50.0
Note that we were talking about average cases. There are slight chances that the attacker mines more blocks sooner than average. However, the block time variation has been handled with the allowance value (see earlier sections).
Note that we include the 4.8 free depth from the rare situation, and the 12 free depth is also not easily obtainable for the adversary. The above numbers are for the worst-case scenario. The actual safety margin is better. For example, we might consider when the free depth is at most 6 instead of 17 for (D, allowance) = (12, 11), (36, 19)
:
// x >= 1200/(12 + 11 + 6) = 41.4 (compared to 30.0)
// x >= 3600/(36 + 19 + 6) = 59.0 (compared to 50.0)
It seems unprofitable to spend at least 25% of the total net hashing power to make a fake deposit of a small amount. The attacker, however, will attempt to stuff as many transactions as possible into one block and hope to deposit all of them successfully.
To counter this measure, we keep the count of deposit transactions per block and escalate the depth requirement once a specific limit is reached.
In the above discussion, we assume the attacker uses new hashing power instead of drawing the existing mining power to compute the attack. This way, the mainnet is generating new blocks at a relatively steady rate. However, if a potent attacker can turn some existing hashing power to attack zkBTC, then the rate at which the mainnet produces new blocks will be slowed down. And our analysis is impacted. This is the typical p vs q
situation in the original Bitcoin Whitepaper.
Nonetheless, our security architecture does not rely on the actual block interval. We use the expected 10 minutes and then allowance
to handle any variation. If the supposed situation were to occur, then instead of invalid deposits being accepted, valid deposits might be declined as the checkpoint depth requirement cannot be met, but only temporarily.
To recover from this situation, the mainnet must have mined blocks faster than the 10-minute expectation to compensate for the 'lost depth'. This could happen a while after the attacker has stopped without gains, or more honest hashing power joins the mining as their owners see the opportunity or the difficulty adjustment results in a lower difficulty due to a prolonged average block interval and hence faster block mining.
Let's assume the checkpoint is very deep already, and there are lots of free depth. The adversary manages to mine 25 blocks in the fork chain, with the timestamp manipulated so that the computed interval interval = (section_end_timestamp - checkpoint_timestamp) / nb_blocks
is close to 10 minutes. Then, the adversary could have all those free depths to meet the checkpoint depth requirements. Of course since 25 blocks have been in the fork chain, the transaction depth requirement is also meet for transactions not too large. Can he get away? Not if we apply the consensus rule that any block's timestamp cannot be more than 2 hours in the future of the network adjusted time. Since there is no network adjusted time in the context of ZKP, we consider using the median timestamp of the past 11 blocks plus their average interval multiplied by 6. (see issue #4). Specifically, we cap the timestamp of the last block used to compute the average interval to the value calculated as median(timestamp of the past 11 blocks) + average block interval(the past 11 blocks) + 2 hours
.
Now, assuming the fast block generation (one block per r
minutes, r < 10
) has been lasting for h
hours (60h
minutes) since the checkpoint, and the adversary has x%
of hashing power of all honest miners combined. The adversary will have to spend max(D, 25) * r / (x/100) = 100mr/x
minutes to mine at least 25 blocks, where m
denotes max(D,25)
. The estimated checkpoint depth, assuming he can instantly compute the proof, would be (100mr/x + 60h) / maniputed_interval
. Since the adversary manipulated the timestamp to get additional 2 hours, we have maniputed_interval = (2 + h)*60/(60h/r) = r(2+h)/h
. Finally, the estimated checkpoint depth is (100mr/x + 60h) / (r(2+h)/h) = (100mr/x + 60h)h/(r(2+h))
.
Yet the adversary could only prove 60h/r + 25
blocks.
Solve 60h/r + 25 >= (100mr/x + 60h)h/(r(2+h)) - allowance
for various r
, h
values and (D, allowance)
combinations:
r | h | D | allowance | x |
---|---|---|---|---|
8 | 6 | 12 | 11 | 39 |
8 | 6 | 18 | 13 | 38 |
8 | 6 | 24 | 15 | 36 |
8 | 6 | 36 | 19 | 48 |
8 | 6 | 48 | 23 | 60 |
8 | 6 | 72 | 31 | 80 |
(see Appendix A for full data)
x
is at least 36. Looks fine already, and even more so considering that the fast block generation does not happen a lot.
While this looks exciting, what if the adversary further manipulates the timestamp of the
We could limit the average block interval of the past 11 blocks to be within L
minutes. In this setting, the simulated 'network adjusted time' would be much earlier than the actual timestamp so that the proven interval would be smaller. However, this does not make much difference, as the mainnet is generating blocks at a rate slower than 10 minutes per block.
With the said limit, the adversary could at most obtain (L - r)*6
more minutes for the manipulated timestamp when the mainnet generates blocks every r
minutes on average. Further, let h
, x
denote the same value as in the last sub-section and m = max(D, 26)
. The manipulated interval becomes maniputed_interval = ((2 + h)*60 + (L - r)*6)/(60h/r) = (20 + 10h + L - r)*r/10h
. The estimated checkpoint depth is (100mr/x + 60h) * 10h / ((20 + 10h + L - r) * r)
. The adversary could prove 60h/r + 26
blocks.
Solve 60h/r + 26 >= (100mr/x + 60h) * 10h / ((20 + 10h + L - r) * r) - allowance
for various L
, h
values and (D, allowance)
combinations (with r
fixed to 8):
L | h | D | allowance | x |
---|---|---|---|---|
11 | 6 | 12 | 11 | 38 |
11 | 6 | 18 | 13 | 36 |
11 | 6 | 24 | 15 | 35 |
11 | 6 | 36 | 19 | 45 |
11 | 6 | 48 | 23 | 56 |
11 | 6 | 72 | 31 | 75 |
(see Appendix B for full data)
We will set L
to 11, and then x
is at least 35, which is good enough (the actual value of L
is 11.1 minutes, or 666 seconds).
In the discussion, the chain tip's signature has become the single point of failure (SPoF). That is, if, for some reason, the ICP canister cannot sign the Bitcoin tip block, then we cannot generate proof that it is acceptable to the deployed smart contract. To overcome this hurdle, we could replace the signature with more depth requirements.
Our current practice is to double the depth requirements and update the allowance value accordingly. This is on top of the depth requirement escalation mentioned earlier.
With this design, we have two new depth requirements: (D, allowance) = (48, 23), (72, 31). The hashing power requirements for the attacker are:
// x >= 4800/(48 + 23 + 17) = 54.5
// x >= 7200/(72 + 31 + 17) = 60.0
The above security parameters are designed based on the current mining rewards and miner concentration (as of May 2025). Things may change over time, and it is hard to predict what might happen 5 or 10 years from now. While our deployed smart contract shall NOT be updated, some parameters could be. We define the extra depth ranging from 0 to 255 to be added to any computed transaction depth requirement, with the current value set to 0.
We use the Ethereum Light Client Protocol (LCP) to determine if the transaction to call the designated redeem function has been completed successfully and the enclosing block has been finalized. This includes the major parts:
- the transaction to call the designated redeem function has been completed successfully and left some logs as receipts;
- the transaction belongs to a block;
- the block is an ancestor of another block, which has been finalized as being signed off by a sync committee;
- there exists a signature chain from the genesis sync committee to the signing sync committee;
By proving the above assertions, we are sure that the redeem function has been executed as expected. Then, we can extract data from the proven logs, assemble a Bitcoin transaction, and send it along with the proof to be verified and signed/executed. Note that all the information needed to assemble the Bitcoin transaction, including all the available UTXOs, is managed with the smart contract.
Our long-term plan is to upgrade Bitcoin to support OP_ZKP
. Then, we can supply Bitcoin with the transaction (inputs, outputs) and its (segregated) witness (proof). The proof serves as the spending conditions of the input UTXOs, much like a signature in a regular Bitcoin transfer transaction. Once the verification is successful, the Bitcoin network can process the transaction, and the user will get the redeemed $BTC.
The interim solution is to use a two-of-three multi-sig scheme. As mentioned at the beginning of this document, we need multiple safe platforms to manage private keys such that:
- Each platform is designed to safe-keep the private key in a way that it is nearly impossible to learn the content of the private key;
- Each platform only signs the redemption transaction after a successful ZKP verification;
- We can tolerate one of the platforms being cracked or failing without losing security or assets.
And our choices are:
- ICP tECDSA, available in a canister programmed to verify proof before signing the transaction. And the private key is never reconstructed during signing.
- Oasis Sapphire, an EVM compatible L1 based on TEE technology. The Intel SGX technology used in Oasis Sapphire can protect both the private key and the integrity of the enclave code so that no one can bypass the proof verification placed before transaction signing.
- Intel SGX-enabled machines, similar to Oasis Sapphire, are operated by the Lightec team. SGX technology ensures that even the Lightec team cannot learn the private key content or bypass the "proof-verfication-before-signature" logic.
Interested readers may refer to ICP or Oasis documents for security-related information. We will cover how we program and operate the SGX enclave.
For a general introduction to SGX, especially about how it could secure computation, we recommend the classic paper Intel SGX Explained by Victor Costan and Srinivas Devadas.
In a nutshell, an SGX enclave provides:
- encrypted memory content which could be decrypted only inside the CPU and visible to its owning enclave, preventing privileged OS processes or even hardware systems (BIOS, memory controller, etc.) from accessing the confidential data;
- program integrity such that once the program is tampered with, it is either an invalid or totally different enclave. In either case, it cannot decrypt any data encrypted by the original enclave.
We are building on top of ego, a popular Golang library to use SGX. The enclave verifies a zero-knowledge proof of a redemption transaction before signing the Bitcoin transaction with a private key it manages. The private key is initially generated by the first instance of the enclave, then exported and encrypted so that only itself or another enclave with the exact binary code could decrypt inside the enclave. Put another way, even the Lightec team cannot read the content of the private key or bypass the zkp verification to obtain a signature.
The below picture outlines the interaction between the client and the server so that the client can retrieve the encrypted private key from the server.
Note that the TLS connectivity between Client and Server is bi-directionally authenticated, with pinned self-signed certificates.
- Each of the client and server generate a TLS certificate using an Ed25519 key pair derived from the hash of its SGX Unique Key, applying SHA256 twice:
privateKey := SHA256(SHA256(UniqueKey))
- The generated certificates are then distributed to the client and server.
- Both the client and server load their peer certificate for bi-directional authentication.
The server should create the secret only once, and seal it with its SGX Unique Key so that it could unseal it after restarting. After that, and once the secret is restored in the enclave, the server is ready for the client to retrieve the secret.
Once the TLS connection is established, the secret exchange process follows these steps:
A. Client Generates an SGX Remote Attestation Report
- The client generates a Secp256k1 key pair, again with the private key being
privateKey := SHA256(SHA256(UniqueKey))
. Note that this might fail with a negleble chance (less than$2^{-127}$ ), which is acceptable in our use. - The public key is used as a parameter to generate an SGX Remote Attestation Report.
- The client sends the SGX report to the server.
B. Server Verifies the Client’s Report and Encrypts the Secret
- The server verifies the client’s SGX Remote Attestation Report, as well as if the Enclave Unique Id matches that of its own.
- It extracts the client-side public key from the client’s report.
- The server encrypts the secret according to the
ECIES
protocol, using the extracted public key and an ephemeral key pair. The underlying symmetric cipher suite isAES128 + HMAC-SHA-256-16
. - It computes the hash of the ciphertext.
- The computed hash is used as a parameter to generate the server’s SGX Remote Attestation Report.
- The server sends the SGX report along with the ciphertext to the client.
C. Client Verifies the Server’s Report and Decrypts the Secret
- The client verifies the server’s SGX Remote Attestation Report, as well as if the Enclave Unique Id matches that of its own.
- It extracts the ciphertext hash from the server’s SGX report.
- The client computes the hash of the received ciphertext.
- It compares the computed hash with the one extracted from the server’s SGX report.
- If the hashes match, the client decrypts the ciphertext using its private key.
- Finally, the client seals the secret using its SGX Unique Key.
Our SGX code will be open once we complete the audit and launch the product.
The Ethereum Light Client Protocol requires this. As a related library was not available when we started to develop zkBTC, we developed such a circuit on our own, and we had submitted a PR to gnark, pending audit, review, and merge.
In deposit and redemption, we need to prove a chain of relationship: for Bitcoin, the blocks are chained with double SHA256; for Ethereum Light Client Protocol, the sync committees are chained with BLS signature, etc. We developed chainark to prove a kind of chained relationship. Basically:
- a
UnitCircuit
is a user-defined circuit that makes up the chaining; - a
RecursiveCircuit
either verifies twoUnitCircuit
proofs or oneRecursiveCircuit
orHybridCircuit
proof immediately followed by aUnitCircuit
proof; - a
HybridCircuit
is similar toRecursiveCircuit
, but instead of aUnitCircuit
proof, it verifies the chaining conditions directly. The benefit ofHybridCircuit
overRecursiveCircuit
is saving a recursion.
The security of chainark is, therefore, of essential importance to zkBTC. Here are the primary design considerations:
- The
FingerPrint
circuit is used throughout the Chainark library to identify circuits. Unlike some simple situations in which an outer circuit verifies a proof from an inner circuit and only needs the in-circuit verification key, we design the chainark to be capable of verifying a chain of any length. The basic idea is for theRecursiveCircuit
orHybridCircuit
to verify its proof (from an earlier proving session). Of course, they cannot use a verification key when its definition has not yet been finished.FingerPrint
is the answer to this dilemma. In the source code, this is theMultiRecursiveCircuit.SelfFps
orHybridCircuit.SelfFps
(an array). - For any circuit to verify if a proof from the
RecursiveCircuit
orHybridCircuit
is acceptable, they need to verify the proof with a proper verification key and check if the recursion has been performed correctly. That is, the verification keys used to in-circuit verify other proofs must match the listed fingerprints exactly one-to-one. These listed fingerprints are used to identify which recursive or hybrid circuits could be trusted.
We use Plonk as supported by gnark. Plonk is instantiated using the BN254 curve for verification purposes in Ethereum.
We use the Aztec setup files with further re-calculation to .lsrs files. Instructions could be found in this repo.
Full decentralization is somewhat contradictory to upgradability, as a system upgrade cannot be implemented without some controls. Yet, we will have to do this for reasons including:
- potential security vulnerabilites newly discovered after product launch, either in the underlying library or in our implementation;
- a new implementation that is much more efficient (10x or even more) and could significantly enhance user experiences and/or save lots of gas;
- the ultimate upgrade from multi-sig managed address to
OP_ZKP
.
We enable the upgradability in two ways:
- to upgrade the ZKP module for deposit, we need to deploy a new deposit module. Then, this new module takes over the ZKP verification for deposit.
- to upgrade the ZKP module for redemption, we will need to deploy a new ICP canister, Oasis smart contract, and SGX enclave, as they are all designed to be non-upgradable (so that even the project team cannot manipulate these confidential containers). The operator address needs to be changed, which results in asset migration.
Since some users may miss the notification of changing to a new deposit address, the old address will be supported in an admin-only manner, as there may be security risks associated with the old modules.
The control required to implement upgradability is minimal, in that the admin role is only used for upgrades.
All security bugs in zkBTC should be reported by email to hello@lightec.xyz.
Your email will be acknowledged within 7 days, and you'll be kept up to date with the progress until resolution. Your issue will be fixed or made public within 90 days. Currently we don't have a bug bounty program yet, but award in L2 Tokens could be discussed in a case-by-case way.
- Most of the smart contracts have been open and could be found in EtherScan.
- Some core circuit components are also open sourced, including chainark (a recursive verification circuit library), BLS12-381 G2 signature verification (a PR to gnark).
- The SGX code base will be open source soon to help the community understand our security design principles (that even the development / operation team cannot access the private keys).
- The rest of the circuits will be open source once we obtain more funding and secure our market place.
r | h | D | allowance | x |
---|---|---|---|---|
8 | 6 | 12 | 11 | 39 |
8 | 6 | 18 | 13 | 38 |
8 | 6 | 24 | 15 | 36 |
8 | 6 | 36 | 19 | 48 |
8 | 6 | 48 | 23 | 60 |
8 | 6 | 72 | 31 | 80 |
8 | 12 | 12 | 11 | 43 |
8 | 12 | 18 | 13 | 42 |
8 | 12 | 24 | 15 | 40 |
8 | 12 | 36 | 19 | 54 |
8 | 12 | 48 | 23 | 67 |
8 | 12 | 72 | 31 | 89 |
8 | 18 | 12 | 11 | 45 |
8 | 18 | 18 | 13 | 43 |
8 | 18 | 24 | 15 | 42 |
8 | 18 | 36 | 19 | 56 |
8 | 18 | 48 | 23 | 70 |
8 | 18 | 72 | 31 | 93 |
8 | 24 | 12 | 11 | 46 |
8 | 24 | 18 | 13 | 44 |
8 | 24 | 24 | 15 | 42 |
8 | 24 | 36 | 19 | 57 |
8 | 24 | 48 | 23 | 71 |
8 | 24 | 72 | 31 | 95 |
8 | 30 | 12 | 11 | 46 |
8 | 30 | 18 | 13 | 45 |
8 | 30 | 24 | 15 | 43 |
8 | 30 | 36 | 19 | 58 |
8 | 30 | 48 | 23 | 72 |
8 | 30 | 72 | 31 | 96 |
8 | 36 | 12 | 11 | 47 |
8 | 36 | 18 | 13 | 45 |
8 | 36 | 24 | 15 | 43 |
8 | 36 | 36 | 19 | 58 |
8 | 36 | 48 | 23 | 73 |
8 | 36 | 72 | 31 | 97 |
8 | 48 | 12 | 11 | 47 |
8 | 48 | 18 | 13 | 45 |
8 | 48 | 24 | 15 | 44 |
8 | 48 | 36 | 19 | 59 |
8 | 48 | 48 | 23 | 73 |
8 | 48 | 72 | 31 | 98 |
8 | 60 | 12 | 11 | 47 |
8 | 60 | 18 | 13 | 46 |
8 | 60 | 24 | 15 | 44 |
8 | 60 | 36 | 19 | 59 |
8 | 60 | 48 | 23 | 74 |
8 | 60 | 72 | 31 | 98 |
8 | 72 | 12 | 11 | 48 |
8 | 72 | 18 | 13 | 46 |
8 | 72 | 24 | 15 | 44 |
8 | 72 | 36 | 19 | 59 |
8 | 72 | 48 | 23 | 74 |
8 | 72 | 72 | 31 | 99 |
8 | 144 | 12 | 11 | 48 |
8 | 144 | 18 | 13 | 46 |
8 | 144 | 24 | 15 | 45 |
8 | 144 | 36 | 19 | 60 |
8 | 144 | 48 | 23 | 75 |
8 | 144 | 72 | 31 | 100 |
8 | 288 | 12 | 11 | 48 |
8 | 288 | 18 | 13 | 46 |
8 | 288 | 24 | 15 | 45 |
8 | 288 | 36 | 19 | 60 |
8 | 288 | 48 | 23 | 75 |
8 | 288 | 72 | 31 | 100 |
8 | 576 | 12 | 11 | 48 |
8 | 576 | 18 | 13 | 47 |
8 | 576 | 24 | 15 | 45 |
8 | 576 | 36 | 19 | 60 |
8 | 576 | 48 | 23 | 76 |
8 | 576 | 72 | 31 | 101 |
9 | 6 | 12 | 11 | 40 |
9 | 6 | 18 | 13 | 39 |
9 | 6 | 24 | 15 | 37 |
9 | 6 | 36 | 19 | 50 |
9 | 6 | 48 | 23 | 62 |
9 | 6 | 72 | 31 | 81 |
9 | 12 | 12 | 11 | 45 |
9 | 12 | 18 | 13 | 43 |
9 | 12 | 24 | 15 | 41 |
9 | 12 | 36 | 19 | 55 |
9 | 12 | 48 | 23 | 69 |
9 | 12 | 72 | 31 | 91 |
9 | 18 | 12 | 11 | 46 |
9 | 18 | 18 | 13 | 45 |
9 | 18 | 24 | 15 | 43 |
9 | 18 | 36 | 19 | 57 |
9 | 18 | 48 | 23 | 72 |
9 | 18 | 72 | 31 | 95 |
9 | 24 | 12 | 11 | 47 |
9 | 24 | 18 | 13 | 45 |
9 | 24 | 24 | 15 | 44 |
9 | 24 | 36 | 19 | 59 |
9 | 24 | 48 | 23 | 73 |
9 | 24 | 72 | 31 | 97 |
9 | 30 | 12 | 11 | 48 |
9 | 30 | 18 | 13 | 46 |
9 | 30 | 24 | 15 | 44 |
9 | 30 | 36 | 19 | 59 |
9 | 30 | 48 | 23 | 74 |
9 | 30 | 72 | 31 | 98 |
9 | 36 | 12 | 11 | 48 |
9 | 36 | 18 | 13 | 46 |
9 | 36 | 24 | 15 | 45 |
9 | 36 | 36 | 19 | 60 |
9 | 36 | 48 | 23 | 75 |
9 | 36 | 72 | 31 | 99 |
9 | 48 | 12 | 11 | 49 |
9 | 48 | 18 | 13 | 47 |
9 | 48 | 24 | 15 | 45 |
9 | 48 | 36 | 19 | 60 |
9 | 48 | 48 | 23 | 75 |
9 | 48 | 72 | 31 | 100 |
9 | 60 | 12 | 11 | 49 |
9 | 60 | 18 | 13 | 47 |
9 | 60 | 24 | 15 | 45 |
9 | 60 | 36 | 19 | 61 |
9 | 60 | 48 | 23 | 76 |
9 | 60 | 72 | 31 | 101 |
9 | 72 | 12 | 11 | 49 |
9 | 72 | 18 | 13 | 47 |
9 | 72 | 24 | 15 | 45 |
9 | 72 | 36 | 19 | 61 |
9 | 72 | 48 | 23 | 76 |
9 | 72 | 72 | 31 | 101 |
9 | 144 | 12 | 11 | 50 |
9 | 144 | 18 | 13 | 48 |
9 | 144 | 24 | 15 | 46 |
9 | 144 | 36 | 19 | 62 |
9 | 144 | 48 | 23 | 77 |
9 | 144 | 72 | 31 | 102 |
9 | 288 | 12 | 11 | 50 |
9 | 288 | 18 | 13 | 48 |
9 | 288 | 24 | 15 | 46 |
9 | 288 | 36 | 19 | 62 |
9 | 288 | 48 | 23 | 77 |
9 | 288 | 72 | 31 | 103 |
9 | 576 | 12 | 11 | 50 |
9 | 576 | 18 | 13 | 48 |
9 | 576 | 24 | 15 | 46 |
9 | 576 | 36 | 19 | 62 |
9 | 576 | 48 | 23 | 78 |
9 | 576 | 72 | 31 | 103 |
L | h | D | allowance | x |
---|---|---|---|---|
11 | 6 | 12 | 11 | 38 |
11 | 6 | 18 | 13 | 36 |
11 | 6 | 24 | 15 | 35 |
11 | 6 | 36 | 19 | 45 |
11 | 6 | 48 | 23 | 56 |
11 | 6 | 72 | 31 | 75 |
11 | 12 | 12 | 11 | 42 |
11 | 12 | 18 | 13 | 40 |
11 | 12 | 24 | 15 | 39 |
11 | 12 | 36 | 19 | 50 |
11 | 12 | 48 | 23 | 63 |
11 | 12 | 72 | 31 | 84 |
11 | 18 | 12 | 11 | 44 |
11 | 18 | 18 | 13 | 42 |
11 | 18 | 24 | 15 | 41 |
11 | 18 | 36 | 19 | 52 |
11 | 18 | 48 | 23 | 66 |
11 | 18 | 72 | 31 | 88 |
11 | 24 | 12 | 11 | 45 |
11 | 24 | 18 | 13 | 43 |
11 | 24 | 24 | 15 | 41 |
11 | 24 | 36 | 19 | 54 |
11 | 24 | 48 | 23 | 67 |
11 | 24 | 72 | 31 | 90 |
11 | 30 | 12 | 11 | 45 |
11 | 30 | 18 | 13 | 43 |
11 | 30 | 24 | 15 | 42 |
11 | 30 | 36 | 19 | 54 |
11 | 30 | 48 | 23 | 68 |
11 | 30 | 72 | 31 | 91 |
11 | 36 | 12 | 11 | 46 |
11 | 36 | 18 | 13 | 44 |
11 | 36 | 24 | 15 | 42 |
11 | 36 | 36 | 19 | 55 |
11 | 36 | 48 | 23 | 69 |
11 | 36 | 72 | 31 | 92 |
11 | 48 | 12 | 11 | 46 |
11 | 48 | 18 | 13 | 44 |
11 | 48 | 24 | 15 | 43 |
11 | 48 | 36 | 19 | 55 |
11 | 48 | 48 | 23 | 70 |
11 | 48 | 72 | 31 | 93 |
11 | 60 | 12 | 11 | 46 |
11 | 60 | 18 | 13 | 45 |
11 | 60 | 24 | 15 | 43 |
11 | 60 | 36 | 19 | 56 |
11 | 60 | 48 | 23 | 70 |
11 | 60 | 72 | 31 | 94 |
11 | 72 | 12 | 11 | 46 |
11 | 72 | 18 | 13 | 45 |
11 | 72 | 24 | 15 | 43 |
11 | 72 | 36 | 19 | 56 |
11 | 72 | 48 | 23 | 70 |
11 | 72 | 72 | 31 | 94 |
11 | 144 | 12 | 11 | 47 |
11 | 144 | 18 | 13 | 45 |
11 | 144 | 24 | 15 | 44 |
11 | 144 | 36 | 19 | 57 |
11 | 144 | 48 | 23 | 71 |
11 | 144 | 72 | 31 | 95 |
11 | 288 | 12 | 11 | 47 |
11 | 288 | 18 | 13 | 46 |
11 | 288 | 24 | 15 | 44 |
11 | 288 | 36 | 19 | 57 |
11 | 288 | 48 | 23 | 72 |
11 | 288 | 72 | 31 | 96 |
11 | 576 | 12 | 11 | 47 |
11 | 576 | 18 | 13 | 46 |
11 | 576 | 24 | 15 | 44 |
11 | 576 | 36 | 19 | 57 |
11 | 576 | 48 | 23 | 72 |
11 | 576 | 72 | 31 | 96 |
12 | 6 | 12 | 11 | 37 |
12 | 6 | 18 | 13 | 35 |
12 | 6 | 24 | 15 | 34 |
12 | 6 | 36 | 19 | 44 |
12 | 6 | 48 | 23 | 55 |
12 | 6 | 72 | 31 | 73 |
12 | 12 | 12 | 11 | 41 |
12 | 12 | 18 | 13 | 40 |
12 | 12 | 24 | 15 | 38 |
12 | 12 | 36 | 19 | 50 |
12 | 12 | 48 | 23 | 62 |
12 | 12 | 72 | 31 | 83 |
12 | 18 | 12 | 11 | 43 |
12 | 18 | 18 | 13 | 41 |
12 | 18 | 24 | 15 | 40 |
12 | 18 | 36 | 19 | 52 |
12 | 18 | 48 | 23 | 65 |
12 | 18 | 72 | 31 | 87 |
12 | 24 | 12 | 11 | 44 |
12 | 24 | 18 | 13 | 42 |
12 | 24 | 24 | 15 | 41 |
12 | 24 | 36 | 19 | 53 |
12 | 24 | 48 | 23 | 66 |
12 | 24 | 72 | 31 | 89 |
12 | 30 | 12 | 11 | 44 |
12 | 30 | 18 | 13 | 43 |
12 | 30 | 24 | 15 | 41 |
12 | 30 | 36 | 19 | 54 |
12 | 30 | 48 | 23 | 67 |
12 | 30 | 72 | 31 | 90 |
12 | 36 | 12 | 11 | 45 |
12 | 36 | 18 | 13 | 43 |
12 | 36 | 24 | 15 | 42 |
12 | 36 | 36 | 19 | 54 |
12 | 36 | 48 | 23 | 68 |
12 | 36 | 72 | 31 | 91 |
12 | 48 | 12 | 11 | 45 |
12 | 48 | 18 | 13 | 44 |
12 | 48 | 24 | 15 | 42 |
12 | 48 | 36 | 19 | 55 |
12 | 48 | 48 | 23 | 69 |
12 | 48 | 72 | 31 | 92 |
12 | 60 | 12 | 11 | 46 |
12 | 60 | 18 | 13 | 44 |
12 | 60 | 24 | 15 | 42 |
12 | 60 | 36 | 19 | 55 |
12 | 60 | 48 | 23 | 69 |
12 | 60 | 72 | 31 | 93 |
12 | 72 | 12 | 11 | 46 |
12 | 72 | 18 | 13 | 44 |
12 | 72 | 24 | 15 | 43 |
12 | 72 | 36 | 19 | 55 |
12 | 72 | 48 | 23 | 69 |
12 | 72 | 72 | 31 | 93 |
12 | 144 | 12 | 11 | 46 |
12 | 144 | 18 | 13 | 45 |
12 | 144 | 24 | 15 | 43 |
12 | 144 | 36 | 19 | 56 |
12 | 144 | 48 | 23 | 70 |
12 | 144 | 72 | 31 | 94 |
12 | 288 | 12 | 11 | 47 |
12 | 288 | 18 | 13 | 45 |
12 | 288 | 24 | 15 | 43 |
12 | 288 | 36 | 19 | 56 |
12 | 288 | 48 | 23 | 71 |
12 | 288 | 72 | 31 | 95 |
12 | 576 | 12 | 11 | 47 |
12 | 576 | 18 | 13 | 45 |
12 | 576 | 24 | 15 | 43 |
12 | 576 | 36 | 19 | 57 |
12 | 576 | 48 | 23 | 71 |
12 | 576 | 72 | 31 | 95 |
15 | 6 | 12 | 11 | 35 |
15 | 6 | 18 | 13 | 33 |
15 | 6 | 24 | 15 | 32 |
15 | 6 | 36 | 19 | 42 |
15 | 6 | 48 | 23 | 52 |
15 | 6 | 72 | 31 | 69 |
15 | 12 | 12 | 11 | 39 |
15 | 12 | 18 | 13 | 38 |
15 | 12 | 24 | 15 | 36 |
15 | 12 | 36 | 19 | 47 |
15 | 12 | 48 | 23 | 59 |
15 | 12 | 72 | 31 | 80 |
15 | 18 | 12 | 11 | 41 |
15 | 18 | 18 | 13 | 40 |
15 | 18 | 24 | 15 | 38 |
15 | 18 | 36 | 19 | 50 |
15 | 18 | 48 | 23 | 62 |
15 | 18 | 72 | 31 | 83 |
15 | 24 | 12 | 11 | 42 |
15 | 24 | 18 | 13 | 40 |
15 | 24 | 24 | 15 | 39 |
15 | 24 | 36 | 19 | 51 |
15 | 24 | 48 | 23 | 64 |
15 | 24 | 72 | 31 | 86 |
15 | 30 | 12 | 11 | 42 |
15 | 30 | 18 | 13 | 41 |
15 | 30 | 24 | 15 | 40 |
15 | 30 | 36 | 19 | 51 |
15 | 30 | 48 | 23 | 65 |
15 | 30 | 72 | 31 | 87 |
15 | 36 | 12 | 11 | 43 |
15 | 36 | 18 | 13 | 41 |
15 | 36 | 24 | 15 | 40 |
15 | 36 | 36 | 19 | 52 |
15 | 36 | 48 | 23 | 65 |
15 | 36 | 72 | 31 | 88 |
15 | 48 | 12 | 11 | 43 |
15 | 48 | 18 | 13 | 42 |
15 | 48 | 24 | 15 | 40 |
15 | 48 | 36 | 19 | 53 |
15 | 48 | 48 | 23 | 66 |
15 | 48 | 72 | 31 | 89 |
15 | 60 | 12 | 11 | 44 |
15 | 60 | 18 | 13 | 42 |
15 | 60 | 24 | 15 | 41 |
15 | 60 | 36 | 19 | 53 |
15 | 60 | 48 | 23 | 67 |
15 | 60 | 72 | 31 | 90 |
15 | 72 | 12 | 11 | 44 |
15 | 72 | 18 | 13 | 42 |
15 | 72 | 24 | 15 | 41 |
15 | 72 | 36 | 19 | 53 |
15 | 72 | 48 | 23 | 67 |
15 | 72 | 72 | 31 | 90 |
15 | 144 | 12 | 11 | 44 |
15 | 144 | 18 | 13 | 43 |
15 | 144 | 24 | 15 | 41 |
15 | 144 | 36 | 19 | 54 |
15 | 144 | 48 | 23 | 68 |
15 | 144 | 72 | 31 | 92 |
15 | 288 | 12 | 11 | 45 |
15 | 288 | 18 | 13 | 43 |
15 | 288 | 24 | 15 | 42 |
15 | 288 | 36 | 19 | 54 |
15 | 288 | 48 | 23 | 68 |
15 | 288 | 72 | 31 | 92 |
15 | 576 | 12 | 11 | 45 |
15 | 576 | 18 | 13 | 43 |
15 | 576 | 24 | 15 | 42 |
15 | 576 | 36 | 19 | 55 |
15 | 576 | 48 | 23 | 69 |
15 | 576 | 72 | 31 | 92 |