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

multisig: experiments #1

Open
wants to merge 1 commit into
base: master
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
5 changes: 5 additions & 0 deletions errors/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,11 @@ func E(args ...interface{}) error {
}
}

// TODO
if e.Err != nil && strings.Contains(e.Err.Error(), "account 2147483647") {
panic("GOT U!")
}

// Promote the Op and Kind of the nested Error to the newly created error,
// if these fields were not part of the args. This improves matching
// capabilities as well as improving the order of these fields in the
Expand Down
2 changes: 2 additions & 0 deletions internal/rpc/jsonrpc/methods.go
Original file line number Diff line number Diff line change
Expand Up @@ -3425,6 +3425,8 @@ func (s *Server) purchaseTicket(ctx context.Context, icmd interface{}) (interfac

ticketsResponse, err := w.PurchaseTickets(ctx, n, request)
if err != nil {
//// TODO
//panic(err)
return nil, err
}
ticketsTx := ticketsResponse.Tickets
Expand Down
5 changes: 3 additions & 2 deletions rpc/walletrpc/api.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions rpc/walletrpc/api_grpc.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

37 changes: 37 additions & 0 deletions wallet/addresses.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,12 @@ package wallet
import (
"context"
"encoding/binary"
"encoding/hex"
"fmt"
"runtime/trace"

"github.com/decred/dcrd/txscript/v4/stdscript"

"decred.org/dcrwallet/v4/errors"
"decred.org/dcrwallet/v4/internal/compat"
"decred.org/dcrwallet/v4/wallet/txsizes"
Expand Down Expand Up @@ -949,6 +953,39 @@ func (src *p2PKHChangeSource) ScriptSize() int {
return txsizes.P2PKHPkScriptSize
}

type p2SHChangeSource struct {
// TODO - basically this change address is targetting imported multisig
// address - which is the change address for multisig scenario.
}

func (src *p2SHChangeSource) Script() ([]byte, uint16, error) {
pk1, err := hex.DecodeString("030e4db4d37cfa43553c645ad20ca79ae79eef966f41243628310e7624d33a4145")
if err != nil {
return nil, 0, errors.E(errors.Invalid, fmt.Sprintf("pk1: hex.DecodeString: %v", err))
}
pk2, err := hex.DecodeString("02dc7aaeb575d3170760f3c719befd52805a0301b200a1e4efb700ebcc379a9af5")
if err != nil {
return nil, 0, errors.E(errors.Invalid, fmt.Sprintf("pk1: hex.DecodeString: %v", err))
}
pk1pk2Script, err := stdscript.MultiSigScriptV0(2, pk1, pk2) // pk1, pk2 order here is important!
if err != nil {
return nil, 0, errors.E(errors.Invalid, fmt.Sprintf("stdscript.MultiSigScriptV0: %v", err))
}
// TODO - using testnet chain params here!
pk1pk2ScriptAddr, err := stdaddr.NewAddressScriptHashV0(pk1pk2Script, chaincfg.TestNet3Params())
if err != nil {
return nil, 0, errors.E(errors.Invalid, fmt.Sprintf("stdaddr.NewAddressScriptHashV0: %v", err))
}
vers, pkScript := pk1pk2ScriptAddr.PaymentScript()
return pkScript, vers, nil
}

func (src *p2SHChangeSource) ScriptSize() int {
// TODO - looks like this should be estimating redeem script size, just leave
// it wrong for now.
return txsizes.P2PKHPkScriptSize
}

// p2PKHTreasuryChangeSource is the change source that shall be used when there
// is change on an OP_TADD treasury send.
type p2PKHTreasuryChangeSource struct {
Expand Down
149 changes: 132 additions & 17 deletions wallet/createtx.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import (
"crypto/rand"
"crypto/tls"
"encoding/binary"
"encoding/hex"
"fmt"
"net"
"sort"
"time"
Expand Down Expand Up @@ -467,6 +469,50 @@ func (w *Wallet) authorTx(ctx context.Context, op errors.Op, a *authorTx) error
return nil
}

// authorImportedMultisigTx TODO description.
// Note, it doesn't sign transaction, only builds it for separate signing later.
func (w *Wallet) authorImportedMultisigTx(ctx context.Context, op errors.Op, a *authorTx) error {
// TODO:
// We probably also want to lock outpoints similar to how it's done in authorTx
// func - since (our multisig address might have multiple UTXOs) we don't want
// some parallel action to spend any of those while we are here.

var atx *txauthor.AuthoredTx
err := walletdb.View(ctx, w.db, func(dbtx walletdb.ReadTx) error {
// Create the unsigned transaction.
_, tipHeight := w.txStore.MainChainTip(dbtx)
ignoreInput := func(op *wire.OutPoint) bool {
//_, ok := w.lockedOutpoints[outpoint{op.Hash, op.Index}]
//return ok
return false
}
// TODO - how much cofns we need here ?
sourceImpl := w.txStore.MakeInputSource(dbtx, a.account,
1, tipHeight, ignoreInput)
var changeSource txauthor.ChangeSource
changeSource = &p2SHChangeSource{}
var err error
atx, err = txauthor.NewUnsignedTransaction(a.outputs, a.txFee,
sourceImpl.SelectInputs, changeSource, w.chainParams.MaxTxSize)
if err != nil {
return err
}

return nil
})
if err != nil {
return errors.E(op, err)
}

err = w.checkHighFees(atx.TotalInput, atx.Tx)
if err != nil {
return errors.E(op, err)
}

a.atx = atx
return nil
}

// recordAuthoredTx records an authored transaction to the wallet's database. It
// also updates the database for change addresses used by the new transaction.
//
Expand Down Expand Up @@ -880,11 +926,12 @@ func makeTicket(params *chaincfg.Params, inputPool *Input, input *Input, addrVot
if addrVote == nil {
return nil, errors.E(errors.Invalid, "nil vote address")
}
vers, pkScript := addrVote.VotingRightsScript()

vers, pk1pk2Script := addrVote.VotingRightsScript()

txOut := &wire.TxOut{
Value: ticketCost,
PkScript: pkScript,
PkScript: pk1pk2Script,
Version: vers,
}
mtx.AddTxOut(txOut)
Expand Down Expand Up @@ -945,25 +992,25 @@ func makeTicket(params *chaincfg.Params, inputPool *Input, input *Input, addrVot
// Create an OP_RETURN push containing the pubkeyhash to send rewards to.
// Apply limits to revocations for fees while not allowing
// fees for votes.
vers, pkScript = addrSubsidy.RewardCommitmentScript(
vers, pk1pk2Script = addrSubsidy.RewardCommitmentScript(
amountsCommitted[userSubsidyNullIdx], 0, revocationFeeLimit)
if err != nil {
return nil, errors.E(errors.Invalid,
errors.Errorf("commitment address %v", addrSubsidy))
}
txout := &wire.TxOut{
Value: 0,
PkScript: pkScript,
PkScript: pk1pk2Script,
Version: vers,
}
mtx.AddTxOut(txout)

// Create a new script which pays to the provided address with an
// SStx change tagged output.
vers, pkScript = addrZeroed.StakeChangeScript()
vers, pk1pk2Script = addrZeroed.StakeChangeScript()
txOut = &wire.TxOut{
Value: 0,
PkScript: pkScript,
PkScript: pk1pk2Script,
Version: vers,
}
mtx.AddTxOut(txOut)
Expand Down Expand Up @@ -1096,16 +1143,36 @@ func (w *Wallet) mixedSplit(ctx context.Context, req *PurchaseTicketsRequest, ne
}

func (w *Wallet) individualSplit(ctx context.Context, req *PurchaseTicketsRequest, neededPerTicket dcrutil.Amount) (tx *wire.MsgTx, outIndexes []int, err error) {
// Fetch the single use split address to break tickets into, to
// immediately be consumed as tickets.
//
// This opens a write transaction.
splitTxAddr, err := w.NewInternalAddress(ctx, req.SourceAccount, WithGapPolicyWrap())
//// Fetch the single use split address to break tickets into, to
//// immediately be consumed as tickets.
////
//// This opens a write transaction.
//splitTxAddr, err := w.NewInternalAddress(ctx, req.SourceAccount, WithGapPolicyWrap())
//if err != nil {
// return
//}

// TODO - could probably check account == ImportedAddrAccount to decide what addr to use
pk1, err := hex.DecodeString("030e4db4d37cfa43553c645ad20ca79ae79eef966f41243628310e7624d33a4145")
if err != nil {
return
return nil, nil, errors.E(errors.Invalid, fmt.Sprintf("pk1: hex.DecodeString: %v", err))
}
pk2, err := hex.DecodeString("02dc7aaeb575d3170760f3c719befd52805a0301b200a1e4efb700ebcc379a9af5")
if err != nil {
return nil, nil, errors.E(errors.Invalid, fmt.Sprintf("pk1: hex.DecodeString: %v", err))
}
pk1pk2Script, err := stdscript.MultiSigScriptV0(2, pk1, pk2) // pk1, pk2 order here is important!
if err != nil {
return nil, nil, errors.E(errors.Invalid, fmt.Sprintf("stdscript.MultiSigScriptV0: %v", err))
}
// TODO - using testnet chain params here!
pk1pk2ScriptAddr, err := stdaddr.NewAddressScriptHashV0(pk1pk2Script, chaincfg.TestNet3Params())
if err != nil {
return nil, nil, errors.E(errors.Invalid, fmt.Sprintf("stdaddr.NewAddressScriptHashV0: %v", err))
}

vers, splitPkScript := splitTxAddr.PaymentScript()
vers, splitPkScript := pk1pk2ScriptAddr.PaymentScript()
//vers, splitPkScript := splitTxAddr.PaymentScript()

// Create the split transaction by using txToOutputs. This varies
// based upon whether or not the user is using a stake pool or not.
Expand All @@ -1123,6 +1190,7 @@ func (w *Wallet) individualSplit(ctx context.Context, req *PurchaseTicketsReques
}

const op errors.Op = "individualSplit"

a := &authorTx{
outputs: splitOuts,
account: req.SourceAccount,
Expand All @@ -1133,9 +1201,25 @@ func (w *Wallet) individualSplit(ctx context.Context, req *PurchaseTicketsReques
dontSignTx: req.DontSignTx,
isTreasury: false,
}
err = w.authorTx(ctx, op, a)
if err != nil {
return
if req.SourceAccount == udb.ImportedAddrAccount {
// TODO: this check ^ isn't enough to gurantee that imported account is necessarily
// == multisig; we must do this only for multisigs, not other account types ?
// however for other imported accounts this fails anyway! (because imported
// account isn't meant for address-deriving and stuff)

// This fee estimation broken for multisig transactions ... paying the double
// should roughly cover our needs for now.
a.txFee = a.txFee * 2

err = w.authorImportedMultisigTx(ctx, op, a)
if err != nil {
return
}
} else {
err = w.authorTx(ctx, op, a)
if err != nil {
return
}
}
err = w.recordAuthoredTx(ctx, op, a)
if err != nil {
Expand All @@ -1147,8 +1231,8 @@ func (w *Wallet) individualSplit(ctx context.Context, req *PurchaseTicketsReques
return
}
}

tx = a.atx.Tx

return
}

Expand Down Expand Up @@ -1249,6 +1333,32 @@ func (w *Wallet) purchaseTickets(ctx context.Context, op errors.Op,
}

stakeAddrFunc := func(op errors.Op, account, branch uint32) (stdaddr.StakeAddress, uint32, error) {
if account == udb.ImportedAddrAccount {
// TODO: this check ^ isn't enough to gurantee that imported account is necessarily
// == multisig; we must do this only for multisigs, not other account types ?
// however for other imported accounts this fails anyway! (because imported
// account isn't meant for address-deriving and stuff)
pk1, err := hex.DecodeString("030e4db4d37cfa43553c645ad20ca79ae79eef966f41243628310e7624d33a4145")
if err != nil {
return nil, 0, errors.E(errors.Invalid, fmt.Sprintf("pk1: hex.DecodeString: %v", err))
}
pk2, err := hex.DecodeString("02dc7aaeb575d3170760f3c719befd52805a0301b200a1e4efb700ebcc379a9af5")
if err != nil {
return nil, 0, errors.E(errors.Invalid, fmt.Sprintf("pk1: hex.DecodeString: %v", err))
}
pk1pk2Script, err := stdscript.MultiSigScriptV0(2, pk1, pk2) // pk1, pk2 order here is important!
if err != nil {
return nil, 0, errors.E(errors.Invalid, fmt.Sprintf("stdscript.MultiSigScriptV0: %v", err))
}
// TODO - using testnet chain params here!
pk1pk2ScriptAddr, err := stdaddr.NewAddressScriptHashV0(pk1pk2Script, chaincfg.TestNet3Params())
if err != nil {
return nil, 0, errors.E(errors.Invalid, fmt.Sprintf("stdaddr.NewAddressScriptHashV0: %v", err))
}

return pk1pk2ScriptAddr, 0, nil
}

const accountName = "" // not used, so can be faked.
a, err := w.nextAddress(ctx, op, w.persistReturnedChild(ctx, nil), accountName,
account, branch, WithGapPolicyIgnore())
Expand Down Expand Up @@ -1328,6 +1438,7 @@ func (w *Wallet) purchaseTickets(ctx context.Context, op errors.Op,
txsizes.TicketCommitmentScriptSize, txsizes.P2PKHPkScriptSize + 1}
estSize = txsizes.EstimateSerializeSizeFromScriptSizes(inSizes,
outSizes, 0)
// TODO ^ these sizes need to be changed for P2SH
} else {
// A pool ticket has:
// - two inputs redeeming a P2PKH for the worst case size
Expand All @@ -1344,8 +1455,12 @@ func (w *Wallet) purchaseTickets(ctx context.Context, op errors.Op,
txsizes.P2PKHPkScriptSize + 1, txsizes.P2PKHPkScriptSize + 1}
estSize = txsizes.EstimateSerializeSizeFromScriptSizes(inSizes,
outSizes, 0)
// TODO ^ these sizes need to be changed for P2SH
}

// This fee estimation broken for multisig transactions ... paying the double
// should roughly cover our needs for now.
ticketRelayFee = ticketRelayFee * 2
ticketFee := txrules.FeeForSerializeSize(ticketRelayFee, estSize)
neededPerTicket = ticketFee + ticketPrice

Expand Down
2 changes: 2 additions & 0 deletions wallet/multisig.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@ func (w *Wallet) PrepareRedeemMultiSigOutTxOutput(msgTx *wire.MsgTx, p2shOutput
return errors.E(op, errors.Errorf("estimated fee %v is above output value %v",
feeEst, p2shOutput.OutputAmount))
}
// This fee estimation above is broken ... adding some constant buffer here to patch it.
feeEst += 2000

toReceive := p2shOutput.OutputAmount - feeEst
// set the output value and add to the tx
Expand Down
4 changes: 2 additions & 2 deletions wallet/udb/addressdb.go
Original file line number Diff line number Diff line change
Expand Up @@ -1323,11 +1323,11 @@ func fetchAddressByHash(ns walletdb.ReadBucket, addrHash []byte) (interface{}, e
// address type. The caller should use type assertions to ascertain the type.
// The caller should prefix the error message with the address which caused the
// failure.
func fetchAddress(ns walletdb.ReadBucket, addressID []byte) (interface{}, error) {
func fetchAddress(ns walletdb.ReadBucket, addressID []byte, addrStr ...string) (interface{}, error) {
addrHash := sha256.Sum256(addressID)
addr, err := fetchAddressByHash(ns, addrHash[:])
if errors.Is(err, errors.NotExist) {
return nil, errors.E(errors.NotExist, errors.Errorf("no address with id %x", addressID))
return nil, errors.E(errors.NotExist, errors.Errorf("no address with id: %x, addrStr: %s)", addressID, addrStr))
}
return addr, err
}
Expand Down
2 changes: 1 addition & 1 deletion wallet/udb/addressmanager.go
Original file line number Diff line number Diff line change
Expand Up @@ -2494,7 +2494,7 @@ func (m *Manager) PrivateKey(ns walletdb.ReadBucket, addr stdaddr.Address) (key
if err != nil {
return nil, nil, err
}
addrInterface, err := fetchAddress(ns, id)
addrInterface, err := fetchAddress(ns, id, addr.String())
if err != nil {
return nil, nil, err
}
Expand Down
1 change: 1 addition & 0 deletions wallet/udb/testdata/v7.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
"os"
"time"

"decred.org/dcrwallet/v4/errors"
"github.com/decred/dcrd/chaincfg"
"github.com/decred/dcrd/chaincfg/chainec"
"github.com/decred/dcrd/chaincfg/chainhash"
Expand Down
Loading