Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Backport] Add coinbase transaction proof to SPV proof #770

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -2449,6 +2449,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 @@ -2483,7 +2495,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 @@ -2516,6 +2528,16 @@ describe("Bridge - Deposit", () => {
"e49585b4cd8a94daeeb926c6f1e96151c74ae1ae0b18c6a6d564000000" +
"0065c05d9ea40cace1b6b0ad0b8a9a18646096b54484fbdd96b1596560" +
"f6999194a815da612ac0001a2e4c6405",
coinbasePreimage:
"0x35175fcdae1fc3d708454466b4512536495526328679c1eb65d6068d" +
"f25119a9",
coinbaseProof:
"0x6c4b2539848240a0e5ebe398adb6f1e12b6c097055b50f7421fe9a33" +
"1129b11f14c82d817a4f9ca5c6713f8a2d660f7f4364833c5a8452d1fb" +
"f0529c889bec6b20fc2c08cfba8c87c53db2595c19a6721968bb858ea8" +
"4da7e0dbcb9647fa55054cd5775e08a11ad69238c23f9d5a4349672691" +
"b6d7a9b04462a16bb3dc7ab4b0f8b7276402b6c114000c59149494f852" +
"84507c253bbc505fec7ea50f370aa150",
}

await expect(
Expand Down Expand Up @@ -2614,6 +2636,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 @@ -2649,6 +2712,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
Loading