Skip to content

Commit

Permalink
Merge pull request from GHSA-wg2x-rv86-mmpx
Browse files Browse the repository at this point in the history
Add coinbase transaction proof to SPV proof
  • Loading branch information
lukasz-zimnoch authored Jan 15, 2024
2 parents 44d9fc8 + 742cce3 commit 6dc5bfe
Show file tree
Hide file tree
Showing 11 changed files with 731 additions and 11 deletions.
24 changes: 19 additions & 5 deletions solidity/contracts/bridge/BitcoinTx.sol
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,11 @@ library BitcoinTx {
/// @notice Single byte-string of 80-byte bitcoin headers,
/// lowest height first.
bytes bitcoinHeaders;
/// @notice The sha256 preimage of the coinbase tx hash
/// i.e. the sha256 hash of the coinbase transaction.
bytes32 coinbasePreimage;
/// @notice The merkle proof of the coinbase transaction.
bytes coinbaseProof;
// This struct doesn't contain `__gap` property as the structure is not
// stored, it is used as a function's calldata argument.
}
Expand Down Expand Up @@ -186,6 +191,10 @@ library BitcoinTx {
txInfo.outputVector.validateVout(),
"Invalid output vector provided"
);
require(
proof.merkleProof.length == proof.coinbaseProof.length,
"Tx not on same level of merkle tree as coinbase"
);

