kaspad/cmd/kaspawallet/libkaspawallet/transaction_test.go
Svarog 639183ba0e
Add support for auto-compound in kaspawallet send (#1951)
* Add GetUTXOsByBalances command to rpc

* Fix wrong commands in GetBalanceByAddress

* Moved calculation of TransactionMass out of TransactionValidator, so t that it can be used in kaspawallet

* Allow CreateUnsignedTransaction to return multiple transactions

* Start working on split

* Implement maybeSplitTransactionInner

* estimateMassIncreaseForSignatures should multiply by the number of inputs

* Implement createSplitTransaction

* Implement mergeTransactions

* Broadcast all transaction, not only 1

* workaround missing UTXOEntry in partially signed transaction

* Bugfix in broadcast loop

* Add underscores in some constants

* Make all nets RelayNonStdTxs: false

* Change estimateMassIncreaseForSignatures to estimateMassAfterSignatures

* Allow situations where merge transaction doesn't have enough funds to pay fees

* Add comments

* A few of renames

* Handle missed errors

* Fix clone of PubKeySignaturePair  to properly clone nil signatures

* Add sanity check to make sure originalTransaction has exactly two outputs

* Re-use change address for splitAddress

* Add one more utxo if the total amount is smaller then what we need to send due to fees

* Fix off-by-1 error in splitTrasnaction

* Add a comment to maybeAutoCompoundTransaction

* Add comment on why we are increasing inputCountPerSplit

* Add comment explaining while originalTransaction has 1 or 2 outputs

* Move oneMoreUTXOForMergeTransaction to split_transaction.go

* Allow to add multiple utxos to pay fee for mergeTransactions, if needed

* calculate split input counts and sizes properly

* Allow multiple transactions inside the create-unsigned-transaction -> sign -> broadcast workflow

* Print the number of transaction which was sent, in case there were multiple

* Rename broadcastConfig.Transaction(File) to Transactions(File)

* Convert alreadySelectedUTXOs to a map

* Fix a typo

* Add comment explaining that we assume all inputs are the same

* Revert over-refactor of rename of config.Transaction -> config.Transactions

* Rename: inputPerSplitCount -> inputsPerSplitCount

* Add comment for splitAndInputPerSplitCounts

* Use createSplitTransaction to calculate the upper bound of mass for split transactions
2022-03-27 20:06:55 +03:00

313 lines
9.6 KiB
Go

package libkaspawallet_test
import (
"fmt"
"strings"
"testing"
"github.com/kaspanet/kaspad/cmd/kaspawallet/libkaspawallet"
"github.com/kaspanet/kaspad/domain/consensus"
"github.com/kaspanet/kaspad/domain/consensus/model/externalapi"
"github.com/kaspanet/kaspad/domain/consensus/utils/consensushashing"
"github.com/kaspanet/kaspad/domain/consensus/utils/testutils"
"github.com/kaspanet/kaspad/domain/consensus/utils/txscript"
"github.com/kaspanet/kaspad/domain/consensus/utils/utxo"
"github.com/kaspanet/kaspad/util"
)
func forSchnorrAndECDSA(t *testing.T, testFunc func(t *testing.T, ecdsa bool)) {
t.Run("schnorr", func(t *testing.T) {
testFunc(t, false)
})
t.Run("ecdsa", func(t *testing.T) {
testFunc(t, true)
})
}
func TestMultisig(t *testing.T) {
testutils.ForAllNets(t, true, func(t *testing.T, consensusConfig *consensus.Config) {
params := &consensusConfig.Params
forSchnorrAndECDSA(t, func(t *testing.T, ecdsa bool) {
consensusConfig.BlockCoinbaseMaturity = 0
tc, teardown, err := consensus.NewFactory().NewTestConsensus(consensusConfig, "TestMultisig")
if err != nil {
t.Fatalf("Error setting up tc: %+v", err)
}
defer teardown(false)
const numKeys = 3
mnemonics := make([]string, numKeys)
publicKeys := make([]string, numKeys)
for i := 0; i < numKeys; i++ {
var err error
mnemonics[i], err = libkaspawallet.CreateMnemonic()
if err != nil {
t.Fatalf("CreateMnemonic: %+v", err)
}
publicKeys[i], err = libkaspawallet.MasterPublicKeyFromMnemonic(&consensusConfig.Params, mnemonics[i], true)
if err != nil {
t.Fatalf("MasterPublicKeyFromMnemonic: %+v", err)
}
}
const minimumSignatures = 2
path := "m/1/2/3"
address, err := libkaspawallet.Address(params, publicKeys, minimumSignatures, path, ecdsa)
if err != nil {
t.Fatalf("Address: %+v", err)
}
if _, ok := address.(*util.AddressScriptHash); !ok {
t.Fatalf("The address is of unexpected type")
}
scriptPublicKey, err := txscript.PayToAddrScript(address)
if err != nil {
t.Fatalf("PayToAddrScript: %+v", err)
}
coinbaseData := &externalapi.DomainCoinbaseData{
ScriptPublicKey: scriptPublicKey,
ExtraData: nil,
}
fundingBlockHash, _, err := tc.AddBlock([]*externalapi.DomainHash{consensusConfig.GenesisHash}, coinbaseData, nil)
if err != nil {
t.Fatalf("AddBlock: %+v", err)
}
block1Hash, _, err := tc.AddBlock([]*externalapi.DomainHash{fundingBlockHash}, nil, nil)
if err != nil {
t.Fatalf("AddBlock: %+v", err)
}
block1, err := tc.GetBlock(block1Hash)
if err != nil {
t.Fatalf("GetBlock: %+v", err)
}
block1Tx := block1.Transactions[0]
block1TxOut := block1Tx.Outputs[0]
selectedUTXOs := []*libkaspawallet.UTXO{
{
Outpoint: &externalapi.DomainOutpoint{
TransactionID: *consensushashing.TransactionID(block1.Transactions[0]),
Index: 0,
},
UTXOEntry: utxo.NewUTXOEntry(block1TxOut.Value, block1TxOut.ScriptPublicKey, true, 0),
DerivationPath: path,
},
}
unsignedTransaction, err := libkaspawallet.CreateUnsignedTransaction(publicKeys, minimumSignatures,
[]*libkaspawallet.Payment{{
Address: address,
Amount: 10,
}}, selectedUTXOs)
if err != nil {
t.Fatalf("CreateUnsignedTransactions: %+v", err)
}
isFullySigned, err := libkaspawallet.IsTransactionFullySigned(unsignedTransaction)
if err != nil {
t.Fatalf("IsTransactionFullySigned: %+v", err)
}
if isFullySigned {
t.Fatalf("Transaction is not expected to be signed")
}
_, err = libkaspawallet.ExtractTransaction(unsignedTransaction, ecdsa)
if err == nil || !strings.Contains(err.Error(), fmt.Sprintf("missing %d signatures", minimumSignatures)) {
t.Fatal("Unexpectedly succeed to extract a valid transaction out of unsigned transaction")
}
signedTxStep1, err := libkaspawallet.Sign(params, mnemonics[:1], unsignedTransaction, ecdsa)
if err != nil {
t.Fatalf("Sign: %+v", err)
}
isFullySigned, err = libkaspawallet.IsTransactionFullySigned(signedTxStep1)
if err != nil {
t.Fatalf("IsTransactionFullySigned: %+v", err)
}
if isFullySigned {
t.Fatalf("Transaction is not expected to be fully signed")
}
signedTxStep2, err := libkaspawallet.Sign(params, mnemonics[1:2], signedTxStep1, ecdsa)
if err != nil {
t.Fatalf("Sign: %+v", err)
}
extractedSignedTxStep2, err := libkaspawallet.ExtractTransaction(signedTxStep2, ecdsa)
if err != nil {
t.Fatalf("ExtractTransaction: %+v", err)
}
signedTxOneStep, err := libkaspawallet.Sign(params, mnemonics[:2], unsignedTransaction, ecdsa)
if err != nil {
t.Fatalf("Sign: %+v", err)
}
extractedSignedTxOneStep, err := libkaspawallet.ExtractTransaction(signedTxOneStep, ecdsa)
if err != nil {
t.Fatalf("ExtractTransaction: %+v", err)
}
// We check IDs instead of comparing the actual transactions because the actual transactions have different
// signature scripts due to non deterministic signature scheme.
if !consensushashing.TransactionID(extractedSignedTxStep2).Equal(consensushashing.TransactionID(extractedSignedTxOneStep)) {
t.Fatalf("Expected extractedSignedTxOneStep and extractedSignedTxStep2 IDs to be equal")
}
_, virtualChangeSet, err := tc.AddBlock([]*externalapi.DomainHash{block1Hash}, nil, []*externalapi.DomainTransaction{extractedSignedTxStep2})
if err != nil {
t.Fatalf("AddBlock: %+v", err)
}
addedUTXO := &externalapi.DomainOutpoint{
TransactionID: *consensushashing.TransactionID(extractedSignedTxStep2),
Index: 0,
}
if !virtualChangeSet.VirtualUTXODiff.ToAdd().Contains(addedUTXO) {
t.Fatalf("Transaction wasn't accepted in the DAG")
}
})
})
}
func TestP2PK(t *testing.T) {
testutils.ForAllNets(t, true, func(t *testing.T, consensusConfig *consensus.Config) {
params := &consensusConfig.Params
forSchnorrAndECDSA(t, func(t *testing.T, ecdsa bool) {
consensusConfig.BlockCoinbaseMaturity = 0
tc, teardown, err := consensus.NewFactory().NewTestConsensus(consensusConfig, "TestMultisig")
if err != nil {
t.Fatalf("Error setting up tc: %+v", err)
}
defer teardown(false)
const numKeys = 1
mnemonics := make([]string, numKeys)
publicKeys := make([]string, numKeys)
for i := 0; i < numKeys; i++ {
var err error
mnemonics[i], err = libkaspawallet.CreateMnemonic()
if err != nil {
t.Fatalf("CreateMnemonic: %+v", err)
}
publicKeys[i], err = libkaspawallet.MasterPublicKeyFromMnemonic(&consensusConfig.Params, mnemonics[i], false)
if err != nil {
t.Fatalf("MasterPublicKeyFromMnemonic: %+v", err)
}
}
const minimumSignatures = 1
path := "m/1/2/3"
address, err := libkaspawallet.Address(params, publicKeys, minimumSignatures, path, ecdsa)
if err != nil {
t.Fatalf("Address: %+v", err)
}
if ecdsa {
if _, ok := address.(*util.AddressPublicKeyECDSA); !ok {
t.Fatalf("The address is of unexpected type")
}
} else {
if _, ok := address.(*util.AddressPublicKey); !ok {
t.Fatalf("The address is of unexpected type")
}
}
scriptPublicKey, err := txscript.PayToAddrScript(address)
if err != nil {
t.Fatalf("PayToAddrScript: %+v", err)
}
coinbaseData := &externalapi.DomainCoinbaseData{
ScriptPublicKey: scriptPublicKey,
ExtraData: nil,
}
fundingBlockHash, _, err := tc.AddBlock([]*externalapi.DomainHash{consensusConfig.GenesisHash}, coinbaseData, nil)
if err != nil {
t.Fatalf("AddBlock: %+v", err)
}
block1Hash, _, err := tc.AddBlock([]*externalapi.DomainHash{fundingBlockHash}, nil, nil)
if err != nil {
t.Fatalf("AddBlock: %+v", err)
}
block1, err := tc.GetBlock(block1Hash)
if err != nil {
t.Fatalf("GetBlock: %+v", err)
}
block1Tx := block1.Transactions[0]
block1TxOut := block1Tx.Outputs[0]
selectedUTXOs := []*libkaspawallet.UTXO{
{
Outpoint: &externalapi.DomainOutpoint{
TransactionID: *consensushashing.TransactionID(block1.Transactions[0]),
Index: 0,
},
UTXOEntry: utxo.NewUTXOEntry(block1TxOut.Value, block1TxOut.ScriptPublicKey, true, 0),
DerivationPath: path,
},
}
unsignedTransaction, err := libkaspawallet.CreateUnsignedTransaction(publicKeys, minimumSignatures,
[]*libkaspawallet.Payment{{
Address: address,
Amount: 10,
}}, selectedUTXOs)
if err != nil {
t.Fatalf("CreateUnsignedTransactions: %+v", err)
}
isFullySigned, err := libkaspawallet.IsTransactionFullySigned(unsignedTransaction)
if err != nil {
t.Fatalf("IsTransactionFullySigned: %+v", err)
}
if isFullySigned {
t.Fatalf("Transaction is not expected to be signed")
}
_, err = libkaspawallet.ExtractTransaction(unsignedTransaction, ecdsa)
if err == nil || !strings.Contains(err.Error(), "missing signature") {
t.Fatal("Unexpectedly succeed to extract a valid transaction out of unsigned transaction")
}
signedTx, err := libkaspawallet.Sign(params, mnemonics, unsignedTransaction, ecdsa)
if err != nil {
t.Fatalf("Sign: %+v", err)
}
tx, err := libkaspawallet.ExtractTransaction(signedTx, ecdsa)
if err != nil {
t.Fatalf("ExtractTransaction: %+v", err)
}
_, virtualChangeSet, err := tc.AddBlock([]*externalapi.DomainHash{block1Hash}, nil, []*externalapi.DomainTransaction{tx})
if err != nil {
t.Fatalf("AddBlock: %+v", err)
}
addedUTXO := &externalapi.DomainOutpoint{
TransactionID: *consensushashing.TransactionID(tx),
Index: 0,
}
if !virtualChangeSet.VirtualUTXODiff.ToAdd().Contains(addedUTXO) {
t.Fatalf("Transaction wasn't accepted in the DAG")
}
})
})
}