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
This commit is contained in:
Svarog 2022-03-27 20:06:55 +03:00 committed by GitHub
parent 9fa08442cf
commit 639183ba0e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 932 additions and 333 deletions

View File

@ -2,13 +2,13 @@ package main
import ( import (
"context" "context"
"encoding/hex"
"fmt" "fmt"
"io/ioutil"
"strings"
"github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/client" "github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/client"
"github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/pb" "github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/pb"
"github.com/pkg/errors" "github.com/pkg/errors"
"io/ioutil"
"strings"
) )
func broadcast(conf *broadcastConfig) error { func broadcast(conf *broadcastConfig) error {
@ -21,34 +21,40 @@ func broadcast(conf *broadcastConfig) error {
ctx, cancel := context.WithTimeout(context.Background(), daemonTimeout) ctx, cancel := context.WithTimeout(context.Background(), daemonTimeout)
defer cancel() defer cancel()
if conf.Transaction == "" && conf.TransactionFile == "" { if conf.Transactions == "" && conf.TransactionsFile == "" {
return errors.Errorf("Either --transaction or --transaction-file is required") return errors.Errorf("Either --transaction or --transaction-file is required")
} }
if conf.Transaction != "" && conf.TransactionFile != "" { if conf.Transactions != "" && conf.TransactionsFile != "" {
return errors.Errorf("Both --transaction and --transaction-file cannot be passed at the same time") return errors.Errorf("Both --transaction and --transaction-file cannot be passed at the same time")
} }
transactionHex := conf.Transaction transactionsHex := conf.Transactions
if conf.TransactionFile != "" { if conf.TransactionsFile != "" {
transactionHexBytes, err := ioutil.ReadFile(conf.TransactionFile) transactionHexBytes, err := ioutil.ReadFile(conf.TransactionsFile)
if err != nil { if err != nil {
return errors.Wrapf(err, "Could not read hex from %s", conf.TransactionFile) return errors.Wrapf(err, "Could not read hex from %s", conf.TransactionsFile)
} }
transactionHex = strings.TrimSpace(string(transactionHexBytes)) transactionsHex = strings.TrimSpace(string(transactionHexBytes))
} }
transaction, err := hex.DecodeString(transactionHex) transactions, err := decodeTransactionsFromHex(transactionsHex)
if err != nil { if err != nil {
return err return err
} }
response, err := daemonClient.Broadcast(ctx, &pb.BroadcastRequest{Transaction: transaction}) transactionsCount := len(transactions)
if err != nil { for i, transaction := range transactions {
return err response, err := daemonClient.Broadcast(ctx, &pb.BroadcastRequest{Transaction: transaction})
if err != nil {
return err
}
if transactionsCount == 1 {
fmt.Println("Transaction was sent successfully")
} else {
fmt.Printf("Transaction %d (out of %d) was sent successfully\n", i+1, transactionsCount)
}
fmt.Printf("Transaction ID: \t%s\n", response.TxID)
} }
fmt.Println("Transaction was sent successfully")
fmt.Printf("Transaction ID: \t%s\n", response.TxID)
return nil return nil
} }

View File

@ -1,9 +1,10 @@
package main package main
import ( import (
"os"
"github.com/kaspanet/kaspad/infrastructure/config" "github.com/kaspanet/kaspad/infrastructure/config"
"github.com/pkg/errors" "github.com/pkg/errors"
"os"
"github.com/jessevdk/go-flags" "github.com/jessevdk/go-flags"
) )
@ -68,15 +69,15 @@ type createUnsignedTransactionConfig struct {
type signConfig struct { type signConfig struct {
KeysFile string `long:"keys-file" short:"f" description:"Keys file location (default: ~/.kaspawallet/keys.json (*nix), %USERPROFILE%\\AppData\\Local\\Kaspawallet\\key.json (Windows))"` KeysFile string `long:"keys-file" short:"f" description:"Keys file location (default: ~/.kaspawallet/keys.json (*nix), %USERPROFILE%\\AppData\\Local\\Kaspawallet\\key.json (Windows))"`
Password string `long:"password" short:"p" description:"Wallet password"` Password string `long:"password" short:"p" description:"Wallet password"`
Transaction string `long:"transaction" short:"t" description:"The unsigned transaction to sign on (encoded in hex)"` Transaction string `long:"transaction" short:"t" description:"The unsigned transaction(s) to sign on (encoded in hex)"`
TransactionFile string `long:"transaction-file" short:"F" description:"The file containing the unsigned transaction to sign on (encoded in hex)"` TransactionFile string `long:"transaction-file" short:"F" description:"The file containing the unsigned transaction(s) to sign on (encoded in hex)"`
config.NetworkFlags config.NetworkFlags
} }
type broadcastConfig struct { type broadcastConfig struct {
DaemonAddress string `long:"daemonaddress" short:"d" description:"Wallet daemon server to connect to (default: localhost:8082)"` DaemonAddress string `long:"daemonaddress" short:"d" description:"Wallet daemon server to connect to (default: localhost:8082)"`
Transaction string `long:"transaction" short:"t" description:"The signed transaction to broadcast (encoded in hex)"` Transactions string `long:"transaction" short:"t" description:"The signed transaction to broadcast (encoded in hex)"`
TransactionFile string `long:"transaction-file" short:"F" description:"The file containing the unsigned transaction to sign on (encoded in hex)"` TransactionsFile string `long:"transaction-file" short:"F" description:"The file containing the unsigned transaction to sign on (encoded in hex)"`
config.NetworkFlags config.NetworkFlags
} }

View File

@ -2,8 +2,8 @@ package main
import ( import (
"context" "context"
"encoding/hex"
"fmt" "fmt"
"github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/client" "github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/client"
"github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/pb" "github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/pb"
"github.com/kaspanet/kaspad/domain/consensus/utils/constants" "github.com/kaspanet/kaspad/domain/consensus/utils/constants"
@ -20,7 +20,7 @@ func createUnsignedTransaction(conf *createUnsignedTransactionConfig) error {
defer cancel() defer cancel()
sendAmountSompi := uint64(conf.SendAmount * constants.SompiPerKaspa) sendAmountSompi := uint64(conf.SendAmount * constants.SompiPerKaspa)
response, err := daemonClient.CreateUnsignedTransaction(ctx, &pb.CreateUnsignedTransactionRequest{ response, err := daemonClient.CreateUnsignedTransactions(ctx, &pb.CreateUnsignedTransactionsRequest{
Address: conf.ToAddress, Address: conf.ToAddress,
Amount: sendAmountSompi, Amount: sendAmountSompi,
}) })
@ -29,6 +29,6 @@ func createUnsignedTransaction(conf *createUnsignedTransactionConfig) error {
} }
fmt.Println("Created unsigned transaction") fmt.Println("Created unsigned transaction")
fmt.Println(hex.EncodeToString(response.UnsignedTransaction)) fmt.Println(encodeTransactionsToHex(response.UnsignedTransactions))
return nil return nil
} }

View File

@ -1,6 +1,6 @@
// Code generated by protoc-gen-go. DO NOT EDIT. // Code generated by protoc-gen-go. DO NOT EDIT.
// versions: // versions:
// protoc-gen-go v1.27.1 // protoc-gen-go v1.26.0
// protoc v3.12.3 // protoc v3.12.3
// source: kaspawalletd.proto // source: kaspawalletd.proto
@ -184,7 +184,7 @@ func (x *AddressBalances) GetPending() uint64 {
return 0 return 0
} }
type CreateUnsignedTransactionRequest struct { type CreateUnsignedTransactionsRequest struct {
state protoimpl.MessageState state protoimpl.MessageState
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields unknownFields protoimpl.UnknownFields
@ -193,8 +193,8 @@ type CreateUnsignedTransactionRequest struct {
Amount uint64 `protobuf:"varint,2,opt,name=amount,proto3" json:"amount,omitempty"` Amount uint64 `protobuf:"varint,2,opt,name=amount,proto3" json:"amount,omitempty"`
} }
func (x *CreateUnsignedTransactionRequest) Reset() { func (x *CreateUnsignedTransactionsRequest) Reset() {
*x = CreateUnsignedTransactionRequest{} *x = CreateUnsignedTransactionsRequest{}
if protoimpl.UnsafeEnabled { if protoimpl.UnsafeEnabled {
mi := &file_kaspawalletd_proto_msgTypes[3] mi := &file_kaspawalletd_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
@ -202,13 +202,13 @@ func (x *CreateUnsignedTransactionRequest) Reset() {
} }
} }
func (x *CreateUnsignedTransactionRequest) String() string { func (x *CreateUnsignedTransactionsRequest) String() string {
return protoimpl.X.MessageStringOf(x) return protoimpl.X.MessageStringOf(x)
} }
func (*CreateUnsignedTransactionRequest) ProtoMessage() {} func (*CreateUnsignedTransactionsRequest) ProtoMessage() {}
func (x *CreateUnsignedTransactionRequest) ProtoReflect() protoreflect.Message { func (x *CreateUnsignedTransactionsRequest) ProtoReflect() protoreflect.Message {
mi := &file_kaspawalletd_proto_msgTypes[3] mi := &file_kaspawalletd_proto_msgTypes[3]
if protoimpl.UnsafeEnabled && x != nil { if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
@ -220,35 +220,35 @@ func (x *CreateUnsignedTransactionRequest) ProtoReflect() protoreflect.Message {
return mi.MessageOf(x) return mi.MessageOf(x)
} }
// Deprecated: Use CreateUnsignedTransactionRequest.ProtoReflect.Descriptor instead. // Deprecated: Use CreateUnsignedTransactionsRequest.ProtoReflect.Descriptor instead.
func (*CreateUnsignedTransactionRequest) Descriptor() ([]byte, []int) { func (*CreateUnsignedTransactionsRequest) Descriptor() ([]byte, []int) {
return file_kaspawalletd_proto_rawDescGZIP(), []int{3} return file_kaspawalletd_proto_rawDescGZIP(), []int{3}
} }
func (x *CreateUnsignedTransactionRequest) GetAddress() string { func (x *CreateUnsignedTransactionsRequest) GetAddress() string {
if x != nil { if x != nil {
return x.Address return x.Address
} }
return "" return ""
} }
func (x *CreateUnsignedTransactionRequest) GetAmount() uint64 { func (x *CreateUnsignedTransactionsRequest) GetAmount() uint64 {
if x != nil { if x != nil {
return x.Amount return x.Amount
} }
return 0 return 0
} }
type CreateUnsignedTransactionResponse struct { type CreateUnsignedTransactionsResponse struct {
state protoimpl.MessageState state protoimpl.MessageState
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields unknownFields protoimpl.UnknownFields
UnsignedTransaction []byte `protobuf:"bytes,1,opt,name=unsignedTransaction,proto3" json:"unsignedTransaction,omitempty"` UnsignedTransactions [][]byte `protobuf:"bytes,1,rep,name=unsignedTransactions,proto3" json:"unsignedTransactions,omitempty"`
} }
func (x *CreateUnsignedTransactionResponse) Reset() { func (x *CreateUnsignedTransactionsResponse) Reset() {
*x = CreateUnsignedTransactionResponse{} *x = CreateUnsignedTransactionsResponse{}
if protoimpl.UnsafeEnabled { if protoimpl.UnsafeEnabled {
mi := &file_kaspawalletd_proto_msgTypes[4] mi := &file_kaspawalletd_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
@ -256,13 +256,13 @@ func (x *CreateUnsignedTransactionResponse) Reset() {
} }
} }
func (x *CreateUnsignedTransactionResponse) String() string { func (x *CreateUnsignedTransactionsResponse) String() string {
return protoimpl.X.MessageStringOf(x) return protoimpl.X.MessageStringOf(x)
} }
func (*CreateUnsignedTransactionResponse) ProtoMessage() {} func (*CreateUnsignedTransactionsResponse) ProtoMessage() {}
func (x *CreateUnsignedTransactionResponse) ProtoReflect() protoreflect.Message { func (x *CreateUnsignedTransactionsResponse) ProtoReflect() protoreflect.Message {
mi := &file_kaspawalletd_proto_msgTypes[4] mi := &file_kaspawalletd_proto_msgTypes[4]
if protoimpl.UnsafeEnabled && x != nil { if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
@ -274,14 +274,14 @@ func (x *CreateUnsignedTransactionResponse) ProtoReflect() protoreflect.Message
return mi.MessageOf(x) return mi.MessageOf(x)
} }
// Deprecated: Use CreateUnsignedTransactionResponse.ProtoReflect.Descriptor instead. // Deprecated: Use CreateUnsignedTransactionsResponse.ProtoReflect.Descriptor instead.
func (*CreateUnsignedTransactionResponse) Descriptor() ([]byte, []int) { func (*CreateUnsignedTransactionsResponse) Descriptor() ([]byte, []int) {
return file_kaspawalletd_proto_rawDescGZIP(), []int{4} return file_kaspawalletd_proto_rawDescGZIP(), []int{4}
} }
func (x *CreateUnsignedTransactionResponse) GetUnsignedTransaction() []byte { func (x *CreateUnsignedTransactionsResponse) GetUnsignedTransactions() [][]byte {
if x != nil { if x != nil {
return x.UnsignedTransaction return x.UnsignedTransactions
} }
return nil return nil
} }
@ -646,64 +646,65 @@ var file_kaspawalletd_proto_rawDesc = []byte{
0x73, 0x12, 0x1c, 0x0a, 0x09, 0x61, 0x76, 0x61, 0x69, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x18, 0x02, 0x73, 0x12, 0x1c, 0x0a, 0x09, 0x61, 0x76, 0x61, 0x69, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x18, 0x02,
0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x61, 0x76, 0x61, 0x69, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x12, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x61, 0x76, 0x61, 0x69, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x12,
0x18, 0x0a, 0x07, 0x70, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, 0x18, 0x0a, 0x07, 0x70, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, 0x04,
0x52, 0x07, 0x70, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x22, 0x54, 0x0a, 0x20, 0x43, 0x72, 0x65, 0x52, 0x07, 0x70, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x22, 0x55, 0x0a, 0x21, 0x43, 0x72, 0x65,
0x61, 0x74, 0x65, 0x55, 0x6e, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x74, 0x65, 0x55, 0x6e, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x54, 0x72, 0x61, 0x6e, 0x73,
0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18,
0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x0a, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52,
0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x61, 0x6d, 0x6f, 0x75,
0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x22, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74,
0x55, 0x0a, 0x21, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x55, 0x6e, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x22, 0x58, 0x0a, 0x22, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x55, 0x6e, 0x73, 0x69, 0x67, 0x6e,
0x64, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x65, 0x64, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65,
0x6f, 0x6e, 0x73, 0x65, 0x12, 0x30, 0x0a, 0x13, 0x75, 0x6e, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x32, 0x0a, 0x14, 0x75, 0x6e, 0x73, 0x69, 0x67, 0x6e,
0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x65, 0x64, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x01,
0x0c, 0x52, 0x13, 0x75, 0x6e, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x20, 0x03, 0x28, 0x0c, 0x52, 0x14, 0x75, 0x6e, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x54, 0x72,
0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x16, 0x0a, 0x14, 0x53, 0x68, 0x6f, 0x77, 0x41, 0x64, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x16, 0x0a, 0x14, 0x53, 0x68,
0x64, 0x72, 0x65, 0x73, 0x73, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x31, 0x6f, 0x77, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65,
0x0a, 0x15, 0x53, 0x68, 0x6f, 0x77, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x65, 0x73, 0x52, 0x73, 0x74, 0x22, 0x31, 0x0a, 0x15, 0x53, 0x68, 0x6f, 0x77, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73,
0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x61,
0x73, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x61, 0x64,
0x73, 0x22, 0x13, 0x0a, 0x11, 0x4e, 0x65, 0x77, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x52, 0x64, 0x72, 0x65, 0x73, 0x73, 0x22, 0x13, 0x0a, 0x11, 0x4e, 0x65, 0x77, 0x41, 0x64, 0x64, 0x72,
0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x2e, 0x0a, 0x12, 0x4e, 0x65, 0x77, 0x41, 0x64, 0x64, 0x65, 0x73, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x2e, 0x0a, 0x12, 0x4e, 0x65,
0x72, 0x65, 0x73, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x77, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, 0x12, 0x18, 0x0a, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28,
0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x22, 0x34, 0x0a, 0x10, 0x42, 0x72, 0x6f, 0x61, 0x64, 0x63, 0x09, 0x52, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x22, 0x34, 0x0a, 0x10, 0x42, 0x72,
0x61, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x20, 0x0a, 0x0b, 0x74, 0x72, 0x6f, 0x61, 0x64, 0x63, 0x61, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x20,
0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0a, 0x0b, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20,
0x0b, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x27, 0x0a, 0x11, 0x01, 0x28, 0x0c, 0x52, 0x0b, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e,
0x42, 0x72, 0x6f, 0x61, 0x64, 0x63, 0x61, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x22, 0x27, 0x0a, 0x11, 0x42, 0x72, 0x6f, 0x61, 0x64, 0x63, 0x61, 0x73, 0x74, 0x52, 0x65, 0x73,
0x65, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x78, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x78, 0x49, 0x44, 0x18, 0x01, 0x20,
0x04, 0x74, 0x78, 0x49, 0x44, 0x22, 0x11, 0x0a, 0x0f, 0x53, 0x68, 0x75, 0x74, 0x64, 0x6f, 0x77, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x78, 0x49, 0x44, 0x22, 0x11, 0x0a, 0x0f, 0x53, 0x68, 0x75,
0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x12, 0x0a, 0x10, 0x53, 0x68, 0x75, 0x74, 0x74, 0x64, 0x6f, 0x77, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x12, 0x0a, 0x10,
0x64, 0x6f, 0x77, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32, 0x91, 0x03, 0x0a, 0x53, 0x68, 0x75, 0x74, 0x64, 0x6f, 0x77, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
0x0c, 0x6b, 0x61, 0x73, 0x70, 0x61, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x64, 0x12, 0x37, 0x0a, 0x32, 0x94, 0x03, 0x0a, 0x0c, 0x6b, 0x61, 0x73, 0x70, 0x61, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74,
0x0a, 0x47, 0x65, 0x74, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x12, 0x12, 0x2e, 0x47, 0x65, 0x64, 0x12, 0x37, 0x0a, 0x0a, 0x47, 0x65, 0x74, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x12,
0x74, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x12, 0x2e, 0x47, 0x65, 0x74, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75,
0x13, 0x2e, 0x47, 0x65, 0x74, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x65, 0x73, 0x74, 0x1a, 0x13, 0x2e, 0x47, 0x65, 0x74, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65,
0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x64, 0x0a, 0x19, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x67, 0x0a, 0x1a, 0x43, 0x72,
0x55, 0x6e, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x65, 0x61, 0x74, 0x65, 0x55, 0x6e, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x54, 0x72, 0x61, 0x6e,
0x69, 0x6f, 0x6e, 0x12, 0x21, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x55, 0x6e, 0x73, 0x69, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x22, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74,
0x67, 0x6e, 0x65, 0x64, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x55, 0x6e, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63,
0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x55, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x23, 0x2e, 0x43,
0x6e, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x72, 0x65, 0x61, 0x74, 0x65, 0x55, 0x6e, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x54, 0x72, 0x61,
0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x40, 0x0a, 0x0d, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
0x53, 0x68, 0x6f, 0x77, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x65, 0x73, 0x12, 0x15, 0x2e, 0x65, 0x22, 0x00, 0x12, 0x40, 0x0a, 0x0d, 0x53, 0x68, 0x6f, 0x77, 0x41, 0x64, 0x64, 0x72, 0x65,
0x53, 0x68, 0x6f, 0x77, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x65, 0x73, 0x52, 0x65, 0x71, 0x73, 0x73, 0x65, 0x73, 0x12, 0x15, 0x2e, 0x53, 0x68, 0x6f, 0x77, 0x41, 0x64, 0x64, 0x72, 0x65,
0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x53, 0x68, 0x6f, 0x77, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x53, 0x68,
0x73, 0x73, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x37, 0x6f, 0x77, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f,
0x0a, 0x0a, 0x4e, 0x65, 0x77, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x12, 0x2e, 0x4e, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x37, 0x0a, 0x0a, 0x4e, 0x65, 0x77, 0x41, 0x64, 0x64, 0x72,
0x65, 0x77, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x65, 0x73, 0x73, 0x12, 0x12, 0x2e, 0x4e, 0x65, 0x77, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73,
0x1a, 0x13, 0x2e, 0x4e, 0x65, 0x77, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x52, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x13, 0x2e, 0x4e, 0x65, 0x77, 0x41, 0x64, 0x64,
0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x31, 0x0a, 0x08, 0x53, 0x68, 0x75, 0x74, 0x64, 0x72, 0x65, 0x73, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x31,
0x6f, 0x77, 0x6e, 0x12, 0x10, 0x2e, 0x53, 0x68, 0x75, 0x74, 0x64, 0x6f, 0x77, 0x6e, 0x52, 0x65, 0x0a, 0x08, 0x53, 0x68, 0x75, 0x74, 0x64, 0x6f, 0x77, 0x6e, 0x12, 0x10, 0x2e, 0x53, 0x68, 0x75,
0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x11, 0x2e, 0x53, 0x68, 0x75, 0x74, 0x64, 0x6f, 0x77, 0x6e, 0x74, 0x64, 0x6f, 0x77, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x11, 0x2e, 0x53,
0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x34, 0x0a, 0x09, 0x42, 0x72, 0x68, 0x75, 0x74, 0x64, 0x6f, 0x77, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22,
0x6f, 0x61, 0x64, 0x63, 0x61, 0x73, 0x74, 0x12, 0x11, 0x2e, 0x42, 0x72, 0x6f, 0x61, 0x64, 0x63, 0x00, 0x12, 0x34, 0x0a, 0x09, 0x42, 0x72, 0x6f, 0x61, 0x64, 0x63, 0x61, 0x73, 0x74, 0x12, 0x11,
0x61, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x12, 0x2e, 0x42, 0x72, 0x6f, 0x2e, 0x42, 0x72, 0x6f, 0x61, 0x64, 0x63, 0x61, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
0x61, 0x64, 0x63, 0x61, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x74, 0x1a, 0x12, 0x2e, 0x42, 0x72, 0x6f, 0x61, 0x64, 0x63, 0x61, 0x73, 0x74, 0x52, 0x65, 0x73,
0x42, 0x36, 0x5a, 0x34, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6b, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, 0x36, 0x5a, 0x34, 0x67, 0x69, 0x74, 0x68, 0x75,
0x61, 0x73, 0x70, 0x61, 0x6e, 0x65, 0x74, 0x2f, 0x6b, 0x61, 0x73, 0x70, 0x61, 0x64, 0x2f, 0x63, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6b, 0x61, 0x73, 0x70, 0x61, 0x6e, 0x65, 0x74, 0x2f, 0x6b,
0x6d, 0x64, 0x2f, 0x6b, 0x61, 0x73, 0x70, 0x61, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x2f, 0x64, 0x61, 0x73, 0x70, 0x61, 0x64, 0x2f, 0x63, 0x6d, 0x64, 0x2f, 0x6b, 0x61, 0x73, 0x70, 0x61, 0x77,
0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2f, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x2f, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2f, 0x70, 0x62, 0x62,
0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
} }
var ( var (
@ -720,30 +721,30 @@ func file_kaspawalletd_proto_rawDescGZIP() []byte {
var file_kaspawalletd_proto_msgTypes = make([]protoimpl.MessageInfo, 13) var file_kaspawalletd_proto_msgTypes = make([]protoimpl.MessageInfo, 13)
var file_kaspawalletd_proto_goTypes = []interface{}{ var file_kaspawalletd_proto_goTypes = []interface{}{
(*GetBalanceRequest)(nil), // 0: GetBalanceRequest (*GetBalanceRequest)(nil), // 0: GetBalanceRequest
(*GetBalanceResponse)(nil), // 1: GetBalanceResponse (*GetBalanceResponse)(nil), // 1: GetBalanceResponse
(*AddressBalances)(nil), // 2: AddressBalances (*AddressBalances)(nil), // 2: AddressBalances
(*CreateUnsignedTransactionRequest)(nil), // 3: CreateUnsignedTransactionRequest (*CreateUnsignedTransactionsRequest)(nil), // 3: CreateUnsignedTransactionsRequest
(*CreateUnsignedTransactionResponse)(nil), // 4: CreateUnsignedTransactionResponse (*CreateUnsignedTransactionsResponse)(nil), // 4: CreateUnsignedTransactionsResponse
(*ShowAddressesRequest)(nil), // 5: ShowAddressesRequest (*ShowAddressesRequest)(nil), // 5: ShowAddressesRequest
(*ShowAddressesResponse)(nil), // 6: ShowAddressesResponse (*ShowAddressesResponse)(nil), // 6: ShowAddressesResponse
(*NewAddressRequest)(nil), // 7: NewAddressRequest (*NewAddressRequest)(nil), // 7: NewAddressRequest
(*NewAddressResponse)(nil), // 8: NewAddressResponse (*NewAddressResponse)(nil), // 8: NewAddressResponse
(*BroadcastRequest)(nil), // 9: BroadcastRequest (*BroadcastRequest)(nil), // 9: BroadcastRequest
(*BroadcastResponse)(nil), // 10: BroadcastResponse (*BroadcastResponse)(nil), // 10: BroadcastResponse
(*ShutdownRequest)(nil), // 11: ShutdownRequest (*ShutdownRequest)(nil), // 11: ShutdownRequest
(*ShutdownResponse)(nil), // 12: ShutdownResponse (*ShutdownResponse)(nil), // 12: ShutdownResponse
} }
var file_kaspawalletd_proto_depIdxs = []int32{ var file_kaspawalletd_proto_depIdxs = []int32{
2, // 0: GetBalanceResponse.addressBalances:type_name -> AddressBalances 2, // 0: GetBalanceResponse.addressBalances:type_name -> AddressBalances
0, // 1: kaspawalletd.GetBalance:input_type -> GetBalanceRequest 0, // 1: kaspawalletd.GetBalance:input_type -> GetBalanceRequest
3, // 2: kaspawalletd.CreateUnsignedTransaction:input_type -> CreateUnsignedTransactionRequest 3, // 2: kaspawalletd.CreateUnsignedTransactions:input_type -> CreateUnsignedTransactionsRequest
5, // 3: kaspawalletd.ShowAddresses:input_type -> ShowAddressesRequest 5, // 3: kaspawalletd.ShowAddresses:input_type -> ShowAddressesRequest
7, // 4: kaspawalletd.NewAddress:input_type -> NewAddressRequest 7, // 4: kaspawalletd.NewAddress:input_type -> NewAddressRequest
11, // 5: kaspawalletd.Shutdown:input_type -> ShutdownRequest 11, // 5: kaspawalletd.Shutdown:input_type -> ShutdownRequest
9, // 6: kaspawalletd.Broadcast:input_type -> BroadcastRequest 9, // 6: kaspawalletd.Broadcast:input_type -> BroadcastRequest
1, // 7: kaspawalletd.GetBalance:output_type -> GetBalanceResponse 1, // 7: kaspawalletd.GetBalance:output_type -> GetBalanceResponse
4, // 8: kaspawalletd.CreateUnsignedTransaction:output_type -> CreateUnsignedTransactionResponse 4, // 8: kaspawalletd.CreateUnsignedTransactions:output_type -> CreateUnsignedTransactionsResponse
6, // 9: kaspawalletd.ShowAddresses:output_type -> ShowAddressesResponse 6, // 9: kaspawalletd.ShowAddresses:output_type -> ShowAddressesResponse
8, // 10: kaspawalletd.NewAddress:output_type -> NewAddressResponse 8, // 10: kaspawalletd.NewAddress:output_type -> NewAddressResponse
12, // 11: kaspawalletd.Shutdown:output_type -> ShutdownResponse 12, // 11: kaspawalletd.Shutdown:output_type -> ShutdownResponse
@ -798,7 +799,7 @@ func file_kaspawalletd_proto_init() {
} }
} }
file_kaspawalletd_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { file_kaspawalletd_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*CreateUnsignedTransactionRequest); i { switch v := v.(*CreateUnsignedTransactionsRequest); i {
case 0: case 0:
return &v.state return &v.state
case 1: case 1:
@ -810,7 +811,7 @@ func file_kaspawalletd_proto_init() {
} }
} }
file_kaspawalletd_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { file_kaspawalletd_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*CreateUnsignedTransactionResponse); i { switch v := v.(*CreateUnsignedTransactionsResponse); i {
case 0: case 0:
return &v.state return &v.state
case 1: case 1:

View File

@ -4,7 +4,7 @@ option go_package = "github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/pb";
service kaspawalletd { service kaspawalletd {
rpc GetBalance (GetBalanceRequest) returns (GetBalanceResponse) {} rpc GetBalance (GetBalanceRequest) returns (GetBalanceResponse) {}
rpc CreateUnsignedTransaction (CreateUnsignedTransactionRequest) returns (CreateUnsignedTransactionResponse) {} rpc CreateUnsignedTransactions (CreateUnsignedTransactionsRequest) returns (CreateUnsignedTransactionsResponse) {}
rpc ShowAddresses (ShowAddressesRequest) returns (ShowAddressesResponse) {} rpc ShowAddresses (ShowAddressesRequest) returns (ShowAddressesResponse) {}
rpc NewAddress (NewAddressRequest) returns (NewAddressResponse) {} rpc NewAddress (NewAddressRequest) returns (NewAddressResponse) {}
rpc Shutdown (ShutdownRequest) returns (ShutdownResponse) {} rpc Shutdown (ShutdownRequest) returns (ShutdownResponse) {}
@ -26,13 +26,13 @@ message AddressBalances {
uint64 pending = 3; uint64 pending = 3;
} }
message CreateUnsignedTransactionRequest { message CreateUnsignedTransactionsRequest {
string address = 1; string address = 1;
uint64 amount = 2; uint64 amount = 2;
} }
message CreateUnsignedTransactionResponse { message CreateUnsignedTransactionsResponse {
bytes unsignedTransaction = 1; repeated bytes unsignedTransactions = 1;
} }
message ShowAddressesRequest { message ShowAddressesRequest {

View File

@ -19,7 +19,7 @@ const _ = grpc.SupportPackageIsVersion7
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
type KaspawalletdClient interface { type KaspawalletdClient interface {
GetBalance(ctx context.Context, in *GetBalanceRequest, opts ...grpc.CallOption) (*GetBalanceResponse, error) GetBalance(ctx context.Context, in *GetBalanceRequest, opts ...grpc.CallOption) (*GetBalanceResponse, error)
CreateUnsignedTransaction(ctx context.Context, in *CreateUnsignedTransactionRequest, opts ...grpc.CallOption) (*CreateUnsignedTransactionResponse, error) CreateUnsignedTransactions(ctx context.Context, in *CreateUnsignedTransactionsRequest, opts ...grpc.CallOption) (*CreateUnsignedTransactionsResponse, error)
ShowAddresses(ctx context.Context, in *ShowAddressesRequest, opts ...grpc.CallOption) (*ShowAddressesResponse, error) ShowAddresses(ctx context.Context, in *ShowAddressesRequest, opts ...grpc.CallOption) (*ShowAddressesResponse, error)
NewAddress(ctx context.Context, in *NewAddressRequest, opts ...grpc.CallOption) (*NewAddressResponse, error) NewAddress(ctx context.Context, in *NewAddressRequest, opts ...grpc.CallOption) (*NewAddressResponse, error)
Shutdown(ctx context.Context, in *ShutdownRequest, opts ...grpc.CallOption) (*ShutdownResponse, error) Shutdown(ctx context.Context, in *ShutdownRequest, opts ...grpc.CallOption) (*ShutdownResponse, error)
@ -43,9 +43,9 @@ func (c *kaspawalletdClient) GetBalance(ctx context.Context, in *GetBalanceReque
return out, nil return out, nil
} }
func (c *kaspawalletdClient) CreateUnsignedTransaction(ctx context.Context, in *CreateUnsignedTransactionRequest, opts ...grpc.CallOption) (*CreateUnsignedTransactionResponse, error) { func (c *kaspawalletdClient) CreateUnsignedTransactions(ctx context.Context, in *CreateUnsignedTransactionsRequest, opts ...grpc.CallOption) (*CreateUnsignedTransactionsResponse, error) {
out := new(CreateUnsignedTransactionResponse) out := new(CreateUnsignedTransactionsResponse)
err := c.cc.Invoke(ctx, "/kaspawalletd/CreateUnsignedTransaction", in, out, opts...) err := c.cc.Invoke(ctx, "/kaspawalletd/CreateUnsignedTransactions", in, out, opts...)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -93,7 +93,7 @@ func (c *kaspawalletdClient) Broadcast(ctx context.Context, in *BroadcastRequest
// for forward compatibility // for forward compatibility
type KaspawalletdServer interface { type KaspawalletdServer interface {
GetBalance(context.Context, *GetBalanceRequest) (*GetBalanceResponse, error) GetBalance(context.Context, *GetBalanceRequest) (*GetBalanceResponse, error)
CreateUnsignedTransaction(context.Context, *CreateUnsignedTransactionRequest) (*CreateUnsignedTransactionResponse, error) CreateUnsignedTransactions(context.Context, *CreateUnsignedTransactionsRequest) (*CreateUnsignedTransactionsResponse, error)
ShowAddresses(context.Context, *ShowAddressesRequest) (*ShowAddressesResponse, error) ShowAddresses(context.Context, *ShowAddressesRequest) (*ShowAddressesResponse, error)
NewAddress(context.Context, *NewAddressRequest) (*NewAddressResponse, error) NewAddress(context.Context, *NewAddressRequest) (*NewAddressResponse, error)
Shutdown(context.Context, *ShutdownRequest) (*ShutdownResponse, error) Shutdown(context.Context, *ShutdownRequest) (*ShutdownResponse, error)
@ -108,8 +108,8 @@ type UnimplementedKaspawalletdServer struct {
func (UnimplementedKaspawalletdServer) GetBalance(context.Context, *GetBalanceRequest) (*GetBalanceResponse, error) { func (UnimplementedKaspawalletdServer) GetBalance(context.Context, *GetBalanceRequest) (*GetBalanceResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetBalance not implemented") return nil, status.Errorf(codes.Unimplemented, "method GetBalance not implemented")
} }
func (UnimplementedKaspawalletdServer) CreateUnsignedTransaction(context.Context, *CreateUnsignedTransactionRequest) (*CreateUnsignedTransactionResponse, error) { func (UnimplementedKaspawalletdServer) CreateUnsignedTransactions(context.Context, *CreateUnsignedTransactionsRequest) (*CreateUnsignedTransactionsResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method CreateUnsignedTransaction not implemented") return nil, status.Errorf(codes.Unimplemented, "method CreateUnsignedTransactions not implemented")
} }
func (UnimplementedKaspawalletdServer) ShowAddresses(context.Context, *ShowAddressesRequest) (*ShowAddressesResponse, error) { func (UnimplementedKaspawalletdServer) ShowAddresses(context.Context, *ShowAddressesRequest) (*ShowAddressesResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ShowAddresses not implemented") return nil, status.Errorf(codes.Unimplemented, "method ShowAddresses not implemented")
@ -154,20 +154,20 @@ func _Kaspawalletd_GetBalance_Handler(srv interface{}, ctx context.Context, dec
return interceptor(ctx, in, info, handler) return interceptor(ctx, in, info, handler)
} }
func _Kaspawalletd_CreateUnsignedTransaction_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { func _Kaspawalletd_CreateUnsignedTransactions_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(CreateUnsignedTransactionRequest) in := new(CreateUnsignedTransactionsRequest)
if err := dec(in); err != nil { if err := dec(in); err != nil {
return nil, err return nil, err
} }
if interceptor == nil { if interceptor == nil {
return srv.(KaspawalletdServer).CreateUnsignedTransaction(ctx, in) return srv.(KaspawalletdServer).CreateUnsignedTransactions(ctx, in)
} }
info := &grpc.UnaryServerInfo{ info := &grpc.UnaryServerInfo{
Server: srv, Server: srv,
FullMethod: "/kaspawalletd/CreateUnsignedTransaction", FullMethod: "/kaspawalletd/CreateUnsignedTransactions",
} }
handler := func(ctx context.Context, req interface{}) (interface{}, error) { handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(KaspawalletdServer).CreateUnsignedTransaction(ctx, req.(*CreateUnsignedTransactionRequest)) return srv.(KaspawalletdServer).CreateUnsignedTransactions(ctx, req.(*CreateUnsignedTransactionsRequest))
} }
return interceptor(ctx, in, info, handler) return interceptor(ctx, in, info, handler)
} }
@ -256,8 +256,8 @@ var Kaspawalletd_ServiceDesc = grpc.ServiceDesc{
Handler: _Kaspawalletd_GetBalance_Handler, Handler: _Kaspawalletd_GetBalance_Handler,
}, },
{ {
MethodName: "CreateUnsignedTransaction", MethodName: "CreateUnsignedTransactions",
Handler: _Kaspawalletd_CreateUnsignedTransaction_Handler, Handler: _Kaspawalletd_CreateUnsignedTransactions_Handler,
}, },
{ {
MethodName: "ShowAddresses", MethodName: "ShowAddresses",

View File

@ -10,15 +10,15 @@ import (
"github.com/pkg/errors" "github.com/pkg/errors"
) )
func (s *server) changeAddress() (util.Address, error) { func (s *server) changeAddress() (util.Address, *walletAddress, error) {
err := s.keysFile.SetLastUsedInternalIndex(s.keysFile.LastUsedInternalIndex() + 1) err := s.keysFile.SetLastUsedInternalIndex(s.keysFile.LastUsedInternalIndex() + 1)
if err != nil { if err != nil {
return nil, err return nil, nil, err
} }
err = s.keysFile.Save() err = s.keysFile.Save()
if err != nil { if err != nil {
return nil, err return nil, nil, err
} }
walletAddr := &walletAddress{ walletAddr := &walletAddress{
@ -27,7 +27,11 @@ func (s *server) changeAddress() (util.Address, error) {
keyChain: libkaspawallet.InternalKeychain, keyChain: libkaspawallet.InternalKeychain,
} }
path := s.walletAddressPath(walletAddr) path := s.walletAddressPath(walletAddr)
return libkaspawallet.Address(s.params, s.keysFile.ExtendedPublicKeys, s.keysFile.MinimumSignatures, path, s.keysFile.ECDSA) address, err := libkaspawallet.Address(s.params, s.keysFile.ExtendedPublicKeys, s.keysFile.MinimumSignatures, path, s.keysFile.ECDSA)
if err != nil {
return nil, nil, err
}
return address, walletAddr, nil
} }
func (s *server) ShowAddresses(_ context.Context, request *pb.ShowAddressesRequest) (*pb.ShowAddressesResponse, error) { func (s *server) ShowAddresses(_ context.Context, request *pb.ShowAddressesRequest) (*pb.ShowAddressesResponse, error) {

View File

@ -10,7 +10,11 @@ import (
"github.com/pkg/errors" "github.com/pkg/errors"
) )
func (s *server) CreateUnsignedTransaction(_ context.Context, request *pb.CreateUnsignedTransactionRequest) (*pb.CreateUnsignedTransactionResponse, error) { // TODO: Implement a better fee estimation mechanism
const feePerInput = 10000
func (s *server) CreateUnsignedTransactions(_ context.Context, request *pb.CreateUnsignedTransactionsRequest) (
*pb.CreateUnsignedTransactionsResponse, error) {
s.lock.Lock() s.lock.Lock()
defer s.lock.Unlock() defer s.lock.Unlock()
@ -28,14 +32,12 @@ func (s *server) CreateUnsignedTransaction(_ context.Context, request *pb.Create
return nil, err return nil, err
} }
// TODO: Implement a better fee estimation mechanism
const feePerInput = 10000
selectedUTXOs, changeSompi, err := s.selectUTXOs(request.Amount, feePerInput) selectedUTXOs, changeSompi, err := s.selectUTXOs(request.Amount, feePerInput)
if err != nil { if err != nil {
return nil, err return nil, err
} }
changeAddress, err := s.changeAddress() changeAddress, changeWalletAddress, err := s.changeAddress()
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -53,7 +55,12 @@ func (s *server) CreateUnsignedTransaction(_ context.Context, request *pb.Create
return nil, err return nil, err
} }
return &pb.CreateUnsignedTransactionResponse{UnsignedTransaction: unsignedTransaction}, nil unsignedTransactions, err := s.maybeAutoCompoundTransaction(unsignedTransaction, toAddress, changeAddress, changeWalletAddress)
if err != nil {
return nil, err
}
return &pb.CreateUnsignedTransactionsResponse{UnsignedTransactions: unsignedTransactions}, nil
} }
func (s *server) selectUTXOs(spendAmount uint64, feePerInput uint64) ( func (s *server) selectUTXOs(spendAmount uint64, feePerInput uint64) (

View File

@ -7,6 +7,8 @@ import (
"sync" "sync"
"time" "time"
"github.com/kaspanet/kaspad/util/txmass"
"github.com/kaspanet/kaspad/util/profiling" "github.com/kaspanet/kaspad/util/profiling"
"github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/pb" "github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/pb"
@ -32,6 +34,7 @@ type server struct {
keysFile *keys.File keysFile *keys.File
shutdown chan struct{} shutdown chan struct{}
addressSet walletAddressSet addressSet walletAddressSet
txMassCalculator *txmass.Calculator
} }
// Start starts the kaspawalletd server // Start starts the kaspawalletd server
@ -69,6 +72,7 @@ func Start(params *dagconfig.Params, listen, rpcServer string, keysFilePath stri
keysFile: keysFile, keysFile: keysFile,
shutdown: make(chan struct{}), shutdown: make(chan struct{}),
addressSet: make(walletAddressSet), addressSet: make(walletAddressSet),
txMassCalculator: txmass.NewCalculator(params.MassPerTxByte, params.MassPerScriptPubKeyByte, params.MassPerSigOp),
} }
spawn("serverInstance.sync", func() { spawn("serverInstance.sync", func() {

View File

@ -0,0 +1,278 @@
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
}

View File

@ -0,0 +1,152 @@
package server
import (
"testing"
"github.com/kaspanet/kaspad/cmd/kaspawallet/libkaspawallet/serialization"
"github.com/kaspanet/kaspad/cmd/kaspawallet/keys"
"github.com/kaspanet/kaspad/util/txmass"
"github.com/kaspanet/kaspad/domain/dagconfig"
"github.com/kaspanet/kaspad/domain/consensus/model/externalapi"
"github.com/kaspanet/kaspad/domain/consensus/utils/consensushashing"
"github.com/kaspanet/kaspad/domain/consensus/utils/txscript"
"github.com/kaspanet/kaspad/domain/consensus/utils/utxo"
"github.com/kaspanet/kaspad/cmd/kaspawallet/libkaspawallet"
"github.com/kaspanet/kaspad/domain/consensus"
"github.com/kaspanet/kaspad/domain/consensus/utils/testutils"
)
func TestEstimateMassAfterSignatures(t *testing.T) {
testutils.ForAllNets(t, true, func(t *testing.T, consensusConfig *consensus.Config) {
unsignedTransactionBytes, mnemonics, params, teardown := testEstimateMassIncreaseForSignaturesSetUp(t, consensusConfig)
defer teardown(false)
serverInstance := &server{
params: params,
keysFile: &keys.File{MinimumSignatures: 2},
shutdown: make(chan struct{}),
addressSet: make(walletAddressSet),
txMassCalculator: txmass.NewCalculator(params.MassPerTxByte, params.MassPerScriptPubKeyByte, params.MassPerSigOp),
}
unsignedTransaction, err := serialization.DeserializePartiallySignedTransaction(unsignedTransactionBytes)
if err != nil {
t.Fatalf("Error deserializing unsignedTransaction: %s", err)
}
estimatedMassAfterSignatures, err := serverInstance.estimateMassAfterSignatures(unsignedTransaction)
if err != nil {
t.Fatalf("Error from estimateMassAfterSignatures: %s", err)
}
signedTxStep1Bytes, err := libkaspawallet.Sign(params, mnemonics[:1], unsignedTransactionBytes, false)
if err != nil {
t.Fatalf("Sign: %+v", err)
}
signedTxStep2Bytes, err := libkaspawallet.Sign(params, mnemonics[1:2], signedTxStep1Bytes, false)
if err != nil {
t.Fatalf("Sign: %+v", err)
}
extractedSignedTx, err := libkaspawallet.ExtractTransaction(signedTxStep2Bytes, false)
if err != nil {
t.Fatalf("ExtractTransaction: %+v", err)
}
actualMassAfterSignatures := serverInstance.txMassCalculator.CalculateTransactionMass(extractedSignedTx)
if estimatedMassAfterSignatures != actualMassAfterSignatures {
t.Errorf("Estimated mass after signatures: %d but actually got %d",
estimatedMassAfterSignatures, actualMassAfterSignatures)
}
})
}
func testEstimateMassIncreaseForSignaturesSetUp(t *testing.T, consensusConfig *consensus.Config) (
[]byte, []string, *dagconfig.Params, func(keepDataDir bool)) {
consensusConfig.BlockCoinbaseMaturity = 0
params := &consensusConfig.Params
tc, teardown, err := consensus.NewFactory().NewTestConsensus(consensusConfig, "TestMultisig")
if err != nil {
t.Fatalf("Error setting up tc: %+v", err)
}
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, false)
if err != nil {
t.Fatalf("Address: %+v", err)
}
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)
}
return unsignedTransaction, mnemonics, params, teardown
}

View File

@ -34,6 +34,44 @@ type PubKeySignaturePair struct {
Signature []byte Signature []byte
} }
// Clone creates a deep-clone of this PartiallySignedTransaction
func (pst *PartiallySignedTransaction) Clone() *PartiallySignedTransaction {
clone := &PartiallySignedTransaction{
Tx: pst.Tx.Clone(),
PartiallySignedInputs: make([]*PartiallySignedInput, len(pst.PartiallySignedInputs)),
}
for i, partiallySignedInput := range pst.PartiallySignedInputs {
clone.PartiallySignedInputs[i] = partiallySignedInput.Clone()
}
return clone
}
// Clone creates a deep-clone of this PartiallySignedInput
func (psi PartiallySignedInput) Clone() *PartiallySignedInput {
clone := &PartiallySignedInput{
PrevOutput: psi.PrevOutput.Clone(),
MinimumSignatures: psi.MinimumSignatures,
PubKeySignaturePairs: make([]*PubKeySignaturePair, len(psi.PubKeySignaturePairs)),
DerivationPath: psi.DerivationPath,
}
for i, pubKeySignaturePair := range psi.PubKeySignaturePairs {
clone.PubKeySignaturePairs[i] = pubKeySignaturePair.Clone()
}
return clone
}
// Clone creates a deep-clone of this PubKeySignaturePair
func (psp PubKeySignaturePair) Clone() *PubKeySignaturePair {
clone := &PubKeySignaturePair{
ExtendedPublicKey: psp.ExtendedPublicKey,
}
if psp.Signature != nil {
clone.Signature = make([]byte, len(psp.Signature))
copy(clone.Signature, psp.Signature)
}
return clone
}
// DeserializePartiallySignedTransaction deserializes a byte slice into PartiallySignedTransaction. // DeserializePartiallySignedTransaction deserializes a byte slice into PartiallySignedTransaction.
func DeserializePartiallySignedTransaction(serializedPartiallySignedTransaction []byte) (*PartiallySignedTransaction, error) { func DeserializePartiallySignedTransaction(serializedPartiallySignedTransaction []byte) (*PartiallySignedTransaction, error) {
protoPartiallySignedTransaction := &protoserialization.PartiallySignedTransaction{} protoPartiallySignedTransaction := &protoserialization.PartiallySignedTransaction{}

View File

@ -40,7 +40,6 @@ func Sign(params *dagconfig.Params, mnemonics []string, serializedPSTx []byte, e
return nil, err return nil, err
} }
} }
return serialization.SerializePartiallySignedTransaction(partiallySignedTransaction) return serialization.SerializePartiallySignedTransaction(partiallySignedTransaction)
} }

View File

@ -159,6 +159,7 @@ func createUnsignedTransaction(
Tx: domainTransaction, Tx: domainTransaction,
PartiallySignedInputs: partiallySignedInputs, PartiallySignedInputs: partiallySignedInputs,
}, nil }, nil
} }
// IsTransactionFullySigned returns whether the transaction is fully signed and ready to broadcast. // IsTransactionFullySigned returns whether the transaction is fully signed and ready to broadcast.
@ -194,10 +195,14 @@ func ExtractTransaction(partiallySignedTransactionBytes []byte, ecdsa bool) (*ex
return nil, err return nil, err
} }
return extractTransaction(partiallySignedTransaction, ecdsa) return ExtractTransactionDeserialized(partiallySignedTransaction, ecdsa)
} }
func extractTransaction(partiallySignedTransaction *serialization.PartiallySignedTransaction, ecdsa bool) (*externalapi.DomainTransaction, error) { // ExtractTransactionDeserialized does the same thing ExtractTransaction does, only receives the PartiallySignedTransaction
// in an already deserialized format
func ExtractTransactionDeserialized(partiallySignedTransaction *serialization.PartiallySignedTransaction, ecdsa bool) (
*externalapi.DomainTransaction, error) {
for i, input := range partiallySignedTransaction.PartiallySignedInputs { for i, input := range partiallySignedTransaction.PartiallySignedInputs {
isMultisig := len(input.PubKeySignaturePairs) > 1 isMultisig := len(input.PubKeySignaturePairs) > 1
scriptBuilder := txscript.NewScriptBuilder() scriptBuilder := txscript.NewScriptBuilder()

View File

@ -2,6 +2,9 @@ package libkaspawallet_test
import ( import (
"fmt" "fmt"
"strings"
"testing"
"github.com/kaspanet/kaspad/cmd/kaspawallet/libkaspawallet" "github.com/kaspanet/kaspad/cmd/kaspawallet/libkaspawallet"
"github.com/kaspanet/kaspad/domain/consensus" "github.com/kaspanet/kaspad/domain/consensus"
"github.com/kaspanet/kaspad/domain/consensus/model/externalapi" "github.com/kaspanet/kaspad/domain/consensus/model/externalapi"
@ -10,8 +13,6 @@ import (
"github.com/kaspanet/kaspad/domain/consensus/utils/txscript" "github.com/kaspanet/kaspad/domain/consensus/utils/txscript"
"github.com/kaspanet/kaspad/domain/consensus/utils/utxo" "github.com/kaspanet/kaspad/domain/consensus/utils/utxo"
"github.com/kaspanet/kaspad/util" "github.com/kaspanet/kaspad/util"
"strings"
"testing"
) )
func forSchnorrAndECDSA(t *testing.T, testFunc func(t *testing.T, ecdsa bool)) { func forSchnorrAndECDSA(t *testing.T, testFunc func(t *testing.T, ecdsa bool)) {
@ -106,7 +107,7 @@ func TestMultisig(t *testing.T) {
Amount: 10, Amount: 10,
}}, selectedUTXOs) }}, selectedUTXOs)
if err != nil { if err != nil {
t.Fatalf("CreateUnsignedTransaction: %+v", err) t.Fatalf("CreateUnsignedTransactions: %+v", err)
} }
isFullySigned, err := libkaspawallet.IsTransactionFullySigned(unsignedTransaction) isFullySigned, err := libkaspawallet.IsTransactionFullySigned(unsignedTransaction)
@ -267,7 +268,7 @@ func TestP2PK(t *testing.T) {
Amount: 10, Amount: 10,
}}, selectedUTXOs) }}, selectedUTXOs)
if err != nil { if err != nil {
t.Fatalf("CreateUnsignedTransaction: %+v", err) t.Fatalf("CreateUnsignedTransactions: %+v", err)
} }
isFullySigned, err := libkaspawallet.IsTransactionFullySigned(unsignedTransaction) isFullySigned, err := libkaspawallet.IsTransactionFullySigned(unsignedTransaction)

View File

@ -3,6 +3,7 @@ package main
import ( import (
"context" "context"
"fmt" "fmt"
"github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/client" "github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/client"
"github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/pb" "github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/pb"
"github.com/kaspanet/kaspad/cmd/kaspawallet/keys" "github.com/kaspanet/kaspad/cmd/kaspawallet/keys"
@ -31,10 +32,11 @@ func send(conf *sendConfig) error {
defer cancel() defer cancel()
sendAmountSompi := uint64(conf.SendAmount * constants.SompiPerKaspa) sendAmountSompi := uint64(conf.SendAmount * constants.SompiPerKaspa)
createUnsignedTransactionResponse, err := daemonClient.CreateUnsignedTransaction(ctx, &pb.CreateUnsignedTransactionRequest{ createUnsignedTransactionsResponse, err :=
Address: conf.ToAddress, daemonClient.CreateUnsignedTransactions(ctx, &pb.CreateUnsignedTransactionsRequest{
Amount: sendAmountSompi, Address: conf.ToAddress,
}) Amount: sendAmountSompi,
})
if err != nil { if err != nil {
return err return err
} }
@ -44,22 +46,37 @@ func send(conf *sendConfig) error {
return err return err
} }
signedTransaction, err := libkaspawallet.Sign(conf.NetParams(), mnemonics, createUnsignedTransactionResponse.UnsignedTransaction, keysFile.ECDSA) signedTransactions := make([][]byte, len(createUnsignedTransactionsResponse.UnsignedTransactions))
if err != nil { for i, unsignedTransaction := range createUnsignedTransactionsResponse.UnsignedTransactions {
return err signedTransaction, err := libkaspawallet.Sign(conf.NetParams(), mnemonics, unsignedTransaction, keysFile.ECDSA)
if err != nil {
return err
}
signedTransactions[i] = signedTransaction
} }
ctx2, cancel2 := context.WithTimeout(context.Background(), daemonTimeout) if len(signedTransactions) > 1 {
defer cancel2() fmt.Printf("Broadcasting %d transactions\n", len(signedTransactions))
broadcastResponse, err := daemonClient.Broadcast(ctx2, &pb.BroadcastRequest{
Transaction: signedTransaction,
})
if err != nil {
return err
} }
for _, signedTransaction := range signedTransactions {
err := func() error { // surround with func so that defer runs separately per transaction
ctx2, cancel2 := context.WithTimeout(context.Background(), daemonTimeout)
defer cancel2()
broadcastResponse, err := daemonClient.Broadcast(ctx2, &pb.BroadcastRequest{
Transaction: signedTransaction,
})
if err != nil {
return err
}
fmt.Println("Transaction was sent successfully") fmt.Println("Transaction was sent successfully")
fmt.Printf("Transaction ID: \t%s\n", broadcastResponse.TxID) fmt.Printf("Transaction ID: \t%s\n", broadcastResponse.TxID)
return nil
}()
if err != nil {
return err
}
}
return nil return nil
} }

View File

@ -1,21 +1,16 @@
package main package main
import ( import (
"encoding/hex"
"fmt" "fmt"
"io/ioutil"
"strings"
"github.com/kaspanet/kaspad/cmd/kaspawallet/keys" "github.com/kaspanet/kaspad/cmd/kaspawallet/keys"
"github.com/kaspanet/kaspad/cmd/kaspawallet/libkaspawallet" "github.com/kaspanet/kaspad/cmd/kaspawallet/libkaspawallet"
"github.com/pkg/errors" "github.com/pkg/errors"
"io/ioutil"
"strings"
) )
func sign(conf *signConfig) error { func sign(conf *signConfig) error {
keysFile, err := keys.ReadKeysFile(conf.NetParams(), conf.KeysFile)
if err != nil {
return err
}
if conf.Transaction == "" && conf.TransactionFile == "" { if conf.Transaction == "" && conf.TransactionFile == "" {
return errors.Errorf("Either --transaction or --transaction-file is required") return errors.Errorf("Either --transaction or --transaction-file is required")
} }
@ -23,41 +18,56 @@ func sign(conf *signConfig) error {
return errors.Errorf("Both --transaction and --transaction-file cannot be passed at the same time") return errors.Errorf("Both --transaction and --transaction-file cannot be passed at the same time")
} }
transactionHex := conf.Transaction keysFile, err := keys.ReadKeysFile(conf.NetParams(), conf.KeysFile)
if conf.TransactionFile != "" {
transactionHexBytes, err := ioutil.ReadFile(conf.TransactionFile)
if err != nil {
return errors.Wrapf(err, "Could not read hex from %s", conf.TransactionFile)
}
transactionHex = strings.TrimSpace(string(transactionHexBytes))
}
partiallySignedTransaction, err := hex.DecodeString(transactionHex)
if err != nil { if err != nil {
return err return err
} }
privateKeys, err := keysFile.DecryptMnemonics(conf.Password) privateKeys, err := keysFile.DecryptMnemonics(conf.Password)
if err != nil { if err != nil {
return err return err
} }
updatedPartiallySignedTransaction, err := libkaspawallet.Sign(conf.NetParams(), privateKeys, partiallySignedTransaction, keysFile.ECDSA) transactionsHex := conf.Transaction
if conf.TransactionFile != "" {
transactionHexBytes, err := ioutil.ReadFile(conf.TransactionFile)
if err != nil {
return errors.Wrapf(err, "Could not read hex from %s", conf.TransactionFile)
}
transactionsHex = strings.TrimSpace(string(transactionHexBytes))
}
partiallySignedTransactions, err := decodeTransactionsFromHex(transactionsHex)
if err != nil { if err != nil {
return err return err
} }
isFullySigned, err := libkaspawallet.IsTransactionFullySigned(updatedPartiallySignedTransaction) updatedPartiallySignedTransactions := make([][]byte, len(partiallySignedTransactions))
if err != nil { for i, partiallySignedTransaction := range partiallySignedTransactions {
return err updatedPartiallySignedTransactions[i], err =
libkaspawallet.Sign(conf.NetParams(), privateKeys, partiallySignedTransaction, keysFile.ECDSA)
if err != nil {
return err
}
} }
if isFullySigned { areAllTransactionsFullySigned := true
for _, updatedPartiallySignedTransaction := range updatedPartiallySignedTransactions {
// This is somewhat redundant to check all transactions, but we do that just-in-case
isFullySigned, err := libkaspawallet.IsTransactionFullySigned(updatedPartiallySignedTransaction)
if err != nil {
return err
}
if !isFullySigned {
areAllTransactionsFullySigned = false
}
}
if areAllTransactionsFullySigned {
fmt.Println("The transaction is signed and ready to broadcast") fmt.Println("The transaction is signed and ready to broadcast")
} else { } else {
fmt.Println("Successfully signed transaction") fmt.Println("Successfully signed transaction")
} }
fmt.Printf("Transaction: %x\n", updatedPartiallySignedTransaction) fmt.Println("Transaction: ")
fmt.Println(encodeTransactionsToHex(updatedPartiallySignedTransactions))
return nil return nil
} }

View File

@ -0,0 +1,33 @@
package main
import (
"encoding/hex"
"strings"
)
// hexTransactionsSeparator is used to mark the end of one transaction and the beginning of the next one.
// We use a separator that is not in the hex alphabet, but which will not split selection with a double click
const hexTransactionsSeparator = "_"
func encodeTransactionsToHex(transactions [][]byte) string {
transactionsInHex := make([]string, len(transactions))
for i, transaction := range transactions {
transactionsInHex[i] = hex.EncodeToString(transaction)
}
return strings.Join(transactionsInHex, hexTransactionsSeparator)
}
func decodeTransactionsFromHex(transactionsHex string) ([][]byte, error) {
splitTransactionsHexes := strings.Split(transactionsHex, hexTransactionsSeparator)
transactions := make([][]byte, len(splitTransactionsHexes))
var err error
for i, transactionHex := range splitTransactionsHexes {
transactions[i], err = hex.DecodeString(transactionHex)
if err != nil {
return nil, err
}
}
return transactions, nil
}

View File

@ -1,8 +1,6 @@
package consensus package consensus
import ( import (
"github.com/kaspanet/kaspad/infrastructure/logger"
"github.com/kaspanet/kaspad/util/staging"
"math/big" "math/big"
"sync" "sync"
@ -10,6 +8,8 @@ import (
"github.com/kaspanet/kaspad/domain/consensus/model" "github.com/kaspanet/kaspad/domain/consensus/model"
"github.com/kaspanet/kaspad/domain/consensus/model/externalapi" "github.com/kaspanet/kaspad/domain/consensus/model/externalapi"
"github.com/kaspanet/kaspad/domain/consensus/ruleerrors" "github.com/kaspanet/kaspad/domain/consensus/ruleerrors"
"github.com/kaspanet/kaspad/infrastructure/logger"
"github.com/kaspanet/kaspad/util/staging"
"github.com/pkg/errors" "github.com/pkg/errors"
) )

View File

@ -1,17 +1,18 @@
package consensus package consensus
import ( import (
"io/ioutil"
"os"
"sync"
"github.com/kaspanet/kaspad/domain/consensus/datastructures/blockwindowheapslicestore" "github.com/kaspanet/kaspad/domain/consensus/datastructures/blockwindowheapslicestore"
"github.com/kaspanet/kaspad/domain/consensus/datastructures/daawindowstore" "github.com/kaspanet/kaspad/domain/consensus/datastructures/daawindowstore"
"github.com/kaspanet/kaspad/domain/consensus/model" "github.com/kaspanet/kaspad/domain/consensus/model"
"github.com/kaspanet/kaspad/domain/consensus/processes/blockparentbuilder" "github.com/kaspanet/kaspad/domain/consensus/processes/blockparentbuilder"
parentssanager "github.com/kaspanet/kaspad/domain/consensus/processes/parentsmanager" parentssanager "github.com/kaspanet/kaspad/domain/consensus/processes/parentsmanager"
"github.com/kaspanet/kaspad/domain/consensus/processes/pruningproofmanager" "github.com/kaspanet/kaspad/domain/consensus/processes/pruningproofmanager"
"io/ioutil"
"os"
"sync"
"github.com/kaspanet/kaspad/domain/prefixmanager/prefix" "github.com/kaspanet/kaspad/domain/prefixmanager/prefix"
"github.com/kaspanet/kaspad/util/txmass"
consensusdatabase "github.com/kaspanet/kaspad/domain/consensus/database" consensusdatabase "github.com/kaspanet/kaspad/domain/consensus/database"
"github.com/kaspanet/kaspad/domain/consensus/datastructures/acceptancedatastore" "github.com/kaspanet/kaspad/domain/consensus/datastructures/acceptancedatastore"
@ -171,6 +172,9 @@ func (f *factory) NewConsensus(config *Config, db infrastructuredatabase.Databas
config.GenesisHash, config.GenesisHash,
config.MaxBlockLevel, config.MaxBlockLevel,
) )
txMassCalculator := txmass.NewCalculator(config.MassPerTxByte, config.MassPerScriptPubKeyByte, config.MassPerSigOp)
pastMedianTimeManager := f.pastMedianTimeConsructor( pastMedianTimeManager := f.pastMedianTimeConsructor(
config.TimestampDeviationTolerance, config.TimestampDeviationTolerance,
dbManager, dbManager,
@ -180,14 +184,12 @@ func (f *factory) NewConsensus(config *Config, db infrastructuredatabase.Databas
config.GenesisHash) config.GenesisHash)
transactionValidator := transactionvalidator.New(config.BlockCoinbaseMaturity, transactionValidator := transactionvalidator.New(config.BlockCoinbaseMaturity,
config.EnableNonNativeSubnetworks, config.EnableNonNativeSubnetworks,
config.MassPerTxByte,
config.MassPerScriptPubKeyByte,
config.MassPerSigOp,
config.MaxCoinbasePayloadLength, config.MaxCoinbasePayloadLength,
dbManager, dbManager,
pastMedianTimeManager, pastMedianTimeManager,
ghostdagDataStore, ghostdagDataStore,
daaBlocksStore) daaBlocksStore,
txMassCalculator)
difficultyManager := f.difficultyConstructor( difficultyManager := f.difficultyConstructor(
dbManager, dbManager,
ghostdagManager, ghostdagManager,
@ -331,6 +333,8 @@ func (f *factory) NewConsensus(config *Config, db infrastructuredatabase.Databas
reachabilityDataStore, reachabilityDataStore,
consensusStateStore, consensusStateStore,
daaBlocksStore, daaBlocksStore,
txMassCalculator,
) )
syncManager := syncmanager.New( syncManager := syncmanager.New(

View File

@ -4,6 +4,8 @@ import (
"math/big" "math/big"
"time" "time"
"github.com/kaspanet/kaspad/util/txmass"
"github.com/kaspanet/kaspad/domain/consensus/model" "github.com/kaspanet/kaspad/domain/consensus/model"
"github.com/kaspanet/kaspad/domain/consensus/model/externalapi" "github.com/kaspanet/kaspad/domain/consensus/model/externalapi"
"github.com/kaspanet/kaspad/util/difficulty" "github.com/kaspanet/kaspad/util/difficulty"
@ -48,6 +50,8 @@ type blockValidator struct {
reachabilityStore model.ReachabilityDataStore reachabilityStore model.ReachabilityDataStore
consensusStateStore model.ConsensusStateStore consensusStateStore model.ConsensusStateStore
daaBlocksStore model.DAABlocksStore daaBlocksStore model.DAABlocksStore
txMassCalculator *txmass.Calculator
} }
// New instantiates a new BlockValidator // New instantiates a new BlockValidator
@ -87,6 +91,8 @@ func New(powMax *big.Int,
reachabilityStore model.ReachabilityDataStore, reachabilityStore model.ReachabilityDataStore,
consensusStateStore model.ConsensusStateStore, consensusStateStore model.ConsensusStateStore,
daaBlocksStore model.DAABlocksStore, daaBlocksStore model.DAABlocksStore,
txMassCalculator *txmass.Calculator,
) model.BlockValidator { ) model.BlockValidator {
return &blockValidator{ return &blockValidator{
@ -126,5 +132,7 @@ func New(powMax *big.Int,
reachabilityStore: reachabilityStore, reachabilityStore: reachabilityStore,
consensusStateStore: consensusStateStore, consensusStateStore: consensusStateStore,
daaBlocksStore: daaBlocksStore, daaBlocksStore: daaBlocksStore,
txMassCalculator: txMassCalculator,
} }
} }

View File

@ -2,99 +2,12 @@ package transactionvalidator
import ( import (
"github.com/kaspanet/kaspad/domain/consensus/model/externalapi" "github.com/kaspanet/kaspad/domain/consensus/model/externalapi"
"github.com/kaspanet/kaspad/domain/consensus/utils/transactionhelper"
) )
// PopulateMass calculates and populates the mass of the given transaction
func (v *transactionValidator) PopulateMass(transaction *externalapi.DomainTransaction) { func (v *transactionValidator) PopulateMass(transaction *externalapi.DomainTransaction) {
if transaction.Mass != 0 { if transaction.Mass != 0 {
return return
} }
transaction.Mass = v.transactionMass(transaction) transaction.Mass = v.txMassCalculator.CalculateTransactionMass(transaction)
}
func (v *transactionValidator) transactionMass(transaction *externalapi.DomainTransaction) uint64 {
if transactionhelper.IsCoinBase(transaction) {
return 0
}
// calculate mass for size
size := transactionEstimatedSerializedSize(transaction)
massForSize := size * v.massPerTxByte
// calculate mass for scriptPubKey
totalScriptPubKeySize := uint64(0)
for _, output := range transaction.Outputs {
totalScriptPubKeySize += 2 //output.ScriptPublicKey.Version (uint16)
totalScriptPubKeySize += uint64(len(output.ScriptPublicKey.Script))
}
massForScriptPubKey := totalScriptPubKeySize * v.massPerScriptPubKeyByte
// calculate mass for SigOps
totalSigOpCount := uint64(0)
for _, input := range transaction.Inputs {
totalSigOpCount += uint64(input.SigOpCount)
}
massForSigOps := totalSigOpCount * v.massPerSigOp
// Sum all components of mass
return massForSize + massForScriptPubKey + massForSigOps
}
// transactionEstimatedSerializedSize is the estimated size of a transaction in some
// serialization. This has to be deterministic, but not necessarily accurate, since
// it's only used as the size component in the transaction and block mass limit
// calculation.
func transactionEstimatedSerializedSize(tx *externalapi.DomainTransaction) uint64 {
if transactionhelper.IsCoinBase(tx) {
return 0
}
size := uint64(0)
size += 2 // Txn Version
size += 8 // number of inputs (uint64)
for _, input := range tx.Inputs {
size += transactionInputEstimatedSerializedSize(input)
}
size += 8 // number of outputs (uint64)
for _, output := range tx.Outputs {
size += TransactionOutputEstimatedSerializedSize(output)
}
size += 8 // lock time (uint64)
size += externalapi.DomainSubnetworkIDSize
size += 8 // gas (uint64)
size += externalapi.DomainHashSize // payload hash
size += 8 // length of the payload (uint64)
size += uint64(len(tx.Payload))
return size
}
func transactionInputEstimatedSerializedSize(input *externalapi.DomainTransactionInput) uint64 {
size := uint64(0)
size += outpointEstimatedSerializedSize()
size += 8 // length of signature script (uint64)
size += uint64(len(input.SignatureScript))
size += 8 // sequence (uint64)
return size
}
func outpointEstimatedSerializedSize() uint64 {
size := uint64(0)
size += externalapi.DomainHashSize // ID
size += 4 // index (uint32)
return size
}
// TransactionOutputEstimatedSerializedSize is the same as transactionEstimatedSerializedSize but for outputs only
func TransactionOutputEstimatedSerializedSize(output *externalapi.DomainTransactionOutput) uint64 {
size := uint64(0)
size += 8 // value (uint64)
size += 2 // output.ScriptPublicKey.Version (uint 16)
size += 8 // length of script public key (uint64)
size += uint64(len(output.ScriptPublicKey.Script))
return size
} }

View File

@ -3,6 +3,7 @@ package transactionvalidator
import ( import (
"github.com/kaspanet/kaspad/domain/consensus/model" "github.com/kaspanet/kaspad/domain/consensus/model"
"github.com/kaspanet/kaspad/domain/consensus/utils/txscript" "github.com/kaspanet/kaspad/domain/consensus/utils/txscript"
"github.com/kaspanet/kaspad/util/txmass"
) )
const sigCacheSize = 10_000 const sigCacheSize = 10_000
@ -16,32 +17,25 @@ type transactionValidator struct {
ghostdagDataStore model.GHOSTDAGDataStore ghostdagDataStore model.GHOSTDAGDataStore
daaBlocksStore model.DAABlocksStore daaBlocksStore model.DAABlocksStore
enableNonNativeSubnetworks bool enableNonNativeSubnetworks bool
massPerTxByte uint64
massPerScriptPubKeyByte uint64
massPerSigOp uint64
maxCoinbasePayloadLength uint64 maxCoinbasePayloadLength uint64
sigCache *txscript.SigCache sigCache *txscript.SigCache
sigCacheECDSA *txscript.SigCacheECDSA sigCacheECDSA *txscript.SigCacheECDSA
txMassCalculator *txmass.Calculator
} }
// New instantiates a new TransactionValidator // New instantiates a new TransactionValidator
func New(blockCoinbaseMaturity uint64, func New(blockCoinbaseMaturity uint64,
enableNonNativeSubnetworks bool, enableNonNativeSubnetworks bool,
massPerTxByte uint64,
massPerScriptPubKeyByte uint64,
massPerSigOp uint64,
maxCoinbasePayloadLength uint64, maxCoinbasePayloadLength uint64,
databaseContext model.DBReader, databaseContext model.DBReader,
pastMedianTimeManager model.PastMedianTimeManager, pastMedianTimeManager model.PastMedianTimeManager,
ghostdagDataStore model.GHOSTDAGDataStore, ghostdagDataStore model.GHOSTDAGDataStore,
daaBlocksStore model.DAABlocksStore) model.TransactionValidator { daaBlocksStore model.DAABlocksStore,
txMassCalculator *txmass.Calculator) model.TransactionValidator {
return &transactionValidator{ return &transactionValidator{
blockCoinbaseMaturity: blockCoinbaseMaturity, blockCoinbaseMaturity: blockCoinbaseMaturity,
enableNonNativeSubnetworks: enableNonNativeSubnetworks, enableNonNativeSubnetworks: enableNonNativeSubnetworks,
massPerTxByte: massPerTxByte,
massPerScriptPubKeyByte: massPerScriptPubKeyByte,
massPerSigOp: massPerSigOp,
maxCoinbasePayloadLength: maxCoinbasePayloadLength, maxCoinbasePayloadLength: maxCoinbasePayloadLength,
databaseContext: databaseContext, databaseContext: databaseContext,
pastMedianTimeManager: pastMedianTimeManager, pastMedianTimeManager: pastMedianTimeManager,
@ -49,5 +43,6 @@ func New(blockCoinbaseMaturity uint64,
daaBlocksStore: daaBlocksStore, daaBlocksStore: daaBlocksStore,
sigCache: txscript.NewSigCache(sigCacheSize), sigCache: txscript.NewSigCache(sigCacheSize),
sigCacheECDSA: txscript.NewSigCacheECDSA(sigCacheSize), sigCacheECDSA: txscript.NewSigCacheECDSA(sigCacheSize),
txMassCalculator: txMassCalculator,
} }
} }

View File

@ -35,4 +35,8 @@ const (
// LockTimeThreshold is the number below which a lock time is // LockTimeThreshold is the number below which a lock time is
// interpreted to be a DAA score. // interpreted to be a DAA score.
LockTimeThreshold = 5e11 // Tue Nov 5 00:53:20 1985 UTC LockTimeThreshold = 5e11 // Tue Nov 5 00:53:20 1985 UTC
// UnacceptedDAAScore is used to for UTXOEntries that were created by transactions in the mempool, or otherwise
// not-yet-accepted transactions.
UnacceptedDAAScore = math.MaxUint64
) )

View File

@ -318,7 +318,7 @@ var TestnetParams = Params{
MinerConfirmationWindow: 2016, MinerConfirmationWindow: 2016,
// Mempool parameters // Mempool parameters
RelayNonStdTxs: true, RelayNonStdTxs: false,
// AcceptUnroutable specifies whether this network accepts unroutable // AcceptUnroutable specifies whether this network accepts unroutable
// IP addresses, such as 10.0.0.0/8 // IP addresses, such as 10.0.0.0/8
@ -386,7 +386,7 @@ var SimnetParams = Params{
MinerConfirmationWindow: 100, MinerConfirmationWindow: 100,
// Mempool parameters // Mempool parameters
RelayNonStdTxs: true, RelayNonStdTxs: false,
// AcceptUnroutable specifies whether this network accepts unroutable // AcceptUnroutable specifies whether this network accepts unroutable
// IP addresses, such as 10.0.0.0/8 // IP addresses, such as 10.0.0.0/8
@ -445,7 +445,7 @@ var DevnetParams = Params{
MinerConfirmationWindow: 2016, MinerConfirmationWindow: 2016,
// Mempool parameters // Mempool parameters
RelayNonStdTxs: true, RelayNonStdTxs: false,
// AcceptUnroutable specifies whether this network accepts unroutable // AcceptUnroutable specifies whether this network accepts unroutable
// IP addresses, such as 10.0.0.0/8 // IP addresses, such as 10.0.0.0/8

View File

@ -3,7 +3,7 @@ package mempool
import ( import (
"fmt" "fmt"
"github.com/kaspanet/kaspad/domain/consensus/processes/transactionvalidator" "github.com/kaspanet/kaspad/util/txmass"
"github.com/kaspanet/kaspad/domain/consensus/utils/consensushashing" "github.com/kaspanet/kaspad/domain/consensus/utils/consensushashing"
@ -36,9 +36,9 @@ const (
// (1 + 15*74 + 3) + (15*34 + 3) + 23 = 1650 // (1 + 15*74 + 3) + (15*34 + 3) + 23 = 1650
maximumStandardSignatureScriptSize = 1650 maximumStandardSignatureScriptSize = 1650
// maximumStandardTransactionMass is the maximum mass allowed for transactions that // MaximumStandardTransactionMass is the maximum mass allowed for transactions that
// are considered standard and will therefore be relayed and considered for mining. // are considered standard and will therefore be relayed and considered for mining.
maximumStandardTransactionMass = 100000 MaximumStandardTransactionMass = 100_000
) )
func (mp *mempool) checkTransactionStandardInIsolation(transaction *externalapi.DomainTransaction) error { func (mp *mempool) checkTransactionStandardInIsolation(transaction *externalapi.DomainTransaction) error {
@ -58,9 +58,9 @@ func (mp *mempool) checkTransactionStandardInIsolation(transaction *externalapi.
// almost as much to process as the sender fees, limit the maximum // almost as much to process as the sender fees, limit the maximum
// size of a transaction. This also helps mitigate CPU exhaustion // size of a transaction. This also helps mitigate CPU exhaustion
// attacks. // attacks.
if transaction.Mass > maximumStandardTransactionMass { if transaction.Mass > MaximumStandardTransactionMass {
str := fmt.Sprintf("transaction mass of %d is larger than max allowed size of %d", str := fmt.Sprintf("transaction mass of %d is larger than max allowed size of %d",
transaction.Mass, maximumStandardTransactionMass) transaction.Mass, MaximumStandardTransactionMass)
return transactionRuleError(RejectNonstandard, str) return transactionRuleError(RejectNonstandard, str)
} }
@ -127,7 +127,7 @@ func (mp *mempool) IsTransactionOutputDust(output *externalapi.DomainTransaction
// The most common scripts are pay-to-pubkey, and as per the above // The most common scripts are pay-to-pubkey, and as per the above
// breakdown, the minimum size of a p2pk input script is 148 bytes. So // breakdown, the minimum size of a p2pk input script is 148 bytes. So
// that figure is used. // that figure is used.
totalSerializedSize := transactionvalidator.TransactionOutputEstimatedSerializedSize(output) + 148 totalSerializedSize := txmass.TransactionOutputEstimatedSerializedSize(output) + 148
// The output is considered dust if the cost to the network to spend the // The output is considered dust if the cost to the network to spend the
// coins is more than 1/3 of the minimum free transaction relay fee. // coins is more than 1/3 of the minimum free transaction relay fee.

View File

@ -6,10 +6,11 @@ package mempool
import ( import (
"bytes" "bytes"
"github.com/kaspanet/kaspad/domain/consensusreference"
"math" "math"
"testing" "testing"
"github.com/kaspanet/kaspad/domain/consensusreference"
"github.com/kaspanet/kaspad/domain/consensus/utils/testutils" "github.com/kaspanet/kaspad/domain/consensus/utils/testutils"
"github.com/kaspanet/kaspad/domain/consensus" "github.com/kaspanet/kaspad/domain/consensus"
@ -45,13 +46,13 @@ func TestCalcMinRequiredTxRelayFee(t *testing.T) {
}, },
{ {
"max standard tx size with default minimum relay fee", "max standard tx size with default minimum relay fee",
maximumStandardTransactionMass, MaximumStandardTransactionMass,
defaultMinimumRelayTransactionFee, defaultMinimumRelayTransactionFee,
100000, 100000,
}, },
{ {
"max standard tx size with max sompi relay fee", "max standard tx size with max sompi relay fee",
maximumStandardTransactionMass, MaximumStandardTransactionMass,
constants.MaxSompi, constants.MaxSompi,
constants.MaxSompi, constants.MaxSompi,
}, },
@ -249,7 +250,7 @@ func TestCheckTransactionStandardInIsolation(t *testing.T) {
name: "Transaction size is too large", name: "Transaction size is too large",
tx: &externalapi.DomainTransaction{Version: 0, Inputs: []*externalapi.DomainTransactionInput{&dummyTxIn}, Outputs: []*externalapi.DomainTransactionOutput{{ tx: &externalapi.DomainTransaction{Version: 0, Inputs: []*externalapi.DomainTransactionInput{&dummyTxIn}, Outputs: []*externalapi.DomainTransactionOutput{{
Value: 0, Value: 0,
ScriptPublicKey: &externalapi.ScriptPublicKey{bytes.Repeat([]byte{0x00}, maximumStandardTransactionMass+1), 0}, ScriptPublicKey: &externalapi.ScriptPublicKey{bytes.Repeat([]byte{0x00}, MaximumStandardTransactionMass+1), 0},
}}}, }}},
height: 300000, height: 300000,
isStandard: false, isStandard: false,

View File

@ -3,6 +3,7 @@ package mempool
import ( import (
"github.com/kaspanet/kaspad/domain/consensus/model/externalapi" "github.com/kaspanet/kaspad/domain/consensus/model/externalapi"
"github.com/kaspanet/kaspad/domain/consensus/ruleerrors" "github.com/kaspanet/kaspad/domain/consensus/ruleerrors"
"github.com/kaspanet/kaspad/domain/consensus/utils/constants"
"github.com/kaspanet/kaspad/domain/consensus/utils/utxo" "github.com/kaspanet/kaspad/domain/consensus/utils/utxo"
"github.com/kaspanet/kaspad/domain/miningmanager/mempool/model" "github.com/kaspanet/kaspad/domain/miningmanager/mempool/model"
"github.com/pkg/errors" "github.com/pkg/errors"
@ -42,6 +43,6 @@ func fillInputs(transaction *externalapi.DomainTransaction, parentsInPool model.
} }
relevantOutput := parent.Transaction().Outputs[input.PreviousOutpoint.Index] relevantOutput := parent.Transaction().Outputs[input.PreviousOutpoint.Index]
input.UTXOEntry = utxo.NewUTXOEntry(relevantOutput.Value, relevantOutput.ScriptPublicKey, input.UTXOEntry = utxo.NewUTXOEntry(relevantOutput.Value, relevantOutput.ScriptPublicKey,
false, model.UnacceptedDAAScore) false, constants.UnacceptedDAAScore)
} }
} }

View File

@ -3,6 +3,8 @@ package mempool
import ( import (
"fmt" "fmt"
"github.com/kaspanet/kaspad/domain/consensus/utils/constants"
"github.com/kaspanet/kaspad/domain/consensus/utils/consensushashing" "github.com/kaspanet/kaspad/domain/consensus/utils/consensushashing"
"github.com/kaspanet/kaspad/domain/consensus/model/externalapi" "github.com/kaspanet/kaspad/domain/consensus/model/externalapi"
@ -42,7 +44,7 @@ func (mpus *mempoolUTXOSet) addTransaction(transaction *model.MempoolTransaction
outpoint := externalapi.DomainOutpoint{TransactionID: *transaction.TransactionID(), Index: uint32(i)} outpoint := externalapi.DomainOutpoint{TransactionID: *transaction.TransactionID(), Index: uint32(i)}
mpus.poolUnspentOutputs[outpoint] = mpus.poolUnspentOutputs[outpoint] =
utxo.NewUTXOEntry(output.Value, output.ScriptPublicKey, false, model.UnacceptedDAAScore) utxo.NewUTXOEntry(output.Value, output.ScriptPublicKey, false, constants.UnacceptedDAAScore)
} }
} }

View File

@ -1,6 +0,0 @@
package model
import "math"
// UnacceptedDAAScore is used to for UTXOEntries that were created by transactions in the mempool.
const UnacceptedDAAScore = math.MaxUint64

View File

@ -3,6 +3,8 @@ package mempool
import ( import (
"fmt" "fmt"
"github.com/kaspanet/kaspad/domain/consensus/utils/constants"
"github.com/kaspanet/kaspad/domain/consensus/ruleerrors" "github.com/kaspanet/kaspad/domain/consensus/ruleerrors"
"github.com/kaspanet/kaspad/domain/consensus/utils/consensushashing" "github.com/kaspanet/kaspad/domain/consensus/utils/consensushashing"
@ -153,7 +155,7 @@ func (op *orphansPool) processOrphansAfterAcceptedTransaction(acceptedTransactio
for _, input := range orphan.Transaction().Inputs { for _, input := range orphan.Transaction().Inputs {
if input.PreviousOutpoint.Equal(&outpoint) && input.UTXOEntry == nil { if input.PreviousOutpoint.Equal(&outpoint) && input.UTXOEntry == nil {
input.UTXOEntry = utxo.NewUTXOEntry(output.Value, output.ScriptPublicKey, false, input.UTXOEntry = utxo.NewUTXOEntry(output.Value, output.ScriptPublicKey, false,
model.UnacceptedDAAScore) constants.UnacceptedDAAScore)
break break
} }
} }

View File

@ -42,16 +42,16 @@ const (
defaultMaxRPCClients = 10 defaultMaxRPCClients = 10
defaultMaxRPCWebsockets = 25 defaultMaxRPCWebsockets = 25
defaultMaxRPCConcurrentReqs = 20 defaultMaxRPCConcurrentReqs = 20
defaultBlockMaxMass = 10000000 defaultBlockMaxMass = 10_000_000
blockMaxMassMin = 1000 blockMaxMassMin = 1000
blockMaxMassMax = 10000000 blockMaxMassMax = 10_000_000
defaultMinRelayTxFee = 1e-5 // 1 sompi per byte defaultMinRelayTxFee = 1e-5 // 1 sompi per byte
defaultMaxOrphanTransactions = 100 defaultMaxOrphanTransactions = 100
//DefaultMaxOrphanTxSize is the default maximum size for an orphan transaction //DefaultMaxOrphanTxSize is the default maximum size for an orphan transaction
DefaultMaxOrphanTxSize = 100000 DefaultMaxOrphanTxSize = 100_000
defaultSigCacheMaxSize = 100000 defaultSigCacheMaxSize = 100_000
sampleConfigFilename = "sample-kaspad.conf" sampleConfigFilename = "sample-kaspad.conf"
defaultMaxUTXOCacheSize = 5000000000 defaultMaxUTXOCacheSize = 5_000_000_000
defaultProtocolVersion = 5 defaultProtocolVersion = 5
) )

119
util/txmass/calculator.go Normal file
View File

@ -0,0 +1,119 @@
package txmass
import (
"github.com/kaspanet/kaspad/domain/consensus/model/externalapi"
"github.com/kaspanet/kaspad/domain/consensus/utils/transactionhelper"
)
// Calculator exposes methods to calculate the mass of a transaction
type Calculator struct {
massPerTxByte uint64
massPerScriptPubKeyByte uint64
massPerSigOp uint64
}
// NewCalculator creates a new instance of Calculator
func NewCalculator(massPerTxByte, massPerScriptPubKeyByte, massPerSigOp uint64) *Calculator {
return &Calculator{
massPerTxByte: massPerTxByte,
massPerScriptPubKeyByte: massPerScriptPubKeyByte,
massPerSigOp: massPerSigOp,
}
}
// MassPerTxByte returns the mass per transaction byte configured for this Calculator
func (c *Calculator) MassPerTxByte() uint64 { return c.massPerTxByte }
// MassPerScriptPubKeyByte returns the mass per ScriptPublicKey byte configured for this Calculator
func (c *Calculator) MassPerScriptPubKeyByte() uint64 { return c.massPerScriptPubKeyByte }
// MassPerSigOp returns the mass per SigOp byte configured for this Calculator
func (c *Calculator) MassPerSigOp() uint64 { return c.massPerSigOp }
// CalculateTransactionMass calculates the mass of the given transaction
func (c *Calculator) CalculateTransactionMass(transaction *externalapi.DomainTransaction) uint64 {
if transactionhelper.IsCoinBase(transaction) {
return 0
}
// calculate mass for size
size := transactionEstimatedSerializedSize(transaction)
massForSize := size * c.massPerTxByte
// calculate mass for scriptPubKey
totalScriptPubKeySize := uint64(0)
for _, output := range transaction.Outputs {
totalScriptPubKeySize += 2 //output.ScriptPublicKey.Version (uint16)
totalScriptPubKeySize += uint64(len(output.ScriptPublicKey.Script))
}
massForScriptPubKey := totalScriptPubKeySize * c.massPerScriptPubKeyByte
// calculate mass for SigOps
totalSigOpCount := uint64(0)
for _, input := range transaction.Inputs {
totalSigOpCount += uint64(input.SigOpCount)
}
massForSigOps := totalSigOpCount * c.massPerSigOp
// Sum all components of mass
return massForSize + massForScriptPubKey + massForSigOps
}
// transactionEstimatedSerializedSize is the estimated size of a transaction in some
// serialization. This has to be deterministic, but not necessarily accurate, since
// it's only used as the size component in the transaction and block mass limit
// calculation.
func transactionEstimatedSerializedSize(tx *externalapi.DomainTransaction) uint64 {
if transactionhelper.IsCoinBase(tx) {
return 0
}
size := uint64(0)
size += 2 // Txn Version
size += 8 // number of inputs (uint64)
for _, input := range tx.Inputs {
size += transactionInputEstimatedSerializedSize(input)
}
size += 8 // number of outputs (uint64)
for _, output := range tx.Outputs {
size += TransactionOutputEstimatedSerializedSize(output)
}
size += 8 // lock time (uint64)
size += externalapi.DomainSubnetworkIDSize
size += 8 // gas (uint64)
size += externalapi.DomainHashSize // payload hash
size += 8 // length of the payload (uint64)
size += uint64(len(tx.Payload))
return size
}
func transactionInputEstimatedSerializedSize(input *externalapi.DomainTransactionInput) uint64 {
size := uint64(0)
size += outpointEstimatedSerializedSize()
size += 8 // length of signature script (uint64)
size += uint64(len(input.SignatureScript))
size += 8 // sequence (uint64)
return size
}
func outpointEstimatedSerializedSize() uint64 {
size := uint64(0)
size += externalapi.DomainHashSize // ID
size += 4 // index (uint32)
return size
}
// TransactionOutputEstimatedSerializedSize is the same as transactionEstimatedSerializedSize but for outputs only
func TransactionOutputEstimatedSerializedSize(output *externalapi.DomainTransactionOutput) uint64 {
size := uint64(0)
size += 8 // value (uint64)
size += 2 // output.ScriptPublicKey.Version (uint 16)
size += 8 // length of script public key (uint64)
size += uint64(len(output.ScriptPublicKey.Script))
return size
}