txHash = abi
.encodePacked(
Expand All @@ -196,15 +205,20 @@ library BitcoinTx {
)
.hash256View();

bytes32 root = proof.bitcoinHeaders.extractMerkleRootLE();

require(
txHash.prove(
proof.bitcoinHeaders.extractMerkleRootLE(),
proof.merkleProof,
proof.txIndexInBlock
),
txHash.prove(root, proof.merkleProof, proof.txIndexInBlock),
"Tx merkle proof is not valid for provided header and tx hash"
);

bytes32 coinbaseHash = sha256(abi.encodePacked(proof.coinbasePreimage));

require(
coinbaseHash.prove(root, proof.coinbaseProof, 0),
"Coinbase merkle proof is not valid for provided header and hash"
);

evaluateProofDifficulty(self, proof.bitcoinHeaders);

return txHash;
Expand Down
8 changes: 4 additions & 4 deletions solidity/contracts/maintainer/MaintainerProxy.sol
Original file line number Diff line number Diff line change
Expand Up @@ -159,12 +159,12 @@ contract MaintainerProxy is Ownable, Reimbursable {
constructor(Bridge _bridge, ReimbursementPool _reimbursementPool) {
bridge = _bridge;
reimbursementPool = _reimbursementPool;
submitDepositSweepProofGasOffset = 27000;
submitRedemptionProofGasOffset = 0;
submitDepositSweepProofGasOffset = 30000;
submitRedemptionProofGasOffset = 4000;
resetMovingFundsTimeoutGasOffset = 1000;
submitMovingFundsProofGasOffset = 15000;
submitMovingFundsProofGasOffset = 20000;
notifyMovingFundsBelowDustGasOffset = 3500;
submitMovedFundsSweepProofGasOffset = 22000;
submitMovedFundsSweepProofGasOffset = 26000;
requestNewWalletGasOffset = 3500;
notifyWalletCloseableGasOffset = 4000;
notifyWalletClosingPeriodElapsedGasOffset = 3000;
Expand Down
24 changes: 24 additions & 0 deletions solidity/contracts/test/TestBitcoinTx.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// SPDX-License-Identifier: GPL-3.0-only

pragma solidity 0.8.17;

import "../bridge/BitcoinTx.sol";
import "../bridge/BridgeState.sol";
import "../bridge/IRelay.sol";

contract TestBitcoinTx {
BridgeState.Storage internal self;

event ProofValidated(bytes32 txHash);

constructor(address _relay) {
self.relay = IRelay(_relay);
}

function validateProof(
BitcoinTx.Info calldata txInfo,
BitcoinTx.Proof calldata proof
) external {
emit ProofValidated(BitcoinTx.validateProof(self, txInfo, proof));
}
}
116 changes: 116 additions & 0 deletions solidity/test/bridge/BitcoinTx.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import { ethers, helpers } from "hardhat"
import { expect } from "chai"
import { ContractTransaction } from "ethers"
import type { SystemTestRelay, TestBitcoinTx } from "../../typechain"
import { assertGasUsed } from "../integration/utils/gas"

const { createSnapshot, restoreSnapshot } = helpers.snapshot

describe("BitcoinTx", () => {
let relay: SystemTestRelay
let bitcoinTx: TestBitcoinTx

before(async () => {
const SystemTestRelay = await ethers.getContractFactory("SystemTestRelay")
relay = await SystemTestRelay.deploy()

const TestBitcoinTx = await ethers.getContractFactory("TestBitcoinTx")
bitcoinTx = await TestBitcoinTx.deploy(relay.address)
})

describe("validateProof", () => {
context("when used with a valid but long proof", () => {
let tx: ContractTransaction

// Source: https://github.com/keep-network/bitcoin-spv/blob/releases/mainnet/solidity/v3.4.0-solc-0.8/testVectors.json#L910-L916
const testData = {
txInfo: {
version: "0x01000000",
inputVector:
"0x011746bd867400f3494b8f44c24b83e1aa58c4f0ff25b4a61cffeffd4bc" +
"0f9ba300000000000ffffffff",
outputVector:
"0x024897070000000000220020a4333e5612ab1a1043b25755c89b16d5518" +
"4a42f81799e623e6bc39db8539c180000000000000000166a14edb1b5c2f3" +
"9af0fec151732585b1049b07895211",
locktime: "0x00000000",
},
proof: {
merkleProof:
"0xe35a0d6de94b656694589964a252957e4673a9fb1d2f8b4a92e3f0a7bb6" +
"54fddb94e5a1e6d7f7f499fd1be5dd30a73bf5584bf137da5fdd77cc21aeb" +
"95b9e35788894be019284bd4fbed6dd6118ac2cb6d26bc4be4e423f55a3a4" +
"8f2874d8d02a65d9c87d07de21d4dfe7b0a9f4a23cc9a58373e9e6931fefd" +
"b5afade5df54c91104048df1ee999240617984e18b6f931e2373673d0195b" +
"8c6987d7ff7650d5ce53bcec46e13ab4f2da1146a7fc621ee672f62bc2274" +
"2486392d75e55e67b09960c3386a0b49e75f1723d6ab28ac9a2028a0c7286" +
"6e2111d79d4817b88e17c821937847768d92837bae3832bb8e5a4ab4434b9" +
"7e00a6c10182f211f592409068d6f5652400d9a3d1cc150a7fb692e874cc4" +
"2d76bdafc842f2fe0f835a7c24d2d60c109b187d64571efbaa8047be85821" +
"f8e67e0e85f2f5894bc63d00c2ed9d64",
txIndexInBlock: 281,
bitcoinHeaders:
"0x0000002073bd2184edd9c4fc76642ea6754ee40136970efc10c41900000" +
"00000000000000296ef123ea96da5cf695f22bf7d94be87d49db1ad7ac371" +
"ac43c4da4161c8c216349c5ba11928170d38782b00000020fe70e48339d6b" +
"17fbbf1340d245338f57336e97767cc240000000000000000005af53b865c" +
"27c6e9b5e5db4c3ea8e024f8329178a79ddb39f7727ea2fe6e6825d1349c5" +
"ba1192817e2d9515900000020baaea6746f4c16ccb7cd961655b636d39b5f" +
"e1519b8f15000000000000000000c63a8848a448a43c9e4402bd893f701cd" +
"11856e14cbbe026699e8fdc445b35a8d93c9c5ba1192817b945dc6c000000" +
"20f402c0b551b944665332466753f1eebb846a64ef24c7170000000000000" +
"0000033fc68e070964e908d961cd11033896fa6c9b8b76f64a2db7ea928af" +
"a7e304257d3f9c5ba11928176164145d0000ff3f63d40efa46403afd71a25" +
"4b54f2b495b7b0164991c2d22000000000000000000f046dc1b71560b7d07" +
"86cfbdb25ae320bd9644c98d5c7c77bf9df05cbe96212758419c5ba119281" +
"7a2bb2caa00000020e2d4f0edd5edd80bdcb880535443747c6b22b48fb620" +
"0d0000000000000000001d3799aa3eb8d18916f46bf2cf807cb89a9b1b4c5" +
"6c3f2693711bf1064d9a32435429c5ba1192817752e49ae",
coinbasePreimage:
"0x77b98a5e6643973bba49dda18a75140306d2d8694b66f2dcb3561ad5aff" +
"0b0c7",
coinbaseProof:
"0xdc20dadef477faab2852f2f8ae0c826aa7e05c4de0d36f0e63630429554" +
"884c371da5974b6f34fa2c3536738f031b49f34e0c9d084d7280f26212e39" +
"007ebe9ea0870c312745b58128a00a6557851e987ece02294d156f0020336" +
"e158928e8964292642c6c4dc469f34b7bacf2d8c42115bab6afc9067f2ed3" +
"0e8749729b63e0889e203ee58e355903c1e71f78c008df6c3597b2cc66d0b" +
"8aae1a4a33caa775498e531cfb6af58e87db99e0f536dd226d18f43e38641" +
"48ba5b7faca5c775f10bc810c602e1af2195a34577976921ce009a4ddc0a0" +
"7f605c96b0f5fcf580831ebbe01a31fa29bde884609d286dccfa5ba8e558c" +
"e3125bd4c3a19e888cf26852286202d2a7d302c75e0ff5ca8fe7299fb0d9d" +
"1132bf2c56c2e3b73df799286193d60c109b187d64571efbaa8047be85821" +
"f8e67e0e85f2f5894bc63d00c2ed9d64",
},
txHash:
"0x48e5a1a0e616d8fd92b4ef228c424e0c816799a256c6a90892195ccfc53300d6",
}

before(async () => {
await createSnapshot()

await relay.setCurrentEpochDifficultyFromHeaders(
testData.proof.bitcoinHeaders
)

tx = await bitcoinTx.validateProof(testData.txInfo, testData.proof)
})

after(async () => {
await restoreSnapshot()
})

it("should validate the proof with success", async () => {
await expect(tx)
.to.emit(bitcoinTx, "ProofValidated")
.withArgs(
"0x48e5a1a0e616d8fd92b4ef228c424e0c816799a256c6a90892195ccfc53300d6"
)
})

it("should consume around 95000 gas", async () => {
await assertGasUsed(tx, 95000, 1000)
})
})
})
})
101 changes: 100 additions & 1 deletion solidity/test/bridge/Bridge.Deposit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3395,6 +3395,18 @@ describe("Bridge - Deposit", () => {
"0x0000e020fbeb3a876746438f1fd793add061b0b7af2f88a387ee" +
"f5b38600000000000000933a0cec98a028727df04dafbbe691c8ad" +
"442351db7321c9f7cc169aa9f64a9a7af6f361cbcd001a65073028",
coinbasePreimage:
"0xe774cd2615268932bf6124630c72313bd7f89f1a8ea2e18e09f1" +
"efefdb78b57c",
coinbaseProof:
"0x1b37fa565263a660309b37f0388d9851bde7c555030091a511af" +
"3f76e547f998364e95feeb9b08f5792ed93641ee32ac35b6cc5d7a" +
"e003634203101f249628a72a30e79e606506ca0c8603f2ad5f8bcf" +
"94b16de2dda71889317fbb1d370863e0cf4e8b68b37a1d56d186b1" +
"d0937333b5e219a5aeac722cab81dcf99dbf44c0063190440e6a92" +
"4fd5622bd7c1e192a8413dabc931f974fde0e2d8bd0dda33264182" +
"be8dab2401ec758a705b648724f93d14c3b72ce4fb3cd7d414e8a1" +
"75ef173e",
}

await expect(
Expand Down Expand Up @@ -3429,7 +3441,7 @@ describe("Bridge - Deposit", () => {
it("should revert", async () => {
// To test this case, an arbitrary transaction with two
// outputs is used. Used transaction:
// https://live.blockcypher.com/btc-testnet/tx/af56cae479215c5e44a6a4db0eeb10a1abdd98020a6c01b9c26ea7b829aa2809
// https://live.blockcypher.com/btc-testnet/tx/c580e0e352570d90e303d912a506055ceeb0ee06f97dce6988c69941374f5479
const sweepTx = {
version: "0x01000000",
inputVector:
Expand Down Expand Up @@ -3462,6 +3474,16 @@ describe("Bridge - Deposit", () => {
"e49585b4cd8a94daeeb926c6f1e96151c74ae1ae0b18c6a6d564000000" +
"0065c05d9ea40cace1b6b0ad0b8a9a18646096b54484fbdd96b1596560" +
"f6999194a815da612ac0001a2e4c6405",
coinbasePreimage:
"0x35175fcdae1fc3d708454466b4512536495526328679c1eb65d6068d" +
"f25119a9",
coinbaseProof:
"0x6c4b2539848240a0e5ebe398adb6f1e12b6c097055b50f7421fe9a33" +
"1129b11f14c82d817a4f9ca5c6713f8a2d660f7f4364833c5a8452d1fb" +
"f0529c889bec6b20fc2c08cfba8c87c53db2595c19a6721968bb858ea8" +
"4da7e0dbcb9647fa55054cd5775e08a11ad69238c23f9d5a4349672691" +
"b6d7a9b04462a16bb3dc7ab4b0f8b7276402b6c114000c59149494f852" +
"84507c253bbc505fec7ea50f370aa150",
}

await expect(
Expand Down Expand Up @@ -3560,6 +3582,47 @@ describe("Bridge - Deposit", () => {
})
})

context(
"when transaction is not on same level of merkle tree as coinbase",
() => {
const data: DepositSweepTestData = JSON.parse(
JSON.stringify(SingleP2SHDeposit)
)
// Take wallet public key hash from first deposit. All
// deposits in same sweep batch should have the same value
// of that field.
const { walletPubKeyHash } = data.deposits[0].reveal

before(async () => {
await createSnapshot()

// Simulate the wallet is a Live one and is known in
// the system.
await bridge.setWallet(walletPubKeyHash, {
...walletDraft,
state: walletState.Live,
})
})

after(async () => {
await restoreSnapshot()
})

it("should revert", async () => {
// Simulate that the proven transaction is deeper in the merkle tree
// than the coinbase. This is achieved by appending additional
// hashes to the merkle proof.
data.sweepProof.merkleProof +=
ethers.utils.sha256("0x01").substring(2) +
ethers.utils.sha256("0x02").substring(2)

await expect(runDepositSweepScenario(data)).to.be.revertedWith(
"Tx not on same level of merkle tree as coinbase"
)
})
}
)

context("when merkle proof is not valid", () => {
const data: DepositSweepTestData = JSON.parse(
JSON.stringify(SingleP2SHDeposit)
Expand Down Expand Up @@ -3595,6 +3658,42 @@ describe("Bridge - Deposit", () => {
})
})

context("when coinbase merkle proof is not valid", () => {
const data: DepositSweepTestData = JSON.parse(
JSON.stringify(SingleP2SHDeposit)
)
// Take wallet public key hash from first deposit. All
// deposits in same sweep batch should have the same value
// of that field.
const { walletPubKeyHash } = data.deposits[0].reveal

before(async () => {
await createSnapshot()

// Simulate the wallet is a Live one and is known in
// the system.
await bridge.setWallet(walletPubKeyHash, {
...walletDraft,
state: walletState.Live,
})
})

after(async () => {
await restoreSnapshot()
})

it("should revert", async () => {
// Corrupt the coinbase preimage.
data.sweepProof.coinbasePreimage = ethers.utils.sha256(
data.sweepProof.coinbasePreimage
)

await expect(runDepositSweepScenario(data)).to.be.revertedWith(
"Coinbase merkle proof is not valid for provided header and hash"
)
})
})

context("when proof difficulty is not current nor previous", () => {
const data: DepositSweepTestData = JSON.parse(
JSON.stringify(SingleP2SHDeposit)
Expand Down
Loading

0 comments on commit 6dc5bfe

Please sign in to comment.