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

Bitcoin relayer improvements for rollkit #15

Open
wants to merge 23 commits into
base: main
Choose a base branch
from
Open
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
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,11 @@ Writer:

A commit transaction containing a taproot with one leaf script

<embedded data>
OP_DROP
OP_FALSE
OP_IF
"roll" marker
<embedded data>
OP_ENDIF
<pubkey>
OP_CHECKSIG

Expand Down
14 changes: 14 additions & 0 deletions generate_block.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Script to generate a new block every second
# Put this script at the root of your unpacked folder
#!/bin/bash

echo "Generating a block every second. Press [CTRL+C] to stop.."

address=`bitcoin-core.cli -regtest -rpcport=18332 -rpcuser=rpcuser -rpcpassword=rpcpass getnewaddress`

while :
do
echo "Generate a new block `date '+%d/%m/%Y %H:%M:%S'`"
bitcoin-core.cli -regtest -rpcport=18332 -rpcuser=rpcuser -rpcpassword=rpcpass generatetoaddress 1 $address
sleep 1
done
231 changes: 161 additions & 70 deletions relayer.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,15 @@ import (
// PROTOCOL_ID allows data identification by looking at the first few bytes
var PROTOCOL_ID = []byte{0x72, 0x6f, 0x6c, 0x6c}

const (
DEFAULT_SAT_AMOUNT = 100000
DEFAULT_SAT_FEE = 200
DEFAULT_PRIVATE_KEY = "5JoQtsKQuH8hC9MyvfJAqo6qmKLm8ePYNucs7tPu2YxG12trzBt"
)

// Sample data and keys for testing.
// bob key pair is used for signing reveal tx
// internal key pair is used for tweaking
var (
bobPrivateKey = "5JoQtsKQuH8hC9MyvfJAqo6qmKLm8ePYNucs7tPu2YxG12trzBt"
internalPrivateKey = "5JGgKfRy6vEcWBpLJV5FXUfMGNXzvdWzQHUM1rVLEUJfvZUSwvS"
)

// chunkSlice splits input slice into max chunkSize length slices
func chunkSlice(slice []byte, chunkSize int) [][]byte {
var chunks [][]byte
Expand All @@ -46,13 +47,8 @@ func chunkSlice(slice []byte, chunkSize int) [][]byte {
// createTaprootAddress returns an address committing to a Taproot script with
// a single leaf containing the spend path with the script:
// <embedded data> OP_DROP <pubkey> OP_CHECKSIG
func createTaprootAddress(embeddedData []byte) (string, error) {
privKey, err := btcutil.DecodeWIF(bobPrivateKey)
if err != nil {
return "", fmt.Errorf("error decoding bob private key: %v", err)
}

pubKey := privKey.PrivKey.PubKey()
func createTaprootAddress(embeddedData []byte, network *chaincfg.Params, revealPrivateKeyWIF *btcutil.WIF) (string, error) {
pubKey := revealPrivateKeyWIF.PrivKey.PubKey()

// Step 1: Construct the Taproot script with one leaf.
builder := txscript.NewScriptBuilder()
Expand All @@ -73,22 +69,15 @@ func createTaprootAddress(embeddedData []byte) (string, error) {
tapLeaf := txscript.NewBaseTapLeaf(pkScript)
tapScriptTree := txscript.AssembleTaprootScriptTree(tapLeaf)

internalPrivKey, err := btcutil.DecodeWIF(internalPrivateKey)
if err != nil {
return "", fmt.Errorf("error decoding internal private key: %v", err)
}

internalPubKey := internalPrivKey.PrivKey.PubKey()

// Step 2: Generate the Taproot tree.
tapScriptRootHash := tapScriptTree.RootNode.TapHash()
outputKey := txscript.ComputeTaprootOutputKey(
internalPubKey, tapScriptRootHash[:],
pubKey, tapScriptRootHash[:],
)

// Step 3: Generate the Bech32m address.
address, err := btcutil.NewAddressTaproot(
schnorr.SerializePubKey(outputKey), &chaincfg.RegressionNetParams)
schnorr.SerializePubKey(outputKey), network)
if err != nil {
return "", fmt.Errorf("error encoding Taproot address: %v", err)
}
Expand All @@ -107,7 +96,10 @@ func payToTaprootScript(taprootKey *btcec.PublicKey) ([]byte, error) {
// Relayer is a bitcoin client wrapper which provides reader and writer methods
// to write binary blobs to the blockchain.
type Relayer struct {
client *rpcclient.Client
client *rpcclient.Client
network *chaincfg.Params
revealSatAmount btcutil.Amount
revealPrivateKeyWIF *btcutil.WIF
}

// close shuts down the client.
Expand All @@ -120,18 +112,13 @@ func (r Relayer) close() {
// the script satisfying the tapscript spend path that commits to the data. It
// returns the hash of the commit transaction and error, if any.
func (r Relayer) commitTx(addr string) (*chainhash.Hash, error) {
// Create a transaction that sends 0.001 BTC to the given address.
address, err := btcutil.DecodeAddress(addr, &chaincfg.RegressionNetParams)
// Create a transaction that sends revealSatAmount BTC to the given address.
address, err := btcutil.DecodeAddress(addr, r.network)
if err != nil {
return nil, fmt.Errorf("error decoding recipient address: %v", err)
}

amount, err := btcutil.NewAmount(0.001)
if err != nil {
return nil, fmt.Errorf("error creating new amount: %v", err)
}

hash, err := r.client.SendToAddress(address, amount)
hash, err := r.client.SendToAddress(address, btcutil.Amount(r.revealSatAmount))
if err != nil {
return nil, fmt.Errorf("error sending to address: %v", err)
}
Expand All @@ -152,26 +139,14 @@ func (r Relayer) revealTx(embeddedData []byte, commitHash *chainhash.Hash) (*cha
var commitIndex int
var commitOutput *wire.TxOut
for i, out := range rawCommitTx.MsgTx().TxOut {
if out.Value == 100000 {
if out.Value == int64(r.revealSatAmount) {
commitIndex = i
commitOutput = out
break
}
}

privKey, err := btcutil.DecodeWIF(bobPrivateKey)
if err != nil {
return nil, fmt.Errorf("error decoding bob private key: %v", err)
}

pubKey := privKey.PrivKey.PubKey()

internalPrivKey, err := btcutil.DecodeWIF(internalPrivateKey)
if err != nil {
return nil, fmt.Errorf("error decoding internal private key: %v", err)
}

internalPubKey := internalPrivKey.PrivKey.PubKey()
pubKey := r.revealPrivateKeyWIF.PrivKey.PubKey()

// Our script will be a simple <embedded-data> OP_DROP OP_CHECKSIG as the
// sole leaf of a tapscript tree.
Expand All @@ -194,12 +169,12 @@ func (r Relayer) revealTx(embeddedData []byte, commitHash *chainhash.Hash) (*cha
tapScriptTree := txscript.AssembleTaprootScriptTree(tapLeaf)

ctrlBlock := tapScriptTree.LeafMerkleProofs[0].ToControlBlock(
internalPubKey,
pubKey,
)

tapScriptRootHash := tapScriptTree.RootNode.TapHash()
outputKey := txscript.ComputeTaprootOutputKey(
internalPubKey, tapScriptRootHash[:],
pubKey, tapScriptRootHash[:],
)
p2trScript, err := payToTaprootScript(outputKey)
if err != nil {
Expand All @@ -213,8 +188,29 @@ func (r Relayer) revealTx(embeddedData []byte, commitHash *chainhash.Hash) (*cha
Index: uint32(commitIndex),
},
})

var fee int64
if r.network.Name != "regtest" {
smartFee, err := r.client.EstimateSmartFee(1, nil)
if err != nil {
return nil, fmt.Errorf("error getting sat fee: %v", err)
}
if smartFee.FeeRate == nil {
return nil, fmt.Errorf("got nil smart fee for non regtest environment: %v", err)
}
revealSatFee, err := btcutil.NewAmount(*smartFee.FeeRate)
if err != nil {
return nil, fmt.Errorf("error getting sat fee: %v", err)
}
txSize := tx.SerializeSize()
fee = int64(revealSatFee) * int64(txSize)
} else {
fee = 1e3
}

txOut := &wire.TxOut{
Value: 1e3, PkScript: p2trScript,
Value: fee,
PkScript: p2trScript,
}
tx.AddTxOut(txOut)

Expand All @@ -227,7 +223,7 @@ func (r Relayer) revealTx(embeddedData []byte, commitHash *chainhash.Hash) (*cha
sig, err := txscript.RawTxInTapscriptSignature(
tx, sigHashes, 0, txOut.Value,
txOut.PkScript, tapLeaf, txscript.SigHashDefault,
privKey.PrivKey,
r.revealPrivateKeyWIF.PrivKey,
)

if err != nil {
Expand All @@ -252,11 +248,14 @@ func (r Relayer) revealTx(embeddedData []byte, commitHash *chainhash.Hash) (*cha
}

type Config struct {
Host string
User string
Pass string
HTTPPostMode bool
DisableTLS bool
Host string
User string
Pass string
HTTPPostMode bool
DisableTLS bool
Network string
RevealSatAmount int64
RevealPrivateKeyWIF string
}

// NewRelayer returns a new relayer. It can error if there's an RPC connection
Expand All @@ -276,11 +275,64 @@ func NewRelayer(config Config) (*Relayer, error) {
if err != nil {
return nil, fmt.Errorf("error creating btcd RPC client: %v", err)
}
var network *chaincfg.Params
switch config.Network {
case "mainnet":
network = &chaincfg.MainNetParams
case "testnet":
network = &chaincfg.TestNet3Params
case "regtest":
network = &chaincfg.RegressionNetParams
default:
network = &chaincfg.RegressionNetParams
}
revealPrivateKeyWIF := config.RevealPrivateKeyWIF
if revealPrivateKeyWIF == "" {
revealPrivateKeyWIF = DEFAULT_PRIVATE_KEY
}
wif, err := btcutil.DecodeWIF(revealPrivateKeyWIF)
if err != nil {
return nil, fmt.Errorf("error decoding reveal private key: %v", err)
}
amount := btcutil.Amount(config.RevealSatAmount)
if amount == 0 {
amount = btcutil.Amount(DEFAULT_SAT_AMOUNT)
}
return &Relayer{
client: client,
client: client,
network: network,
revealSatAmount: amount,
revealPrivateKeyWIF: wif,
}, nil
}

func (r Relayer) ReadTransaction(hash *chainhash.Hash) ([]byte, error) {
tx, err := r.client.GetRawTransaction(hash)
if err != nil {
return nil, err
}
if len(tx.MsgTx().TxIn[0].Witness) > 1 {
witness := tx.MsgTx().TxIn[0].Witness[1]
pushData, err := ExtractPushData(0, witness)
if err != nil {
return nil, err
}
// skip PROTOCOL_ID
if pushData != nil && bytes.HasPrefix(pushData, PROTOCOL_ID) {
return pushData[4:], nil
}
}
return nil, nil
}

func (r Relayer) LatestHeight() (int64, error) {
latest, err := r.client.GetBlockCount()
if err != nil {
return -1, err
}
return latest, nil
}

func (r Relayer) Read(height uint64) ([][]byte, error) {
hash, err := r.client.GetBlockHash(int64(height))
if err != nil {
Expand All @@ -294,22 +346,14 @@ func (r Relayer) Read(height uint64) ([][]byte, error) {
var data [][]byte
for _, tx := range block.Transactions {
if len(tx.TxIn[0].Witness) > 1 {
// FIXME: UGLY HACK
// ideally we should template match the script and extract
// see: txscript.ExtractAtomicSwapDataPushes
witness := tx.TxIn[0].Witness[1]
start := bytes.Index(witness, PROTOCOL_ID)
// script template:
// < --------- 35 bytes --------->
// OP_FALSE OP_IF + "roll" marker + <data> + canonical int + 32 bytes pubkey + OP_CHECKSIG
// ^ ^
// start ------------ ^
// end -------------------------------------
if start > 0 && len(witness) > start+35 {
end := len(witness) - 35
if end > start {
data = append(data, witness[start+4:end])
}
pushData, err := ExtractPushData(0, witness)
if err != nil {
return nil, err
}
// skip PROTOCOL_ID
if pushData != nil && bytes.HasPrefix(pushData, PROTOCOL_ID) {
data = append(data, pushData[4:])
}
}
}
Expand All @@ -318,7 +362,7 @@ func (r Relayer) Read(height uint64) ([][]byte, error) {

func (r Relayer) Write(data []byte) (*chainhash.Hash, error) {
data = append(PROTOCOL_ID, data...)
address, err := createTaprootAddress(data)
address, err := createTaprootAddress(data, r.network, r.revealPrivateKeyWIF)
if err != nil {
return nil, err
}
Expand All @@ -332,3 +376,50 @@ func (r Relayer) Write(data []byte) (*chainhash.Hash, error) {
}
return hash, nil
}

func ExtractPushData(version uint16, pkScript []byte) ([]byte, error) {
type templateMatch struct {
expectPushData bool
maxPushDatas int
opcode byte
extractedData []byte
}
var template = [6]templateMatch{
{opcode: txscript.OP_FALSE},
{opcode: txscript.OP_IF},
{expectPushData: true, maxPushDatas: 10},
{opcode: txscript.OP_ENDIF},
{expectPushData: true, maxPushDatas: 1},
{opcode: txscript.OP_CHECKSIG},
}

var templateOffset int
tokenizer := txscript.MakeScriptTokenizer(version, pkScript)
out:
for tokenizer.Next() {
// Not a rollkit script if it has more opcodes than expected in the
// template.
if templateOffset >= len(template) {
return nil, nil
}

op := tokenizer.Opcode()
tplEntry := &template[templateOffset]
if tplEntry.expectPushData {
for i := 0; i < tplEntry.maxPushDatas; i++ {
data := tokenizer.Data()
if data == nil {
break out
}
tplEntry.extractedData = append(tplEntry.extractedData, data...)
tokenizer.Next()
}
} else if op != tplEntry.opcode {
return nil, nil
}

templateOffset++
}
// TODO: skipping err checks
return template[2].extractedData, nil
}
Loading