New address encoding (#356)

* [common] rework address encoding

* new address encoding

* replace offchain address by vtxo output key in DB

* merge migrations files into init one

* fix txbuilder fixtures

* fix transaction events
This commit is contained in:
Louis Singer
2024-10-18 16:50:07 +02:00
committed by GitHub
parent b1c9261f14
commit b536a9e652
58 changed files with 2243 additions and 1896 deletions

View File

@@ -1,8 +1,10 @@
package bitcointree
import (
"encoding/hex"
"fmt"
"github.com/ark-network/ark/common"
"github.com/ark-network/ark/common/tree"
"github.com/btcsuite/btcd/btcec/v2/schnorr"
"github.com/btcsuite/btcd/btcec/v2/schnorr/musig2"
@@ -15,7 +17,7 @@ import (
// CraftSharedOutput returns the taproot script and the amount of the initial root output
func CraftSharedOutput(
cosigners []*secp256k1.PublicKey, aspPubkey *secp256k1.PublicKey, receivers []Receiver,
cosigners []*secp256k1.PublicKey, aspPubkey *secp256k1.PublicKey, receivers []tree.VtxoLeaf,
feeSatsPerNode uint64, roundLifetime int64,
) ([]byte, int64, error) {
aggregatedKey, _, err := createAggregatedKeyWithSweep(
@@ -32,7 +34,7 @@ func CraftSharedOutput(
amount := root.getAmount() + int64(feeSatsPerNode)
scriptPubKey, err := taprootOutputScript(aggregatedKey.FinalKey)
scriptPubKey, err := common.P2TRScript(aggregatedKey.FinalKey)
if err != nil {
return nil, 0, err
}
@@ -42,7 +44,7 @@ func CraftSharedOutput(
// CraftCongestionTree creates all the tree's transactions
func CraftCongestionTree(
initialInput *wire.OutPoint, cosigners []*secp256k1.PublicKey, aspPubkey *secp256k1.PublicKey, receivers []Receiver,
initialInput *wire.OutPoint, cosigners []*secp256k1.PublicKey, aspPubkey *secp256k1.PublicKey, receivers []tree.VtxoLeaf,
feeSatsPerNode uint64, roundLifetime int64,
) (tree.CongestionTree, error) {
aggregatedKey, sweepTapLeaf, err := createAggregatedKeyWithSweep(
@@ -108,8 +110,8 @@ type node interface {
}
type leaf struct {
vtxoScript VtxoScript
amount int64
amount int64
pubkey *secp256k1.PublicKey
}
type branch struct {
@@ -142,12 +144,7 @@ func (l *leaf) getAmount() int64 {
}
func (l *leaf) getOutputs() ([]*wire.TxOut, error) {
taprootKey, _, err := l.vtxoScript.TapTree()
if err != nil {
return nil, err
}
script, err := taprootOutputScript(taprootKey)
script, err := common.P2TRScript(l.pubkey)
if err != nil {
return nil, err
}
@@ -161,7 +158,7 @@ func (l *leaf) getOutputs() ([]*wire.TxOut, error) {
}
func (b *branch) getOutputs() ([]*wire.TxOut, error) {
sharedOutputScript, err := taprootOutputScript(b.aggregatedKey.FinalKey)
sharedOutputScript, err := common.P2TRScript(b.aggregatedKey.FinalKey)
if err != nil {
return nil, err
}
@@ -246,7 +243,7 @@ func getTx(
func createRootNode(
aggregatedKey *musig2.AggregateKey,
cosigners []*secp256k1.PublicKey,
receivers []Receiver,
receivers []tree.VtxoLeaf,
feeSatsPerNode uint64,
) (root node, err error) {
if len(receivers) == 0 {
@@ -255,9 +252,19 @@ func createRootNode(
nodes := make([]node, 0, len(receivers))
for _, r := range receivers {
pubkeyBytes, err := hex.DecodeString(r.Pubkey)
if err != nil {
return nil, err
}
pubkey, err := schnorr.ParsePubKey(pubkeyBytes)
if err != nil {
return nil, err
}
leafNode := &leaf{
vtxoScript: r.Script,
amount: int64(r.Amount),
amount: int64(r.Amount),
pubkey: pubkey,
}
nodes = append(nodes, leafNode)
}
@@ -339,9 +346,3 @@ func createUpperLevel(nodes []node, aggregatedKey *musig2.AggregateKey, cosigner
}
return pairs, nil
}
func taprootOutputScript(taprootKey *secp256k1.PublicKey) ([]byte, error) {
return txscript.NewScriptBuilder().AddOp(txscript.OP_1).AddData(
schnorr.SerializePubKey(taprootKey),
).Script()
}

View File

@@ -7,6 +7,7 @@ import (
"io"
"strings"
"github.com/ark-network/ark/common"
"github.com/ark-network/ark/common/tree"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcec/v2/schnorr"
@@ -512,7 +513,7 @@ func prevOutFetcherFactory(
func(partial *psbt.Packet) (txscript.PrevOutputFetcher, error),
error,
) {
pkscript, err := taprootOutputScript(finalAggregatedKey)
pkscript, err := common.P2TRScript(finalAggregatedKey)
if err != nil {
return nil, err
}

View File

@@ -2,13 +2,13 @@ package bitcointree_test
import (
"bytes"
"encoding/hex"
"encoding/json"
"fmt"
"os"
"testing"
"github.com/ark-network/ark/common/bitcointree"
"github.com/ark-network/ark/common/tree"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
@@ -44,7 +44,7 @@ func TestRoundTripSignTree(t *testing.T) {
_, sharedOutputAmount, err := bitcointree.CraftSharedOutput(
cosigners,
asp.PubKey(),
castReceivers(f.Receivers, asp.PubKey()),
castReceivers(f.Receivers),
minRelayFee,
lifetime,
)
@@ -58,7 +58,7 @@ func TestRoundTripSignTree(t *testing.T) {
},
cosigners,
asp.PubKey(),
castReceivers(f.Receivers, asp.PubKey()),
castReceivers(f.Receivers),
minRelayFee,
lifetime,
)
@@ -222,29 +222,11 @@ type receiverFixture struct {
Pubkey string `json:"pubkey"`
}
func (r receiverFixture) toVtxoScript(asp *secp256k1.PublicKey) bitcointree.VtxoScript {
bytesKey, err := hex.DecodeString(r.Pubkey)
if err != nil {
panic(err)
}
pubkey, err := secp256k1.ParsePubKey(bytesKey)
if err != nil {
panic(err)
}
return &bitcointree.DefaultVtxoScript{
Owner: pubkey,
Asp: asp,
ExitDelay: exitDelay,
}
}
func castReceivers(receivers []receiverFixture, asp *secp256k1.PublicKey) []bitcointree.Receiver {
receiversOut := make([]bitcointree.Receiver, 0, len(receivers))
func castReceivers(receivers []receiverFixture) []tree.VtxoLeaf {
receiversOut := make([]tree.VtxoLeaf, 0, len(receivers))
for _, r := range receivers {
receiversOut = append(receiversOut, bitcointree.Receiver{
Script: r.toVtxoScript(asp),
receiversOut = append(receiversOut, tree.VtxoLeaf{
Pubkey: r.Pubkey,
Amount: uint64(r.Amount),
})
}

View File

@@ -4,7 +4,7 @@
{
"receivers": [
{
"pubkey": "020000000000000000000000000000000000000000000000000000000000000002",
"pubkey": "0000000000000000000000000000000000000000000000000000000000000002",
"amount": 1100
}
]
@@ -12,11 +12,11 @@
{
"receivers": [
{
"pubkey": "020000000000000000000000000000000000000000000000000000000000000002",
"pubkey": "0000000000000000000000000000000000000000000000000000000000000002",
"amount": 1100
},
{
"pubkey": "020000000000000000000000000000000000000000000000000000000000000002",
"pubkey": "0000000000000000000000000000000000000000000000000000000000000002",
"amount": 8000
}
]
@@ -24,23 +24,23 @@
{
"receivers": [
{
"pubkey": "020000000000000000000000000000000000000000000000000000000000000002",
"pubkey": "0000000000000000000000000000000000000000000000000000000000000002",
"amount": 1100
},
{
"pubkey": "020000000000000000000000000000000000000000000000000000000000000002",
"pubkey": "0000000000000000000000000000000000000000000000000000000000000002",
"amount": 1100
},
{
"pubkey": "020000000000000000000000000000000000000000000000000000000000000002",
"pubkey": "0000000000000000000000000000000000000000000000000000000000000002",
"amount": 1100
},
{
"pubkey": "020000000000000000000000000000000000000000000000000000000000000002",
"pubkey": "0000000000000000000000000000000000000000000000000000000000000002",
"amount": 1000
},
{
"pubkey": "020000000000000000000000000000000000000000000000000000000000000002",
"pubkey": "0000000000000000000000000000000000000000000000000000000000000002",
"amount": 1100
}
]

View File

@@ -1,6 +0,0 @@
package bitcointree
type Receiver struct {
Script VtxoScript
Amount uint64
}

View File

@@ -3,63 +3,68 @@ package common
import (
"fmt"
"github.com/btcsuite/btcd/btcec/v2/schnorr"
"github.com/btcsuite/btcd/btcutil/bech32"
"github.com/decred/dcrd/dcrec/secp256k1/v4"
)
func EncodeAddress(
hrp string, userKey, aspKey *secp256k1.PublicKey,
) (addr string, err error) {
if userKey == nil {
err = fmt.Errorf("missing public key")
return
// Address represents an Ark address with HRP, ASP public key, and VTXO Taproot public key
type Address struct {
HRP string
Asp *secp256k1.PublicKey
VtxoTapKey *secp256k1.PublicKey
}
// Encode converts the address to its bech32m string representation
func (a *Address) Encode() (string, error) {
if a.Asp == nil {
return "", fmt.Errorf("missing asp public key")
}
if aspKey == nil {
err = fmt.Errorf("missing asp public key")
return
}
if hrp != Liquid.Addr && hrp != LiquidTestNet.Addr {
err = fmt.Errorf("invalid prefix")
return
if a.VtxoTapKey == nil {
return "", fmt.Errorf("missing vtxo tap public key")
}
combinedKey := append(
aspKey.SerializeCompressed(), userKey.SerializeCompressed()...,
schnorr.SerializePubKey(a.Asp), schnorr.SerializePubKey(a.VtxoTapKey)...,
)
grp, err := bech32.ConvertBits(combinedKey, 8, 5, true)
if err != nil {
return
return "", err
}
addr, err = bech32.EncodeM(hrp, grp)
return
return bech32.EncodeM(a.HRP, grp)
}
func DecodeAddress(
addr string,
) (hrp string, userKey, aspKey *secp256k1.PublicKey, err error) {
// DecodeAddress parses a bech32m encoded address string and returns an Address object
func DecodeAddress(addr string) (*Address, error) {
if len(addr) == 0 {
return nil, fmt.Errorf("address is empty")
}
prefix, buf, err := bech32.DecodeNoLimit(addr)
if err != nil {
return
return nil, err
}
if prefix != Liquid.Addr && prefix != LiquidTestNet.Addr && prefix != LiquidRegTest.Addr {
err = fmt.Errorf("invalid prefix")
return
return nil, fmt.Errorf("invalid prefix")
}
grp, err := bech32.ConvertBits(buf, 5, 8, false)
if err != nil {
return
return nil, err
}
aKey, err := secp256k1.ParsePubKey(grp[:33])
aKey, err := schnorr.ParsePubKey(grp[:32])
if err != nil {
err = fmt.Errorf("failed to parse public key: %s", err)
return
return nil, fmt.Errorf("failed to parse public key: %s", err)
}
uKey, err := secp256k1.ParsePubKey(grp[33:])
vtxoKey, err := schnorr.ParsePubKey(grp[32:])
if err != nil {
err = fmt.Errorf("failed to parse asp public key: %s", err)
return
return nil, fmt.Errorf("failed to parse asp public key: %s", err)
}
hrp = prefix
userKey = uKey
aspKey = aKey
return
return &Address{
HRP: prefix,
Asp: aKey,
VtxoTapKey: vtxoKey,
}, nil
}

View File

@@ -40,31 +40,29 @@ func TestAddressEncoding(t *testing.T) {
t.Run("valid", func(t *testing.T) {
for _, f := range fixtures.Address.Valid {
hrp, userKey, aspKey, err := common.DecodeAddress(f.Addr)
addr, err := common.DecodeAddress(f.Addr)
require.NoError(t, err)
require.NotEmpty(t, hrp)
require.NotNil(t, userKey)
require.NotNil(t, aspKey)
require.NotEmpty(t, addr.HRP)
require.NotNil(t, addr.Asp)
require.NotNil(t, addr.VtxoTapKey)
require.NoError(t, err)
require.Equal(t, f.ExpectedUserKey, hex.EncodeToString(userKey.SerializeCompressed()))
require.Equal(t, f.ExpectedUserKey, hex.EncodeToString(addr.VtxoTapKey.SerializeCompressed()))
require.NoError(t, err)
require.Equal(t, f.ExpectedAspKey, hex.EncodeToString(aspKey.SerializeCompressed()))
require.Equal(t, f.ExpectedAspKey, hex.EncodeToString(addr.Asp.SerializeCompressed()))
addr, err := common.EncodeAddress(hrp, userKey, aspKey)
encoded, err := addr.Encode()
require.NoError(t, err)
require.Equal(t, f.Addr, addr)
require.Equal(t, f.Addr, encoded)
}
})
t.Run("invalid", func(t *testing.T) {
for _, f := range fixtures.Address.Invalid {
hrp, userKey, aspKey, err := common.DecodeAddress(f.Addr)
addr, err := common.DecodeAddress(f.Addr)
require.EqualError(t, err, f.ExpectedError)
require.Empty(t, hrp)
require.Nil(t, userKey)
require.Nil(t, aspKey)
require.Nil(t, addr)
}
})
}

View File

@@ -2,9 +2,9 @@
"address": {
"valid": [
{
"addr": "ark1qgvdtj5ttpuhkldavhq8thtm5auyk0ec4dcmrfdgu0u5hgp9we22vqa7mdkrrulzu48law4zzvzz8k59hul0ayl2urt905we5wf6gee68sfrfj35",
"expectedUserKey": "03bedb6c31f3e2e54ffebaa2130423da85bf3efe93eae0d657d1d9a393a4673a3c",
"expectedAspKey": "0218d5ca8b58797b7dbd65c075dd7ba7784b3f38ab71b1a5a8e3f94ba0257654a6"
"addr": "tark1x0lm8hhr2wc6n6lyemtyh9rz8rg2ftpkfun46aca56kjg3ws0tsztfpuanaquxc6faedvjk3tax0575y6perapg3e95654pk8r4fjecs5fyd2",
"expectedUserKey": "0225a43cecfa0e1b1a4f72d64ad15f4cfa7a84d0723e8511c969aa543638ea9967",
"expectedAspKey": "0233ffb3dee353b1a9ebe4ced64b946238d0a4ac364f275d771da6ad2445d07ae0"
}
],
"invalid": [

View File

@@ -1,9 +1,9 @@
package tree
import (
"encoding/hex"
"fmt"
"github.com/ark-network/ark/common"
"github.com/btcsuite/btcd/btcec/v2/schnorr"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/txscript"
@@ -13,7 +13,7 @@ import (
)
func CraftCongestionTree(
asset string, aspPubkey *secp256k1.PublicKey, receivers []Receiver,
asset string, aspPubkey *secp256k1.PublicKey, receivers []VtxoLeaf,
feeSatsPerNode uint64, roundLifetime int64,
) (
buildCongestionTree TreeFactory,
@@ -41,9 +41,14 @@ func CraftCongestionTree(
return
}
type vtxoOutput struct {
pubkey *secp256k1.PublicKey
amount uint64
}
type node struct {
sweepKey *secp256k1.PublicKey
receivers []Receiver
receivers []vtxoOutput
left *node
right *node
asset string
@@ -61,7 +66,7 @@ func (n *node) isLeaf() bool {
func (n *node) getAmount() uint64 {
var amount uint64
for _, r := range n.receivers {
amount += r.Amount
amount += r.amount
}
if n.isLeaf() {
@@ -107,7 +112,7 @@ func (n *node) getChildren() []*node {
func (n *node) getOutputs() ([]psetv2.OutputArgs, error) {
if n.isLeaf() {
taprootKey, _, err := n.getVtxoWitnessData()
taprootKey, err := n.getVtxoWitnessData()
if err != nil {
return nil, err
}
@@ -168,7 +173,7 @@ func (n *node) getWitnessData() (
}
if n.isLeaf() {
taprootKey, _, err := n.getVtxoWitnessData()
taprootKey, err := n.getVtxoWitnessData()
if err != nil {
return nil, nil, err
}
@@ -241,15 +246,14 @@ func (n *node) getWitnessData() (
}
func (n *node) getVtxoWitnessData() (
*secp256k1.PublicKey, common.TaprootTree, error,
*secp256k1.PublicKey, error,
) {
if !n.isLeaf() {
return nil, nil, fmt.Errorf("cannot call vtxoWitness on a non-leaf node")
return nil, fmt.Errorf("cannot call vtxoWitness on a non-leaf node")
}
receiver := n.receivers[0]
return receiver.Script.TapTree()
return receiver.pubkey, nil
}
func (n *node) getTreeNode(
@@ -373,7 +377,7 @@ func (n *node) createFinalCongestionTree() TreeFactory {
}
func createPartialCongestionTree(
asset string, aspPubkey *secp256k1.PublicKey, receivers []Receiver,
asset string, aspPubkey *secp256k1.PublicKey, receivers []VtxoLeaf,
feeSatsPerNode uint64, roundLifetime int64,
) (root *node, err error) {
if len(receivers) == 0 {
@@ -382,9 +386,19 @@ func createPartialCongestionTree(
nodes := make([]*node, 0, len(receivers))
for _, r := range receivers {
pubkeyBytes, err := hex.DecodeString(r.Pubkey)
if err != nil {
return nil, err
}
pubkey, err := schnorr.ParsePubKey(pubkeyBytes)
if err != nil {
return nil, err
}
leafNode := &node{
sweepKey: aspPubkey,
receivers: []Receiver{r},
receivers: []vtxoOutput{{pubkey, r.Amount}},
asset: asset,
feeSats: feeSatsPerNode,
roundLifetime: roundLifetime,

View File

@@ -6,7 +6,7 @@ import (
type TreeFactory func(outpoint psetv2.InputArgs) (CongestionTree, error)
type Receiver struct {
Script VtxoScript
type VtxoLeaf struct {
Pubkey string
Amount uint64
}