mirror of
https://github.com/kaspanet/kaspad.git
synced 2025-05-21 22:36:42 +00:00

* 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
279 lines
10 KiB
Go
279 lines
10 KiB
Go
package server
|
|
|
|
import (
|
|
"github.com/kaspanet/go-secp256k1"
|
|
"github.com/pkg/errors"
|
|
|
|
"github.com/kaspanet/kaspad/cmd/kaspawallet/libkaspawallet"
|
|
"github.com/kaspanet/kaspad/cmd/kaspawallet/libkaspawallet/serialization"
|
|
"github.com/kaspanet/kaspad/domain/consensus/model/externalapi"
|
|
"github.com/kaspanet/kaspad/domain/consensus/utils/consensushashing"
|
|
"github.com/kaspanet/kaspad/domain/consensus/utils/constants"
|
|
"github.com/kaspanet/kaspad/domain/consensus/utils/utxo"
|
|
"github.com/kaspanet/kaspad/domain/miningmanager/mempool"
|
|
"github.com/kaspanet/kaspad/util"
|
|
)
|
|
|
|
// maybeAutoCompoundTransaction checks if a transaction's mass is higher that what is allowed for a standard
|
|
// transaction.
|
|
// If it is - the transaction is split into multiple transactions, each with a portion of the inputs and a single output
|
|
// into a change address.
|
|
// An additional `mergeTransaction` is generated - which merges the outputs of the above splits into a single output
|
|
// paying to the original transaction's payee.
|
|
func (s *server) maybeAutoCompoundTransaction(transactionBytes []byte, toAddress util.Address,
|
|
changeAddress util.Address, changeWalletAddress *walletAddress) ([][]byte, error) {
|
|
transaction, err := serialization.DeserializePartiallySignedTransaction(transactionBytes)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
splitTransactions, err := s.maybeSplitTransaction(transaction, changeAddress)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(splitTransactions) > 1 {
|
|
mergeTransaction, err := s.mergeTransaction(splitTransactions, transaction, toAddress, changeAddress, changeWalletAddress)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
splitTransactions = append(splitTransactions, mergeTransaction)
|
|
}
|
|
|
|
splitTransactionsBytes := make([][]byte, len(splitTransactions))
|
|
for i, splitTransaction := range splitTransactions {
|
|
splitTransactionsBytes[i], err = serialization.SerializePartiallySignedTransaction(splitTransaction)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
return splitTransactionsBytes, nil
|
|
}
|
|
|
|
func (s *server) mergeTransaction(
|
|
splitTransactions []*serialization.PartiallySignedTransaction,
|
|
originalTransaction *serialization.PartiallySignedTransaction,
|
|
toAddress util.Address,
|
|
changeAddress util.Address,
|
|
changeWalletAddress *walletAddress,
|
|
) (*serialization.PartiallySignedTransaction, error) {
|
|
numOutputs := len(originalTransaction.Tx.Outputs)
|
|
if numOutputs > 2 || numOutputs == 0 {
|
|
// This is a sanity check to make sure originalTransaction has either 1 or 2 outputs:
|
|
// 1. For the payment itself
|
|
// 2. (optional) for change
|
|
return nil, errors.Errorf("original transaction has %d outputs, while 1 or 2 are expected",
|
|
len(originalTransaction.Tx.Outputs))
|
|
}
|
|
|
|
totalValue := uint64(0)
|
|
sentValue := originalTransaction.Tx.Outputs[0].Value
|
|
utxos := make([]*libkaspawallet.UTXO, len(splitTransactions))
|
|
for i, splitTransaction := range splitTransactions {
|
|
output := splitTransaction.Tx.Outputs[0]
|
|
utxos[i] = &libkaspawallet.UTXO{
|
|
Outpoint: &externalapi.DomainOutpoint{
|
|
TransactionID: *consensushashing.TransactionID(splitTransaction.Tx),
|
|
Index: 0,
|
|
},
|
|
UTXOEntry: utxo.NewUTXOEntry(output.Value, output.ScriptPublicKey, false, constants.UnacceptedDAAScore),
|
|
DerivationPath: s.walletAddressPath(changeWalletAddress),
|
|
}
|
|
totalValue += output.Value
|
|
totalValue -= feePerInput
|
|
}
|
|
|
|
if totalValue < sentValue {
|
|
// sometimes the fees from compound transactions make the total output higher than what's available from selected
|
|
// utxos, in such cases - find one more UTXO and use it.
|
|
additionalUTXOs, totalValueAdded, err := s.moreUTXOsForMergeTransaction(utxos, sentValue-totalValue)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
utxos = append(utxos, additionalUTXOs...)
|
|
totalValue += totalValueAdded
|
|
}
|
|
|
|
payments := []*libkaspawallet.Payment{{
|
|
Address: toAddress,
|
|
Amount: sentValue,
|
|
}}
|
|
if totalValue > sentValue {
|
|
payments = append(payments, &libkaspawallet.Payment{
|
|
Address: changeAddress,
|
|
Amount: totalValue - sentValue,
|
|
})
|
|
}
|
|
|
|
mergeTransactionBytes, err := libkaspawallet.CreateUnsignedTransaction(s.keysFile.ExtendedPublicKeys,
|
|
s.keysFile.MinimumSignatures, payments, utxos)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return serialization.DeserializePartiallySignedTransaction(mergeTransactionBytes)
|
|
}
|
|
|
|
func (s *server) maybeSplitTransaction(transaction *serialization.PartiallySignedTransaction,
|
|
changeAddress util.Address) ([]*serialization.PartiallySignedTransaction, error) {
|
|
|
|
transactionMass, err := s.estimateMassAfterSignatures(transaction)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if transactionMass < mempool.MaximumStandardTransactionMass {
|
|
return []*serialization.PartiallySignedTransaction{transaction}, nil
|
|
}
|
|
|
|
splitCount, inputCountPerSplit, err := s.splitAndInputPerSplitCounts(transaction, transactionMass, changeAddress)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
splitTransactions := make([]*serialization.PartiallySignedTransaction, splitCount)
|
|
for i := 0; i < splitCount; i++ {
|
|
startIndex := i * inputCountPerSplit
|
|
endIndex := startIndex + inputCountPerSplit
|
|
var err error
|
|
splitTransactions[i], err = s.createSplitTransaction(transaction, changeAddress, startIndex, endIndex)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
return splitTransactions, nil
|
|
}
|
|
|
|
// splitAndInputPerSplitCounts calculates the number of splits to create, and the number of inputs to assign per split.
|
|
func (s *server) splitAndInputPerSplitCounts(transaction *serialization.PartiallySignedTransaction, transactionMass uint64,
|
|
changeAddress util.Address) (splitCount, inputsPerSplitCount int, err error) {
|
|
|
|
// Create a dummy transaction which is a clone of the original transaction, but without inputs,
|
|
// to calculate how much mass do all the inputs have
|
|
transactionWithoutInputs := transaction.Tx.Clone()
|
|
transactionWithoutInputs.Inputs = []*externalapi.DomainTransactionInput{}
|
|
massWithoutInputs := s.txMassCalculator.CalculateTransactionMass(transactionWithoutInputs)
|
|
|
|
massOfAllInputs := transactionMass - massWithoutInputs
|
|
|
|
// Since the transaction was generated by kaspawallet, we assume all inputs have the same number of signatures, and
|
|
// thus - the same mass.
|
|
inputCount := len(transaction.Tx.Inputs)
|
|
massPerInput := massOfAllInputs / uint64(inputCount)
|
|
if massOfAllInputs%uint64(inputCount) > 0 {
|
|
massPerInput++
|
|
}
|
|
|
|
// Create another dummy transaction, this time one similar to the split transactions we wish to generate,
|
|
// but with 0 inputs, to calculate how much mass for inputs do we have available in the split transactions
|
|
splitTransactionWithoutInputs, err := s.createSplitTransaction(transaction, changeAddress, 0, 0)
|
|
if err != nil {
|
|
return 0, 0, err
|
|
}
|
|
massForEverythingExceptInputsInSplitTransaction :=
|
|
s.txMassCalculator.CalculateTransactionMass(splitTransactionWithoutInputs.Tx)
|
|
massForInputsInSplitTransaction := mempool.MaximumStandardTransactionMass - massForEverythingExceptInputsInSplitTransaction
|
|
|
|
inputsPerSplitCount = int(massForInputsInSplitTransaction / massPerInput)
|
|
splitCount = inputCount / inputsPerSplitCount
|
|
if inputCount%inputsPerSplitCount > 0 {
|
|
splitCount++
|
|
}
|
|
|
|
return splitCount, inputsPerSplitCount, nil
|
|
}
|
|
|
|
func (s *server) createSplitTransaction(transaction *serialization.PartiallySignedTransaction,
|
|
changeAddress util.Address, startIndex int, endIndex int) (*serialization.PartiallySignedTransaction, error) {
|
|
|
|
selectedUTXOs := make([]*libkaspawallet.UTXO, 0, endIndex-startIndex)
|
|
totalSompi := uint64(0)
|
|
|
|
for i := startIndex; i < endIndex && i < len(transaction.PartiallySignedInputs); i++ {
|
|
partiallySignedInput := transaction.PartiallySignedInputs[i]
|
|
selectedUTXOs = append(selectedUTXOs, &libkaspawallet.UTXO{
|
|
Outpoint: &transaction.Tx.Inputs[i].PreviousOutpoint,
|
|
UTXOEntry: utxo.NewUTXOEntry(
|
|
partiallySignedInput.PrevOutput.Value, partiallySignedInput.PrevOutput.ScriptPublicKey,
|
|
false, constants.UnacceptedDAAScore),
|
|
DerivationPath: partiallySignedInput.DerivationPath,
|
|
})
|
|
|
|
totalSompi += selectedUTXOs[i-startIndex].UTXOEntry.Amount()
|
|
totalSompi -= feePerInput
|
|
}
|
|
unsignedTransactionBytes, err := libkaspawallet.CreateUnsignedTransaction(s.keysFile.ExtendedPublicKeys,
|
|
s.keysFile.MinimumSignatures,
|
|
[]*libkaspawallet.Payment{{
|
|
Address: changeAddress,
|
|
Amount: totalSompi,
|
|
}}, selectedUTXOs)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return serialization.DeserializePartiallySignedTransaction(unsignedTransactionBytes)
|
|
}
|
|
|
|
func (s *server) estimateMassAfterSignatures(transaction *serialization.PartiallySignedTransaction) (uint64, error) {
|
|
transaction = transaction.Clone()
|
|
var signatureSize uint64
|
|
if s.keysFile.ECDSA {
|
|
signatureSize = secp256k1.SerializedECDSASignatureSize
|
|
} else {
|
|
signatureSize = secp256k1.SerializedSchnorrSignatureSize
|
|
}
|
|
|
|
for i, input := range transaction.PartiallySignedInputs {
|
|
for j, pubKeyPair := range input.PubKeySignaturePairs {
|
|
if uint32(j) >= s.keysFile.MinimumSignatures {
|
|
break
|
|
}
|
|
pubKeyPair.Signature = make([]byte, signatureSize+1) // +1 for SigHashType
|
|
}
|
|
transaction.Tx.Inputs[i].SigOpCount = byte(len(input.PubKeySignaturePairs))
|
|
}
|
|
|
|
transactionWithSignatures, err := libkaspawallet.ExtractTransactionDeserialized(transaction, s.keysFile.ECDSA)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
return s.txMassCalculator.CalculateTransactionMass(transactionWithSignatures), nil
|
|
}
|
|
|
|
func (s *server) moreUTXOsForMergeTransaction(alreadySelectedUTXOs []*libkaspawallet.UTXO, requiredAmount uint64) (
|
|
additionalUTXOs []*libkaspawallet.UTXO, totalValueAdded uint64, err error) {
|
|
|
|
dagInfo, err := s.rpcClient.GetBlockDAGInfo()
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
alreadySelectedUTXOsMap := make(map[externalapi.DomainOutpoint]struct{}, len(alreadySelectedUTXOs))
|
|
for _, alreadySelectedUTXO := range alreadySelectedUTXOs {
|
|
alreadySelectedUTXOsMap[*alreadySelectedUTXO.Outpoint] = struct{}{}
|
|
}
|
|
|
|
for _, utxo := range s.utxosSortedByAmount {
|
|
if _, ok := alreadySelectedUTXOsMap[*utxo.Outpoint]; ok {
|
|
continue
|
|
}
|
|
if !isUTXOSpendable(utxo, dagInfo.VirtualDAAScore, s.params.BlockCoinbaseMaturity) {
|
|
continue
|
|
}
|
|
additionalUTXOs = append(additionalUTXOs, &libkaspawallet.UTXO{
|
|
Outpoint: utxo.Outpoint,
|
|
UTXOEntry: utxo.UTXOEntry,
|
|
DerivationPath: s.walletAddressPath(utxo.address)})
|
|
totalValueAdded += utxo.UTXOEntry.Amount() - feePerInput
|
|
if totalValueAdded >= requiredAmount {
|
|
break
|
|
}
|
|
}
|
|
if totalValueAdded < requiredAmount {
|
|
return nil, 0, errors.Errorf("Insufficient funds for merge transaction")
|
|
}
|
|
|
|
return additionalUTXOs, totalValueAdded, nil
|
|
}
|