Skip to content

Commit

Permalink
Claim pegin (#165)
Browse files Browse the repository at this point in the history
* added factory method FromPublicKeys for creating multiscript payment

* merkle tree v1-unstable

* invalid peg-in tx, wip

* refactor after review

* first working claim-pegin version

* skipping test since it requires nigiri

* comments, test fix

* removing todos
  • Loading branch information
sekulicd authored May 24, 2021
1 parent fc6154a commit 69bc2b8
Show file tree
Hide file tree
Showing 9 changed files with 959 additions and 32 deletions.
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ help:

test:
export API_URL=http://localhost:3001; \
export API_BTC_URL=http://localhost:3000; \
go test -count=1 -v ./...
247 changes: 247 additions & 0 deletions block/merkle_block.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,247 @@
package block

import (
"bytes"
"encoding/hex"
"errors"

"github.com/btcsuite/btcd/wire"

"github.com/btcsuite/btcd/blockchain"

"github.com/btcsuite/btcd/chaincfg/chainhash"
)

const (
// The maximum allowed weight for a block, see BIP 141 (network rule)
maxBlockWeight = 4000000
witnessScaleFactor = 4
minTransactionWeight = witnessScaleFactor * 60 // 60 is the lower bound for the size of a valid serialized tx

)

type MerkleBlock struct {
BlockHeader *wire.BlockHeader
PartialMerkleTree *PartialMerkleTree
}

type PartialMerkleTree struct {
TxTotalCount uint32
TxHashes [][]byte
FBad bool
VBits []bool
}

func NewMerkleBlockFromBuffer(buf *bytes.Buffer) (*MerkleBlock, error) {
return deserializeMerkleBlock(buf)
}

func NewMerkleBlockFromHex(h string) (*MerkleBlock, error) {
hexBytes, err := hex.DecodeString(h)
if err != nil {
return nil, err
}
buf := bytes.NewBuffer(hexBytes)
return NewMerkleBlockFromBuffer(buf)
}

func (m *MerkleBlock) ExtractMatches() (*chainhash.Hash, []chainhash.Hash, error) {
vMatch := make([]chainhash.Hash, 0)

if m.PartialMerkleTree.TxTotalCount == 0 {
return nil, nil, errors.New("tx count equal 0")
}

if m.PartialMerkleTree.TxTotalCount > maxBlockWeight/minTransactionWeight {
return nil, nil, errors.New("invalid tx count")
}
if len(m.PartialMerkleTree.TxHashes) > int(m.PartialMerkleTree.TxTotalCount) {
return nil, nil, errors.New(
"there can never be more hashes provided than one for every txid",
)
}

if len(m.PartialMerkleTree.VBits) < len(m.PartialMerkleTree.TxHashes) {
return nil, nil, errors.New(
"there must be at least one bit per node in the partial tree, " +
"and at least one node per hash",
)
}

// Calculate the number of merkle branches (height) in the tree.
height := uint32(0)
for m.calcTreeWidth(height) > 1 {
height++
}

var bitsUsed, hashUsed, position = 0, 0, 0
hashMerkleRoot, err := m.traverseAndExtract(
height,
position,
&bitsUsed,
&hashUsed,
&vMatch,
)
if err != nil {
return nil, nil, err
}

if m.PartialMerkleTree.FBad {
return nil, nil, errors.New(
"there must be at least one bit per node in the partial tree, " +
"and at least one node per hash",
)
}
// verify that all bits were consumed (except for the padding caused by serializing it as a byte sequence)
if (bitsUsed+7)/8 != (len(m.PartialMerkleTree.VBits)+7)/8 {
return nil, nil, errors.New(
"except for the padding caused by serializing it as a byte" +
" sequence, not all bits were consumed",
)
}
// verify that all hashes were consumed
if hashUsed != len(m.PartialMerkleTree.TxHashes) {
return nil, nil, errors.New("not all hashes were consumed")
}

return hashMerkleRoot, vMatch, nil
}

func (m *MerkleBlock) traverseAndExtract(
height uint32,
position int,
bitsUsed *int,
hashUsed *int,
vMatch *[]chainhash.Hash,
) (*chainhash.Hash, error) {
if *bitsUsed >= len(m.PartialMerkleTree.VBits) {
m.PartialMerkleTree.FBad = true
return nil, errors.New("overflowed the bits array")
}

fParentOfMatch := m.PartialMerkleTree.VBits[*bitsUsed]
*bitsUsed++
if height == 0 || !fParentOfMatch {
// if at height 0, or nothing interesting below, use stored hash and do not descend
if *hashUsed >= len(m.PartialMerkleTree.TxHashes) {
m.PartialMerkleTree.FBad = true
return nil, errors.New("overflowed the hash array")
}
hash, err := chainhash.NewHash(m.PartialMerkleTree.TxHashes[*hashUsed])
if err != nil {
return nil, err
}

*hashUsed++
if height == 0 && fParentOfMatch { // in case of height 0, we have a matched txid
*vMatch = append(*vMatch, *hash)
}

return hash, nil
} else {
//otherwise, descend into the subtrees to extract matched txids and hashes
left, err := m.traverseAndExtract(
height-1,
position*2,
bitsUsed,
hashUsed,
vMatch,
)
if err != nil {
return nil, err
}
var right *chainhash.Hash
if position*2+1 < int(m.calcTreeWidth(height-1)) {
right, err = m.traverseAndExtract(
height-1,
position*2+1,
bitsUsed,
hashUsed,
vMatch,
)
if err != nil {
return nil, err
}
if left.IsEqual(right) {
// The left and right branches should never be identical, as the transaction
// hashes covered by them must each be unique.
m.PartialMerkleTree.FBad = true
}
} else {
right = left
}

return blockchain.HashMerkleBranches(left, right), nil
}
}

// calcTreeWidth calculates and returns the the number of nodes (width) or a
// merkle tree at the given depth-first height.
func (m *MerkleBlock) calcTreeWidth(height uint32) uint32 {
return (m.PartialMerkleTree.TxTotalCount + (1 << height) - 1) >> height
}

func deserializePartialMerkleTree(
mb wire.MsgMerkleBlock,
) (*PartialMerkleTree, error) {
txHashes := make([][]byte, 0, len(mb.Hashes))
for _, v := range mb.Hashes {

txHashes = append(txHashes, v.CloneBytes())
}

return &PartialMerkleTree{
TxTotalCount: mb.Transactions,
TxHashes: txHashes,
FBad: false,
VBits: serializeVBits(mb.Flags),
}, nil
}

func deserializeMerkleBlock(buf *bytes.Buffer) (*MerkleBlock, error) {
mb := wire.MsgMerkleBlock{}
err := mb.BtcDecode(buf, wire.ProtocolVersion, wire.LatestEncoding)
if err != nil {
return nil, err
}

partialMerkleTree, err := deserializePartialMerkleTree(mb)
if err != nil {
return nil, err
}

return &MerkleBlock{
BlockHeader: &mb.Header,
PartialMerkleTree: partialMerkleTree,
}, nil
}

func serializeVBits(b []byte) []bool {
bits := make([]bool, 0)

for _, v := range b {
l := byteToBits(v)
for _, v := range l {
if v == 1 {
bits = append(bits, true)
} else {
bits = append(bits, false)
}
}
}

return bits
}

func byteToBits(b byte) []byte {
return []byte{
(b >> 0) & 0x1,
(b >> 1) & 0x1,
(b >> 2) & 0x1,
(b >> 3) & 0x1,
(b >> 4) & 0x1,
(b >> 5) & 0x1,
(b >> 6) & 0x1,
(b >> 7) & 0x1,
}
}
44 changes: 44 additions & 0 deletions block/merkle_block_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package block

import (
"encoding/hex"
"testing"

"github.com/stretchr/testify/assert"

"github.com/vulpemventures/go-elements/elementsutil"
)

func TestDeserializeMerkleBlock(t *testing.T) {
txOutProof := "0000003095a03ddcb18a359a1d41072ed373d45e57200f57d6f318238a7a6eb18df4c02dd4187f08f314f8436ac76f38cbe03a791a0893f26444ea5d5ec8f5b9b95a93015280ab60ffff7f20010000000200000002bdde18f707d02aa18ba82926965dc8bb8991d9510cd98e2812cc44b7aae8d3959745d887c8459cd4441f1dfe1a2d7ecbe225db87eeaa68bb22df6fdbf7017c480105"

merkleBlock, err := NewMerkleBlockFromHex(txOutProof)
if err != nil {
t.Fatal(err)
}

for _, v := range merkleBlock.PartialMerkleTree.TxHashes {
t.Log(hex.EncodeToString(elementsutil.ReverseBytes(v)))
}
}

func TestExtractMatches(t *testing.T) {
txOutProof := "0000003095a03ddcb18a359a1d41072ed373d45e57200f57d6f318238a7a6eb18df4c02dd4187f08f314f8436ac76f38cbe03a791a0893f26444ea5d5ec8f5b9b95a93015280ab60ffff7f20010000000200000002bdde18f707d02aa18ba82926965dc8bb8991d9510cd98e2812cc44b7aae8d3959745d887c8459cd4441f1dfe1a2d7ecbe225db87eeaa68bb22df6fdbf7017c480105"

merkleBlock, err := NewMerkleBlockFromHex(txOutProof)
if err != nil {
t.Fatal(err)
}

hashMerkleRoot, matchedHashes, err := merkleBlock.ExtractMatches()
if err != nil {
t.Fatal(err)
}

assert.Equal(
t,
true,
merkleBlock.BlockHeader.MerkleRoot.IsEqual(hashMerkleRoot),
)
assert.Equal(t, 1, len(matchedHashes))
}
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,6 @@ require (
github.com/btcsuite/btcutil/psbt v1.0.2
github.com/stretchr/testify v1.7.0
github.com/vulpemventures/fastsha256 v0.0.0-20160815193821-637e65642941
github.com/vulpemventures/go-secp256k1-zkp v1.1.1-0.20210422114014-d9d676d706c1
github.com/vulpemventures/go-secp256k1-zkp v1.1.2
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad
)
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,8 @@ github.com/vulpemventures/go-secp256k1-zkp v1.1.0 h1:Z3qKc/lYxEQ1cukwwjD4n7EBCrg
github.com/vulpemventures/go-secp256k1-zkp v1.1.0/go.mod h1:zo7CpgkuPgoe7fAV+inyxsI9IhGmcoFgyD8nqZaPSOM=
github.com/vulpemventures/go-secp256k1-zkp v1.1.1-0.20210422114014-d9d676d706c1 h1:9rPKzVfNwHve4TkCtcECzT1/zTCuN90K08VXjaI0bb0=
github.com/vulpemventures/go-secp256k1-zkp v1.1.1-0.20210422114014-d9d676d706c1/go.mod h1:zo7CpgkuPgoe7fAV+inyxsI9IhGmcoFgyD8nqZaPSOM=
github.com/vulpemventures/go-secp256k1-zkp v1.1.2 h1:ZfM0r4QtkpQbNywlT9LFXXZBuMQ1Q8QiHGUTz4QV88M=
github.com/vulpemventures/go-secp256k1-zkp v1.1.2/go.mod h1:zo7CpgkuPgoe7fAV+inyxsI9IhGmcoFgyD8nqZaPSOM=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg=
Expand Down
Loading

0 comments on commit 69bc2b8

Please sign in to comment.