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

Client-side deposit sweep: Validate proposals #3521

Merged
merged 33 commits into from
Apr 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
c4ca78f
Add some Bitcoin tooling allowing to handle the on-chain `BitcoinTx.I…
lukasz-zimnoch Apr 13, 2023
8af0fd8
Integrate with the chain function allowing to validate the sweep prop…
lukasz-zimnoch Apr 13, 2023
9b308a3
Merge branch 'deposit-sweep-support' into deposit-sweep-support-2
lukasz-zimnoch Apr 13, 2023
9beb922
Add `convertDepositSweepProposalToAbiType` function
lukasz-zimnoch Apr 13, 2023
b498888
Merge branch 'deposit-sweep-support' into deposit-sweep-support-2
lukasz-zimnoch Apr 13, 2023
6814d17
Merge branch 'main' into deposit-sweep-support-2
lukasz-zimnoch Apr 17, 2023
34af2ad
Introduce the wallet executor along with all necessary changes
lukasz-zimnoch Apr 17, 2023
f9ffe5f
Acquire lock within `walletExecutor.signBatch`
lukasz-zimnoch Apr 18, 2023
88f0381
Fix broken unit tests
lukasz-zimnoch Apr 18, 2023
3c9b4b3
Refactor wallet registry to hold wallet PKH
lukasz-zimnoch Apr 18, 2023
e956587
Fix compilation error
lukasz-zimnoch Apr 18, 2023
aa3c4c9
Make formatter happy
lukasz-zimnoch Apr 18, 2023
8a11564
Replace `walletExecutor` with `walletDispatcher`
lukasz-zimnoch Apr 18, 2023
5e568ec
Outline of the deposit sweep action logic
lukasz-zimnoch Apr 19, 2023
0dd2132
Implement GetBlockNumberByTimestamp function
lukasz-zimnoch Apr 20, 2023
8581aab
Expose `GetDepositRequest` function
lukasz-zimnoch Apr 20, 2023
25ce495
Fix unit tests
lukasz-zimnoch Apr 20, 2023
3a6b61c
Wiring up things within deposit sweep action
lukasz-zimnoch Apr 20, 2023
dabbee3
Add TODO about `depositSweepAction.execute` unit tests
lukasz-zimnoch Apr 20, 2023
4015afb
Add unit tests for `walletRegistry.getWalletByPublicKeyHash`
lukasz-zimnoch Apr 20, 2023
6117fd6
Unit tests for wallet dispatcher
lukasz-zimnoch Apr 20, 2023
e68ed99
Explain the `validateDepositSweepProposal` just-in-case check
lukasz-zimnoch Apr 21, 2023
bae3661
Small docs/logs improvements
lukasz-zimnoch Apr 21, 2023
9b1ce32
Check the bool return value of `getSigningExecutor` just in case
lukasz-zimnoch Apr 21, 2023
08769b5
TODO about making heartbeats a wallet action
lukasz-zimnoch Apr 24, 2023
575f695
Rearrange and document `node`'s fields
lukasz-zimnoch Apr 24, 2023
f49238d
Make formatter happy
lukasz-zimnoch Apr 24, 2023
94d72d7
Add additional explanation for hash orders for `transactionFixture`
lukasz-zimnoch Apr 24, 2023
0bfa45a
Comment about nil `Vault` in `DepositRevealedEvent` and `DepositChain…
lukasz-zimnoch Apr 24, 2023
80b6a3e
Additional explanation for `DepositChainRequest`
lukasz-zimnoch Apr 24, 2023
7a41a8f
Narrow the docstring of `depositSweepProposalConfirmationBlocks`
lukasz-zimnoch Apr 24, 2023
9e52fa1
Improve docs of `GetBlockNumberByTimestamp`
lukasz-zimnoch Apr 24, 2023
9571928
Remove obsolete comment
lukasz-zimnoch Apr 24, 2023
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
7 changes: 7 additions & 0 deletions cmd/start.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package cmd
import (
"context"
"fmt"
"github.com/keep-network/keep-core/pkg/bitcoin/electrum"

"github.com/spf13/cobra"

Expand Down Expand Up @@ -114,6 +115,11 @@ func start(cmd *cobra.Command) error {
// Skip initialization for bootstrap nodes as they are only used for network
// discovery.
if !clientConfig.LibP2P.Bootstrap {
btcChain, err := electrum.Connect(ctx, clientConfig.Bitcoin.Electrum)
if err != nil {
return fmt.Errorf("could not connect to Electrum chain: [%v]", err)
}

storage, err := storage.Initialize(
clientConfig.Storage,
clientConfig.Ethereum.KeyFilePassword,
Expand Down Expand Up @@ -166,6 +172,7 @@ func start(cmd *cobra.Command) error {
err = tbtc.Initialize(
ctx,
tbtcChain,
btcChain,
netProvider,
tbtcKeyStorePersistence,
tbtcDataPersistence,
Expand Down
60 changes: 59 additions & 1 deletion pkg/bitcoin/transaction.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package bitcoin

import "bytes"
import (
"bytes"
"encoding/binary"
"github.com/btcsuite/btcd/wire"
)

// TransactionSerializationFormat represents the Bitcoin transaction
// serialization format.
Expand Down Expand Up @@ -89,6 +93,60 @@ func (t *Transaction) Serialize(
}
}

// SerializeVersion serializes the transaction version to a little-endian
// 4-byte array.
func (t *Transaction) SerializeVersion() [4]byte {
result := [4]byte{}
binary.LittleEndian.PutUint32(result[:], uint32(t.Version))
return result
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would really help to have the tables on the top of type Transaction struct and then for each Serialize* function denote which parts we are serializing.

For example, in SerializeInputs the output has [ tx_in_count | tx_in].

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Honestly, I think that will cause confusion. The tables you mentioned are tailored to describe the BitcoinTx.Info where inputs and outputs are represented as bytes vectors. The bitcoin.Transaction is more specific as it uses helper types like TransactionInput and TransactionOutput that are also documented. Also, all fields of the aforementioned structs have their own docstrings. There are also references to the original Bitcoin docs. Maybe you can provide the places where the docs are confusing or not accurate so I can improve them?

Regarding your example with SerializeInputs, this function has the following docstring:

// SerializeInputs serializes the transaction inputs to a byte array prepended
// with a CompactSizeUint denoting the total number of inputs.

It uses the nomenclature we already have in the bitcoin package so I think it is more consistent than [ tx_in_count | tx_in] which is a bit cryptic in this context.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was looking at the Solidity documentation forBitcoinTx.Info to understand why are we taking 4 bytes or why are we skipping 4 bytes in other functions. The references to developer.bitcoin.org are helpful but IMO they are not as helpful as having the structure described in the code comments as we have in BitcoinTx.Info. It is not a blocker, I can leave this conversation open to marinate for a while to see if we can find a way to port some of those docs to Go.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure! What I want to avoid is putting too many docs as they add additional maintenance work and can obfuscate the picture even more. The Go bitcoin package differs from the Solidity code we were working with in the past. In Solidity, we used to deal with pure bytes quite often so a huge amount of docs was the only way to explain what happens. In Go bitcoin package, the code itself is more verbose. All Bitcoin primitives and their contents are represented by dedicated Go types which have their own documentation. In that context, having a huge block of docs above bitcoin.Transaction just feels a bit redundant.

// SerializeInputs serializes the transaction inputs to a byte array prepended
// with a CompactSizeUint denoting the total number of inputs.
func (t *Transaction) SerializeInputs() []byte {
internal := newInternalTransaction()
internal.fromTransaction(t)

inputsByteSize := wire.VarIntSerializeSize(uint64(len(internal.TxIn)))
for _, txIn := range internal.TxIn {
inputsByteSize += txIn.SerializeSize()
}

// The first 4 bytes are version. The input vector starts at the 5th byte.
startingByte := 4
endingByte := startingByte + inputsByteSize

return t.Serialize(Standard)[startingByte:endingByte]
}

// SerializeOutputs serializes the transaction outputs to a byte array prepended
// with a CompactSizeUint denoting the total number of outputs.
func (t *Transaction) SerializeOutputs() []byte {
internal := newInternalTransaction()
internal.fromTransaction(t)

outputsByteSize := wire.VarIntSerializeSize(uint64(len(internal.TxOut)))
for _, txOut := range internal.TxOut {
outputsByteSize += txOut.SerializeSize()
}

serializedTx := t.Serialize(Standard)

// The last 4 bytes are locktime. The output vector ends just before it.
endingByte := len(serializedTx) - 4
startingByte := endingByte - outputsByteSize

return serializedTx[startingByte:endingByte]
}

// SerializeLocktime serializes the transaction locktime to a little-endian
// 4-byte array.
func (t *Transaction) SerializeLocktime() [4]byte {
result := [4]byte{}
binary.LittleEndian.PutUint32(result[:], t.Locktime)
return result
}

// Deserialize deserializes the given byte array to a Transaction.
func (t *Transaction) Deserialize(data []byte) error {
internal := newInternalTransaction()
Expand Down
73 changes: 72 additions & 1 deletion pkg/bitcoin/transaction_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,72 @@ func TestTransaction_SerializeRoundtrip(t *testing.T) {
}
}

func TestTransaction_SerializeVersion(t *testing.T) {
transaction := transactionFixture(t)

serializedVersion := transaction.SerializeVersion()

testutils.AssertBytesEqual(
t,
hexToSlice(t, "01000000"),
serializedVersion[:],
)
}

func TestTransaction_SerializeInputs(t *testing.T) {
transaction := transactionFixture(t)

serializedInputs := transaction.SerializeInputs()
expectedSerializedInputs := hexToSlice(
t,
"036896f9abcac13ce6bd2b80d125bedf997ff6330e999f2f60"+
"5ea15ea542f2eaf80000000000ffffffffed0ae94da996c6f3b89dfe967675d"+
pdyraga marked this conversation as resolved.
Show resolved Hide resolved
"4808251db93e81022ae9e038d06f92efed400000000c948304502210092327d"+
"dff69a2b8c7ae787c5d590a2f14586089e6339e942d56e82aa42052cd902204"+
"c0d1700ba1ac617da27fee032a57937c9607f0187199ed3c46954df845643d7"+
"012103989d253b17a6a0f41838b84ff0d20e8898f9d7b1a98f2564da4cc29dc"+
"f8581d94c5c14934b98637ca318a4d6e7ca6ffd1690b8e77df6377508f9f0c9"+
"0d000395237576a9148db50eb52063ea9d98b3eac91489a90f738986f68763a"+
"c6776a914e257eccafbc07c381642ce6e7e55120fb077fbed8804e0250162b1"+
"75ac68ffffffffe37f552fc23fa0032bfd00c8eef5f5c22bf85fe4c6e735857"+
"719ff8a4ff66eb80000000000ffffffff",
)

testutils.AssertBytesEqual(
t,
expectedSerializedInputs,
serializedInputs,
)
}

func TestTransaction_SerializeOutputs(t *testing.T) {
transaction := transactionFixture(t)

serializedOutputs := transaction.SerializeOutputs()
expectedSerializedOutputs := hexToSlice(
t,
"0180ed0000000000001600148db50eb52063ea9d98b3eac91489a90f738986f6",
)

testutils.AssertBytesEqual(
t,
expectedSerializedOutputs,
serializedOutputs,
)
}

func TestTransaction_SerializeLocktime(t *testing.T) {
transaction := transactionFixture(t)

serializedLocktime := transaction.SerializeLocktime()

testutils.AssertBytesEqual(
t,
hexToSlice(t, "00000000"),
serializedLocktime[:],
)
}

func TestTransaction_Hash(t *testing.T) {
hash := transactionFixture(t).Hash()

Expand Down Expand Up @@ -99,7 +165,12 @@ func TestTransaction_WitnessHash(t *testing.T) {
}

// transactionFixture returns a real testnet transaction:
// https://live.blockcypher.com/btc-testnet/tx/435d4aff6d4bc34134877bd3213c17970142fdd04d4113d534120033b9eecb2e
// https://live.blockcypher.com/btc-testnet/tx/435d4aff6d4bc34134877bd3213c17970142fdd04d4113d534120033b9eecb2e.
//
// All hashes passed to hexToHash function uses human-friendly bitcoin.ReversedByteOrder
// so, those hashes can be checked in block explorers as is. Based on them, hexToHash
// constructs proper instances of bitcoin.Hash and converts them to
// bitcoin.InternalByteOrder for serialization.
func transactionFixture(t *testing.T) *Transaction {
tx := new(Transaction)

Expand Down
146 changes: 144 additions & 2 deletions pkg/chain/ethereum/ethereum.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@ package ethereum
import (
"context"
"fmt"
"github.com/ethereum/go-ethereum/core/types"
"github.com/hashicorp/go-multierror"
"math/big"
"sync"

"github.com/hashicorp/go-multierror"
"time"

"github.com/ethereum/go-ethereum/accounts/keystore"
"github.com/ethereum/go-ethereum/ethclient"
Expand Down Expand Up @@ -318,6 +319,147 @@ func (bc *baseChain) OperatorKeyPair() (
return privateKey, publicKey, nil
}

// GetBlockNumberByTimestamp gets the block number for the given timestamp.
// In the best case, the block with the exact same timestamp is returned.
// If the aforementioned is not possible, it tries to return the closest
// possible block.
//
// WARNING: THIS FUNCTION MAY NOT BE PERFORMANT FOR BLOCKS EARLIER THAN 15537393
// (SEPTEMBER 15, 2022, AT 1:42:42 EST) BEFORE THE ETH2 MERGE. PRE-MERGE
// AVERAGE BLOCK TIME WAS HIGHER THAN THE VALUE ASSUMED WITHIN THIS FUNCTION
// SO MORE OVERSHOOTS WILL BE DONE DURING THE BLOCK PREDICTION. OVERSHOOTS
// MUST BE COMPENSATED BY ADDITIONAL CLIENT CALLS THAT TAKE TIME.
func (bc *baseChain) GetBlockNumberByTimestamp(
timestamp uint64,
) (uint64, error) {
block, err := bc.currentBlock()
if err != nil {
return 0, fmt.Errorf("cannot get current block: [%v]", err)
}

if block.Time() < timestamp {
return 0, fmt.Errorf("requested timestamp is in the future")
}

// Corner case shortcut.
if block.Time() == timestamp {
return block.NumberU64(), nil
}

// The Ethereum average block time (https://etherscan.io/chart/blocktime)
// is slightly over 12s most of the time. If we assume it to be 12s here,
// the below for-loop will often overshoot and hit blocks that are before
// the requested timestamp. That means we will often fall into the second
// compensation for-loop that walks forward block by block thus more
// client calls will be made and more time will be needed. Instead of that,
// we are assuming a slightly greater average block time than the actual
// one. In this case, the below for-loop will often end with blocks
// that are slightly after the requested timestamp. To compensate this case,
// we can just compare the obtained block with the previous one and chose
// the better one.
const averageBlockTime = 13

for block.Time() > timestamp {
// timeDiff is always >0 due to the for-loop condition.
timeDiff := block.Time() - timestamp
// blockDiff is an integer whose value can be:
// - >=1 if timeDiff >= averageBlockTime
// - ==0 if timeDiff < averageBlockTime
// It cannot be <0 as timeDiff is always >0.
blockDiff := timeDiff / averageBlockTime
// blockDiff equal to 0 would cause an infinite loop as it would always
// fetch the same block so, we must break.
if blockDiff < 1 {
break
}

block, err = bc.blockByNumber(block.NumberU64() - blockDiff)
if err != nil {
return 0, fmt.Errorf("cannot get block: [%v]", err)
}
}

// Once we quit the above for-loop, the following cases are possible:
// - Case 1: block.Time() < timestamp
// - Case 2: block.Time() > timestamp (difference is < averageBlockTime)
// - Case 3: block.Time() == timestamp
//
// First, try to reduce Case 1 by walking forward block by block until
// we achieve Case 2 or 3.
for block.Time() < timestamp {
block, err = bc.blockByNumber(block.NumberU64() + 1)
if err != nil {
return 0, fmt.Errorf("cannot get block: [%v]", err)
}
}
// At this point, only Case 2 or 3 are possible. If we have Case 2,
// just get the previous block and compare which one lies closer to
// the requested timestamp.
if block.Time() > timestamp {
previousBlock, err := bc.blockByNumber(block.NumberU64() - 1)
if err != nil {
return 0, fmt.Errorf("cannot get block: [%v]", err)
}

return closerBlock(timestamp, previousBlock, block).NumberU64(), nil
}

return block.NumberU64(), nil
}

// currentBlock fetches the current block.
func (bc *baseChain) currentBlock() (*types.Block, error) {
currentBlockNumber, err := bc.blockCounter.CurrentBlock()
if err != nil {
return nil, err
}

currentBlock, err := bc.blockByNumber(currentBlockNumber)
if err != nil {
return nil, err
}

return currentBlock, nil
}

// blockByNumber returns the block for the given block number. Times out
// if the underlying client call takes more than 10 seconds.
func (bc *baseChain) blockByNumber(number uint64) (*types.Block, error) {
ctx, cancelCtx := context.WithTimeout(context.Background(), 10*time.Second)
defer cancelCtx()

return bc.client.BlockByNumber(ctx, big.NewInt(int64(number)))
}

// closerBlock check timestamps of blocks b1 and b2 and returns the block
// whose timestamp lies closer to the requested timestamp. If the distance
// is same for both blocks, the block with greater block number is returned.
func closerBlock(timestamp uint64, b1, b2 *types.Block) *types.Block {
abs := func(x int64) int64 {
if x < 0 {
return -x
}
return x
}

b1Diff := abs(int64(b1.Time() - timestamp))
b2Diff := abs(int64(b2.Time() - timestamp))

// If the differences are same, return the block with greater number.
if b1Diff == b2Diff {
if b2.NumberU64() > b1.NumberU64() {
return b2
}
return b1
}

// Return the block whose timestamp is closer to the requested timestamp.
if b1Diff < b2Diff {
return b1
}
return b2
}

// wrapClientAddons wraps the client instance with add-ons like logging, rate
// limiting and so on.
func wrapClientAddons(
Expand Down
Loading