diff --git a/cmd/kaspawallet/broadcast.go b/cmd/kaspawallet/broadcast.go index 9b4c2e2d3e..a6f84e7fe2 100644 --- a/cmd/kaspawallet/broadcast.go +++ b/cmd/kaspawallet/broadcast.go @@ -2,13 +2,13 @@ package main import ( "context" - "encoding/hex" "fmt" + "io/ioutil" + "strings" + "github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/client" "github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/pb" "github.com/pkg/errors" - "io/ioutil" - "strings" ) func broadcast(conf *broadcastConfig) error { @@ -21,34 +21,40 @@ func broadcast(conf *broadcastConfig) error { ctx, cancel := context.WithTimeout(context.Background(), daemonTimeout) defer cancel() - if conf.Transaction == "" && conf.TransactionFile == "" { + if conf.Transactions == "" && conf.TransactionsFile == "" { 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") } - transactionHex := conf.Transaction - if conf.TransactionFile != "" { - transactionHexBytes, err := ioutil.ReadFile(conf.TransactionFile) + transactionsHex := conf.Transactions + if conf.TransactionsFile != "" { + transactionHexBytes, err := ioutil.ReadFile(conf.TransactionsFile) 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 { return err } - response, err := daemonClient.Broadcast(ctx, &pb.BroadcastRequest{Transaction: transaction}) - if err != nil { - return err + transactionsCount := len(transactions) + for i, transaction := range transactions { + 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 } diff --git a/cmd/kaspawallet/config.go b/cmd/kaspawallet/config.go index b02a9446b7..2ef7196695 100644 --- a/cmd/kaspawallet/config.go +++ b/cmd/kaspawallet/config.go @@ -1,9 +1,10 @@ package main import ( + "os" + "github.com/kaspanet/kaspad/infrastructure/config" "github.com/pkg/errors" - "os" "github.com/jessevdk/go-flags" ) @@ -68,15 +69,15 @@ type createUnsignedTransactionConfig 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))"` 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)"` - TransactionFile string `long:"transaction-file" short:"F" description:"The file containing 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(s) to sign on (encoded in hex)"` config.NetworkFlags } type broadcastConfig struct { - 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)"` - TransactionFile string `long:"transaction-file" short:"F" description:"The file containing the unsigned transaction to sign on (encoded in hex)"` + DaemonAddress string `long:"daemonaddress" short:"d" description:"Wallet daemon server to connect to (default: localhost:8082)"` + Transactions string `long:"transaction" short:"t" description:"The signed transaction to broadcast (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 } diff --git a/cmd/kaspawallet/create_unsigned_tx.go b/cmd/kaspawallet/create_unsigned_tx.go index 22eac3ec6a..5d67493251 100644 --- a/cmd/kaspawallet/create_unsigned_tx.go +++ b/cmd/kaspawallet/create_unsigned_tx.go @@ -2,8 +2,8 @@ package main import ( "context" - "encoding/hex" "fmt" + "github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/client" "github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/pb" "github.com/kaspanet/kaspad/domain/consensus/utils/constants" @@ -20,7 +20,7 @@ func createUnsignedTransaction(conf *createUnsignedTransactionConfig) error { defer cancel() sendAmountSompi := uint64(conf.SendAmount * constants.SompiPerKaspa) - response, err := daemonClient.CreateUnsignedTransaction(ctx, &pb.CreateUnsignedTransactionRequest{ + response, err := daemonClient.CreateUnsignedTransactions(ctx, &pb.CreateUnsignedTransactionsRequest{ Address: conf.ToAddress, Amount: sendAmountSompi, }) @@ -29,6 +29,6 @@ func createUnsignedTransaction(conf *createUnsignedTransactionConfig) error { } fmt.Println("Created unsigned transaction") - fmt.Println(hex.EncodeToString(response.UnsignedTransaction)) + fmt.Println(encodeTransactionsToHex(response.UnsignedTransactions)) return nil } diff --git a/cmd/kaspawallet/daemon/pb/kaspawalletd.pb.go b/cmd/kaspawallet/daemon/pb/kaspawalletd.pb.go index a72f259e8b..c43b947abb 100644 --- a/cmd/kaspawallet/daemon/pb/kaspawalletd.pb.go +++ b/cmd/kaspawallet/daemon/pb/kaspawalletd.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.27.1 +// protoc-gen-go v1.26.0 // protoc v3.12.3 // source: kaspawalletd.proto @@ -184,7 +184,7 @@ func (x *AddressBalances) GetPending() uint64 { return 0 } -type CreateUnsignedTransactionRequest struct { +type CreateUnsignedTransactionsRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields @@ -193,8 +193,8 @@ type CreateUnsignedTransactionRequest struct { Amount uint64 `protobuf:"varint,2,opt,name=amount,proto3" json:"amount,omitempty"` } -func (x *CreateUnsignedTransactionRequest) Reset() { - *x = CreateUnsignedTransactionRequest{} +func (x *CreateUnsignedTransactionsRequest) Reset() { + *x = CreateUnsignedTransactionsRequest{} if protoimpl.UnsafeEnabled { mi := &file_kaspawalletd_proto_msgTypes[3] 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) } -func (*CreateUnsignedTransactionRequest) ProtoMessage() {} +func (*CreateUnsignedTransactionsRequest) ProtoMessage() {} -func (x *CreateUnsignedTransactionRequest) ProtoReflect() protoreflect.Message { +func (x *CreateUnsignedTransactionsRequest) ProtoReflect() protoreflect.Message { mi := &file_kaspawalletd_proto_msgTypes[3] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -220,35 +220,35 @@ func (x *CreateUnsignedTransactionRequest) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use CreateUnsignedTransactionRequest.ProtoReflect.Descriptor instead. -func (*CreateUnsignedTransactionRequest) Descriptor() ([]byte, []int) { +// Deprecated: Use CreateUnsignedTransactionsRequest.ProtoReflect.Descriptor instead. +func (*CreateUnsignedTransactionsRequest) Descriptor() ([]byte, []int) { return file_kaspawalletd_proto_rawDescGZIP(), []int{3} } -func (x *CreateUnsignedTransactionRequest) GetAddress() string { +func (x *CreateUnsignedTransactionsRequest) GetAddress() string { if x != nil { return x.Address } return "" } -func (x *CreateUnsignedTransactionRequest) GetAmount() uint64 { +func (x *CreateUnsignedTransactionsRequest) GetAmount() uint64 { if x != nil { return x.Amount } return 0 } -type CreateUnsignedTransactionResponse struct { +type CreateUnsignedTransactionsResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache 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() { - *x = CreateUnsignedTransactionResponse{} +func (x *CreateUnsignedTransactionsResponse) Reset() { + *x = CreateUnsignedTransactionsResponse{} if protoimpl.UnsafeEnabled { mi := &file_kaspawalletd_proto_msgTypes[4] 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) } -func (*CreateUnsignedTransactionResponse) ProtoMessage() {} +func (*CreateUnsignedTransactionsResponse) ProtoMessage() {} -func (x *CreateUnsignedTransactionResponse) ProtoReflect() protoreflect.Message { +func (x *CreateUnsignedTransactionsResponse) ProtoReflect() protoreflect.Message { mi := &file_kaspawalletd_proto_msgTypes[4] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -274,14 +274,14 @@ func (x *CreateUnsignedTransactionResponse) ProtoReflect() protoreflect.Message return mi.MessageOf(x) } -// Deprecated: Use CreateUnsignedTransactionResponse.ProtoReflect.Descriptor instead. -func (*CreateUnsignedTransactionResponse) Descriptor() ([]byte, []int) { +// Deprecated: Use CreateUnsignedTransactionsResponse.ProtoReflect.Descriptor instead. +func (*CreateUnsignedTransactionsResponse) Descriptor() ([]byte, []int) { return file_kaspawalletd_proto_rawDescGZIP(), []int{4} } -func (x *CreateUnsignedTransactionResponse) GetUnsignedTransaction() []byte { +func (x *CreateUnsignedTransactionsResponse) GetUnsignedTransactions() [][]byte { if x != nil { - return x.UnsignedTransaction + return x.UnsignedTransactions } 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, 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, - 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, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, - 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, - 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x61, 0x6d, 0x6f, 0x75, 0x6e, - 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x22, - 0x55, 0x0a, 0x21, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x55, 0x6e, 0x73, 0x69, 0x67, 0x6e, 0x65, - 0x64, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x30, 0x0a, 0x13, 0x75, 0x6e, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, - 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x0c, 0x52, 0x13, 0x75, 0x6e, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x54, 0x72, 0x61, 0x6e, 0x73, - 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x16, 0x0a, 0x14, 0x53, 0x68, 0x6f, 0x77, 0x41, 0x64, - 0x64, 0x72, 0x65, 0x73, 0x73, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x31, - 0x0a, 0x15, 0x53, 0x68, 0x6f, 0x77, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x65, 0x73, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, - 0x73, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, - 0x73, 0x22, 0x13, 0x0a, 0x11, 0x4e, 0x65, 0x77, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x2e, 0x0a, 0x12, 0x4e, 0x65, 0x77, 0x41, 0x64, 0x64, - 0x72, 0x65, 0x73, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x18, 0x0a, 0x07, - 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, - 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x22, 0x34, 0x0a, 0x10, 0x42, 0x72, 0x6f, 0x61, 0x64, 0x63, - 0x61, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x20, 0x0a, 0x0b, 0x74, 0x72, - 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, - 0x0b, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x27, 0x0a, 0x11, - 0x42, 0x72, 0x6f, 0x61, 0x64, 0x63, 0x61, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x78, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x04, 0x74, 0x78, 0x49, 0x44, 0x22, 0x11, 0x0a, 0x0f, 0x53, 0x68, 0x75, 0x74, 0x64, 0x6f, 0x77, - 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x12, 0x0a, 0x10, 0x53, 0x68, 0x75, 0x74, - 0x64, 0x6f, 0x77, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32, 0x91, 0x03, 0x0a, - 0x0c, 0x6b, 0x61, 0x73, 0x70, 0x61, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x64, 0x12, 0x37, 0x0a, - 0x0a, 0x47, 0x65, 0x74, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x12, 0x12, 0x2e, 0x47, 0x65, - 0x74, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x13, 0x2e, 0x47, 0x65, 0x74, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x64, 0x0a, 0x19, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, - 0x55, 0x6e, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, - 0x69, 0x6f, 0x6e, 0x12, 0x21, 0x2e, 0x43, 0x72, 0x65, 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, 0x1a, 0x22, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x55, - 0x6e, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, - 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x40, 0x0a, 0x0d, - 0x53, 0x68, 0x6f, 0x77, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x65, 0x73, 0x12, 0x15, 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, 0x6f, 0x77, 0x41, 0x64, 0x64, 0x72, 0x65, - 0x73, 0x73, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x37, - 0x0a, 0x0a, 0x4e, 0x65, 0x77, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x12, 0x2e, 0x4e, - 0x65, 0x77, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x13, 0x2e, 0x4e, 0x65, 0x77, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x31, 0x0a, 0x08, 0x53, 0x68, 0x75, 0x74, 0x64, - 0x6f, 0x77, 0x6e, 0x12, 0x10, 0x2e, 0x53, 0x68, 0x75, 0x74, 0x64, 0x6f, 0x77, 0x6e, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x11, 0x2e, 0x53, 0x68, 0x75, 0x74, 0x64, 0x6f, 0x77, 0x6e, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x34, 0x0a, 0x09, 0x42, 0x72, - 0x6f, 0x61, 0x64, 0x63, 0x61, 0x73, 0x74, 0x12, 0x11, 0x2e, 0x42, 0x72, 0x6f, 0x61, 0x64, 0x63, - 0x61, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x12, 0x2e, 0x42, 0x72, 0x6f, - 0x61, 0x64, 0x63, 0x61, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, - 0x42, 0x36, 0x5a, 0x34, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6b, - 0x61, 0x73, 0x70, 0x61, 0x6e, 0x65, 0x74, 0x2f, 0x6b, 0x61, 0x73, 0x70, 0x61, 0x64, 0x2f, 0x63, - 0x6d, 0x64, 0x2f, 0x6b, 0x61, 0x73, 0x70, 0x61, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x2f, 0x64, - 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2f, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, + 0x0a, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x61, 0x6d, 0x6f, 0x75, + 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, + 0x22, 0x58, 0x0a, 0x22, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x55, 0x6e, 0x73, 0x69, 0x67, 0x6e, + 0x65, 0x64, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x32, 0x0a, 0x14, 0x75, 0x6e, 0x73, 0x69, 0x67, 0x6e, + 0x65, 0x64, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x01, + 0x20, 0x03, 0x28, 0x0c, 0x52, 0x14, 0x75, 0x6e, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x54, 0x72, + 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x16, 0x0a, 0x14, 0x53, 0x68, + 0x6f, 0x77, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x22, 0x31, 0x0a, 0x15, 0x53, 0x68, 0x6f, 0x77, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, + 0x73, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x61, + 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x61, 0x64, + 0x64, 0x72, 0x65, 0x73, 0x73, 0x22, 0x13, 0x0a, 0x11, 0x4e, 0x65, 0x77, 0x41, 0x64, 0x64, 0x72, + 0x65, 0x73, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x2e, 0x0a, 0x12, 0x4e, 0x65, + 0x77, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x18, 0x0a, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x22, 0x34, 0x0a, 0x10, 0x42, 0x72, + 0x6f, 0x61, 0x64, 0x63, 0x61, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x20, + 0x0a, 0x0b, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x0c, 0x52, 0x0b, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, + 0x22, 0x27, 0x0a, 0x11, 0x42, 0x72, 0x6f, 0x61, 0x64, 0x63, 0x61, 0x73, 0x74, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x78, 0x49, 0x44, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x78, 0x49, 0x44, 0x22, 0x11, 0x0a, 0x0f, 0x53, 0x68, 0x75, + 0x74, 0x64, 0x6f, 0x77, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x12, 0x0a, 0x10, + 0x53, 0x68, 0x75, 0x74, 0x64, 0x6f, 0x77, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x32, 0x94, 0x03, 0x0a, 0x0c, 0x6b, 0x61, 0x73, 0x70, 0x61, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, + 0x64, 0x12, 0x37, 0x0a, 0x0a, 0x47, 0x65, 0x74, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x12, + 0x12, 0x2e, 0x47, 0x65, 0x74, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x13, 0x2e, 0x47, 0x65, 0x74, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x67, 0x0a, 0x1a, 0x43, 0x72, + 0x65, 0x61, 0x74, 0x65, 0x55, 0x6e, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x54, 0x72, 0x61, 0x6e, + 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x22, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, + 0x65, 0x55, 0x6e, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, + 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x23, 0x2e, 0x43, + 0x72, 0x65, 0x61, 0x74, 0x65, 0x55, 0x6e, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x54, 0x72, 0x61, + 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x22, 0x00, 0x12, 0x40, 0x0a, 0x0d, 0x53, 0x68, 0x6f, 0x77, 0x41, 0x64, 0x64, 0x72, 0x65, + 0x73, 0x73, 0x65, 0x73, 0x12, 0x15, 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, + 0x6f, 0x77, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x37, 0x0a, 0x0a, 0x4e, 0x65, 0x77, 0x41, 0x64, 0x64, 0x72, + 0x65, 0x73, 0x73, 0x12, 0x12, 0x2e, 0x4e, 0x65, 0x77, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x13, 0x2e, 0x4e, 0x65, 0x77, 0x41, 0x64, 0x64, + 0x72, 0x65, 0x73, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x31, + 0x0a, 0x08, 0x53, 0x68, 0x75, 0x74, 0x64, 0x6f, 0x77, 0x6e, 0x12, 0x10, 0x2e, 0x53, 0x68, 0x75, + 0x74, 0x64, 0x6f, 0x77, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x11, 0x2e, 0x53, + 0x68, 0x75, 0x74, 0x64, 0x6f, 0x77, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, + 0x00, 0x12, 0x34, 0x0a, 0x09, 0x42, 0x72, 0x6f, 0x61, 0x64, 0x63, 0x61, 0x73, 0x74, 0x12, 0x11, + 0x2e, 0x42, 0x72, 0x6f, 0x61, 0x64, 0x63, 0x61, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x12, 0x2e, 0x42, 0x72, 0x6f, 0x61, 0x64, 0x63, 0x61, 0x73, 0x74, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, 0x36, 0x5a, 0x34, 0x67, 0x69, 0x74, 0x68, 0x75, + 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6b, 0x61, 0x73, 0x70, 0x61, 0x6e, 0x65, 0x74, 0x2f, 0x6b, + 0x61, 0x73, 0x70, 0x61, 0x64, 0x2f, 0x63, 0x6d, 0x64, 0x2f, 0x6b, 0x61, 0x73, 0x70, 0x61, 0x77, + 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x2f, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2f, 0x70, 0x62, 0x62, + 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -720,30 +721,30 @@ func file_kaspawalletd_proto_rawDescGZIP() []byte { var file_kaspawalletd_proto_msgTypes = make([]protoimpl.MessageInfo, 13) var file_kaspawalletd_proto_goTypes = []interface{}{ - (*GetBalanceRequest)(nil), // 0: GetBalanceRequest - (*GetBalanceResponse)(nil), // 1: GetBalanceResponse - (*AddressBalances)(nil), // 2: AddressBalances - (*CreateUnsignedTransactionRequest)(nil), // 3: CreateUnsignedTransactionRequest - (*CreateUnsignedTransactionResponse)(nil), // 4: CreateUnsignedTransactionResponse - (*ShowAddressesRequest)(nil), // 5: ShowAddressesRequest - (*ShowAddressesResponse)(nil), // 6: ShowAddressesResponse - (*NewAddressRequest)(nil), // 7: NewAddressRequest - (*NewAddressResponse)(nil), // 8: NewAddressResponse - (*BroadcastRequest)(nil), // 9: BroadcastRequest - (*BroadcastResponse)(nil), // 10: BroadcastResponse - (*ShutdownRequest)(nil), // 11: ShutdownRequest - (*ShutdownResponse)(nil), // 12: ShutdownResponse + (*GetBalanceRequest)(nil), // 0: GetBalanceRequest + (*GetBalanceResponse)(nil), // 1: GetBalanceResponse + (*AddressBalances)(nil), // 2: AddressBalances + (*CreateUnsignedTransactionsRequest)(nil), // 3: CreateUnsignedTransactionsRequest + (*CreateUnsignedTransactionsResponse)(nil), // 4: CreateUnsignedTransactionsResponse + (*ShowAddressesRequest)(nil), // 5: ShowAddressesRequest + (*ShowAddressesResponse)(nil), // 6: ShowAddressesResponse + (*NewAddressRequest)(nil), // 7: NewAddressRequest + (*NewAddressResponse)(nil), // 8: NewAddressResponse + (*BroadcastRequest)(nil), // 9: BroadcastRequest + (*BroadcastResponse)(nil), // 10: BroadcastResponse + (*ShutdownRequest)(nil), // 11: ShutdownRequest + (*ShutdownResponse)(nil), // 12: ShutdownResponse } var file_kaspawalletd_proto_depIdxs = []int32{ 2, // 0: GetBalanceResponse.addressBalances:type_name -> AddressBalances 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 7, // 4: kaspawalletd.NewAddress:input_type -> NewAddressRequest 11, // 5: kaspawalletd.Shutdown:input_type -> ShutdownRequest 9, // 6: kaspawalletd.Broadcast:input_type -> BroadcastRequest 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 8, // 10: kaspawalletd.NewAddress:output_type -> NewAddressResponse 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{} { - switch v := v.(*CreateUnsignedTransactionRequest); i { + switch v := v.(*CreateUnsignedTransactionsRequest); i { case 0: return &v.state case 1: @@ -810,7 +811,7 @@ func file_kaspawalletd_proto_init() { } } file_kaspawalletd_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*CreateUnsignedTransactionResponse); i { + switch v := v.(*CreateUnsignedTransactionsResponse); i { case 0: return &v.state case 1: diff --git a/cmd/kaspawallet/daemon/pb/kaspawalletd.proto b/cmd/kaspawallet/daemon/pb/kaspawalletd.proto index 3c34b42e89..1992d5cd57 100644 --- a/cmd/kaspawallet/daemon/pb/kaspawalletd.proto +++ b/cmd/kaspawallet/daemon/pb/kaspawalletd.proto @@ -4,7 +4,7 @@ option go_package = "github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/pb"; service kaspawalletd { rpc GetBalance (GetBalanceRequest) returns (GetBalanceResponse) {} - rpc CreateUnsignedTransaction (CreateUnsignedTransactionRequest) returns (CreateUnsignedTransactionResponse) {} + rpc CreateUnsignedTransactions (CreateUnsignedTransactionsRequest) returns (CreateUnsignedTransactionsResponse) {} rpc ShowAddresses (ShowAddressesRequest) returns (ShowAddressesResponse) {} rpc NewAddress (NewAddressRequest) returns (NewAddressResponse) {} rpc Shutdown (ShutdownRequest) returns (ShutdownResponse) {} @@ -26,13 +26,13 @@ message AddressBalances { uint64 pending = 3; } -message CreateUnsignedTransactionRequest { +message CreateUnsignedTransactionsRequest { string address = 1; uint64 amount = 2; } -message CreateUnsignedTransactionResponse { - bytes unsignedTransaction = 1; +message CreateUnsignedTransactionsResponse { + repeated bytes unsignedTransactions = 1; } message ShowAddressesRequest { diff --git a/cmd/kaspawallet/daemon/pb/kaspawalletd_grpc.pb.go b/cmd/kaspawallet/daemon/pb/kaspawalletd_grpc.pb.go index 04112b6fff..f23b245d1b 100644 --- a/cmd/kaspawallet/daemon/pb/kaspawalletd_grpc.pb.go +++ b/cmd/kaspawallet/daemon/pb/kaspawalletd_grpc.pb.go @@ -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. type KaspawalletdClient interface { 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) NewAddress(ctx context.Context, in *NewAddressRequest, opts ...grpc.CallOption) (*NewAddressResponse, 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 } -func (c *kaspawalletdClient) CreateUnsignedTransaction(ctx context.Context, in *CreateUnsignedTransactionRequest, opts ...grpc.CallOption) (*CreateUnsignedTransactionResponse, error) { - out := new(CreateUnsignedTransactionResponse) - err := c.cc.Invoke(ctx, "/kaspawalletd/CreateUnsignedTransaction", in, out, opts...) +func (c *kaspawalletdClient) CreateUnsignedTransactions(ctx context.Context, in *CreateUnsignedTransactionsRequest, opts ...grpc.CallOption) (*CreateUnsignedTransactionsResponse, error) { + out := new(CreateUnsignedTransactionsResponse) + err := c.cc.Invoke(ctx, "/kaspawalletd/CreateUnsignedTransactions", in, out, opts...) if err != nil { return nil, err } @@ -93,7 +93,7 @@ func (c *kaspawalletdClient) Broadcast(ctx context.Context, in *BroadcastRequest // for forward compatibility type KaspawalletdServer interface { GetBalance(context.Context, *GetBalanceRequest) (*GetBalanceResponse, error) - CreateUnsignedTransaction(context.Context, *CreateUnsignedTransactionRequest) (*CreateUnsignedTransactionResponse, error) + CreateUnsignedTransactions(context.Context, *CreateUnsignedTransactionsRequest) (*CreateUnsignedTransactionsResponse, error) ShowAddresses(context.Context, *ShowAddressesRequest) (*ShowAddressesResponse, error) NewAddress(context.Context, *NewAddressRequest) (*NewAddressResponse, error) Shutdown(context.Context, *ShutdownRequest) (*ShutdownResponse, error) @@ -108,8 +108,8 @@ type UnimplementedKaspawalletdServer struct { func (UnimplementedKaspawalletdServer) GetBalance(context.Context, *GetBalanceRequest) (*GetBalanceResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method GetBalance not implemented") } -func (UnimplementedKaspawalletdServer) CreateUnsignedTransaction(context.Context, *CreateUnsignedTransactionRequest) (*CreateUnsignedTransactionResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method CreateUnsignedTransaction not implemented") +func (UnimplementedKaspawalletdServer) CreateUnsignedTransactions(context.Context, *CreateUnsignedTransactionsRequest) (*CreateUnsignedTransactionsResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method CreateUnsignedTransactions not implemented") } func (UnimplementedKaspawalletdServer) ShowAddresses(context.Context, *ShowAddressesRequest) (*ShowAddressesResponse, error) { 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) } -func _Kaspawalletd_CreateUnsignedTransaction_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(CreateUnsignedTransactionRequest) +func _Kaspawalletd_CreateUnsignedTransactions_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CreateUnsignedTransactionsRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { - return srv.(KaspawalletdServer).CreateUnsignedTransaction(ctx, in) + return srv.(KaspawalletdServer).CreateUnsignedTransactions(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/kaspawalletd/CreateUnsignedTransaction", + FullMethod: "/kaspawalletd/CreateUnsignedTransactions", } 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) } @@ -256,8 +256,8 @@ var Kaspawalletd_ServiceDesc = grpc.ServiceDesc{ Handler: _Kaspawalletd_GetBalance_Handler, }, { - MethodName: "CreateUnsignedTransaction", - Handler: _Kaspawalletd_CreateUnsignedTransaction_Handler, + MethodName: "CreateUnsignedTransactions", + Handler: _Kaspawalletd_CreateUnsignedTransactions_Handler, }, { MethodName: "ShowAddresses", diff --git a/cmd/kaspawallet/daemon/server/address.go b/cmd/kaspawallet/daemon/server/address.go index 621c442328..188ba896bb 100644 --- a/cmd/kaspawallet/daemon/server/address.go +++ b/cmd/kaspawallet/daemon/server/address.go @@ -10,15 +10,15 @@ import ( "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) if err != nil { - return nil, err + return nil, nil, err } err = s.keysFile.Save() if err != nil { - return nil, err + return nil, nil, err } walletAddr := &walletAddress{ @@ -27,7 +27,11 @@ func (s *server) changeAddress() (util.Address, error) { keyChain: libkaspawallet.InternalKeychain, } 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) { diff --git a/cmd/kaspawallet/daemon/server/create_unsigned_transaction.go b/cmd/kaspawallet/daemon/server/create_unsigned_transaction.go index 68e6364ef6..ac12711ba9 100644 --- a/cmd/kaspawallet/daemon/server/create_unsigned_transaction.go +++ b/cmd/kaspawallet/daemon/server/create_unsigned_transaction.go @@ -10,7 +10,11 @@ import ( "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() defer s.lock.Unlock() @@ -28,14 +32,12 @@ func (s *server) CreateUnsignedTransaction(_ context.Context, request *pb.Create return nil, err } - // TODO: Implement a better fee estimation mechanism - const feePerInput = 10000 selectedUTXOs, changeSompi, err := s.selectUTXOs(request.Amount, feePerInput) if err != nil { return nil, err } - changeAddress, err := s.changeAddress() + changeAddress, changeWalletAddress, err := s.changeAddress() if err != nil { return nil, err } @@ -53,7 +55,12 @@ func (s *server) CreateUnsignedTransaction(_ context.Context, request *pb.Create 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) ( diff --git a/cmd/kaspawallet/daemon/server/server.go b/cmd/kaspawallet/daemon/server/server.go index d9fe254624..1335aa3a59 100644 --- a/cmd/kaspawallet/daemon/server/server.go +++ b/cmd/kaspawallet/daemon/server/server.go @@ -7,6 +7,8 @@ import ( "sync" "time" + "github.com/kaspanet/kaspad/util/txmass" + "github.com/kaspanet/kaspad/util/profiling" "github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/pb" @@ -32,6 +34,7 @@ type server struct { keysFile *keys.File shutdown chan struct{} addressSet walletAddressSet + txMassCalculator *txmass.Calculator } // Start starts the kaspawalletd server @@ -69,6 +72,7 @@ func Start(params *dagconfig.Params, listen, rpcServer string, keysFilePath stri keysFile: keysFile, shutdown: make(chan struct{}), addressSet: make(walletAddressSet), + txMassCalculator: txmass.NewCalculator(params.MassPerTxByte, params.MassPerScriptPubKeyByte, params.MassPerSigOp), } spawn("serverInstance.sync", func() { diff --git a/cmd/kaspawallet/daemon/server/split_transaction.go b/cmd/kaspawallet/daemon/server/split_transaction.go new file mode 100644 index 0000000000..fd672b019d --- /dev/null +++ b/cmd/kaspawallet/daemon/server/split_transaction.go @@ -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 +} diff --git a/cmd/kaspawallet/daemon/server/split_transaction_test.go b/cmd/kaspawallet/daemon/server/split_transaction_test.go new file mode 100644 index 0000000000..0f521bb62f --- /dev/null +++ b/cmd/kaspawallet/daemon/server/split_transaction_test.go @@ -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 +} diff --git a/cmd/kaspawallet/libkaspawallet/serialization/serialization.go b/cmd/kaspawallet/libkaspawallet/serialization/serialization.go index 3abdaef4a8..4abc6b5d97 100644 --- a/cmd/kaspawallet/libkaspawallet/serialization/serialization.go +++ b/cmd/kaspawallet/libkaspawallet/serialization/serialization.go @@ -34,6 +34,44 @@ type PubKeySignaturePair struct { 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. func DeserializePartiallySignedTransaction(serializedPartiallySignedTransaction []byte) (*PartiallySignedTransaction, error) { protoPartiallySignedTransaction := &protoserialization.PartiallySignedTransaction{} diff --git a/cmd/kaspawallet/libkaspawallet/sign.go b/cmd/kaspawallet/libkaspawallet/sign.go index 5bef3bf084..98057ddb05 100644 --- a/cmd/kaspawallet/libkaspawallet/sign.go +++ b/cmd/kaspawallet/libkaspawallet/sign.go @@ -40,7 +40,6 @@ func Sign(params *dagconfig.Params, mnemonics []string, serializedPSTx []byte, e return nil, err } } - return serialization.SerializePartiallySignedTransaction(partiallySignedTransaction) } diff --git a/cmd/kaspawallet/libkaspawallet/transaction.go b/cmd/kaspawallet/libkaspawallet/transaction.go index a1b2e549f2..62664b66bd 100644 --- a/cmd/kaspawallet/libkaspawallet/transaction.go +++ b/cmd/kaspawallet/libkaspawallet/transaction.go @@ -159,6 +159,7 @@ func createUnsignedTransaction( Tx: domainTransaction, PartiallySignedInputs: partiallySignedInputs, }, nil + } // 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 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 { isMultisig := len(input.PubKeySignaturePairs) > 1 scriptBuilder := txscript.NewScriptBuilder() diff --git a/cmd/kaspawallet/libkaspawallet/transaction_test.go b/cmd/kaspawallet/libkaspawallet/transaction_test.go index d7c7a74216..1b705e739b 100644 --- a/cmd/kaspawallet/libkaspawallet/transaction_test.go +++ b/cmd/kaspawallet/libkaspawallet/transaction_test.go @@ -2,6 +2,9 @@ package libkaspawallet_test import ( "fmt" + "strings" + "testing" + "github.com/kaspanet/kaspad/cmd/kaspawallet/libkaspawallet" "github.com/kaspanet/kaspad/domain/consensus" "github.com/kaspanet/kaspad/domain/consensus/model/externalapi" @@ -10,8 +13,6 @@ import ( "github.com/kaspanet/kaspad/domain/consensus/utils/txscript" "github.com/kaspanet/kaspad/domain/consensus/utils/utxo" "github.com/kaspanet/kaspad/util" - "strings" - "testing" ) func forSchnorrAndECDSA(t *testing.T, testFunc func(t *testing.T, ecdsa bool)) { @@ -106,7 +107,7 @@ func TestMultisig(t *testing.T) { Amount: 10, }}, selectedUTXOs) if err != nil { - t.Fatalf("CreateUnsignedTransaction: %+v", err) + t.Fatalf("CreateUnsignedTransactions: %+v", err) } isFullySigned, err := libkaspawallet.IsTransactionFullySigned(unsignedTransaction) @@ -267,7 +268,7 @@ func TestP2PK(t *testing.T) { Amount: 10, }}, selectedUTXOs) if err != nil { - t.Fatalf("CreateUnsignedTransaction: %+v", err) + t.Fatalf("CreateUnsignedTransactions: %+v", err) } isFullySigned, err := libkaspawallet.IsTransactionFullySigned(unsignedTransaction) diff --git a/cmd/kaspawallet/send.go b/cmd/kaspawallet/send.go index e101c54404..c3fa1e8000 100644 --- a/cmd/kaspawallet/send.go +++ b/cmd/kaspawallet/send.go @@ -3,6 +3,7 @@ package main import ( "context" "fmt" + "github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/client" "github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/pb" "github.com/kaspanet/kaspad/cmd/kaspawallet/keys" @@ -31,10 +32,11 @@ func send(conf *sendConfig) error { defer cancel() sendAmountSompi := uint64(conf.SendAmount * constants.SompiPerKaspa) - createUnsignedTransactionResponse, err := daemonClient.CreateUnsignedTransaction(ctx, &pb.CreateUnsignedTransactionRequest{ - Address: conf.ToAddress, - Amount: sendAmountSompi, - }) + createUnsignedTransactionsResponse, err := + daemonClient.CreateUnsignedTransactions(ctx, &pb.CreateUnsignedTransactionsRequest{ + Address: conf.ToAddress, + Amount: sendAmountSompi, + }) if err != nil { return err } @@ -44,22 +46,37 @@ func send(conf *sendConfig) error { return err } - signedTransaction, err := libkaspawallet.Sign(conf.NetParams(), mnemonics, createUnsignedTransactionResponse.UnsignedTransaction, keysFile.ECDSA) - if err != nil { - return err + signedTransactions := make([][]byte, len(createUnsignedTransactionsResponse.UnsignedTransactions)) + for i, unsignedTransaction := range createUnsignedTransactionsResponse.UnsignedTransactions { + 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) - defer cancel2() - broadcastResponse, err := daemonClient.Broadcast(ctx2, &pb.BroadcastRequest{ - Transaction: signedTransaction, - }) - if err != nil { - return err + if len(signedTransactions) > 1 { + fmt.Printf("Broadcasting %d transactions\n", len(signedTransactions)) } + 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.Printf("Transaction ID: \t%s\n", broadcastResponse.TxID) + fmt.Println("Transaction was sent successfully") + fmt.Printf("Transaction ID: \t%s\n", broadcastResponse.TxID) + return nil + }() + if err != nil { + return err + } + } return nil } diff --git a/cmd/kaspawallet/sign.go b/cmd/kaspawallet/sign.go index 6ee60c8bf5..4ea60f7873 100644 --- a/cmd/kaspawallet/sign.go +++ b/cmd/kaspawallet/sign.go @@ -1,21 +1,16 @@ package main import ( - "encoding/hex" "fmt" + "io/ioutil" + "strings" + "github.com/kaspanet/kaspad/cmd/kaspawallet/keys" "github.com/kaspanet/kaspad/cmd/kaspawallet/libkaspawallet" "github.com/pkg/errors" - "io/ioutil" - "strings" ) func sign(conf *signConfig) error { - keysFile, err := keys.ReadKeysFile(conf.NetParams(), conf.KeysFile) - if err != nil { - return err - } - if conf.Transaction == "" && conf.TransactionFile == "" { 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") } - transactionHex := 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) - } - transactionHex = strings.TrimSpace(string(transactionHexBytes)) - } - - partiallySignedTransaction, err := hex.DecodeString(transactionHex) + keysFile, err := keys.ReadKeysFile(conf.NetParams(), conf.KeysFile) if err != nil { return err } - privateKeys, err := keysFile.DecryptMnemonics(conf.Password) if err != nil { 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 { return err } - isFullySigned, err := libkaspawallet.IsTransactionFullySigned(updatedPartiallySignedTransaction) - if err != nil { - return err + updatedPartiallySignedTransactions := make([][]byte, len(partiallySignedTransactions)) + for i, partiallySignedTransaction := range partiallySignedTransactions { + updatedPartiallySignedTransactions[i], err = + libkaspawallet.Sign(conf.NetParams(), privateKeys, partiallySignedTransaction, keysFile.ECDSA) + if err != nil { + return err + } + } + + 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 isFullySigned { + if areAllTransactionsFullySigned { fmt.Println("The transaction is signed and ready to broadcast") } else { fmt.Println("Successfully signed transaction") } - fmt.Printf("Transaction: %x\n", updatedPartiallySignedTransaction) + fmt.Println("Transaction: ") + fmt.Println(encodeTransactionsToHex(updatedPartiallySignedTransactions)) return nil } diff --git a/cmd/kaspawallet/transactions_hex_encoding.go b/cmd/kaspawallet/transactions_hex_encoding.go new file mode 100644 index 0000000000..e7e857bf11 --- /dev/null +++ b/cmd/kaspawallet/transactions_hex_encoding.go @@ -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 +} diff --git a/domain/consensus/consensus.go b/domain/consensus/consensus.go index 19efb52c1f..df1777fcd8 100644 --- a/domain/consensus/consensus.go +++ b/domain/consensus/consensus.go @@ -1,8 +1,6 @@ package consensus import ( - "github.com/kaspanet/kaspad/infrastructure/logger" - "github.com/kaspanet/kaspad/util/staging" "math/big" "sync" @@ -10,6 +8,8 @@ import ( "github.com/kaspanet/kaspad/domain/consensus/model" "github.com/kaspanet/kaspad/domain/consensus/model/externalapi" "github.com/kaspanet/kaspad/domain/consensus/ruleerrors" + "github.com/kaspanet/kaspad/infrastructure/logger" + "github.com/kaspanet/kaspad/util/staging" "github.com/pkg/errors" ) diff --git a/domain/consensus/factory.go b/domain/consensus/factory.go index ee6dc0634c..d427f50ab8 100644 --- a/domain/consensus/factory.go +++ b/domain/consensus/factory.go @@ -1,17 +1,18 @@ package consensus import ( + "io/ioutil" + "os" + "sync" + "github.com/kaspanet/kaspad/domain/consensus/datastructures/blockwindowheapslicestore" "github.com/kaspanet/kaspad/domain/consensus/datastructures/daawindowstore" "github.com/kaspanet/kaspad/domain/consensus/model" "github.com/kaspanet/kaspad/domain/consensus/processes/blockparentbuilder" parentssanager "github.com/kaspanet/kaspad/domain/consensus/processes/parentsmanager" "github.com/kaspanet/kaspad/domain/consensus/processes/pruningproofmanager" - "io/ioutil" - "os" - "sync" - "github.com/kaspanet/kaspad/domain/prefixmanager/prefix" + "github.com/kaspanet/kaspad/util/txmass" consensusdatabase "github.com/kaspanet/kaspad/domain/consensus/database" "github.com/kaspanet/kaspad/domain/consensus/datastructures/acceptancedatastore" @@ -171,6 +172,9 @@ func (f *factory) NewConsensus(config *Config, db infrastructuredatabase.Databas config.GenesisHash, config.MaxBlockLevel, ) + + txMassCalculator := txmass.NewCalculator(config.MassPerTxByte, config.MassPerScriptPubKeyByte, config.MassPerSigOp) + pastMedianTimeManager := f.pastMedianTimeConsructor( config.TimestampDeviationTolerance, dbManager, @@ -180,14 +184,12 @@ func (f *factory) NewConsensus(config *Config, db infrastructuredatabase.Databas config.GenesisHash) transactionValidator := transactionvalidator.New(config.BlockCoinbaseMaturity, config.EnableNonNativeSubnetworks, - config.MassPerTxByte, - config.MassPerScriptPubKeyByte, - config.MassPerSigOp, config.MaxCoinbasePayloadLength, dbManager, pastMedianTimeManager, ghostdagDataStore, - daaBlocksStore) + daaBlocksStore, + txMassCalculator) difficultyManager := f.difficultyConstructor( dbManager, ghostdagManager, @@ -331,6 +333,8 @@ func (f *factory) NewConsensus(config *Config, db infrastructuredatabase.Databas reachabilityDataStore, consensusStateStore, daaBlocksStore, + + txMassCalculator, ) syncManager := syncmanager.New( diff --git a/domain/consensus/processes/blockvalidator/blockvalidator.go b/domain/consensus/processes/blockvalidator/blockvalidator.go index 7a9be22b46..870ce8c421 100644 --- a/domain/consensus/processes/blockvalidator/blockvalidator.go +++ b/domain/consensus/processes/blockvalidator/blockvalidator.go @@ -4,6 +4,8 @@ import ( "math/big" "time" + "github.com/kaspanet/kaspad/util/txmass" + "github.com/kaspanet/kaspad/domain/consensus/model" "github.com/kaspanet/kaspad/domain/consensus/model/externalapi" "github.com/kaspanet/kaspad/util/difficulty" @@ -48,6 +50,8 @@ type blockValidator struct { reachabilityStore model.ReachabilityDataStore consensusStateStore model.ConsensusStateStore daaBlocksStore model.DAABlocksStore + + txMassCalculator *txmass.Calculator } // New instantiates a new BlockValidator @@ -87,6 +91,8 @@ func New(powMax *big.Int, reachabilityStore model.ReachabilityDataStore, consensusStateStore model.ConsensusStateStore, daaBlocksStore model.DAABlocksStore, + + txMassCalculator *txmass.Calculator, ) model.BlockValidator { return &blockValidator{ @@ -126,5 +132,7 @@ func New(powMax *big.Int, reachabilityStore: reachabilityStore, consensusStateStore: consensusStateStore, daaBlocksStore: daaBlocksStore, + + txMassCalculator: txMassCalculator, } } diff --git a/domain/consensus/processes/transactionvalidator/mass.go b/domain/consensus/processes/transactionvalidator/mass.go index c4e4a7aafb..d994104705 100644 --- a/domain/consensus/processes/transactionvalidator/mass.go +++ b/domain/consensus/processes/transactionvalidator/mass.go @@ -2,99 +2,12 @@ package transactionvalidator import ( "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) { if transaction.Mass != 0 { return } - transaction.Mass = v.transactionMass(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 + transaction.Mass = v.txMassCalculator.CalculateTransactionMass(transaction) } diff --git a/domain/consensus/processes/transactionvalidator/transactionvalidator.go b/domain/consensus/processes/transactionvalidator/transactionvalidator.go index 19f73188f5..73621e7560 100644 --- a/domain/consensus/processes/transactionvalidator/transactionvalidator.go +++ b/domain/consensus/processes/transactionvalidator/transactionvalidator.go @@ -3,6 +3,7 @@ package transactionvalidator import ( "github.com/kaspanet/kaspad/domain/consensus/model" "github.com/kaspanet/kaspad/domain/consensus/utils/txscript" + "github.com/kaspanet/kaspad/util/txmass" ) const sigCacheSize = 10_000 @@ -16,32 +17,25 @@ type transactionValidator struct { ghostdagDataStore model.GHOSTDAGDataStore daaBlocksStore model.DAABlocksStore enableNonNativeSubnetworks bool - massPerTxByte uint64 - massPerScriptPubKeyByte uint64 - massPerSigOp uint64 maxCoinbasePayloadLength uint64 sigCache *txscript.SigCache sigCacheECDSA *txscript.SigCacheECDSA + txMassCalculator *txmass.Calculator } // New instantiates a new TransactionValidator func New(blockCoinbaseMaturity uint64, enableNonNativeSubnetworks bool, - massPerTxByte uint64, - massPerScriptPubKeyByte uint64, - massPerSigOp uint64, maxCoinbasePayloadLength uint64, databaseContext model.DBReader, pastMedianTimeManager model.PastMedianTimeManager, ghostdagDataStore model.GHOSTDAGDataStore, - daaBlocksStore model.DAABlocksStore) model.TransactionValidator { + daaBlocksStore model.DAABlocksStore, + txMassCalculator *txmass.Calculator) model.TransactionValidator { return &transactionValidator{ blockCoinbaseMaturity: blockCoinbaseMaturity, enableNonNativeSubnetworks: enableNonNativeSubnetworks, - massPerTxByte: massPerTxByte, - massPerScriptPubKeyByte: massPerScriptPubKeyByte, - massPerSigOp: massPerSigOp, maxCoinbasePayloadLength: maxCoinbasePayloadLength, databaseContext: databaseContext, pastMedianTimeManager: pastMedianTimeManager, @@ -49,5 +43,6 @@ func New(blockCoinbaseMaturity uint64, daaBlocksStore: daaBlocksStore, sigCache: txscript.NewSigCache(sigCacheSize), sigCacheECDSA: txscript.NewSigCacheECDSA(sigCacheSize), + txMassCalculator: txMassCalculator, } } diff --git a/domain/consensus/utils/constants/constants.go b/domain/consensus/utils/constants/constants.go index aa1edf119d..31a1971996 100644 --- a/domain/consensus/utils/constants/constants.go +++ b/domain/consensus/utils/constants/constants.go @@ -35,4 +35,8 @@ const ( // LockTimeThreshold is the number below which a lock time is // interpreted to be a DAA score. 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 ) diff --git a/domain/dagconfig/params.go b/domain/dagconfig/params.go index 6d9198497a..2deda4e920 100644 --- a/domain/dagconfig/params.go +++ b/domain/dagconfig/params.go @@ -318,7 +318,7 @@ var TestnetParams = Params{ MinerConfirmationWindow: 2016, // Mempool parameters - RelayNonStdTxs: true, + RelayNonStdTxs: false, // AcceptUnroutable specifies whether this network accepts unroutable // IP addresses, such as 10.0.0.0/8 @@ -386,7 +386,7 @@ var SimnetParams = Params{ MinerConfirmationWindow: 100, // Mempool parameters - RelayNonStdTxs: true, + RelayNonStdTxs: false, // AcceptUnroutable specifies whether this network accepts unroutable // IP addresses, such as 10.0.0.0/8 @@ -445,7 +445,7 @@ var DevnetParams = Params{ MinerConfirmationWindow: 2016, // Mempool parameters - RelayNonStdTxs: true, + RelayNonStdTxs: false, // AcceptUnroutable specifies whether this network accepts unroutable // IP addresses, such as 10.0.0.0/8 diff --git a/domain/miningmanager/mempool/check_transaction_standard.go b/domain/miningmanager/mempool/check_transaction_standard.go index 39df3429d4..9a66e2b677 100644 --- a/domain/miningmanager/mempool/check_transaction_standard.go +++ b/domain/miningmanager/mempool/check_transaction_standard.go @@ -3,7 +3,7 @@ package mempool import ( "fmt" - "github.com/kaspanet/kaspad/domain/consensus/processes/transactionvalidator" + "github.com/kaspanet/kaspad/util/txmass" "github.com/kaspanet/kaspad/domain/consensus/utils/consensushashing" @@ -36,9 +36,9 @@ const ( // (1 + 15*74 + 3) + (15*34 + 3) + 23 = 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. - maximumStandardTransactionMass = 100000 + MaximumStandardTransactionMass = 100_000 ) 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 // size of a transaction. This also helps mitigate CPU exhaustion // attacks. - if transaction.Mass > maximumStandardTransactionMass { + if transaction.Mass > MaximumStandardTransactionMass { 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) } @@ -127,7 +127,7 @@ func (mp *mempool) IsTransactionOutputDust(output *externalapi.DomainTransaction // 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 // 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 // coins is more than 1/3 of the minimum free transaction relay fee. diff --git a/domain/miningmanager/mempool/check_transaction_standard_test.go b/domain/miningmanager/mempool/check_transaction_standard_test.go index 04e6686749..9366e5a534 100644 --- a/domain/miningmanager/mempool/check_transaction_standard_test.go +++ b/domain/miningmanager/mempool/check_transaction_standard_test.go @@ -6,10 +6,11 @@ package mempool import ( "bytes" - "github.com/kaspanet/kaspad/domain/consensusreference" "math" "testing" + "github.com/kaspanet/kaspad/domain/consensusreference" + "github.com/kaspanet/kaspad/domain/consensus/utils/testutils" "github.com/kaspanet/kaspad/domain/consensus" @@ -45,13 +46,13 @@ func TestCalcMinRequiredTxRelayFee(t *testing.T) { }, { "max standard tx size with default minimum relay fee", - maximumStandardTransactionMass, + MaximumStandardTransactionMass, defaultMinimumRelayTransactionFee, 100000, }, { "max standard tx size with max sompi relay fee", - maximumStandardTransactionMass, + MaximumStandardTransactionMass, constants.MaxSompi, constants.MaxSompi, }, @@ -249,7 +250,7 @@ func TestCheckTransactionStandardInIsolation(t *testing.T) { name: "Transaction size is too large", tx: &externalapi.DomainTransaction{Version: 0, Inputs: []*externalapi.DomainTransactionInput{&dummyTxIn}, Outputs: []*externalapi.DomainTransactionOutput{{ Value: 0, - ScriptPublicKey: &externalapi.ScriptPublicKey{bytes.Repeat([]byte{0x00}, maximumStandardTransactionMass+1), 0}, + ScriptPublicKey: &externalapi.ScriptPublicKey{bytes.Repeat([]byte{0x00}, MaximumStandardTransactionMass+1), 0}, }}}, height: 300000, isStandard: false, diff --git a/domain/miningmanager/mempool/fill_inputs_and_get_missing_parents.go b/domain/miningmanager/mempool/fill_inputs_and_get_missing_parents.go index e269a079ae..ad9cfc6075 100644 --- a/domain/miningmanager/mempool/fill_inputs_and_get_missing_parents.go +++ b/domain/miningmanager/mempool/fill_inputs_and_get_missing_parents.go @@ -3,6 +3,7 @@ package mempool import ( "github.com/kaspanet/kaspad/domain/consensus/model/externalapi" "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/miningmanager/mempool/model" "github.com/pkg/errors" @@ -42,6 +43,6 @@ func fillInputs(transaction *externalapi.DomainTransaction, parentsInPool model. } relevantOutput := parent.Transaction().Outputs[input.PreviousOutpoint.Index] input.UTXOEntry = utxo.NewUTXOEntry(relevantOutput.Value, relevantOutput.ScriptPublicKey, - false, model.UnacceptedDAAScore) + false, constants.UnacceptedDAAScore) } } diff --git a/domain/miningmanager/mempool/mempool_utxo_set.go b/domain/miningmanager/mempool/mempool_utxo_set.go index efb1dd9b84..aa20d05d5d 100644 --- a/domain/miningmanager/mempool/mempool_utxo_set.go +++ b/domain/miningmanager/mempool/mempool_utxo_set.go @@ -3,6 +3,8 @@ package mempool import ( "fmt" + "github.com/kaspanet/kaspad/domain/consensus/utils/constants" + "github.com/kaspanet/kaspad/domain/consensus/utils/consensushashing" "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)} mpus.poolUnspentOutputs[outpoint] = - utxo.NewUTXOEntry(output.Value, output.ScriptPublicKey, false, model.UnacceptedDAAScore) + utxo.NewUTXOEntry(output.Value, output.ScriptPublicKey, false, constants.UnacceptedDAAScore) } } diff --git a/domain/miningmanager/mempool/model/constants.go b/domain/miningmanager/mempool/model/constants.go deleted file mode 100644 index d2d743bdc2..0000000000 --- a/domain/miningmanager/mempool/model/constants.go +++ /dev/null @@ -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 diff --git a/domain/miningmanager/mempool/orphan_pool.go b/domain/miningmanager/mempool/orphan_pool.go index df54903fd7..ce7d77774c 100644 --- a/domain/miningmanager/mempool/orphan_pool.go +++ b/domain/miningmanager/mempool/orphan_pool.go @@ -3,6 +3,8 @@ package mempool import ( "fmt" + "github.com/kaspanet/kaspad/domain/consensus/utils/constants" + "github.com/kaspanet/kaspad/domain/consensus/ruleerrors" "github.com/kaspanet/kaspad/domain/consensus/utils/consensushashing" @@ -153,7 +155,7 @@ func (op *orphansPool) processOrphansAfterAcceptedTransaction(acceptedTransactio for _, input := range orphan.Transaction().Inputs { if input.PreviousOutpoint.Equal(&outpoint) && input.UTXOEntry == nil { input.UTXOEntry = utxo.NewUTXOEntry(output.Value, output.ScriptPublicKey, false, - model.UnacceptedDAAScore) + constants.UnacceptedDAAScore) break } } diff --git a/infrastructure/config/config.go b/infrastructure/config/config.go index d219076e57..c8c8fd024a 100644 --- a/infrastructure/config/config.go +++ b/infrastructure/config/config.go @@ -42,16 +42,16 @@ const ( defaultMaxRPCClients = 10 defaultMaxRPCWebsockets = 25 defaultMaxRPCConcurrentReqs = 20 - defaultBlockMaxMass = 10000000 + defaultBlockMaxMass = 10_000_000 blockMaxMassMin = 1000 - blockMaxMassMax = 10000000 + blockMaxMassMax = 10_000_000 defaultMinRelayTxFee = 1e-5 // 1 sompi per byte defaultMaxOrphanTransactions = 100 //DefaultMaxOrphanTxSize is the default maximum size for an orphan transaction - DefaultMaxOrphanTxSize = 100000 - defaultSigCacheMaxSize = 100000 + DefaultMaxOrphanTxSize = 100_000 + defaultSigCacheMaxSize = 100_000 sampleConfigFilename = "sample-kaspad.conf" - defaultMaxUTXOCacheSize = 5000000000 + defaultMaxUTXOCacheSize = 5_000_000_000 defaultProtocolVersion = 5 ) diff --git a/util/txmass/calculator.go b/util/txmass/calculator.go new file mode 100644 index 0000000000..5206f7fe57 --- /dev/null +++ b/util/txmass/calculator.go @@ -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 +}