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

@@ -156,6 +156,7 @@ func (s *covenantService) GetBoardingAddress(ctx context.Context, userPubkey *se
func (s *covenantService) SpendVtxos(ctx context.Context, inputs []ports.Input) (string, error) {
vtxosInputs := make([]domain.Vtxo, 0)
boardingInputs := make([]ports.BoardingInput, 0)
descriptors := make(map[domain.VtxoKey]string)
now := time.Now().Unix()
@@ -218,13 +219,14 @@ func (s *covenantService) SpendVtxos(ctx context.Context, inputs []ports.Input)
}
vtxosInputs = append(vtxosInputs, vtxo)
descriptors[vtxo.VtxoKey] = input.Descriptor
}
payment, err := domain.NewPayment(vtxosInputs)
if err != nil {
return "", err
}
if err := s.paymentRequests.push(*payment, boardingInputs); err != nil {
if err := s.paymentRequests.push(*payment, boardingInputs, descriptors); err != nil {
return "", err
}
return payment.Id, nil
@@ -324,7 +326,7 @@ func (s *covenantService) CompleteAsyncPayment(ctx context.Context, redeemTx str
return fmt.Errorf("unimplemented")
}
func (s *covenantService) CreateAsyncPayment(ctx context.Context, inputs []ports.Input, receivers []domain.Receiver) (string, error) {
func (s *covenantService) CreateAsyncPayment(_ context.Context, _ []AsyncPaymentInput, _ []domain.Receiver) (string, error) {
return "", fmt.Errorf("unimplemented")
}
@@ -345,9 +347,14 @@ func (s *covenantService) SignRoundTx(ctx context.Context, signedRoundTx string)
return nil
}
func (s *covenantService) ListVtxos(ctx context.Context, pubkey *secp256k1.PublicKey) ([]domain.Vtxo, []domain.Vtxo, error) {
pk := hex.EncodeToString(pubkey.SerializeCompressed())
return s.repoManager.Vtxos().GetAllVtxos(ctx, pk)
func (s *covenantService) ListVtxos(ctx context.Context, address string) ([]domain.Vtxo, []domain.Vtxo, error) {
decodedAddress, err := common.DecodeAddress(address)
if err != nil {
return nil, nil, fmt.Errorf("failed to decode address: %s", err)
}
pubkey := hex.EncodeToString(schnorr.SerializePubKey(decodedAddress.VtxoTapKey))
return s.repoManager.Vtxos().GetAllVtxos(ctx, pubkey)
}
func (s *covenantService) GetEventsChannel(ctx context.Context) <-chan domain.RoundEvent {
@@ -482,7 +489,7 @@ func (s *covenantService) startFinalization() {
if num > paymentsThreshold {
num = paymentsThreshold
}
payments, boardingInputs, _ := s.paymentRequests.pop(num)
payments, boardingInputs, descriptors, _ := s.paymentRequests.pop(num)
if _, err := round.RegisterPayments(payments); err != nil {
round.Fail(fmt.Errorf("failed to register payments: %s", err))
log.WithError(err).Warn("failed to register payments")
@@ -517,7 +524,7 @@ func (s *covenantService) startFinalization() {
minRelayFeeRate := s.wallet.MinRelayFeeRate(ctx)
if needForfeits {
connectors, forfeitTxs, err = s.builder.BuildForfeitTxs(unsignedPoolTx, payments, minRelayFeeRate)
connectors, forfeitTxs, err = s.builder.BuildForfeitTxs(unsignedPoolTx, payments, descriptors, minRelayFeeRate)
if err != nil {
round.Fail(fmt.Errorf("failed to create connectors and forfeit txs: %s", err))
log.WithError(err).Warn("failed to create connectors and forfeit txs")
@@ -933,53 +940,20 @@ func (s *covenantService) getNewVtxos(round *domain.Round) []domain.Vtxo {
continue // skip fee outputs
}
desc := ""
found := false
for _, p := range round.Payments {
if found {
break
}
for _, r := range p.Receivers {
if r.IsOnchain() {
continue
}
vtxoScript, err := tree.ParseVtxoScript(r.Descriptor)
if err != nil {
log.WithError(err).Warn("failed to parse vtxo descriptor")
continue
}
tapKey, _, err := vtxoScript.TapTree()
if err != nil {
log.WithError(err).Warn("failed to compute vtxo tap key")
continue
}
script, err := common.P2TRScript(tapKey)
if err != nil {
log.WithError(err).Warn("failed to create vtxo scriptpubkey")
continue
}
if bytes.Equal(script, out.Script) {
found = true
desc = r.Descriptor
break
}
}
vtxoTapKey, err := schnorr.ParsePubKey(out.Script[2:])
if err != nil {
log.WithError(err).Warn("failed to parse vtxo tap key")
continue
}
if found {
vtxos = append(vtxos, domain.Vtxo{
VtxoKey: domain.VtxoKey{Txid: node.Txid, VOut: uint32(i)},
Receiver: domain.Receiver{Descriptor: desc, Amount: uint64(out.Value)},
RoundTxid: round.Txid,
})
break
}
vtxoPubkey := hex.EncodeToString(schnorr.SerializePubKey(vtxoTapKey))
vtxos = append(vtxos, domain.Vtxo{
VtxoKey: domain.VtxoKey{Txid: node.Txid, VOut: uint32(i)},
Pubkey: vtxoPubkey,
Amount: uint64(out.Value),
RoundTxid: round.Txid,
})
}
}
return vtxos
@@ -1050,17 +1024,17 @@ func (s *covenantService) restoreWatchingVtxos() error {
func (s *covenantService) extractVtxosScripts(vtxos []domain.Vtxo) ([]string, error) {
indexedScripts := make(map[string]struct{})
for _, vtxo := range vtxos {
vtxoScript, err := tree.ParseVtxoScript(vtxo.Receiver.Descriptor)
vtxoTapKeyBytes, err := hex.DecodeString(vtxo.Pubkey)
if err != nil {
return nil, err
}
tapKey, _, err := vtxoScript.TapTree()
vtxoTapKey, err := schnorr.ParsePubKey(vtxoTapKeyBytes)
if err != nil {
return nil, err
}
script, err := common.P2TRScript(tapKey)
script, err := common.P2TRScript(vtxoTapKey)
if err != nil {
return nil, err
}

View File

@@ -19,6 +19,7 @@ import (
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/btcutil/psbt"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/decred/dcrd/dcrec/secp256k1/v4"
@@ -177,37 +178,28 @@ func (s *covenantlessService) CompleteAsyncPayment(
// verify that the vtxo is spendable
vtxo, err := vtxoRepo.GetVtxos(ctx, []domain.VtxoKey{{Txid: vtxoOutpoint.Hash.String(), VOut: vtxoOutpoint.Index}})
vtxos, err := vtxoRepo.GetVtxos(ctx, []domain.VtxoKey{{Txid: vtxoOutpoint.Hash.String(), VOut: vtxoOutpoint.Index}})
if err != nil {
return fmt.Errorf("failed to get vtxo: %s", err)
}
if len(vtxo) == 0 {
if len(vtxos) == 0 {
return fmt.Errorf("vtxo not found")
}
if vtxo[0].Spent {
vtxo := vtxos[0]
if vtxo.Spent {
return fmt.Errorf("vtxo already spent")
}
if vtxo[0].Redeemed {
if vtxo.Redeemed {
return fmt.Errorf("vtxo already redeemed")
}
if vtxo[0].Swept {
if vtxo.Swept {
return fmt.Errorf("vtxo already swept")
}
vtxoScript, err := bitcointree.ParseVtxoScript(vtxo[0].Descriptor)
if err != nil {
return fmt.Errorf("failed to parse vtxo script: %s", err)
}
vtxoTapKey, _, err := vtxoScript.TapTree()
if err != nil {
return fmt.Errorf("failed to get taproot key: %s", err)
}
// verify that the user signs a forfeit closure
var userPubKey *secp256k1.PublicKey
@@ -228,6 +220,16 @@ func (s *covenantlessService) CompleteAsyncPayment(
return fmt.Errorf("redeem transaction is not signed")
}
vtxoPublicKeyBytes, err := hex.DecodeString(vtxo.Pubkey)
if err != nil {
return fmt.Errorf("failed to decode vtxo pubkey: %s", err)
}
vtxoTapKey, err := schnorr.ParsePubKey(vtxoPublicKeyBytes)
if err != nil {
return fmt.Errorf("failed to parse vtxo pubkey: %s", err)
}
// verify witness utxo
pkscript, err := common.P2TRScript(vtxoTapKey)
if err != nil {
@@ -238,7 +240,7 @@ func (s *covenantlessService) CompleteAsyncPayment(
return fmt.Errorf("witness utxo script mismatch")
}
if input.WitnessUtxo.Value != int64(vtxo[0].Amount) {
if input.WitnessUtxo.Value != int64(vtxo.Amount) {
return fmt.Errorf("witness utxo value mismatch")
}
}
@@ -260,19 +262,23 @@ func (s *covenantlessService) CompleteAsyncPayment(
vtxos := make([]domain.Vtxo, 0, len(asyncPayData.receivers))
for outIndex, out := range redeemPtx.UnsignedTx.TxOut {
desc := asyncPayData.receivers[outIndex].Descriptor
_, _, _, _, err := descriptor.ParseReversibleVtxoDescriptor(desc)
isPending := err == nil
vtxoTapKey, err := schnorr.ParsePubKey(out.PkScript[2:])
if err != nil {
return fmt.Errorf("failed to parse vtxo taproot key: %s", err)
}
vtxoPubkey := hex.EncodeToString(schnorr.SerializePubKey(vtxoTapKey))
// all pending except the last one
isPending := outIndex < len(asyncPayData.receivers)-1
vtxos = append(vtxos, domain.Vtxo{
VtxoKey: domain.VtxoKey{
Txid: redeemTxid,
VOut: uint32(outIndex),
},
Receiver: domain.Receiver{
Descriptor: desc,
Amount: uint64(out.Value),
},
Pubkey: vtxoPubkey,
Amount: uint64(out.Value),
ExpireAt: asyncPayData.expireAt,
RedeemTx: redeemTx,
Pending: isPending,
@@ -309,11 +315,16 @@ func (s *covenantlessService) CompleteAsyncPayment(
}
func (s *covenantlessService) CreateAsyncPayment(
ctx context.Context, inputs []ports.Input, receivers []domain.Receiver,
ctx context.Context, inputs []AsyncPaymentInput, receivers []domain.Receiver,
) (string, error) {
vtxosKeys := make([]domain.VtxoKey, 0, len(inputs))
descriptors := make(map[domain.VtxoKey]string)
forfeitLeaves := make(map[domain.VtxoKey]chainhash.Hash)
for _, in := range inputs {
vtxosKeys = append(vtxosKeys, in.VtxoKey)
descriptors[in.VtxoKey] = in.Descriptor
forfeitLeaves[in.VtxoKey] = in.ForfeitLeafHash
}
vtxos, err := s.repoManager.Vtxos().GetVtxos(ctx, vtxosKeys)
@@ -351,7 +362,7 @@ func (s *covenantlessService) CreateAsyncPayment(
}
redeemTx, err := s.builder.BuildAsyncPaymentTransactions(
vtxosInputs, s.pubkey, receivers,
vtxosInputs, descriptors, forfeitLeaves, receivers,
)
if err != nil {
return "", fmt.Errorf("failed to build async payment txs: %s", err)
@@ -404,7 +415,7 @@ func (s *covenantlessService) SpendVtxos(ctx context.Context, inputs []ports.Inp
now := time.Now().Unix()
boardingTxs := make(map[string]wire.MsgTx, 0) // txid -> txhex
descriptors := make(map[domain.VtxoKey]string)
for _, input := range inputs {
vtxosResult, err := s.repoManager.Vtxos().GetVtxos(ctx, []domain.VtxoKey{input.VtxoKey})
if err != nil || len(vtxosResult) == 0 {
@@ -461,6 +472,8 @@ func (s *covenantlessService) SpendVtxos(ctx context.Context, inputs []ports.Inp
return "", fmt.Errorf("input %s:%d already swept", vtxo.Txid, vtxo.VOut)
}
descriptors[vtxo.VtxoKey] = input.Descriptor
vtxosInputs = append(vtxosInputs, vtxo)
}
@@ -468,7 +481,7 @@ func (s *covenantlessService) SpendVtxos(ctx context.Context, inputs []ports.Inp
if err != nil {
return "", err
}
if err := s.paymentRequests.push(*payment, boardingInputs); err != nil {
if err := s.paymentRequests.push(*payment, boardingInputs, descriptors); err != nil {
return "", err
}
return payment.Id, nil
@@ -572,9 +585,14 @@ func (s *covenantlessService) SignRoundTx(ctx context.Context, signedRoundTx str
return nil
}
func (s *covenantlessService) ListVtxos(ctx context.Context, pubkey *secp256k1.PublicKey) ([]domain.Vtxo, []domain.Vtxo, error) {
pk := hex.EncodeToString(pubkey.SerializeCompressed())
return s.repoManager.Vtxos().GetAllVtxos(ctx, pk)
func (s *covenantlessService) ListVtxos(ctx context.Context, address string) ([]domain.Vtxo, []domain.Vtxo, error) {
decodedAddress, err := common.DecodeAddress(address)
if err != nil {
return nil, nil, fmt.Errorf("failed to decode address: %s", err)
}
pubkey := hex.EncodeToString(schnorr.SerializePubKey(decodedAddress.VtxoTapKey))
return s.repoManager.Vtxos().GetAllVtxos(ctx, pubkey)
}
func (s *covenantlessService) GetEventsChannel(ctx context.Context) <-chan domain.RoundEvent {
@@ -771,7 +789,7 @@ func (s *covenantlessService) startFinalization() {
if num > paymentsThreshold {
num = paymentsThreshold
}
payments, boardingInputs, cosigners := s.paymentRequests.pop(num)
payments, boardingInputs, descriptors, cosigners := s.paymentRequests.pop(num)
if len(payments) > len(cosigners) {
err := fmt.Errorf("missing ephemeral key for payments")
round.Fail(fmt.Errorf("round aborted: %s", err))
@@ -973,7 +991,7 @@ func (s *covenantlessService) startFinalization() {
minRelayFeeRate := s.wallet.MinRelayFeeRate(ctx)
if needForfeits {
connectors, forfeitTxs, err = s.builder.BuildForfeitTxs(unsignedRoundTx, payments, minRelayFeeRate)
connectors, forfeitTxs, err = s.builder.BuildForfeitTxs(unsignedRoundTx, payments, descriptors, minRelayFeeRate)
if err != nil {
round.Fail(fmt.Errorf("failed to create connectors and forfeit txs: %s", err))
log.WithError(err).Warn("failed to create connectors and forfeit txs")
@@ -1332,53 +1350,18 @@ func (s *covenantlessService) getNewVtxos(round *domain.Round) []domain.Vtxo {
continue
}
for i, out := range tx.UnsignedTx.TxOut {
desc := ""
found := false
for _, p := range round.Payments {
if found {
break
}
for _, r := range p.Receivers {
if r.IsOnchain() {
continue
}
vtxoScript, err := bitcointree.ParseVtxoScript(r.Descriptor)
if err != nil {
log.WithError(err).Warn("failed to parse vtxo descriptor")
continue
}
tapKey, _, err := vtxoScript.TapTree()
if err != nil {
log.WithError(err).Warn("failed to compute vtxo tap key")
continue
}
script, err := common.P2TRScript(tapKey)
if err != nil {
log.WithError(err).Warn("failed to create vtxo scriptpubkey")
continue
}
if bytes.Equal(script, out.PkScript) {
found = true
desc = r.Descriptor
break
}
}
vtxoTapKey, err := schnorr.ParsePubKey(out.PkScript[2:])
if err != nil {
log.WithError(err).Warn("failed to parse vtxo tap key")
continue
}
if found {
vtxos = append(vtxos, domain.Vtxo{
VtxoKey: domain.VtxoKey{Txid: node.Txid, VOut: uint32(i)},
Receiver: domain.Receiver{Descriptor: desc, Amount: uint64(out.Value)},
RoundTxid: round.Txid,
})
break
}
vtxos = append(vtxos, domain.Vtxo{
VtxoKey: domain.VtxoKey{Txid: node.Txid, VOut: uint32(i)},
Pubkey: hex.EncodeToString(schnorr.SerializePubKey(vtxoTapKey)),
Amount: uint64(out.Value),
RoundTxid: round.Txid,
})
}
}
return vtxos
@@ -1450,17 +1433,17 @@ func (s *covenantlessService) extractVtxosScripts(vtxos []domain.Vtxo) ([]string
indexedScripts := make(map[string]struct{})
for _, vtxo := range vtxos {
vtxoScript, err := bitcointree.ParseVtxoScript(vtxo.Receiver.Descriptor)
vtxoTapKeyBytes, err := hex.DecodeString(vtxo.Pubkey)
if err != nil {
return nil, err
}
tapKey, _, err := vtxoScript.TapTree()
vtxoTapKey, err := schnorr.ParsePubKey(vtxoTapKeyBytes)
if err != nil {
return nil, err
}
script, err := common.P2TRScript(tapKey)
script, err := common.P2TRScript(vtxoTapKey)
if err != nil {
return nil, err
}

View File

@@ -5,6 +5,7 @@ import (
"github.com/ark-network/ark/server/internal/core/domain"
"github.com/ark-network/ark/server/internal/core/ports"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/decred/dcrd/dcrec/secp256k1/v4"
)
@@ -12,6 +13,11 @@ var (
paymentsThreshold = int64(128)
)
type AsyncPaymentInput struct {
ports.Input
ForfeitLeafHash chainhash.Hash
}
type Service interface {
Start() error
Stop()
@@ -27,12 +33,12 @@ type Service interface {
ctx context.Context, paymentId string,
) (lastEvent domain.RoundEvent, err error)
ListVtxos(
ctx context.Context, pubkey *secp256k1.PublicKey,
ctx context.Context, address string,
) (spendableVtxos, spentVtxos []domain.Vtxo, err error)
GetInfo(ctx context.Context) (*ServiceInfo, error)
// Async payments
CreateAsyncPayment(
ctx context.Context, inputs []ports.Input, receivers []domain.Receiver,
ctx context.Context, inputs []AsyncPaymentInput, receivers []domain.Receiver,
) (string, error)
CompleteAsyncPayment(
ctx context.Context, redeemTx string,

View File

@@ -24,13 +24,14 @@ type timedPayment struct {
type paymentsMap struct {
lock *sync.RWMutex
payments map[string]*timedPayment
descriptors map[domain.VtxoKey]string
ephemeralKeys map[string]*secp256k1.PublicKey
}
func newPaymentsMap() *paymentsMap {
paymentsById := make(map[string]*timedPayment)
lock := &sync.RWMutex{}
return &paymentsMap{lock, paymentsById, make(map[string]*secp256k1.PublicKey)}
return &paymentsMap{lock, paymentsById, make(map[domain.VtxoKey]string), make(map[string]*secp256k1.PublicKey)}
}
func (m *paymentsMap) len() int64 {
@@ -58,7 +59,11 @@ func (m *paymentsMap) delete(id string) error {
return nil
}
func (m *paymentsMap) push(payment domain.Payment, boardingInputs []ports.BoardingInput) error {
func (m *paymentsMap) push(
payment domain.Payment,
boardingInputs []ports.BoardingInput,
descriptors map[domain.VtxoKey]string,
) error {
m.lock.Lock()
defer m.lock.Unlock()
@@ -86,6 +91,10 @@ func (m *paymentsMap) push(payment domain.Payment, boardingInputs []ports.Boardi
}
}
for key, desc := range descriptors {
m.descriptors[key] = desc
}
m.payments[payment.Id] = &timedPayment{payment, boardingInputs, time.Now(), time.Time{}}
return nil
}
@@ -102,7 +111,7 @@ func (m *paymentsMap) pushEphemeralKey(paymentId string, pubkey *secp256k1.Publi
return nil
}
func (m *paymentsMap) pop(num int64) ([]domain.Payment, []ports.BoardingInput, []*secp256k1.PublicKey) {
func (m *paymentsMap) pop(num int64) ([]domain.Payment, []ports.BoardingInput, map[domain.VtxoKey]string, []*secp256k1.PublicKey) {
m.lock.Lock()
defer m.lock.Unlock()
@@ -129,6 +138,7 @@ func (m *paymentsMap) pop(num int64) ([]domain.Payment, []ports.BoardingInput, [
payments := make([]domain.Payment, 0, num)
boardingInputs := make([]ports.BoardingInput, 0)
cosigners := make([]*secp256k1.PublicKey, 0, num)
descriptors := make(map[domain.VtxoKey]string)
for _, p := range paymentsByTime[:num] {
boardingInputs = append(boardingInputs, p.boardingInputs...)
payments = append(payments, p.Payment)
@@ -136,9 +146,15 @@ func (m *paymentsMap) pop(num int64) ([]domain.Payment, []ports.BoardingInput, [
cosigners = append(cosigners, pubkey)
delete(m.ephemeralKeys, p.Payment.Id)
}
for _, input := range payments {
for _, vtxo := range input.Inputs {
descriptors[vtxo.VtxoKey] = m.descriptors[vtxo.VtxoKey]
delete(m.descriptors, vtxo.VtxoKey)
}
}
delete(m.payments, p.Id)
}
return payments, boardingInputs, cosigners
return payments, boardingInputs, descriptors, cosigners
}
func (m *paymentsMap) update(payment domain.Payment) error {