mirror of
https://github.com/kaspanet/kaspad.git
synced 2025-03-30 15:08:33 +00:00
Compare commits
40 Commits
v0.12.10-r
...
master
Author | SHA1 | Date | |
---|---|---|---|
![]() |
03cc7dfc19 | ||
![]() |
ed745a9acb | ||
![]() |
c23c1d141c | ||
![]() |
352d261fd6 | ||
![]() |
43b9523919 | ||
![]() |
6085d1fc84 | ||
![]() |
1e9ddc42d0 | ||
![]() |
48a142e12f | ||
![]() |
86b89065cf | ||
![]() |
f41dc7fa0b | ||
![]() |
6b38bf7069 | ||
![]() |
d2453f8e7b | ||
![]() |
629faa8436 | ||
![]() |
91e6c6b74b | ||
![]() |
0819244ba1 | ||
![]() |
a0149cd8d0 | ||
![]() |
5a3b8a0066 | ||
![]() |
8e71f79f98 | ||
![]() |
346341a709 | ||
![]() |
8c881aea39 | ||
![]() |
40ec440dcf | ||
![]() |
88bdcb43bc | ||
![]() |
9d1e44673f | ||
![]() |
387fade044 | ||
![]() |
c417c8b525 | ||
![]() |
bd1420220a | ||
![]() |
5640ec4020 | ||
![]() |
1c0887ca60 | ||
![]() |
7be3f41aa7 | ||
![]() |
26c4c73624 | ||
![]() |
880d917e58 | ||
![]() |
3c53c6d8cd | ||
![]() |
3c4b973090 | ||
![]() |
8aee8f81c5 | ||
![]() |
ec3441e63f | ||
![]() |
e3ba1ca07e | ||
![]() |
27fdbd9c88 | ||
![]() |
377d9aaaeb | ||
![]() |
beee947dda | ||
![]() |
d4a27bf1c1 |
18
.github/workflows/deploy.yaml
vendored
18
.github/workflows/deploy.yaml
vendored
@ -1,7 +1,7 @@
|
||||
name: Build and upload assets
|
||||
on:
|
||||
release:
|
||||
types: [ published ]
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@ -9,7 +9,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ ubuntu-latest, windows-latest, macos-latest ]
|
||||
os: [ubuntu-latest, windows-latest, macos-latest]
|
||||
name: Building, ${{ matrix.os }}
|
||||
steps:
|
||||
- name: Fix CRLF on Windows
|
||||
@ -17,13 +17,12 @@ jobs:
|
||||
run: git config --global core.autocrlf false
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@v2
|
||||
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v2
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.18
|
||||
go-version: 1.21
|
||||
|
||||
- name: Build on Linux
|
||||
if: runner.os == 'Linux'
|
||||
@ -31,7 +30,7 @@ jobs:
|
||||
# `-tags netgo,osusergo` means use pure go replacements for "os/user" and "net"
|
||||
# `-s -w` strips the binary to produce smaller size binaries
|
||||
run: |
|
||||
go build -v -ldflags="-s -w -extldflags=-static" -tags netgo,osusergo -o ./bin/ . ./cmd/...
|
||||
go build -v -ldflags="-s -w -extldflags=-static" -tags netgo,osusergo -o ./bin/ ./cmd/...
|
||||
archive="bin/kaspad-${{ github.event.release.tag_name }}-linux.zip"
|
||||
asset_name="kaspad-${{ github.event.release.tag_name }}-linux.zip"
|
||||
zip -r "${archive}" ./bin/*
|
||||
@ -42,7 +41,7 @@ jobs:
|
||||
if: runner.os == 'Windows'
|
||||
shell: bash
|
||||
run: |
|
||||
go build -v -ldflags="-s -w" -o bin/ . ./cmd/...
|
||||
go build -v -ldflags="-s -w" -o bin/ ./cmd/...
|
||||
archive="bin/kaspad-${{ github.event.release.tag_name }}-win64.zip"
|
||||
asset_name="kaspad-${{ github.event.release.tag_name }}-win64.zip"
|
||||
powershell "Compress-Archive bin/* \"${archive}\""
|
||||
@ -52,14 +51,13 @@ jobs:
|
||||
- name: Build on MacOS
|
||||
if: runner.os == 'macOS'
|
||||
run: |
|
||||
go build -v -ldflags="-s -w" -o ./bin/ . ./cmd/...
|
||||
go build -v -ldflags="-s -w" -o ./bin/ ./cmd/...
|
||||
archive="bin/kaspad-${{ github.event.release.tag_name }}-osx.zip"
|
||||
asset_name="kaspad-${{ github.event.release.tag_name }}-osx.zip"
|
||||
zip -r "${archive}" ./bin/*
|
||||
echo "archive=${archive}" >> $GITHUB_ENV
|
||||
echo "asset_name=${asset_name}" >> $GITHUB_ENV
|
||||
|
||||
|
||||
- name: Upload release asset
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
|
8
.github/workflows/race.yaml
vendored
8
.github/workflows/race.yaml
vendored
@ -11,18 +11,18 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
branch: [ master, latest ]
|
||||
branch: [master, latest]
|
||||
name: Race detection on ${{ matrix.branch }}
|
||||
steps:
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v2
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.18
|
||||
go-version: 1.23
|
||||
|
||||
- name: Set scheduled branch name
|
||||
shell: bash
|
||||
|
28
.github/workflows/tests.yaml
vendored
28
.github/workflows/tests.yaml
vendored
@ -8,22 +8,20 @@ on:
|
||||
types: [opened, synchronize, edited]
|
||||
|
||||
jobs:
|
||||
|
||||
build:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ ubuntu-latest, macos-latest ]
|
||||
os: [ubuntu-latest, macos-latest]
|
||||
name: Tests, ${{ matrix.os }}
|
||||
steps:
|
||||
|
||||
- name: Fix CRLF on Windows
|
||||
if: runner.os == 'Windows'
|
||||
run: git config --global core.autocrlf false
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# Increase the pagefile size on Windows to aviod running out of memory
|
||||
- name: Increase pagefile size on Windows
|
||||
@ -31,14 +29,13 @@ jobs:
|
||||
run: powershell -command .github\workflows\SetPageFileSize.ps1
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v2
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.18
|
||||
|
||||
go-version: 1.23
|
||||
|
||||
# Source: https://github.com/actions/cache/blob/main/examples.md#go---modules
|
||||
- name: Go Cache
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/go/pkg/mod
|
||||
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
||||
@ -49,19 +46,17 @@ jobs:
|
||||
shell: bash
|
||||
run: ./build_and_test.sh -v
|
||||
|
||||
|
||||
stability-test-fast:
|
||||
runs-on: ubuntu-latest
|
||||
name: Fast stability tests, ${{ github.head_ref }}
|
||||
steps:
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v2
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.18
|
||||
go-version: 1.23
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@ -75,18 +70,17 @@ jobs:
|
||||
working-directory: stability-tests
|
||||
run: ./install_and_test.sh
|
||||
|
||||
|
||||
coverage:
|
||||
runs-on: ubuntu-latest
|
||||
name: Produce code coverage
|
||||
steps:
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v2
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.18
|
||||
go-version: 1.23
|
||||
|
||||
- name: Delete the stability tests from coverage
|
||||
run: rm -r stability-tests
|
||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -53,6 +53,7 @@ _testmain.go
|
||||
debug
|
||||
debug.test
|
||||
__debug_bin
|
||||
*__debug_*
|
||||
|
||||
# CI
|
||||
version.txt
|
||||
|
43
CODE_OF_CONDUCT.md
Normal file
43
CODE_OF_CONDUCT.md
Normal file
@ -0,0 +1,43 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to creating a positive environment include:
|
||||
|
||||
* Using welcoming and inclusive language
|
||||
* Being respectful of differing viewpoints and experiences
|
||||
* Gracefully accepting constructive criticism
|
||||
* Focusing on what is best for the community
|
||||
* Showing empathy towards other community members
|
||||
|
||||
Examples of unacceptable behavior by participants include:
|
||||
|
||||
* The use of sexualized language or imagery and unwelcome sexual attention or advances
|
||||
* Trolling, insulting/derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or electronic address, without explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a professional setting
|
||||
|
||||
## Our Responsibilities
|
||||
|
||||
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
|
||||
|
||||
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project maintainers on this [Google form][gform]. The project maintainers will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project maintainers are obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
|
||||
|
||||
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
|
||||
|
||||
[gform]: https://forms.gle/dnKXMJL7VxdUjt3x5
|
||||
[homepage]: http://contributor-covenant.org
|
||||
[version]: http://contributor-covenant.org/version/1/4/
|
14
README.md
14
README.md
@ -1,11 +1,15 @@
|
||||
# DEPRECATED
|
||||
|
||||
Kaspad
|
||||
====
|
||||
The full node reference implementation was [rewritten in Rust](https://github.com/kaspanet/rusty-kaspa), as a result, the Go implementation is now deprecated.
|
||||
|
||||
PLEASE NOTE: Any pull requests or issues that will be opened in this repository will be closed without treatment, except for issues or pull requests related to the kaspawallet, which remains maintained. In any other case, please use the [Rust implementation](https://github.com/kaspanet/rusty-kaspa) instead.
|
||||
|
||||
# Kaspad
|
||||
|
||||
[](https://choosealicense.com/licenses/isc/)
|
||||
[](http://godoc.org/github.com/kaspanet/kaspad)
|
||||
|
||||
Kaspad is the reference full node Kaspa implementation written in Go (golang).
|
||||
Kaspad was the reference full node Kaspa implementation written in Go (golang).
|
||||
|
||||
## What is kaspa
|
||||
|
||||
@ -13,7 +17,7 @@ Kaspa is an attempt at a proof-of-work cryptocurrency with instant confirmations
|
||||
|
||||
## Requirements
|
||||
|
||||
Go 1.18 or later.
|
||||
Go 1.23 or later.
|
||||
|
||||
## Installation
|
||||
|
||||
@ -40,7 +44,6 @@ $ go install . ./cmd/...
|
||||
not already add the bin directory to your system path during Go installation,
|
||||
you are encouraged to do so now.
|
||||
|
||||
|
||||
## Getting Started
|
||||
|
||||
Kaspad has several configuration options available to tweak how it runs, but all
|
||||
@ -51,6 +54,7 @@ $ kaspad
|
||||
```
|
||||
|
||||
## Discord
|
||||
|
||||
Join our discord server using the following link: https://discord.gg/YNYnNN5Pf2
|
||||
|
||||
## Issue Tracker
|
||||
|
@ -6,7 +6,7 @@ supported kaspa messages to and from the appmessage. This package does not deal
|
||||
with the specifics of message handling such as what to do when a message is
|
||||
received. This provides the caller with a high level of flexibility.
|
||||
|
||||
Kaspa Message Overview
|
||||
# Kaspa Message Overview
|
||||
|
||||
The kaspa protocol consists of exchanging messages between peers. Each
|
||||
message is preceded by a header which identifies information about it such as
|
||||
@ -22,7 +22,7 @@ messages, all of the details of marshalling and unmarshalling to and from the
|
||||
appmessage using kaspa encoding are handled so the caller doesn't have to concern
|
||||
themselves with the specifics.
|
||||
|
||||
Message Interaction
|
||||
# Message Interaction
|
||||
|
||||
The following provides a quick summary of how the kaspa messages are intended
|
||||
to interact with one another. As stated above, these interactions are not
|
||||
@ -45,13 +45,13 @@ interactions in no particular order.
|
||||
notfound message (MsgNotFound)
|
||||
ping message (MsgPing) pong message (MsgPong)
|
||||
|
||||
Common Parameters
|
||||
# Common Parameters
|
||||
|
||||
There are several common parameters that arise when using this package to read
|
||||
and write kaspa messages. The following sections provide a quick overview of
|
||||
these parameters so the next sections can build on them.
|
||||
|
||||
Protocol Version
|
||||
# Protocol Version
|
||||
|
||||
The protocol version should be negotiated with the remote peer at a higher
|
||||
level than this package via the version (MsgVersion) message exchange, however,
|
||||
@ -60,18 +60,18 @@ latest protocol version this package supports and is typically the value to use
|
||||
for all outbound connections before a potentially lower protocol version is
|
||||
negotiated.
|
||||
|
||||
Kaspa Network
|
||||
# Kaspa Network
|
||||
|
||||
The kaspa network is a magic number which is used to identify the start of a
|
||||
message and which kaspa network the message applies to. This package provides
|
||||
the following constants:
|
||||
|
||||
appmessage.Mainnet
|
||||
appmessage.Testnet (Test network)
|
||||
appmessage.Simnet (Simulation test network)
|
||||
appmessage.Devnet (Development network)
|
||||
appmessage.Mainnet
|
||||
appmessage.Testnet (Test network)
|
||||
appmessage.Simnet (Simulation test network)
|
||||
appmessage.Devnet (Development network)
|
||||
|
||||
Determining Message Type
|
||||
# Determining Message Type
|
||||
|
||||
As discussed in the kaspa message overview section, this package reads
|
||||
and writes kaspa messages using a generic interface named Message. In
|
||||
@ -89,7 +89,7 @@ switch or type assertion. An example of a type switch follows:
|
||||
fmt.Printf("Number of tx in block: %d", msg.Header.TxnCount)
|
||||
}
|
||||
|
||||
Reading Messages
|
||||
# Reading Messages
|
||||
|
||||
In order to unmarshall kaspa messages from the appmessage, use the ReadMessage
|
||||
function. It accepts any io.Reader, but typically this will be a net.Conn to
|
||||
@ -104,7 +104,7 @@ a remote node running a kaspa peer. Example syntax is:
|
||||
// Log and handle the error
|
||||
}
|
||||
|
||||
Writing Messages
|
||||
# Writing Messages
|
||||
|
||||
In order to marshall kaspa messages to the appmessage, use the WriteMessage
|
||||
function. It accepts any io.Writer, but typically this will be a net.Conn to
|
||||
@ -122,7 +122,7 @@ from a remote peer is:
|
||||
// Log and handle the error
|
||||
}
|
||||
|
||||
Errors
|
||||
# Errors
|
||||
|
||||
Errors returned by this package are either the raw errors provided by underlying
|
||||
calls to read/write from streams such as io.EOF, io.ErrUnexpectedEOF, and
|
||||
|
@ -2,9 +2,10 @@ package appmessage
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"github.com/pkg/errors"
|
||||
"math/big"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/kaspanet/kaspad/domain/consensus/utils/blockheader"
|
||||
"github.com/kaspanet/kaspad/domain/consensus/utils/hashes"
|
||||
"github.com/kaspanet/kaspad/domain/consensus/utils/utxo"
|
||||
@ -213,13 +214,14 @@ func RPCTransactionToDomainTransaction(rpcTransaction *RPCTransaction) (*externa
|
||||
}
|
||||
|
||||
return &externalapi.DomainTransaction{
|
||||
Version: rpcTransaction.Version,
|
||||
Inputs: inputs,
|
||||
Outputs: outputs,
|
||||
LockTime: rpcTransaction.LockTime,
|
||||
SubnetworkID: *subnetworkID,
|
||||
Gas: rpcTransaction.LockTime,
|
||||
Payload: payload,
|
||||
Version: rpcTransaction.Version,
|
||||
Inputs: inputs,
|
||||
Outputs: outputs,
|
||||
LockTime: rpcTransaction.LockTime,
|
||||
SubnetworkID: *subnetworkID,
|
||||
Gas: rpcTransaction.Gas,
|
||||
MassCommitment: rpcTransaction.Mass,
|
||||
Payload: payload,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@ -286,7 +288,8 @@ func DomainTransactionToRPCTransaction(transaction *externalapi.DomainTransactio
|
||||
Outputs: outputs,
|
||||
LockTime: transaction.LockTime,
|
||||
SubnetworkID: subnetworkID,
|
||||
Gas: transaction.LockTime,
|
||||
Gas: transaction.Gas,
|
||||
Mass: transaction.MassCommitment,
|
||||
Payload: payload,
|
||||
}
|
||||
}
|
||||
|
@ -163,6 +163,10 @@ const (
|
||||
CmdGetMempoolEntriesByAddressesResponseMessage
|
||||
CmdGetCoinSupplyRequestMessage
|
||||
CmdGetCoinSupplyResponseMessage
|
||||
CmdGetFeeEstimateRequestMessage
|
||||
CmdGetFeeEstimateResponseMessage
|
||||
CmdSubmitTransactionReplacementRequestMessage
|
||||
CmdSubmitTransactionReplacementResponseMessage
|
||||
)
|
||||
|
||||
// ProtocolMessageCommandToString maps all MessageCommands to their string representation
|
||||
@ -300,6 +304,10 @@ var RPCMessageCommandToString = map[MessageCommand]string{
|
||||
CmdGetMempoolEntriesByAddressesResponseMessage: "GetMempoolEntriesByAddressesResponse",
|
||||
CmdGetCoinSupplyRequestMessage: "GetCoinSupplyRequest",
|
||||
CmdGetCoinSupplyResponseMessage: "GetCoinSupplyResponse",
|
||||
CmdGetFeeEstimateRequestMessage: "GetFeeEstimateRequest",
|
||||
CmdGetFeeEstimateResponseMessage: "GetFeeEstimateResponse",
|
||||
CmdSubmitTransactionReplacementRequestMessage: "SubmitTransactionReplacementRequest",
|
||||
CmdSubmitTransactionReplacementResponseMessage: "SubmitTransactionReplacementResponse",
|
||||
}
|
||||
|
||||
// Message is an interface that describes a kaspa message. A type that
|
||||
|
@ -132,7 +132,7 @@ func TestConvertToPartial(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
//blockOne is the first block in the mainnet block DAG.
|
||||
// blockOne is the first block in the mainnet block DAG.
|
||||
var blockOne = MsgBlock{
|
||||
Header: MsgBlockHeader{
|
||||
Version: 0,
|
||||
|
47
app/appmessage/rpc_fee_estimate.go
Normal file
47
app/appmessage/rpc_fee_estimate.go
Normal file
@ -0,0 +1,47 @@
|
||||
package appmessage
|
||||
|
||||
// GetFeeEstimateRequestMessage is an appmessage corresponding to
|
||||
// its respective RPC message
|
||||
type GetFeeEstimateRequestMessage struct {
|
||||
baseMessage
|
||||
}
|
||||
|
||||
// Command returns the protocol command string for the message
|
||||
func (msg *GetFeeEstimateRequestMessage) Command() MessageCommand {
|
||||
return CmdGetFeeEstimateRequestMessage
|
||||
}
|
||||
|
||||
// NewGetFeeEstimateRequestMessage returns a instance of the message
|
||||
func NewGetFeeEstimateRequestMessage() *GetFeeEstimateRequestMessage {
|
||||
return &GetFeeEstimateRequestMessage{}
|
||||
}
|
||||
|
||||
type RPCFeeRateBucket struct {
|
||||
Feerate float64
|
||||
EstimatedSeconds float64
|
||||
}
|
||||
|
||||
type RPCFeeEstimate struct {
|
||||
PriorityBucket RPCFeeRateBucket
|
||||
NormalBuckets []RPCFeeRateBucket
|
||||
LowBuckets []RPCFeeRateBucket
|
||||
}
|
||||
|
||||
// GetCoinSupplyResponseMessage is an appmessage corresponding to
|
||||
// its respective RPC message
|
||||
type GetFeeEstimateResponseMessage struct {
|
||||
baseMessage
|
||||
Estimate RPCFeeEstimate
|
||||
|
||||
Error *RPCError
|
||||
}
|
||||
|
||||
// Command returns the protocol command string for the message
|
||||
func (msg *GetFeeEstimateResponseMessage) Command() MessageCommand {
|
||||
return CmdGetFeeEstimateResponseMessage
|
||||
}
|
||||
|
||||
// NewGetFeeEstimateResponseMessage returns a instance of the message
|
||||
func NewGetFeeEstimateResponseMessage() *GetFeeEstimateResponseMessage {
|
||||
return &GetFeeEstimateResponseMessage{}
|
||||
}
|
@ -52,6 +52,7 @@ type RPCTransaction struct {
|
||||
SubnetworkID string
|
||||
Gas uint64
|
||||
Payload string
|
||||
Mass uint64
|
||||
VerboseData *RPCTransactionVerboseData
|
||||
}
|
||||
|
||||
|
42
app/appmessage/rpc_submit_transaction_replacement.go
Normal file
42
app/appmessage/rpc_submit_transaction_replacement.go
Normal file
@ -0,0 +1,42 @@
|
||||
package appmessage
|
||||
|
||||
// SubmitTransactionReplacementRequestMessage is an appmessage corresponding to
|
||||
// its respective RPC message
|
||||
type SubmitTransactionReplacementRequestMessage struct {
|
||||
baseMessage
|
||||
Transaction *RPCTransaction
|
||||
}
|
||||
|
||||
// Command returns the protocol command string for the message
|
||||
func (msg *SubmitTransactionReplacementRequestMessage) Command() MessageCommand {
|
||||
return CmdSubmitTransactionReplacementRequestMessage
|
||||
}
|
||||
|
||||
// NewSubmitTransactionReplacementRequestMessage returns a instance of the message
|
||||
func NewSubmitTransactionReplacementRequestMessage(transaction *RPCTransaction) *SubmitTransactionReplacementRequestMessage {
|
||||
return &SubmitTransactionReplacementRequestMessage{
|
||||
Transaction: transaction,
|
||||
}
|
||||
}
|
||||
|
||||
// SubmitTransactionReplacementResponseMessage is an appmessage corresponding to
|
||||
// its respective RPC message
|
||||
type SubmitTransactionReplacementResponseMessage struct {
|
||||
baseMessage
|
||||
TransactionID string
|
||||
ReplacedTransaction *RPCTransaction
|
||||
|
||||
Error *RPCError
|
||||
}
|
||||
|
||||
// Command returns the protocol command string for the message
|
||||
func (msg *SubmitTransactionReplacementResponseMessage) Command() MessageCommand {
|
||||
return CmdSubmitTransactionReplacementResponseMessage
|
||||
}
|
||||
|
||||
// NewSubmitTransactionReplacementResponseMessage returns a instance of the message
|
||||
func NewSubmitTransactionReplacementResponseMessage(transactionID string) *SubmitTransactionReplacementResponseMessage {
|
||||
return &SubmitTransactionReplacementResponseMessage{
|
||||
TransactionID: transactionID,
|
||||
}
|
||||
}
|
@ -175,6 +175,11 @@ func (flow *handleIBDFlow) negotiateMissingSyncerChainSegment() (*externalapi.Do
|
||||
chainNegotiationRestartCounter := 0
|
||||
chainNegotiationZoomCounts := 0
|
||||
initialLocatorLen := len(locatorHashes)
|
||||
pruningPoint, err := flow.Domain().Consensus().PruningPoint()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
for {
|
||||
var lowestUnknownSyncerChainHash, currentHighestKnownSyncerChainHash *externalapi.DomainHash
|
||||
for _, syncerChainHash := range locatorHashes {
|
||||
@ -187,8 +192,21 @@ func (flow *handleIBDFlow) negotiateMissingSyncerChainSegment() (*externalapi.Do
|
||||
return nil, nil, protocolerrors.Errorf(true, "Sent invalid chain block %s", syncerChainHash)
|
||||
}
|
||||
|
||||
currentHighestKnownSyncerChainHash = syncerChainHash
|
||||
break
|
||||
isPruningPointOnSyncerChain, err := flow.Domain().Consensus().IsInSelectedParentChainOf(pruningPoint, syncerChainHash)
|
||||
if err != nil {
|
||||
log.Errorf("Error checking isPruningPointOnSyncerChain: %s", err)
|
||||
}
|
||||
|
||||
// We're only interested in syncer chain blocks that have our pruning
|
||||
// point in their selected chain. Otherwise, it means one of the following:
|
||||
// 1) We will not switch the virtual selected chain to the syncers chain since it will violate finality
|
||||
// (hence we can ignore it unless merged by others).
|
||||
// 2) syncerChainHash is actually in the past of our pruning point so there's no
|
||||
// point in syncing from it.
|
||||
if err == nil && isPruningPointOnSyncerChain {
|
||||
currentHighestKnownSyncerChainHash = syncerChainHash
|
||||
break
|
||||
}
|
||||
}
|
||||
lowestUnknownSyncerChainHash = syncerChainHash
|
||||
}
|
||||
@ -285,7 +303,11 @@ func (flow *handleIBDFlow) isGenesisVirtualSelectedParent() (bool, error) {
|
||||
func (flow *handleIBDFlow) logIBDFinished(isFinishedSuccessfully bool, err error) {
|
||||
successString := "successfully"
|
||||
if !isFinishedSuccessfully {
|
||||
successString = fmt.Sprintf("(interrupted: %s)", err)
|
||||
if err != nil {
|
||||
successString = fmt.Sprintf("(interrupted: %s)", err)
|
||||
} else {
|
||||
successString = fmt.Sprintf("(interrupted)")
|
||||
}
|
||||
}
|
||||
log.Infof("IBD with peer %s finished %s", flow.peer, successString)
|
||||
}
|
||||
|
@ -85,7 +85,12 @@ func (flow *handleIBDFlow) shouldSyncAndShouldDownloadHeadersProof(
|
||||
return true, true, nil
|
||||
}
|
||||
|
||||
return false, false, nil
|
||||
if highestKnownSyncerChainHash == nil {
|
||||
log.Infof("Stopping IBD since IBD from this node will cause a finality conflict")
|
||||
return false, false, nil
|
||||
}
|
||||
|
||||
return false, true, nil
|
||||
}
|
||||
|
||||
return false, true, nil
|
||||
|
@ -81,10 +81,6 @@ func (ctx *Context) PopulateBlockWithVerboseData(block *appmessage.RPCBlock, dom
|
||||
block.VerboseData.SelectedParentHash = blockInfo.SelectedParent.String()
|
||||
}
|
||||
|
||||
if blockInfo.BlockStatus == externalapi.StatusHeaderOnly {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get the block if we didn't receive it previously
|
||||
if domainBlock == nil {
|
||||
domainBlock, err = ctx.Domain.Consensus().GetBlockEvenIfHeaderOnly(blockHash)
|
||||
@ -93,6 +89,10 @@ func (ctx *Context) PopulateBlockWithVerboseData(block *appmessage.RPCBlock, dom
|
||||
}
|
||||
}
|
||||
|
||||
if len(domainBlock.Transactions) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
transactionIDs := make([]string, len(domainBlock.Transactions))
|
||||
for i, transaction := range domainBlock.Transactions {
|
||||
transactionIDs[i] = consensushashing.TransactionID(transaction).String()
|
||||
|
@ -28,7 +28,8 @@ func HandleSubmitTransaction(context *rpccontext.Context, _ *router.Router, requ
|
||||
}
|
||||
|
||||
log.Debugf("Rejected transaction %s: %s", transactionID, err)
|
||||
errorMessage := &appmessage.SubmitTransactionResponseMessage{}
|
||||
// Return the ID also in the case of error, so that clients can match the response to the correct transaction submit request
|
||||
errorMessage := appmessage.NewSubmitTransactionResponseMessage(transactionID.String())
|
||||
errorMessage.Error = appmessage.RPCErrorf("Rejected transaction %s: %s", transactionID, err)
|
||||
return errorMessage, nil
|
||||
}
|
||||
|
@ -5,13 +5,10 @@ FLAGS=$@
|
||||
go version
|
||||
|
||||
go get $FLAGS -t -d ./...
|
||||
GO111MODULE=off go get $FLAGS golang.org/x/lint/golint
|
||||
go install $FLAGS honnef.co/go/tools/cmd/staticcheck@latest
|
||||
|
||||
test -z "$(go fmt ./...)"
|
||||
|
||||
golint -set_exit_status ./...
|
||||
|
||||
staticcheck -checks SA4006,SA4008,SA4009,SA4010,SA5003,SA1004,SA1014,SA1021,SA1023,SA1024,SA1025,SA1026,SA1027,SA1028,SA2000,SA2001,SA2003,SA4000,SA4001,SA4003,SA4004,SA4011,SA4012,SA4013,SA4014,SA4015,SA4016,SA4017,SA4018,SA4019,SA4020,SA4021,SA4022,SA4023,SA5000,SA5002,SA5004,SA5005,SA5007,SA5008,SA5009,SA5010,SA5011,SA5012,SA6001,SA6002,SA9001,SA9002,SA9003,SA9004,SA9005,SA9006,ST1019 ./...
|
||||
|
||||
go build $FLAGS -o kaspad .
|
||||
|
@ -1,3 +1,66 @@
|
||||
Kaspad v0.12.17 - 2024-02-19
|
||||
===========================
|
||||
|
||||
* Wallet-related improvements and fixes (#2253, #2257, #2258, #2262)
|
||||
|
||||
Kaspad v0.12.16 - 2023-12-25
|
||||
===========================
|
||||
|
||||
* Adapt wallet UTXO selection to dust patch (#2254)
|
||||
|
||||
Kaspad v0.12.15 - 2023-12-16
|
||||
===========================
|
||||
|
||||
* Update ECDSA address test to use a valid public key (#2202)
|
||||
* Fix off by small amounts in sent amount kaspa (#2220)
|
||||
* Use removeRedeemers correctly by (#2235)
|
||||
* Fix windows asset building by increasing go version (#2245)
|
||||
* Added a mainnet dnsseeder (#2247)
|
||||
* Fix extract atomic swap data pushes (#2203)
|
||||
* Adapt kaspawallet to support testnet 11 (#2211)
|
||||
* Fix type detection in RemoveInvalidTransactions (#2252)
|
||||
|
||||
Kaspad v0.12.14 - 2023-09-26
|
||||
===========================
|
||||
|
||||
* Anti-spam measurements against dust attack (#2223)
|
||||
|
||||
Kaspad v0.12.13 - 2023-03-06
|
||||
===========================
|
||||
|
||||
* Bump golang.org/x/crypto from 0.0.0-20210513164829-c07d793c2f9a to 0.1.0 (#2195)
|
||||
* Bump golang.org/x/net from 0.0.0-20210405180319-a5a99cb37ef4 to 0.7.0 (#2194)
|
||||
* Avoid sending transactions with no funds (#2193)
|
||||
|
||||
Kaspad v0.12.12 - 2023-03-06
|
||||
===========================
|
||||
|
||||
* Rename last references to blockheight (#2089)
|
||||
* Add code of conduct (#2183)
|
||||
* Extend TestGetPreciseSigOps with more tests (#2188)
|
||||
* Add Dockerfile to kaspawallet (#2187)
|
||||
* Add `--send-all` to `kaspawallet send` command (#2181)
|
||||
* Bump golang.org/x/text from 0.3.5 to 0.3.8 (#2190)
|
||||
* Upgrade to go 1.19 (#2191)
|
||||
|
||||
Kaspad v0.12.11 - 2022-12-1
|
||||
===========================
|
||||
|
||||
* Fix IBD sync conditions (#2174)
|
||||
|
||||
Kaspad v0.12.10 - 2022-11-23
|
||||
===========================
|
||||
|
||||
* Increase devnet's initial difficulty (#2167)
|
||||
|
||||
Bug fixes:
|
||||
* Check rule errors when validating blocks with trusted data (#2171)
|
||||
* Compare blue score with selected tip when checking if a pruning point proof is needed (#2169)
|
||||
* Add found to GetBlock (#2165)
|
||||
|
||||
Wallet new features:
|
||||
* Use one of the From addresses as a change address (#2164)
|
||||
|
||||
Kaspad v0.12.9 - 2022-10-23
|
||||
===========================
|
||||
|
||||
|
@ -4,7 +4,7 @@ kaspactl is an RPC client for kaspad
|
||||
|
||||
## Requirements
|
||||
|
||||
Go 1.18 or later.
|
||||
Go 1.23 or later.
|
||||
|
||||
## Installation
|
||||
|
||||
@ -50,4 +50,4 @@ For example:
|
||||
$ kaspactl '{"getBlockDagInfoRequest":{}}'
|
||||
```
|
||||
|
||||
For a list of all available requests check out the [RPC documentation](infrastructure/network/netadapter/server/grpcserver/protowire/rpc.md)
|
||||
For a list of all available requests check out the [RPC documentation](infrastructure/network/netadapter/server/grpcserver/protowire/rpc.md)
|
||||
|
@ -1,5 +1,5 @@
|
||||
# -- multistage docker build: stage #1: build stage
|
||||
FROM golang:1.18-alpine AS build
|
||||
FROM golang:1.23-alpine AS build
|
||||
|
||||
RUN mkdir -p /go/src/github.com/kaspanet/kaspad
|
||||
|
||||
|
@ -4,7 +4,7 @@ Kaspaminer is a CPU-based miner for kaspad
|
||||
|
||||
## Requirements
|
||||
|
||||
Go 1.18 or later.
|
||||
Go 1.23 or later.
|
||||
|
||||
## Installation
|
||||
|
||||
@ -30,7 +30,7 @@ $ go install .
|
||||
- Kapaminer should now be installed in `$(go env GOPATH)/bin`. If you did
|
||||
not already add the bin directory to your system path during Go installation,
|
||||
you are encouraged to do so now.
|
||||
|
||||
|
||||
## Usage
|
||||
|
||||
The full kaspaminer configuration options can be seen with:
|
||||
@ -40,6 +40,7 @@ $ kaspaminer --help
|
||||
```
|
||||
|
||||
But the minimum configuration needed to run it is:
|
||||
|
||||
```bash
|
||||
$ kaspaminer --miningaddr=<YOUR_MINING_ADDRESS>
|
||||
```
|
||||
```
|
||||
|
@ -1,5 +1,5 @@
|
||||
# -- multistage docker build: stage #1: build stage
|
||||
FROM golang:1.18-alpine AS build
|
||||
FROM golang:1.23-alpine AS build
|
||||
|
||||
RUN mkdir -p /go/src/github.com/kaspanet/kaspad
|
||||
|
||||
|
@ -8,6 +8,7 @@ import (
|
||||
|
||||
"github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/client"
|
||||
"github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/pb"
|
||||
"github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/server"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
@ -37,7 +38,7 @@ func broadcast(conf *broadcastConfig) error {
|
||||
transactionsHex = strings.TrimSpace(string(transactionHexBytes))
|
||||
}
|
||||
|
||||
transactions, err := decodeTransactionsFromHex(transactionsHex)
|
||||
transactions, err := server.DecodeTransactionsFromHex(transactionsHex)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
57
cmd/kaspawallet/broadcast_replacement.go
Normal file
57
cmd/kaspawallet/broadcast_replacement.go
Normal file
@ -0,0 +1,57 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
|
||||
"github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/client"
|
||||
"github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/pb"
|
||||
"github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/server"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func broadcastReplacement(conf *broadcastConfig) error {
|
||||
daemonClient, tearDown, err := client.Connect(conf.DaemonAddress)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tearDown()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), daemonTimeout)
|
||||
defer cancel()
|
||||
|
||||
if conf.Transactions == "" && conf.TransactionsFile == "" {
|
||||
return errors.Errorf("Either --transaction or --transaction-file is required")
|
||||
}
|
||||
if conf.Transactions != "" && conf.TransactionsFile != "" {
|
||||
return errors.Errorf("Both --transaction and --transaction-file cannot be passed at the same time")
|
||||
}
|
||||
|
||||
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.TransactionsFile)
|
||||
}
|
||||
transactionsHex = strings.TrimSpace(string(transactionHexBytes))
|
||||
}
|
||||
|
||||
transactions, err := server.DecodeTransactionsFromHex(transactionsHex)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
response, err := daemonClient.BroadcastReplacement(ctx, &pb.BroadcastRequest{Transactions: transactions})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println("Transactions were sent successfully")
|
||||
fmt.Println("Transaction ID(s): ")
|
||||
for _, txID := range response.TxIDs {
|
||||
fmt.Printf("\t%s\n", txID)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
117
cmd/kaspawallet/bump_fee.go
Normal file
117
cmd/kaspawallet/bump_fee.go
Normal file
@ -0,0 +1,117 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/client"
|
||||
"github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/pb"
|
||||
"github.com/kaspanet/kaspad/cmd/kaspawallet/keys"
|
||||
"github.com/kaspanet/kaspad/cmd/kaspawallet/libkaspawallet"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func bumpFee(conf *bumpFeeConfig) error {
|
||||
keysFile, err := keys.ReadKeysFile(conf.NetParams(), conf.KeysFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(keysFile.ExtendedPublicKeys) > len(keysFile.EncryptedMnemonics) {
|
||||
return errors.Errorf("Cannot use 'bump-fee' command for multisig wallet without all of the keys")
|
||||
}
|
||||
|
||||
daemonClient, tearDown, err := client.Connect(conf.DaemonAddress)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tearDown()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), daemonTimeout)
|
||||
defer cancel()
|
||||
|
||||
var feePolicy *pb.FeePolicy
|
||||
if conf.FeeRate > 0 {
|
||||
feePolicy = &pb.FeePolicy{
|
||||
FeePolicy: &pb.FeePolicy_ExactFeeRate{
|
||||
ExactFeeRate: conf.FeeRate,
|
||||
},
|
||||
}
|
||||
} else if conf.MaxFeeRate > 0 {
|
||||
feePolicy = &pb.FeePolicy{
|
||||
FeePolicy: &pb.FeePolicy_MaxFeeRate{MaxFeeRate: conf.MaxFeeRate},
|
||||
}
|
||||
} else if conf.MaxFee > 0 {
|
||||
feePolicy = &pb.FeePolicy{
|
||||
FeePolicy: &pb.FeePolicy_MaxFee{MaxFee: conf.MaxFee},
|
||||
}
|
||||
}
|
||||
|
||||
createUnsignedTransactionsResponse, err :=
|
||||
daemonClient.BumpFee(ctx, &pb.BumpFeeRequest{
|
||||
TxID: conf.TxID,
|
||||
From: conf.FromAddresses,
|
||||
UseExistingChangeAddress: conf.UseExistingChangeAddress,
|
||||
FeePolicy: feePolicy,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(conf.Password) == 0 {
|
||||
conf.Password = keys.GetPassword("Password:")
|
||||
}
|
||||
mnemonics, err := keysFile.DecryptMnemonics(conf.Password)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "message authentication failed") {
|
||||
fmt.Fprintf(os.Stderr, "Password decryption failed. Sometimes this is a result of not "+
|
||||
"specifying the same keys file used by the wallet daemon process.\n")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
signedTransactions := make([][]byte, len(createUnsignedTransactionsResponse.Transactions))
|
||||
for i, unsignedTransaction := range createUnsignedTransactionsResponse.Transactions {
|
||||
signedTransaction, err := libkaspawallet.Sign(conf.NetParams(), mnemonics, unsignedTransaction, keysFile.ECDSA)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
signedTransactions[i] = signedTransaction
|
||||
}
|
||||
|
||||
fmt.Printf("Broadcasting %d transaction(s)\n", len(signedTransactions))
|
||||
// Since we waited for user input when getting the password, which could take unbound amount of time -
|
||||
// create a new context for broadcast, to reset the timeout.
|
||||
broadcastCtx, broadcastCancel := context.WithTimeout(context.Background(), daemonTimeout)
|
||||
defer broadcastCancel()
|
||||
|
||||
const chunkSize = 100 // To avoid sending a message bigger than the gRPC max message size, we split it to chunks
|
||||
for offset := 0; offset < len(signedTransactions); offset += chunkSize {
|
||||
end := len(signedTransactions)
|
||||
if offset+chunkSize <= len(signedTransactions) {
|
||||
end = offset + chunkSize
|
||||
}
|
||||
|
||||
chunk := signedTransactions[offset:end]
|
||||
response, err := daemonClient.BroadcastReplacement(broadcastCtx, &pb.BroadcastRequest{Transactions: chunk})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Printf("Broadcasted %d transaction(s) (broadcasted %.2f%% of the transactions so far)\n", len(chunk), 100*float64(end)/float64(len(signedTransactions)))
|
||||
fmt.Println("Broadcasted Transaction ID(s): ")
|
||||
for _, txID := range response.TxIDs {
|
||||
fmt.Printf("\t%s\n", txID)
|
||||
}
|
||||
}
|
||||
|
||||
if conf.Verbose {
|
||||
fmt.Println("Serialized Transaction(s) (can be parsed via the `parse` command or resent via `broadcast`): ")
|
||||
for _, signedTx := range signedTransactions {
|
||||
fmt.Printf("\t%x\n\n", signedTx)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
58
cmd/kaspawallet/bump_fee_unsigned.go
Normal file
58
cmd/kaspawallet/bump_fee_unsigned.go
Normal file
@ -0,0 +1,58 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/client"
|
||||
"github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/pb"
|
||||
"github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/server"
|
||||
)
|
||||
|
||||
func bumpFeeUnsigned(conf *bumpFeeUnsignedConfig) error {
|
||||
daemonClient, tearDown, err := client.Connect(conf.DaemonAddress)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tearDown()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), daemonTimeout)
|
||||
defer cancel()
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var feePolicy *pb.FeePolicy
|
||||
if conf.FeeRate > 0 {
|
||||
feePolicy = &pb.FeePolicy{
|
||||
FeePolicy: &pb.FeePolicy_ExactFeeRate{
|
||||
ExactFeeRate: conf.FeeRate,
|
||||
},
|
||||
}
|
||||
} else if conf.MaxFeeRate > 0 {
|
||||
feePolicy = &pb.FeePolicy{
|
||||
FeePolicy: &pb.FeePolicy_MaxFeeRate{MaxFeeRate: conf.MaxFeeRate},
|
||||
}
|
||||
} else if conf.MaxFee > 0 {
|
||||
feePolicy = &pb.FeePolicy{
|
||||
FeePolicy: &pb.FeePolicy_MaxFee{MaxFee: conf.MaxFee},
|
||||
}
|
||||
}
|
||||
|
||||
response, err := daemonClient.BumpFee(ctx, &pb.BumpFeeRequest{
|
||||
TxID: conf.TxID,
|
||||
From: conf.FromAddresses,
|
||||
UseExistingChangeAddress: conf.UseExistingChangeAddress,
|
||||
FeePolicy: feePolicy,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Fprintln(os.Stderr, "Created unsigned transaction")
|
||||
fmt.Println(server.EncodeTransactionsToHex(response.Transactions))
|
||||
|
||||
return nil
|
||||
}
|
@ -22,6 +22,11 @@ const (
|
||||
newAddressSubCmd = "new-address"
|
||||
dumpUnencryptedDataSubCmd = "dump-unencrypted-data"
|
||||
startDaemonSubCmd = "start-daemon"
|
||||
versionSubCmd = "version"
|
||||
getDaemonVersionSubCmd = "get-daemon-version"
|
||||
bumpFeeSubCmd = "bump-fee"
|
||||
bumpFeeUnsignedSubCmd = "bump-fee-unsigned"
|
||||
broadcastReplacementSubCmd = "broadcast-replacement"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -30,6 +35,7 @@ const (
|
||||
)
|
||||
|
||||
type configFlags struct {
|
||||
ShowVersion bool `short:"V" long:"version" description:"Display version information and exit"`
|
||||
config.NetworkFlags
|
||||
}
|
||||
|
||||
@ -56,9 +62,13 @@ type sendConfig struct {
|
||||
Password string `long:"password" short:"p" description:"Wallet password"`
|
||||
DaemonAddress string `long:"daemonaddress" short:"d" description:"Wallet daemon server to connect to"`
|
||||
ToAddress string `long:"to-address" short:"t" description:"The public address to send Kaspa to" required:"true"`
|
||||
FromAddresses []string `long:"from-address" short:"a" description:"Specific public address to send Kaspa from. Use multiple times to accept several addresses" required:"false"`
|
||||
SendAmount float64 `long:"send-amount" short:"v" description:"An amount to send in Kaspa (e.g. 1234.12345678)" required:"true"`
|
||||
FromAddresses []string `long:"from-address" short:"a" description:"Specific public address to send Kaspa from. Repeat multiple times (adding -a before each) to accept several addresses" required:"false"`
|
||||
SendAmount string `long:"send-amount" short:"v" description:"An amount to send in Kaspa (e.g. 1234.12345678)"`
|
||||
IsSendAll bool `long:"send-all" description:"Send all the Kaspa in the wallet (mutually exclusive with --send-amount). If --from-address was used, will send all only from the specified addresses."`
|
||||
UseExistingChangeAddress bool `long:"use-existing-change-address" short:"u" description:"Will use an existing change address (in case no change address was ever used, it will use a new one)"`
|
||||
MaxFeeRate float64 `long:"max-fee-rate" short:"m" description:"Maximum fee rate in Sompi/gram to use for the transaction. The wallet will take the minimum between the fee rate estimate from the connected node and this value."`
|
||||
FeeRate float64 `long:"fee-rate" short:"r" description:"Fee rate in Sompi/gram to use for the transaction. This option will override any fee estimate from the connected node."`
|
||||
MaxFee uint64 `long:"max-fee" short:"x" description:"Maximum fee in Sompi (not Sompi/gram) to use for the transaction. The wallet will take the minimum between the fee estimate from the connected node and this value. If no other fee policy is specified, it will set the max fee to 1 KAS"`
|
||||
Verbose bool `long:"show-serialized" short:"s" description:"Show a list of hex encoded sent transactions"`
|
||||
config.NetworkFlags
|
||||
}
|
||||
@ -73,8 +83,12 @@ type createUnsignedTransactionConfig struct {
|
||||
DaemonAddress string `long:"daemonaddress" short:"d" description:"Wallet daemon server to connect to"`
|
||||
ToAddress string `long:"to-address" short:"t" description:"The public address to send Kaspa to" required:"true"`
|
||||
FromAddresses []string `long:"from-address" short:"a" description:"Specific public address to send Kaspa from. Use multiple times to accept several addresses" required:"false"`
|
||||
SendAmount float64 `long:"send-amount" short:"v" description:"An amount to send in Kaspa (e.g. 1234.12345678)" required:"true"`
|
||||
SendAmount string `long:"send-amount" short:"v" description:"An amount to send in Kaspa (e.g. 1234.12345678)"`
|
||||
IsSendAll bool `long:"send-all" description:"Send all the Kaspa in the wallet (mutually exclusive with --send-amount)"`
|
||||
UseExistingChangeAddress bool `long:"use-existing-change-address" short:"u" description:"Will use an existing change address (in case no change address was ever used, it will use a new one)"`
|
||||
MaxFeeRate float64 `long:"max-fee-rate" short:"m" description:"Maximum fee rate in Sompi/gram to use for the transaction. The wallet will take the minimum between the fee rate estimate from the connected node and this value."`
|
||||
FeeRate float64 `long:"fee-rate" short:"r" description:"Fee rate in Sompi/gram to use for the transaction. This option will override any fee estimate from the connected node."`
|
||||
MaxFee uint64 `long:"max-fee" short:"x" description:"Maximum fee in Sompi (not Sompi/gram) to use for the transaction. The wallet will take the minimum between the fee estimate from the connected node and this value. If no other fee policy is specified, it will set the max fee to 1 KAS"`
|
||||
config.NetworkFlags
|
||||
}
|
||||
|
||||
@ -94,6 +108,7 @@ type broadcastConfig struct {
|
||||
}
|
||||
|
||||
type parseConfig struct {
|
||||
KeysFile string `long:"keys-file" short:"f" description:"Keys file location (default: ~/.kaspawallet/keys.json (*nix), %USERPROFILE%\\AppData\\Local\\Kaspawallet\\key.json (Windows))"`
|
||||
Transaction string `long:"transaction" short:"t" description:"The transaction to parse (encoded in hex)"`
|
||||
TransactionFile string `long:"transaction-file" short:"F" description:"The file containing the transaction to parse (encoded in hex)"`
|
||||
Verbose bool `long:"verbose" short:"v" description:"Verbose: show transaction inputs"`
|
||||
@ -127,6 +142,38 @@ type dumpUnencryptedDataConfig struct {
|
||||
config.NetworkFlags
|
||||
}
|
||||
|
||||
type bumpFeeUnsignedConfig struct {
|
||||
TxID string `long:"txid" short:"i" description:"The transaction ID to bump the fee for"`
|
||||
DaemonAddress string `long:"daemonaddress" short:"d" description:"Wallet daemon server to connect to"`
|
||||
FromAddresses []string `long:"from-address" short:"a" description:"Specific public address to send Kaspa from. Use multiple times to accept several addresses" required:"false"`
|
||||
UseExistingChangeAddress bool `long:"use-existing-change-address" short:"u" description:"Will use an existing change address (in case no change address was ever used, it will use a new one)"`
|
||||
MaxFeeRate float64 `long:"max-fee-rate" short:"m" description:"Maximum fee rate in Sompi/gram to use for the transaction. The wallet will take the minimum between the fee rate estimate from the connected node and this value."`
|
||||
FeeRate float64 `long:"fee-rate" short:"r" description:"Fee rate in Sompi/gram to use for the transaction. This option will override any fee estimate from the connected node."`
|
||||
MaxFee uint64 `long:"max-fee" short:"x" description:"Maximum fee in Sompi (not Sompi/gram) to use for the transaction. The wallet will take the minimum between the fee estimate from the connected node and this value. If no other fee policy is specified, it will set the max fee to 1 KAS"`
|
||||
config.NetworkFlags
|
||||
}
|
||||
|
||||
type bumpFeeConfig struct {
|
||||
TxID string `long:"txid" short:"i" description:"The transaction ID to bump the fee for"`
|
||||
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"`
|
||||
DaemonAddress string `long:"daemonaddress" short:"d" description:"Wallet daemon server to connect to"`
|
||||
FromAddresses []string `long:"from-address" short:"a" description:"Specific public address to send Kaspa from. Repeat multiple times (adding -a before each) to accept several addresses" required:"false"`
|
||||
UseExistingChangeAddress bool `long:"use-existing-change-address" short:"u" description:"Will use an existing change address (in case no change address was ever used, it will use a new one)"`
|
||||
MaxFeeRate float64 `long:"max-fee-rate" short:"m" description:"Maximum fee rate in Sompi/gram to use for the transaction. The wallet will take the minimum between the fee rate estimate from the connected node and this value."`
|
||||
FeeRate float64 `long:"fee-rate" short:"r" description:"Fee rate in Sompi/gram to use for the transaction. This option will override any fee estimate from the connected node."`
|
||||
MaxFee uint64 `long:"max-fee" short:"x" description:"Maximum fee in Sompi (not Sompi/gram) to use for the transaction. The wallet will take the minimum between the fee estimate from the connected node and this value. If no other fee policy is specified, it will set the max fee to 1 KAS"`
|
||||
Verbose bool `long:"show-serialized" short:"s" description:"Show a list of hex encoded sent transactions"`
|
||||
config.NetworkFlags
|
||||
}
|
||||
|
||||
type versionConfig struct {
|
||||
}
|
||||
|
||||
type getDaemonVersionConfig struct {
|
||||
DaemonAddress string `long:"daemonaddress" short:"d" description:"Wallet daemon server to connect to"`
|
||||
}
|
||||
|
||||
func parseCommandLine() (subCommand string, config interface{}) {
|
||||
cfg := &configFlags{}
|
||||
parser := flags.NewParser(cfg, flags.PrintErrors|flags.HelpFlag)
|
||||
@ -183,6 +230,15 @@ func parseCommandLine() (subCommand string, config interface{}) {
|
||||
Listen: defaultListen,
|
||||
}
|
||||
parser.AddCommand(startDaemonSubCmd, "Start the wallet daemon", "Start the wallet daemon", startDaemonConf)
|
||||
parser.AddCommand(versionSubCmd, "Get the wallet version", "Get the wallet version", &versionConfig{})
|
||||
getDaemonVersionConf := &getDaemonVersionConfig{DaemonAddress: defaultListen}
|
||||
parser.AddCommand(getDaemonVersionSubCmd, "Get the wallet daemon version", "Get the wallet daemon version", getDaemonVersionConf)
|
||||
bumpFeeConf := &bumpFeeConfig{DaemonAddress: defaultListen}
|
||||
parser.AddCommand(bumpFeeSubCmd, "Bump transaction fee (with signing and broadcast)", "Bump transaction fee (with signing and broadcast)", bumpFeeConf)
|
||||
bumpFeeUnsignedConf := &bumpFeeUnsignedConfig{DaemonAddress: defaultListen}
|
||||
parser.AddCommand(bumpFeeUnsignedSubCmd, "Bump transaction fee (without signing)", "Bump transaction fee (without signing)", bumpFeeUnsignedConf)
|
||||
parser.AddCommand(broadcastReplacementSubCmd, "Broadcast the given transaction replacement",
|
||||
"Broadcast the given transaction replacement", broadcastConf)
|
||||
|
||||
_, err := parser.Parse()
|
||||
if err != nil {
|
||||
@ -216,6 +272,10 @@ func parseCommandLine() (subCommand string, config interface{}) {
|
||||
if err != nil {
|
||||
printErrorAndExit(err)
|
||||
}
|
||||
err = validateSendConfig(sendConf)
|
||||
if err != nil {
|
||||
printErrorAndExit(err)
|
||||
}
|
||||
config = sendConf
|
||||
case sweepSubCmd:
|
||||
combineNetworkFlags(&sweepConf.NetworkFlags, &cfg.NetworkFlags)
|
||||
@ -230,6 +290,10 @@ func parseCommandLine() (subCommand string, config interface{}) {
|
||||
if err != nil {
|
||||
printErrorAndExit(err)
|
||||
}
|
||||
err = validateCreateUnsignedTransactionConf(createUnsignedTransactionConf)
|
||||
if err != nil {
|
||||
printErrorAndExit(err)
|
||||
}
|
||||
config = createUnsignedTransactionConf
|
||||
case signSubCmd:
|
||||
combineNetworkFlags(&signConf.NetworkFlags, &cfg.NetworkFlags)
|
||||
@ -245,6 +309,13 @@ func parseCommandLine() (subCommand string, config interface{}) {
|
||||
printErrorAndExit(err)
|
||||
}
|
||||
config = broadcastConf
|
||||
case broadcastReplacementSubCmd:
|
||||
combineNetworkFlags(&broadcastConf.NetworkFlags, &cfg.NetworkFlags)
|
||||
err := broadcastConf.ResolveNetwork(parser)
|
||||
if err != nil {
|
||||
printErrorAndExit(err)
|
||||
}
|
||||
config = broadcastConf
|
||||
case parseSubCmd:
|
||||
combineNetworkFlags(&parseConf.NetworkFlags, &cfg.NetworkFlags)
|
||||
err := parseConf.ResolveNetwork(parser)
|
||||
@ -280,11 +351,123 @@ func parseCommandLine() (subCommand string, config interface{}) {
|
||||
printErrorAndExit(err)
|
||||
}
|
||||
config = startDaemonConf
|
||||
case versionSubCmd:
|
||||
case getDaemonVersionSubCmd:
|
||||
config = getDaemonVersionConf
|
||||
case bumpFeeSubCmd:
|
||||
combineNetworkFlags(&bumpFeeConf.NetworkFlags, &cfg.NetworkFlags)
|
||||
err := bumpFeeConf.ResolveNetwork(parser)
|
||||
if err != nil {
|
||||
printErrorAndExit(err)
|
||||
}
|
||||
|
||||
err = validateBumpFeeConfig(bumpFeeConf)
|
||||
if err != nil {
|
||||
printErrorAndExit(err)
|
||||
}
|
||||
|
||||
config = bumpFeeConf
|
||||
case bumpFeeUnsignedSubCmd:
|
||||
combineNetworkFlags(&bumpFeeUnsignedConf.NetworkFlags, &cfg.NetworkFlags)
|
||||
err := bumpFeeUnsignedConf.ResolveNetwork(parser)
|
||||
if err != nil {
|
||||
printErrorAndExit(err)
|
||||
}
|
||||
|
||||
err = validateBumpFeeUnsignedConfig(bumpFeeUnsignedConf)
|
||||
if err != nil {
|
||||
printErrorAndExit(err)
|
||||
}
|
||||
|
||||
config = bumpFeeUnsignedConf
|
||||
}
|
||||
|
||||
return parser.Command.Active.Name, config
|
||||
}
|
||||
|
||||
func validateCreateUnsignedTransactionConf(conf *createUnsignedTransactionConfig) error {
|
||||
if (!conf.IsSendAll && conf.SendAmount == "") ||
|
||||
(conf.IsSendAll && conf.SendAmount != "") {
|
||||
|
||||
return errors.New("exactly one of '--send-amount' or '--all' must be specified")
|
||||
}
|
||||
|
||||
if conf.MaxFeeRate < 0 {
|
||||
return errors.New("--max-fee-rate must be a positive number")
|
||||
}
|
||||
|
||||
if conf.FeeRate < 0 {
|
||||
return errors.New("--fee-rate must be a positive number")
|
||||
}
|
||||
|
||||
if boolToUint8(conf.MaxFeeRate > 0)+boolToUint8(conf.FeeRate > 0)+boolToUint8(conf.MaxFee > 0) > 1 {
|
||||
return errors.New("at most one of '--max-fee-rate', '--fee-rate' or '--max-fee' can be specified")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateSendConfig(conf *sendConfig) error {
|
||||
if (!conf.IsSendAll && conf.SendAmount == "") ||
|
||||
(conf.IsSendAll && conf.SendAmount != "") {
|
||||
|
||||
return errors.New("exactly one of '--send-amount' or '--all' must be specified")
|
||||
}
|
||||
|
||||
if conf.MaxFeeRate < 0 {
|
||||
return errors.New("--max-fee-rate must be a positive number")
|
||||
}
|
||||
|
||||
if conf.FeeRate < 0 {
|
||||
return errors.New("--fee-rate must be a positive number")
|
||||
}
|
||||
|
||||
if boolToUint8(conf.MaxFeeRate > 0)+boolToUint8(conf.FeeRate > 0)+boolToUint8(conf.MaxFee > 0) > 1 {
|
||||
return errors.New("at most one of '--max-fee-rate', '--fee-rate' or '--max-fee' can be specified")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateBumpFeeConfig(conf *bumpFeeConfig) error {
|
||||
if conf.MaxFeeRate < 0 {
|
||||
return errors.New("--max-fee-rate must be a positive number")
|
||||
}
|
||||
|
||||
if conf.FeeRate < 0 {
|
||||
return errors.New("--fee-rate must be a positive number")
|
||||
}
|
||||
|
||||
if boolToUint8(conf.MaxFeeRate > 0)+boolToUint8(conf.FeeRate > 0)+boolToUint8(conf.MaxFee > 0) > 1 {
|
||||
return errors.New("at most one of '--max-fee-rate', '--fee-rate' or '--max-fee' can be specified")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateBumpFeeUnsignedConfig(conf *bumpFeeUnsignedConfig) error {
|
||||
if conf.MaxFeeRate < 0 {
|
||||
return errors.New("--max-fee-rate must be a positive number")
|
||||
}
|
||||
|
||||
if conf.FeeRate < 0 {
|
||||
return errors.New("--fee-rate must be a positive number")
|
||||
}
|
||||
|
||||
if boolToUint8(conf.MaxFeeRate > 0)+boolToUint8(conf.FeeRate > 0)+boolToUint8(conf.MaxFee > 0) > 1 {
|
||||
return errors.New("at most one of '--max-fee-rate', '--fee-rate' or '--max-fee' can be specified")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func boolToUint8(b bool) uint8 {
|
||||
if b {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func combineNetworkFlags(dst, src *config.NetworkFlags) {
|
||||
dst.Testnet = dst.Testnet || src.Testnet
|
||||
dst.Simnet = dst.Simnet || src.Simnet
|
||||
|
@ -7,7 +7,8 @@ import (
|
||||
|
||||
"github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/client"
|
||||
"github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/pb"
|
||||
"github.com/kaspanet/kaspad/domain/consensus/utils/constants"
|
||||
"github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/server"
|
||||
"github.com/kaspanet/kaspad/cmd/kaspawallet/utils"
|
||||
)
|
||||
|
||||
func createUnsignedTransaction(conf *createUnsignedTransactionConfig) error {
|
||||
@ -20,19 +21,46 @@ func createUnsignedTransaction(conf *createUnsignedTransactionConfig) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), daemonTimeout)
|
||||
defer cancel()
|
||||
|
||||
sendAmountSompi := uint64(conf.SendAmount * constants.SompiPerKaspa)
|
||||
var sendAmountSompi uint64
|
||||
|
||||
if !conf.IsSendAll {
|
||||
sendAmountSompi, err = utils.KasToSompi(conf.SendAmount)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
var feePolicy *pb.FeePolicy
|
||||
if conf.FeeRate > 0 {
|
||||
feePolicy = &pb.FeePolicy{
|
||||
FeePolicy: &pb.FeePolicy_ExactFeeRate{
|
||||
ExactFeeRate: conf.FeeRate,
|
||||
},
|
||||
}
|
||||
} else if conf.MaxFeeRate > 0 {
|
||||
feePolicy = &pb.FeePolicy{
|
||||
FeePolicy: &pb.FeePolicy_MaxFeeRate{MaxFeeRate: conf.MaxFeeRate},
|
||||
}
|
||||
} else if conf.MaxFee > 0 {
|
||||
feePolicy = &pb.FeePolicy{
|
||||
FeePolicy: &pb.FeePolicy_MaxFee{MaxFee: conf.MaxFee},
|
||||
}
|
||||
}
|
||||
|
||||
response, err := daemonClient.CreateUnsignedTransactions(ctx, &pb.CreateUnsignedTransactionsRequest{
|
||||
From: conf.FromAddresses,
|
||||
Address: conf.ToAddress,
|
||||
Amount: sendAmountSompi,
|
||||
IsSendAll: conf.IsSendAll,
|
||||
UseExistingChangeAddress: conf.UseExistingChangeAddress,
|
||||
FeePolicy: feePolicy,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Fprintln(os.Stderr, "Created unsigned transaction")
|
||||
fmt.Println(encodeTransactionsToHex(response.UnsignedTransactions))
|
||||
fmt.Println(server.EncodeTransactionsToHex(response.UnsignedTransactions))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -2,9 +2,10 @@ package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/server"
|
||||
"time"
|
||||
|
||||
"github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/server"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/pb"
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -4,21 +4,28 @@ option go_package = "github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/pb";
|
||||
package kaspawalletd;
|
||||
|
||||
service kaspawalletd {
|
||||
rpc GetBalance (GetBalanceRequest) returns (GetBalanceResponse) {}
|
||||
rpc GetExternalSpendableUTXOs (GetExternalSpendableUTXOsRequest) returns (GetExternalSpendableUTXOsResponse) {}
|
||||
rpc CreateUnsignedTransactions (CreateUnsignedTransactionsRequest) returns (CreateUnsignedTransactionsResponse) {}
|
||||
rpc ShowAddresses (ShowAddressesRequest) returns (ShowAddressesResponse) {}
|
||||
rpc NewAddress (NewAddressRequest) returns (NewAddressResponse) {}
|
||||
rpc Shutdown (ShutdownRequest) returns (ShutdownResponse) {}
|
||||
rpc Broadcast (BroadcastRequest) returns (BroadcastResponse) {}
|
||||
// Since SendRequest contains a password - this command should only be used on a trusted or secure connection
|
||||
rpc GetBalance(GetBalanceRequest) returns (GetBalanceResponse) {}
|
||||
rpc GetExternalSpendableUTXOs(GetExternalSpendableUTXOsRequest)
|
||||
returns (GetExternalSpendableUTXOsResponse) {}
|
||||
rpc CreateUnsignedTransactions(CreateUnsignedTransactionsRequest)
|
||||
returns (CreateUnsignedTransactionsResponse) {}
|
||||
rpc ShowAddresses(ShowAddressesRequest) returns (ShowAddressesResponse) {}
|
||||
rpc NewAddress(NewAddressRequest) returns (NewAddressResponse) {}
|
||||
rpc Shutdown(ShutdownRequest) returns (ShutdownResponse) {}
|
||||
rpc Broadcast(BroadcastRequest) returns (BroadcastResponse) {}
|
||||
// BroadcastReplacement assumes that all transactions depend on the first one
|
||||
rpc BroadcastReplacement(BroadcastRequest) returns (BroadcastResponse) {}
|
||||
// Since SendRequest contains a password - this command should only be used on
|
||||
// a trusted or secure connection
|
||||
rpc Send(SendRequest) returns (SendResponse) {}
|
||||
// Since SignRequest contains a password - this command should only be used on a trusted or secure connection
|
||||
// Since SignRequest contains a password - this command should only be used on
|
||||
// a trusted or secure connection
|
||||
rpc Sign(SignRequest) returns (SignResponse) {}
|
||||
rpc GetVersion(GetVersionRequest) returns (GetVersionResponse) {}
|
||||
rpc BumpFee(BumpFeeRequest) returns (BumpFeeResponse) {}
|
||||
}
|
||||
|
||||
message GetBalanceRequest {
|
||||
}
|
||||
message GetBalanceRequest {}
|
||||
|
||||
message GetBalanceResponse {
|
||||
uint64 available = 1;
|
||||
@ -32,45 +39,45 @@ message AddressBalances {
|
||||
uint64 pending = 3;
|
||||
}
|
||||
|
||||
message FeePolicy {
|
||||
oneof feePolicy {
|
||||
double maxFeeRate = 6;
|
||||
double exactFeeRate = 7;
|
||||
uint64 maxFee = 8;
|
||||
}
|
||||
}
|
||||
|
||||
message CreateUnsignedTransactionsRequest {
|
||||
string address = 1;
|
||||
uint64 amount = 2;
|
||||
repeated string from = 3;
|
||||
bool useExistingChangeAddress = 4;
|
||||
bool isSendAll = 5;
|
||||
FeePolicy feePolicy = 6;
|
||||
}
|
||||
|
||||
message CreateUnsignedTransactionsResponse {
|
||||
repeated bytes unsignedTransactions = 1;
|
||||
}
|
||||
|
||||
message ShowAddressesRequest {
|
||||
}
|
||||
message ShowAddressesRequest {}
|
||||
|
||||
message ShowAddressesResponse {
|
||||
repeated string address = 1;
|
||||
}
|
||||
message ShowAddressesResponse { repeated string address = 1; }
|
||||
|
||||
message NewAddressRequest {
|
||||
}
|
||||
message NewAddressRequest {}
|
||||
|
||||
message NewAddressResponse {
|
||||
string address = 1;
|
||||
}
|
||||
message NewAddressResponse { string address = 1; }
|
||||
|
||||
message BroadcastRequest {
|
||||
bool isDomain = 1;
|
||||
repeated bytes transactions = 2;
|
||||
}
|
||||
|
||||
message BroadcastResponse {
|
||||
repeated string txIDs = 1;
|
||||
}
|
||||
message BroadcastResponse { repeated string txIDs = 1; }
|
||||
|
||||
message ShutdownRequest {
|
||||
}
|
||||
message ShutdownRequest {}
|
||||
|
||||
message ShutdownResponse {
|
||||
}
|
||||
message ShutdownResponse {}
|
||||
|
||||
message Outpoint {
|
||||
string transactionId = 1;
|
||||
@ -95,33 +102,50 @@ message UtxoEntry {
|
||||
bool isCoinbase = 4;
|
||||
}
|
||||
|
||||
message GetExternalSpendableUTXOsRequest{
|
||||
string address = 1;
|
||||
}
|
||||
message GetExternalSpendableUTXOsRequest { string address = 1; }
|
||||
|
||||
message GetExternalSpendableUTXOsResponse{
|
||||
message GetExternalSpendableUTXOsResponse {
|
||||
repeated UtxosByAddressesEntry Entries = 1;
|
||||
}
|
||||
// Since SendRequest contains a password - this command should only be used on a trusted or secure connection
|
||||
message SendRequest{
|
||||
// Since SendRequest contains a password - this command should only be used on a
|
||||
// trusted or secure connection
|
||||
message SendRequest {
|
||||
string toAddress = 1;
|
||||
uint64 amount = 2;
|
||||
string password = 3;
|
||||
repeated string from = 4;
|
||||
bool useExistingChangeAddress = 5;
|
||||
bool isSendAll = 6;
|
||||
FeePolicy feePolicy = 7;
|
||||
}
|
||||
|
||||
message SendResponse{
|
||||
message SendResponse {
|
||||
repeated string txIDs = 1;
|
||||
repeated bytes signedTransactions = 2;
|
||||
}
|
||||
|
||||
// Since SignRequest contains a password - this command should only be used on a trusted or secure connection
|
||||
message SignRequest{
|
||||
// Since SignRequest contains a password - this command should only be used on a
|
||||
// trusted or secure connection
|
||||
message SignRequest {
|
||||
repeated bytes unsignedTransactions = 1;
|
||||
string password = 2;
|
||||
}
|
||||
|
||||
message SignResponse{
|
||||
repeated bytes signedTransactions = 1;
|
||||
message SignResponse { repeated bytes signedTransactions = 1; }
|
||||
|
||||
message GetVersionRequest {}
|
||||
|
||||
message GetVersionResponse { string version = 1; }
|
||||
|
||||
message BumpFeeRequest {
|
||||
string password = 1;
|
||||
repeated string from = 2;
|
||||
bool useExistingChangeAddress = 3;
|
||||
FeePolicy feePolicy = 4;
|
||||
string txID = 5;
|
||||
}
|
||||
|
||||
message BumpFeeResponse {
|
||||
repeated bytes transactions = 1;
|
||||
repeated string txIDs = 2;
|
||||
}
|
||||
|
@ -29,10 +29,16 @@ type KaspawalletdClient interface {
|
||||
NewAddress(ctx context.Context, in *NewAddressRequest, opts ...grpc.CallOption) (*NewAddressResponse, error)
|
||||
Shutdown(ctx context.Context, in *ShutdownRequest, opts ...grpc.CallOption) (*ShutdownResponse, error)
|
||||
Broadcast(ctx context.Context, in *BroadcastRequest, opts ...grpc.CallOption) (*BroadcastResponse, error)
|
||||
// Since SendRequest contains a password - this command should only be used on a trusted or secure connection
|
||||
// BroadcastReplacement assumes that all transactions depend on the first one
|
||||
BroadcastReplacement(ctx context.Context, in *BroadcastRequest, opts ...grpc.CallOption) (*BroadcastResponse, error)
|
||||
// Since SendRequest contains a password - this command should only be used on
|
||||
// a trusted or secure connection
|
||||
Send(ctx context.Context, in *SendRequest, opts ...grpc.CallOption) (*SendResponse, error)
|
||||
// Since SignRequest contains a password - this command should only be used on a trusted or secure connection
|
||||
// Since SignRequest contains a password - this command should only be used on
|
||||
// a trusted or secure connection
|
||||
Sign(ctx context.Context, in *SignRequest, opts ...grpc.CallOption) (*SignResponse, error)
|
||||
GetVersion(ctx context.Context, in *GetVersionRequest, opts ...grpc.CallOption) (*GetVersionResponse, error)
|
||||
BumpFee(ctx context.Context, in *BumpFeeRequest, opts ...grpc.CallOption) (*BumpFeeResponse, error)
|
||||
}
|
||||
|
||||
type kaspawalletdClient struct {
|
||||
@ -106,6 +112,15 @@ func (c *kaspawalletdClient) Broadcast(ctx context.Context, in *BroadcastRequest
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *kaspawalletdClient) BroadcastReplacement(ctx context.Context, in *BroadcastRequest, opts ...grpc.CallOption) (*BroadcastResponse, error) {
|
||||
out := new(BroadcastResponse)
|
||||
err := c.cc.Invoke(ctx, "/kaspawalletd.kaspawalletd/BroadcastReplacement", in, out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *kaspawalletdClient) Send(ctx context.Context, in *SendRequest, opts ...grpc.CallOption) (*SendResponse, error) {
|
||||
out := new(SendResponse)
|
||||
err := c.cc.Invoke(ctx, "/kaspawalletd.kaspawalletd/Send", in, out, opts...)
|
||||
@ -124,6 +139,24 @@ func (c *kaspawalletdClient) Sign(ctx context.Context, in *SignRequest, opts ...
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *kaspawalletdClient) GetVersion(ctx context.Context, in *GetVersionRequest, opts ...grpc.CallOption) (*GetVersionResponse, error) {
|
||||
out := new(GetVersionResponse)
|
||||
err := c.cc.Invoke(ctx, "/kaspawalletd.kaspawalletd/GetVersion", in, out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *kaspawalletdClient) BumpFee(ctx context.Context, in *BumpFeeRequest, opts ...grpc.CallOption) (*BumpFeeResponse, error) {
|
||||
out := new(BumpFeeResponse)
|
||||
err := c.cc.Invoke(ctx, "/kaspawalletd.kaspawalletd/BumpFee", in, out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// KaspawalletdServer is the server API for Kaspawalletd service.
|
||||
// All implementations must embed UnimplementedKaspawalletdServer
|
||||
// for forward compatibility
|
||||
@ -135,10 +168,16 @@ type KaspawalletdServer interface {
|
||||
NewAddress(context.Context, *NewAddressRequest) (*NewAddressResponse, error)
|
||||
Shutdown(context.Context, *ShutdownRequest) (*ShutdownResponse, error)
|
||||
Broadcast(context.Context, *BroadcastRequest) (*BroadcastResponse, error)
|
||||
// Since SendRequest contains a password - this command should only be used on a trusted or secure connection
|
||||
// BroadcastReplacement assumes that all transactions depend on the first one
|
||||
BroadcastReplacement(context.Context, *BroadcastRequest) (*BroadcastResponse, error)
|
||||
// Since SendRequest contains a password - this command should only be used on
|
||||
// a trusted or secure connection
|
||||
Send(context.Context, *SendRequest) (*SendResponse, error)
|
||||
// Since SignRequest contains a password - this command should only be used on a trusted or secure connection
|
||||
// Since SignRequest contains a password - this command should only be used on
|
||||
// a trusted or secure connection
|
||||
Sign(context.Context, *SignRequest) (*SignResponse, error)
|
||||
GetVersion(context.Context, *GetVersionRequest) (*GetVersionResponse, error)
|
||||
BumpFee(context.Context, *BumpFeeRequest) (*BumpFeeResponse, error)
|
||||
mustEmbedUnimplementedKaspawalletdServer()
|
||||
}
|
||||
|
||||
@ -167,12 +206,21 @@ func (UnimplementedKaspawalletdServer) Shutdown(context.Context, *ShutdownReques
|
||||
func (UnimplementedKaspawalletdServer) Broadcast(context.Context, *BroadcastRequest) (*BroadcastResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method Broadcast not implemented")
|
||||
}
|
||||
func (UnimplementedKaspawalletdServer) BroadcastReplacement(context.Context, *BroadcastRequest) (*BroadcastResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method BroadcastReplacement not implemented")
|
||||
}
|
||||
func (UnimplementedKaspawalletdServer) Send(context.Context, *SendRequest) (*SendResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method Send not implemented")
|
||||
}
|
||||
func (UnimplementedKaspawalletdServer) Sign(context.Context, *SignRequest) (*SignResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method Sign not implemented")
|
||||
}
|
||||
func (UnimplementedKaspawalletdServer) GetVersion(context.Context, *GetVersionRequest) (*GetVersionResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetVersion not implemented")
|
||||
}
|
||||
func (UnimplementedKaspawalletdServer) BumpFee(context.Context, *BumpFeeRequest) (*BumpFeeResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method BumpFee not implemented")
|
||||
}
|
||||
func (UnimplementedKaspawalletdServer) mustEmbedUnimplementedKaspawalletdServer() {}
|
||||
|
||||
// UnsafeKaspawalletdServer may be embedded to opt out of forward compatibility for this service.
|
||||
@ -312,6 +360,24 @@ func _Kaspawalletd_Broadcast_Handler(srv interface{}, ctx context.Context, dec f
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _Kaspawalletd_BroadcastReplacement_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(BroadcastRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(KaspawalletdServer).BroadcastReplacement(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/kaspawalletd.kaspawalletd/BroadcastReplacement",
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(KaspawalletdServer).BroadcastReplacement(ctx, req.(*BroadcastRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _Kaspawalletd_Send_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(SendRequest)
|
||||
if err := dec(in); err != nil {
|
||||
@ -348,6 +414,42 @@ func _Kaspawalletd_Sign_Handler(srv interface{}, ctx context.Context, dec func(i
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _Kaspawalletd_GetVersion_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(GetVersionRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(KaspawalletdServer).GetVersion(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/kaspawalletd.kaspawalletd/GetVersion",
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(KaspawalletdServer).GetVersion(ctx, req.(*GetVersionRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _Kaspawalletd_BumpFee_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(BumpFeeRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(KaspawalletdServer).BumpFee(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/kaspawalletd.kaspawalletd/BumpFee",
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(KaspawalletdServer).BumpFee(ctx, req.(*BumpFeeRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
// Kaspawalletd_ServiceDesc is the grpc.ServiceDesc for Kaspawalletd service.
|
||||
// It's only intended for direct use with grpc.RegisterService,
|
||||
// and not to be introspected or modified (even as a copy)
|
||||
@ -383,6 +485,10 @@ var Kaspawalletd_ServiceDesc = grpc.ServiceDesc{
|
||||
MethodName: "Broadcast",
|
||||
Handler: _Kaspawalletd_Broadcast_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "BroadcastReplacement",
|
||||
Handler: _Kaspawalletd_BroadcastReplacement_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "Send",
|
||||
Handler: _Kaspawalletd_Send_Handler,
|
||||
@ -391,6 +497,14 @@ var Kaspawalletd_ServiceDesc = grpc.ServiceDesc{
|
||||
MethodName: "Sign",
|
||||
Handler: _Kaspawalletd_Sign_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "GetVersion",
|
||||
Handler: _Kaspawalletd_GetVersion_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "BumpFee",
|
||||
Handler: _Kaspawalletd_BumpFee_Handler,
|
||||
},
|
||||
},
|
||||
Streams: []grpc.StreamDesc{},
|
||||
Metadata: "kaspawalletd.proto",
|
||||
|
@ -2,6 +2,7 @@ package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/pb"
|
||||
"github.com/kaspanet/kaspad/cmd/kaspawallet/libkaspawallet"
|
||||
@ -14,13 +15,15 @@ func (s *server) GetBalance(_ context.Context, _ *pb.GetBalanceRequest) (*pb.Get
|
||||
s.lock.RLock()
|
||||
defer s.lock.RUnlock()
|
||||
|
||||
if !s.isSynced() {
|
||||
return nil, errors.Errorf("wallet daemon is not synced yet, %s", s.formatSyncStateReport())
|
||||
}
|
||||
|
||||
dagInfo, err := s.rpcClient.GetBlockDAGInfo()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
daaScore := dagInfo.VirtualDAAScore
|
||||
maturity := s.params.BlockCoinbaseMaturity
|
||||
|
||||
balancesMap := make(balancesMapType, 0)
|
||||
for _, entry := range s.utxosSortedByAmount {
|
||||
amount := entry.UTXOEntry.Amount()
|
||||
@ -30,7 +33,7 @@ func (s *server) GetBalance(_ context.Context, _ *pb.GetBalanceRequest) (*pb.Get
|
||||
balances = new(balancesType)
|
||||
balancesMap[address] = balances
|
||||
}
|
||||
if isUTXOSpendable(entry, daaScore, maturity) {
|
||||
if s.isUTXOSpendable(entry, daaScore) {
|
||||
balances.available += amount
|
||||
} else {
|
||||
balances.pending += amount
|
||||
@ -55,6 +58,8 @@ func (s *server) GetBalance(_ context.Context, _ *pb.GetBalanceRequest) (*pb.Get
|
||||
pending += balances.pending
|
||||
}
|
||||
|
||||
log.Infof("GetBalance request scanned %d UTXOs overall over %d addresses", len(s.utxosSortedByAmount), len(balancesMap))
|
||||
|
||||
return &pb.GetBalanceResponse{
|
||||
Available: available,
|
||||
Pending: pending,
|
||||
@ -62,9 +67,9 @@ func (s *server) GetBalance(_ context.Context, _ *pb.GetBalanceRequest) (*pb.Get
|
||||
}, nil
|
||||
}
|
||||
|
||||
func isUTXOSpendable(entry *walletUTXO, virtualDAAScore uint64, coinbaseMaturity uint64) bool {
|
||||
func (s *server) isUTXOSpendable(entry *walletUTXO, virtualDAAScore uint64) bool {
|
||||
if !entry.UTXOEntry.IsCoinbase() {
|
||||
return true
|
||||
}
|
||||
return entry.UTXOEntry.BlockDAAScore()+coinbaseMaturity < virtualDAAScore
|
||||
return entry.UTXOEntry.BlockDAAScore()+s.coinbaseMaturity < virtualDAAScore
|
||||
}
|
||||
|
@ -2,14 +2,16 @@ package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/kaspanet/kaspad/app/appmessage"
|
||||
"github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/pb"
|
||||
"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/infrastructure/network/rpcclient"
|
||||
"github.com/pkg/errors"
|
||||
"time"
|
||||
)
|
||||
|
||||
func (s *server) Broadcast(_ context.Context, request *pb.BroadcastRequest) (*pb.BroadcastResponse, error) {
|
||||
@ -54,16 +56,12 @@ func (s *server) broadcast(transactions [][]byte, isDomain bool) ([]string, erro
|
||||
}
|
||||
}
|
||||
|
||||
err = s.refreshUTXOs()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.forceSync()
|
||||
return txIDs, nil
|
||||
}
|
||||
|
||||
func sendTransaction(client *rpcclient.RPCClient, tx *externalapi.DomainTransaction) (string, error) {
|
||||
submitTransactionResponse, err := client.SubmitTransaction(appmessage.DomainTransactionToRPCTransaction(tx), false)
|
||||
submitTransactionResponse, err := client.SubmitTransaction(appmessage.DomainTransactionToRPCTransaction(tx), consensushashing.TransactionID(tx).String(), false)
|
||||
if err != nil {
|
||||
return "", errors.Wrapf(err, "error submitting transaction")
|
||||
}
|
||||
|
81
cmd/kaspawallet/daemon/server/broadcast_replacement.go
Normal file
81
cmd/kaspawallet/daemon/server/broadcast_replacement.go
Normal file
@ -0,0 +1,81 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/kaspanet/kaspad/app/appmessage"
|
||||
"github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/pb"
|
||||
"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/infrastructure/network/rpcclient"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func (s *server) BroadcastReplacement(_ context.Context, request *pb.BroadcastRequest) (*pb.BroadcastResponse, error) {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
|
||||
txIDs, err := s.broadcastReplacement(request.Transactions, request.IsDomain)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &pb.BroadcastResponse{TxIDs: txIDs}, nil
|
||||
}
|
||||
|
||||
// broadcastReplacement assumes that all transactions depend on the first one
|
||||
func (s *server) broadcastReplacement(transactions [][]byte, isDomain bool) ([]string, error) {
|
||||
|
||||
txIDs := make([]string, len(transactions))
|
||||
var tx *externalapi.DomainTransaction
|
||||
var err error
|
||||
|
||||
for i, transaction := range transactions {
|
||||
|
||||
if isDomain {
|
||||
tx, err = serialization.DeserializeDomainTransaction(transaction)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else if !isDomain { //default in proto3 is false
|
||||
tx, err = libkaspawallet.ExtractTransaction(transaction, s.keysFile.ECDSA)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Once the first transaction is added to the mempool, the transactions that depend
|
||||
// on the replaced transaction will be removed, so there's no need to submit them
|
||||
// as RBF transactions.
|
||||
if i == 0 {
|
||||
txIDs[i], err = sendTransactionRBF(s.rpcClient, tx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
txIDs[i], err = sendTransaction(s.rpcClient, tx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
for _, input := range tx.Inputs {
|
||||
s.usedOutpoints[input.PreviousOutpoint] = time.Now()
|
||||
}
|
||||
}
|
||||
|
||||
s.forceSync()
|
||||
return txIDs, nil
|
||||
}
|
||||
|
||||
func sendTransactionRBF(client *rpcclient.RPCClient, tx *externalapi.DomainTransaction) (string, error) {
|
||||
submitTransactionResponse, err := client.SubmitTransactionReplacement(appmessage.DomainTransactionToRPCTransaction(tx), consensushashing.TransactionID(tx).String())
|
||||
if err != nil {
|
||||
return "", errors.Wrapf(err, "error submitting transaction replacement")
|
||||
}
|
||||
return submitTransactionResponse.TransactionID, nil
|
||||
}
|
156
cmd/kaspawallet/daemon/server/bump_fee.go
Normal file
156
cmd/kaspawallet/daemon/server/bump_fee.go
Normal file
@ -0,0 +1,156 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/kaspanet/kaspad/app/appmessage"
|
||||
"github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/pb"
|
||||
"github.com/kaspanet/kaspad/cmd/kaspawallet/libkaspawallet"
|
||||
"github.com/kaspanet/kaspad/domain/consensus/model/externalapi"
|
||||
"github.com/kaspanet/kaspad/domain/consensus/utils/txscript"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func (s *server) BumpFee(_ context.Context, request *pb.BumpFeeRequest) (*pb.BumpFeeResponse, error) {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
|
||||
entry, err := s.rpcClient.GetMempoolEntry(request.TxID, false, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
domainTx, err := appmessage.RPCTransactionToDomainTransaction(entry.Entry.Transaction)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
outpointsToInputs := make(map[externalapi.DomainOutpoint]*externalapi.DomainTransactionInput)
|
||||
var maxUTXO *walletUTXO
|
||||
for _, input := range domainTx.Inputs {
|
||||
outpointsToInputs[input.PreviousOutpoint] = input
|
||||
utxo, ok := s.mempoolExcludedUTXOs[input.PreviousOutpoint]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
input.UTXOEntry = utxo.UTXOEntry
|
||||
if maxUTXO == nil || utxo.UTXOEntry.Amount() > maxUTXO.UTXOEntry.Amount() {
|
||||
maxUTXO = utxo
|
||||
}
|
||||
}
|
||||
|
||||
if maxUTXO == nil {
|
||||
// If we got here it means for some reason s.mempoolExcludedUTXOs is not up to date and we need to search for the UTXOs in s.utxosSortedByAmount
|
||||
for _, utxo := range s.utxosSortedByAmount {
|
||||
input, ok := outpointsToInputs[*utxo.Outpoint]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
input.UTXOEntry = utxo.UTXOEntry
|
||||
if maxUTXO == nil || utxo.UTXOEntry.Amount() > maxUTXO.UTXOEntry.Amount() {
|
||||
maxUTXO = utxo
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if maxUTXO == nil {
|
||||
return nil, errors.Errorf("no UTXOs were found for transaction %s. This probably means the transaction is already accepted", request.TxID)
|
||||
}
|
||||
|
||||
mass := s.txMassCalculator.CalculateTransactionOverallMass(domainTx)
|
||||
feeRate := float64(entry.Entry.Fee) / float64(mass)
|
||||
newFeeRate, maxFee, err := s.calculateFeeLimits(request.FeePolicy)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if feeRate >= newFeeRate {
|
||||
return nil, errors.Errorf("new fee rate (%f) is not higher than the current fee rate (%f)", newFeeRate, feeRate)
|
||||
}
|
||||
|
||||
if len(domainTx.Outputs) == 0 || len(domainTx.Outputs) > 2 {
|
||||
return nil, errors.Errorf("kaspawallet supports only transactions with 1 or 2 outputs in transaction %s, but this transaction got %d", request.TxID, len(domainTx.Outputs))
|
||||
}
|
||||
|
||||
var fromAddresses []*walletAddress
|
||||
for _, from := range request.From {
|
||||
fromAddress, exists := s.addressSet[from]
|
||||
if !exists {
|
||||
return nil, errors.Errorf("specified from address %s does not exists", from)
|
||||
}
|
||||
fromAddresses = append(fromAddresses, fromAddress)
|
||||
}
|
||||
|
||||
allowUsed := make(map[externalapi.DomainOutpoint]struct{})
|
||||
for outpoint := range outpointsToInputs {
|
||||
allowUsed[outpoint] = struct{}{}
|
||||
}
|
||||
selectedUTXOs, spendValue, changeSompi, err := s.selectUTXOsWithPreselected([]*walletUTXO{maxUTXO}, allowUsed, domainTx.Outputs[0].Value, false, newFeeRate, maxFee, fromAddresses)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, toAddress, err := txscript.ExtractScriptPubKeyAddress(domainTx.Outputs[0].ScriptPublicKey, s.params)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
changeAddress, changeWalletAddress, err := s.changeAddress(request.UseExistingChangeAddress, fromAddresses)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(selectedUTXOs) == 0 {
|
||||
return nil, errors.Errorf("couldn't find funds to spend")
|
||||
}
|
||||
|
||||
payments := []*libkaspawallet.Payment{{
|
||||
Address: toAddress,
|
||||
Amount: spendValue,
|
||||
}}
|
||||
if changeSompi > 0 {
|
||||
changeAddress, _, err := s.changeAddress(request.UseExistingChangeAddress, fromAddresses)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
payments = append(payments, &libkaspawallet.Payment{
|
||||
Address: changeAddress,
|
||||
Amount: changeSompi,
|
||||
})
|
||||
}
|
||||
unsignedTransaction, err := libkaspawallet.CreateUnsignedTransaction(s.keysFile.ExtendedPublicKeys,
|
||||
s.keysFile.MinimumSignatures,
|
||||
payments, selectedUTXOs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
unsignedTransactions, err := s.maybeAutoCompoundTransaction(unsignedTransaction, toAddress, changeAddress, changeWalletAddress, newFeeRate, maxFee)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if request.Password == "" {
|
||||
return &pb.BumpFeeResponse{
|
||||
Transactions: unsignedTransactions,
|
||||
}, nil
|
||||
}
|
||||
|
||||
signedTransactions, err := s.signTransactions(unsignedTransactions, request.Password)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
txIDs, err := s.broadcastReplacement(signedTransactions, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &pb.BumpFeeResponse{
|
||||
TxIDs: txIDs,
|
||||
Transactions: signedTransactions,
|
||||
}, nil
|
||||
}
|
@ -3,18 +3,27 @@ package server
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
"math"
|
||||
|
||||
"github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/pb"
|
||||
"github.com/kaspanet/kaspad/cmd/kaspawallet/libkaspawallet"
|
||||
"github.com/kaspanet/kaspad/domain/consensus/model/externalapi"
|
||||
"github.com/kaspanet/kaspad/domain/consensus/utils/constants"
|
||||
"github.com/kaspanet/kaspad/domain/consensus/utils/utxo"
|
||||
"github.com/kaspanet/kaspad/util"
|
||||
"github.com/pkg/errors"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
// TODO: Implement a better fee estimation mechanism
|
||||
const feePerInput = 10000
|
||||
// The minimal change amount to target in order to avoid large storage mass (see KIP9 for more details).
|
||||
// By having at least 10KAS in the change output we make sure that the storage mass charged for change is
|
||||
// at most 1000 gram. Generally, if the payment is above 10KAS as well, the resulting storage mass will be
|
||||
// in the order of magnitude of compute mass and wil not incur additional charges.
|
||||
// Additionally, every transaction with send value > ~0.1 KAS should succeed (at most ~99K storage mass for payment
|
||||
// output, thus overall lower than standard mass upper bound which is 100K gram)
|
||||
const minChangeTarget = constants.SompiPerKaspa * 10
|
||||
|
||||
// The current minimal fee rate according to mempool standards
|
||||
const minFeeRate = 1.0
|
||||
|
||||
func (s *server) CreateUnsignedTransactions(_ context.Context, request *pb.CreateUnsignedTransactionsRequest) (
|
||||
*pb.CreateUnsignedTransactionsResponse, error,
|
||||
@ -22,7 +31,8 @@ func (s *server) CreateUnsignedTransactions(_ context.Context, request *pb.Creat
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
|
||||
unsignedTransactions, err := s.createUnsignedTransactions(request.Address, request.Amount, request.From, request.UseExistingChangeAddress)
|
||||
unsignedTransactions, err := s.createUnsignedTransactions(request.Address, request.Amount, request.IsSendAll,
|
||||
request.From, request.UseExistingChangeAddress, request.FeePolicy)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -30,11 +40,59 @@ func (s *server) CreateUnsignedTransactions(_ context.Context, request *pb.Creat
|
||||
return &pb.CreateUnsignedTransactionsResponse{UnsignedTransactions: unsignedTransactions}, nil
|
||||
}
|
||||
|
||||
func (s *server) createUnsignedTransactions(address string, amount uint64, fromAddressesString []string, useExistingChangeAddress bool) ([][]byte, error) {
|
||||
func (s *server) calculateFeeLimits(requestFeePolicy *pb.FeePolicy) (feeRate float64, maxFee uint64, err error) {
|
||||
feeRate = minFeeRate
|
||||
maxFee = math.MaxUint64
|
||||
|
||||
if requestFeePolicy == nil {
|
||||
requestFeePolicy = &pb.FeePolicy{}
|
||||
}
|
||||
|
||||
switch requestFeePolicy := requestFeePolicy.FeePolicy.(type) {
|
||||
case *pb.FeePolicy_ExactFeeRate:
|
||||
feeRate = requestFeePolicy.ExactFeeRate
|
||||
if feeRate < minFeeRate {
|
||||
return 0, 0, errors.Errorf("requested fee rate %f is too low, minimum fee rate is %f", feeRate, minFeeRate)
|
||||
}
|
||||
case *pb.FeePolicy_MaxFeeRate:
|
||||
estimate, err := s.rpcClient.GetFeeEstimate()
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
if requestFeePolicy.MaxFeeRate < minFeeRate {
|
||||
return 0, 0, errors.Errorf("requested max fee rate %f is too low, minimum fee rate is %f", requestFeePolicy.MaxFeeRate, minFeeRate)
|
||||
}
|
||||
feeRate = math.Min(estimate.Estimate.NormalBuckets[0].Feerate, requestFeePolicy.MaxFeeRate)
|
||||
case *pb.FeePolicy_MaxFee:
|
||||
estimate, err := s.rpcClient.GetFeeEstimate()
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
feeRate = estimate.Estimate.NormalBuckets[0].Feerate
|
||||
maxFee = requestFeePolicy.MaxFee
|
||||
case nil:
|
||||
estimate, err := s.rpcClient.GetFeeEstimate()
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
feeRate = estimate.Estimate.NormalBuckets[0].Feerate
|
||||
// Default to a bound of max 1 KAS as fee
|
||||
maxFee = constants.SompiPerKaspa
|
||||
}
|
||||
|
||||
return feeRate, maxFee, nil
|
||||
}
|
||||
|
||||
func (s *server) createUnsignedTransactions(address string, amount uint64, isSendAll bool, fromAddressesString []string, useExistingChangeAddress bool, requestFeePolicy *pb.FeePolicy) ([][]byte, error) {
|
||||
if !s.isSynced() {
|
||||
return nil, errors.Errorf("wallet daemon is not synced yet, %s", s.formatSyncStateReport())
|
||||
}
|
||||
|
||||
feeRate, maxFee, err := s.calculateFeeLimits(requestFeePolicy)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// make sure address string is correct before proceeding to a
|
||||
// potentially long UTXO refreshment operation
|
||||
toAddress, err := util.DecodeAddress(address, s.params.Prefix)
|
||||
@ -42,33 +100,32 @@ func (s *server) createUnsignedTransactions(address string, amount uint64, fromA
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = s.refreshUTXOs()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var fromAddresses []*walletAddress
|
||||
for _, from := range fromAddressesString {
|
||||
fromAddress, exists := s.addressSet[from]
|
||||
if !exists {
|
||||
return nil, fmt.Errorf("Specified from address %s does not exists", from)
|
||||
return nil, fmt.Errorf("specified from address %s does not exists", from)
|
||||
}
|
||||
fromAddresses = append(fromAddresses, fromAddress)
|
||||
}
|
||||
|
||||
selectedUTXOs, changeSompi, err := s.selectUTXOs(amount, feePerInput, fromAddresses)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
changeAddress, changeWalletAddress, err := s.changeAddress(useExistingChangeAddress, fromAddresses)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
selectedUTXOs, spendValue, changeSompi, err := s.selectUTXOs(amount, isSendAll, feeRate, maxFee, fromAddresses)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(selectedUTXOs) == 0 {
|
||||
return nil, errors.Errorf("couldn't find funds to spend")
|
||||
}
|
||||
|
||||
payments := []*libkaspawallet.Payment{{
|
||||
Address: toAddress,
|
||||
Amount: amount,
|
||||
Amount: spendValue,
|
||||
}}
|
||||
if changeSompi > 0 {
|
||||
payments = append(payments, &libkaspawallet.Payment{
|
||||
@ -83,35 +140,52 @@ func (s *server) createUnsignedTransactions(address string, amount uint64, fromA
|
||||
return nil, err
|
||||
}
|
||||
|
||||
unsignedTransactions, err := s.maybeAutoCompoundTransaction(unsignedTransaction, toAddress, changeAddress, changeWalletAddress)
|
||||
unsignedTransactions, err := s.maybeAutoCompoundTransaction(unsignedTransaction, toAddress, changeAddress, changeWalletAddress, feeRate, maxFee)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return unsignedTransactions, nil
|
||||
}
|
||||
|
||||
func (s *server) selectUTXOs(spendAmount uint64, feePerInput uint64, fromAddresses []*walletAddress) (
|
||||
selectedUTXOs []*libkaspawallet.UTXO, changeSompi uint64, err error,
|
||||
) {
|
||||
selectedUTXOs = []*libkaspawallet.UTXO{}
|
||||
func (s *server) selectUTXOs(spendAmount uint64, isSendAll bool, feeRate float64, maxFee uint64, fromAddresses []*walletAddress) (
|
||||
selectedUTXOs []*libkaspawallet.UTXO, totalReceived uint64, changeSompi uint64, err error) {
|
||||
return s.selectUTXOsWithPreselected(nil, map[externalapi.DomainOutpoint]struct{}{}, spendAmount, isSendAll, feeRate, maxFee, fromAddresses)
|
||||
}
|
||||
|
||||
func (s *server) selectUTXOsWithPreselected(preSelectedUTXOs []*walletUTXO, allowUsed map[externalapi.DomainOutpoint]struct{}, spendAmount uint64, isSendAll bool, feeRate float64, maxFee uint64, fromAddresses []*walletAddress) (
|
||||
selectedUTXOs []*libkaspawallet.UTXO, totalReceived uint64, changeSompi uint64, err error) {
|
||||
|
||||
preSelectedSet := make(map[externalapi.DomainOutpoint]struct{})
|
||||
for _, utxo := range preSelectedUTXOs {
|
||||
preSelectedSet[*utxo.Outpoint] = struct{}{}
|
||||
}
|
||||
totalValue := uint64(0)
|
||||
|
||||
dagInfo, err := s.rpcClient.GetBlockDAGInfo()
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
return nil, 0, 0, err
|
||||
}
|
||||
|
||||
for _, utxo := range s.utxosSortedByAmount {
|
||||
if (fromAddresses != nil && !slices.Contains(fromAddresses, utxo.address)) ||
|
||||
!isUTXOSpendable(utxo, dagInfo.VirtualDAAScore, s.params.BlockCoinbaseMaturity) {
|
||||
continue
|
||||
var fee uint64
|
||||
iteration := func(utxo *walletUTXO, avoidPreselected bool) (bool, error) {
|
||||
if (fromAddresses != nil && !walletAddressesContain(fromAddresses, utxo.address)) ||
|
||||
!s.isUTXOSpendable(utxo, dagInfo.VirtualDAAScore) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
if broadcastTime, ok := s.usedOutpoints[*utxo.Outpoint]; ok {
|
||||
if time.Since(broadcastTime) > time.Minute {
|
||||
delete(s.usedOutpoints, *utxo.Outpoint)
|
||||
} else {
|
||||
continue
|
||||
if _, ok := allowUsed[*utxo.Outpoint]; !ok {
|
||||
if s.usedOutpointHasExpired(broadcastTime) {
|
||||
delete(s.usedOutpoints, *utxo.Outpoint)
|
||||
} else {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if avoidPreselected {
|
||||
if _, ok := preSelectedSet[*utxo.Outpoint]; ok {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
@ -120,21 +194,171 @@ func (s *server) selectUTXOs(spendAmount uint64, feePerInput uint64, fromAddress
|
||||
UTXOEntry: utxo.UTXOEntry,
|
||||
DerivationPath: s.walletAddressPath(utxo.address),
|
||||
})
|
||||
totalValue += utxo.UTXOEntry.Amount()
|
||||
|
||||
fee := feePerInput * uint64(len(selectedUTXOs))
|
||||
totalValue += utxo.UTXOEntry.Amount()
|
||||
estimatedRecipientValue := spendAmount
|
||||
if isSendAll {
|
||||
estimatedRecipientValue = totalValue
|
||||
}
|
||||
|
||||
fee, err = s.estimateFee(selectedUTXOs, feeRate, maxFee, estimatedRecipientValue)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
totalSpend := spendAmount + fee
|
||||
if totalValue >= totalSpend {
|
||||
// Two break cases (if not send all):
|
||||
// 1. totalValue == totalSpend, so there's no change needed -> number of outputs = 1, so a single input is sufficient
|
||||
// 2. totalValue > totalSpend, so there will be change and 2 outputs, therefor in order to not struggle with --
|
||||
// 2.1 go-nodes dust patch we try and find at least 2 inputs (even though the next one is not necessary in terms of spend value)
|
||||
// 2.2 KIP9 we try and make sure that the change amount is not too small
|
||||
if !isSendAll && (totalValue == totalSpend || (totalValue >= totalSpend+minChangeTarget && len(selectedUTXOs) > 1)) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
shouldContinue := true
|
||||
for _, utxo := range preSelectedUTXOs {
|
||||
shouldContinue, err = iteration(utxo, false)
|
||||
if err != nil {
|
||||
return nil, 0, 0, err
|
||||
}
|
||||
|
||||
if !shouldContinue {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
fee := feePerInput * uint64(len(selectedUTXOs))
|
||||
totalSpend := spendAmount + fee
|
||||
if shouldContinue {
|
||||
for _, utxo := range s.utxosSortedByAmount {
|
||||
shouldContinue, err := iteration(utxo, true)
|
||||
if err != nil {
|
||||
return nil, 0, 0, err
|
||||
}
|
||||
|
||||
if !shouldContinue {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var totalSpend uint64
|
||||
if isSendAll {
|
||||
totalSpend = totalValue
|
||||
totalReceived = totalValue - fee
|
||||
} else {
|
||||
totalSpend = spendAmount + fee
|
||||
totalReceived = spendAmount
|
||||
}
|
||||
if totalValue < totalSpend {
|
||||
return nil, 0, errors.Errorf("Insufficient funds for send: %f required, while only %f available",
|
||||
return nil, 0, 0, errors.Errorf("Insufficient funds for send: %f required, while only %f available",
|
||||
float64(totalSpend)/constants.SompiPerKaspa, float64(totalValue)/constants.SompiPerKaspa)
|
||||
}
|
||||
|
||||
return selectedUTXOs, totalValue - totalSpend, nil
|
||||
return selectedUTXOs, totalReceived, totalValue - totalSpend, nil
|
||||
}
|
||||
|
||||
func (s *server) estimateFee(selectedUTXOs []*libkaspawallet.UTXO, feeRate float64, maxFee uint64, recipientValue uint64) (uint64, error) {
|
||||
fakePubKey := [util.PublicKeySizeECDSA]byte{}
|
||||
fakeAddr, err := util.NewAddressPublicKeyECDSA(fakePubKey[:], s.params.Prefix) // We assume the worst case where the recipient address is ECDSA. In this case the scriptPubKey will be the longest.
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
totalValue := uint64(0)
|
||||
for _, utxo := range selectedUTXOs {
|
||||
totalValue += utxo.UTXOEntry.Amount()
|
||||
}
|
||||
|
||||
// This is an approximation for the distribution of value between the recipient output and the change output.
|
||||
var mockPayments []*libkaspawallet.Payment
|
||||
if totalValue > recipientValue {
|
||||
mockPayments = []*libkaspawallet.Payment{
|
||||
{
|
||||
Address: fakeAddr,
|
||||
Amount: recipientValue,
|
||||
},
|
||||
{
|
||||
Address: fakeAddr,
|
||||
Amount: totalValue - recipientValue, // We ignore the fee since we expect it to be insignificant in mass calculation.
|
||||
},
|
||||
}
|
||||
} else {
|
||||
mockPayments = []*libkaspawallet.Payment{
|
||||
{
|
||||
Address: fakeAddr,
|
||||
Amount: totalValue,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
mockTx, err := libkaspawallet.CreateUnsignedTransaction(s.keysFile.ExtendedPublicKeys,
|
||||
s.keysFile.MinimumSignatures,
|
||||
mockPayments, selectedUTXOs)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
mass, err := s.estimateMassAfterSignatures(mockTx)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return min(uint64(math.Ceil(float64(mass)*feeRate)), maxFee), nil
|
||||
}
|
||||
|
||||
func (s *server) estimateFeePerInput(feeRate float64) (uint64, error) {
|
||||
mockUTXO := &libkaspawallet.UTXO{
|
||||
Outpoint: &externalapi.DomainOutpoint{
|
||||
TransactionID: externalapi.DomainTransactionID{},
|
||||
Index: 0,
|
||||
},
|
||||
UTXOEntry: utxo.NewUTXOEntry(1, &externalapi.ScriptPublicKey{
|
||||
Script: nil,
|
||||
Version: 0,
|
||||
}, false, 0),
|
||||
DerivationPath: "m",
|
||||
}
|
||||
|
||||
mockTx, err := libkaspawallet.CreateUnsignedTransaction(s.keysFile.ExtendedPublicKeys,
|
||||
s.keysFile.MinimumSignatures,
|
||||
nil, []*libkaspawallet.UTXO{mockUTXO})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
// Here we use compute mass to avoid dividing by zero. This is ok since `s.estimateFeePerInput` is only used
|
||||
// in the case of compound transactions that have a compute mass higher than its storage mass.
|
||||
mass, err := s.estimateComputeMassAfterSignatures(mockTx)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
mockTxWithoutUTXO, err := libkaspawallet.CreateUnsignedTransaction(s.keysFile.ExtendedPublicKeys,
|
||||
s.keysFile.MinimumSignatures,
|
||||
nil, nil)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
massWithoutUTXO, err := s.estimateComputeMassAfterSignatures(mockTxWithoutUTXO)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
inputMass := mass - massWithoutUTXO
|
||||
|
||||
return uint64(float64(inputMass) * feeRate), nil
|
||||
}
|
||||
|
||||
func walletAddressesContain(addresses []*walletAddress, contain *walletAddress) bool {
|
||||
for _, address := range addresses {
|
||||
if *address == *contain {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
@ -21,7 +21,15 @@ func (s *server) GetExternalSpendableUTXOs(_ context.Context, request *pb.GetExt
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
selectedUTXOs, err := s.selectExternalSpendableUTXOs(externalUTXOs, request.Address)
|
||||
|
||||
estimate, err := s.rpcClient.GetFeeEstimate()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
feeRate := estimate.Estimate.NormalBuckets[0].Feerate
|
||||
|
||||
selectedUTXOs, err := s.selectExternalSpendableUTXOs(externalUTXOs, feeRate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -30,7 +38,7 @@ func (s *server) GetExternalSpendableUTXOs(_ context.Context, request *pb.GetExt
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *server) selectExternalSpendableUTXOs(externalUTXOs *appmessage.GetUTXOsByAddressesResponseMessage, address string) ([]*pb.UtxosByAddressesEntry, error) {
|
||||
func (s *server) selectExternalSpendableUTXOs(externalUTXOs *appmessage.GetUTXOsByAddressesResponseMessage, feeRate float64) ([]*pb.UtxosByAddressesEntry, error) {
|
||||
dagInfo, err := s.rpcClient.GetBlockDAGInfo()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -42,8 +50,13 @@ func (s *server) selectExternalSpendableUTXOs(externalUTXOs *appmessage.GetUTXOs
|
||||
//we do not make because we do not know size, because of unspendable utxos
|
||||
var selectedExternalUtxos []*pb.UtxosByAddressesEntry
|
||||
|
||||
feePerInput, err := s.estimateFeePerInput(feeRate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, entry := range externalUTXOs.Entries {
|
||||
if !isExternalUTXOSpendable(entry, daaScore, maturity) {
|
||||
if !isExternalUTXOSpendable(entry, daaScore, maturity, feePerInput) {
|
||||
continue
|
||||
}
|
||||
selectedExternalUtxos = append(selectedExternalUtxos, libkaspawallet.AppMessageUTXOToKaspawalletdUTXO(entry))
|
||||
@ -52,7 +65,7 @@ func (s *server) selectExternalSpendableUTXOs(externalUTXOs *appmessage.GetUTXOs
|
||||
return selectedExternalUtxos, nil
|
||||
}
|
||||
|
||||
func isExternalUTXOSpendable(entry *appmessage.UTXOsByAddressesEntry, virtualDAAScore uint64, coinbaseMaturity uint64) bool {
|
||||
func isExternalUTXOSpendable(entry *appmessage.UTXOsByAddressesEntry, virtualDAAScore uint64, coinbaseMaturity uint64, feePerInput uint64) bool {
|
||||
if !entry.UTXOEntry.IsCoinbase {
|
||||
return true
|
||||
} else if entry.UTXOEntry.Amount <= feePerInput {
|
||||
|
@ -4,13 +4,16 @@ import (
|
||||
"context"
|
||||
|
||||
"github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/pb"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func (s *server) Send(_ context.Context, request *pb.SendRequest) (*pb.SendResponse, error) {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
|
||||
unsignedTransactions, err := s.createUnsignedTransactions(request.ToAddress, request.Amount, request.From, request.UseExistingChangeAddress)
|
||||
unsignedTransactions, err := s.createUnsignedTransactions(request.ToAddress, request.Amount, request.IsSendAll,
|
||||
request.From, request.UseExistingChangeAddress, request.FeePolicy)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -22,7 +25,7 @@ func (s *server) Send(_ context.Context, request *pb.SendRequest) (*pb.SendRespo
|
||||
|
||||
txIDs, err := s.broadcast(signedTransactions, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, errors.Wrapf(err, "error broadcasting transactions %s", EncodeTransactionsToHex(signedTransactions))
|
||||
}
|
||||
|
||||
return &pb.SendResponse{TxIDs: txIDs, SignedTransactions: signedTransactions}, nil
|
||||
|
@ -5,8 +5,11 @@ import (
|
||||
"net"
|
||||
"os"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/kaspanet/kaspad/version"
|
||||
|
||||
"github.com/kaspanet/kaspad/domain/consensus/model/externalapi"
|
||||
|
||||
"github.com/kaspanet/kaspad/util/txmass"
|
||||
@ -27,17 +30,23 @@ import (
|
||||
type server struct {
|
||||
pb.UnimplementedKaspawalletdServer
|
||||
|
||||
rpcClient *rpcclient.RPCClient
|
||||
params *dagconfig.Params
|
||||
rpcClient *rpcclient.RPCClient // RPC client for ongoing user requests
|
||||
backgroundRPCClient *rpcclient.RPCClient // RPC client dedicated for address and UTXO background fetching
|
||||
params *dagconfig.Params
|
||||
coinbaseMaturity uint64 // Is different from default if we use testnet-11
|
||||
|
||||
lock sync.RWMutex
|
||||
utxosSortedByAmount []*walletUTXO
|
||||
nextSyncStartIndex uint32
|
||||
keysFile *keys.File
|
||||
shutdown chan struct{}
|
||||
addressSet walletAddressSet
|
||||
txMassCalculator *txmass.Calculator
|
||||
usedOutpoints map[externalapi.DomainOutpoint]time.Time
|
||||
lock sync.RWMutex
|
||||
utxosSortedByAmount []*walletUTXO
|
||||
mempoolExcludedUTXOs map[externalapi.DomainOutpoint]*walletUTXO
|
||||
nextSyncStartIndex uint32
|
||||
keysFile *keys.File
|
||||
shutdown chan struct{}
|
||||
forceSyncChan chan struct{}
|
||||
startTimeOfLastCompletedRefresh time.Time
|
||||
addressSet walletAddressSet
|
||||
txMassCalculator *txmass.Calculator
|
||||
usedOutpoints map[externalapi.DomainOutpoint]time.Time
|
||||
firstSyncDone atomic.Bool
|
||||
|
||||
isLogFinalProgressLineShown bool
|
||||
maxUsedAddressesForLog uint32
|
||||
@ -59,6 +68,7 @@ func Start(params *dagconfig.Params, listen, rpcServer string, keysFilePath stri
|
||||
profiling.Start(profile, log)
|
||||
}
|
||||
|
||||
log.Infof("Version %s", version.Version())
|
||||
listener, err := net.Listen("tcp", listen)
|
||||
if err != nil {
|
||||
return (errors.Wrapf(err, "Error listening to TCP on %s", listen))
|
||||
@ -70,6 +80,10 @@ func Start(params *dagconfig.Params, listen, rpcServer string, keysFilePath stri
|
||||
if err != nil {
|
||||
return (errors.Wrapf(err, "Error connecting to RPC server %s", rpcServer))
|
||||
}
|
||||
backgroundRPCClient, err := connectToRPC(params, rpcServer, timeout)
|
||||
if err != nil {
|
||||
return (errors.Wrapf(err, "Error making a second connection to RPC server %s", rpcServer))
|
||||
}
|
||||
|
||||
log.Infof("Connected, reading keys file %s...", keysFilePath)
|
||||
keysFile, err := keys.ReadKeysFile(params, keysFilePath)
|
||||
@ -82,13 +96,27 @@ func Start(params *dagconfig.Params, listen, rpcServer string, keysFilePath stri
|
||||
return err
|
||||
}
|
||||
|
||||
dagInfo, err := rpcClient.GetBlockDAGInfo()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
coinbaseMaturity := params.BlockCoinbaseMaturity
|
||||
if dagInfo.NetworkName == "kaspa-testnet-11" {
|
||||
coinbaseMaturity = 1000
|
||||
}
|
||||
|
||||
serverInstance := &server{
|
||||
rpcClient: rpcClient,
|
||||
backgroundRPCClient: backgroundRPCClient,
|
||||
params: params,
|
||||
coinbaseMaturity: coinbaseMaturity,
|
||||
utxosSortedByAmount: []*walletUTXO{},
|
||||
mempoolExcludedUTXOs: map[externalapi.DomainOutpoint]*walletUTXO{},
|
||||
nextSyncStartIndex: 0,
|
||||
keysFile: keysFile,
|
||||
shutdown: make(chan struct{}),
|
||||
forceSyncChan: make(chan struct{}),
|
||||
addressSet: make(walletAddressSet),
|
||||
txMassCalculator: txmass.NewCalculator(params.MassPerTxByte, params.MassPerScriptPubKeyByte, params.MassPerSigOp),
|
||||
usedOutpoints: map[externalapi.DomainOutpoint]time.Time{},
|
||||
@ -98,8 +126,8 @@ func Start(params *dagconfig.Params, listen, rpcServer string, keysFilePath stri
|
||||
}
|
||||
|
||||
log.Infof("Read, syncing the wallet...")
|
||||
spawn("serverInstance.sync", func() {
|
||||
err := serverInstance.sync()
|
||||
spawn("serverInstance.syncLoop", func() {
|
||||
err := serverInstance.syncLoop()
|
||||
if err != nil {
|
||||
printErrorAndExit(errors.Wrap(err, "error syncing the wallet"))
|
||||
}
|
||||
|
@ -12,6 +12,7 @@ import (
|
||||
"github.com/kaspanet/kaspad/domain/consensus/utils/utxo"
|
||||
"github.com/kaspanet/kaspad/domain/miningmanager/mempool"
|
||||
"github.com/kaspanet/kaspad/util"
|
||||
"github.com/kaspanet/kaspad/util/txmass"
|
||||
)
|
||||
|
||||
// maybeAutoCompoundTransaction checks if a transaction's mass is higher that what is allowed for a standard
|
||||
@ -20,14 +21,10 @@ import (
|
||||
// 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
|
||||
}
|
||||
func (s *server) maybeAutoCompoundTransaction(transaction *serialization.PartiallySignedTransaction, toAddress util.Address,
|
||||
changeAddress util.Address, changeWalletAddress *walletAddress, feeRate float64, maxFee uint64) ([][]byte, error) {
|
||||
|
||||
splitTransactions, err := s.maybeSplitAndMergeTransaction(transaction, toAddress, changeAddress, changeWalletAddress)
|
||||
splitTransactions, err := s.maybeSplitAndMergeTransaction(transaction, toAddress, changeAddress, changeWalletAddress, feeRate, maxFee)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -47,6 +44,8 @@ func (s *server) mergeTransaction(
|
||||
toAddress util.Address,
|
||||
changeAddress util.Address,
|
||||
changeWalletAddress *walletAddress,
|
||||
feeRate float64,
|
||||
maxFee uint64,
|
||||
) (*serialization.PartiallySignedTransaction, error) {
|
||||
numOutputs := len(originalTransaction.Tx.Outputs)
|
||||
if numOutputs > 2 || numOutputs == 0 {
|
||||
@ -71,13 +70,19 @@ func (s *server) mergeTransaction(
|
||||
DerivationPath: s.walletAddressPath(changeWalletAddress),
|
||||
}
|
||||
totalValue += output.Value
|
||||
totalValue -= feePerInput
|
||||
}
|
||||
// We're overestimating a bit by assuming that any transaction will have a change output
|
||||
fee, err := s.estimateFee(utxos, feeRate, maxFee, sentValue)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
totalValue -= fee
|
||||
|
||||
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)
|
||||
additionalUTXOs, totalValueAdded, err := s.moreUTXOsForMergeTransaction(utxos, sentValue-totalValue, feeRate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -96,19 +101,54 @@ func (s *server) mergeTransaction(
|
||||
})
|
||||
}
|
||||
|
||||
mergeTransactionBytes, err := libkaspawallet.CreateUnsignedTransaction(s.keysFile.ExtendedPublicKeys,
|
||||
return libkaspawallet.CreateUnsignedTransaction(s.keysFile.ExtendedPublicKeys,
|
||||
s.keysFile.MinimumSignatures, payments, utxos)
|
||||
}
|
||||
|
||||
func (s *server) transactionFeeRate(psTx *serialization.PartiallySignedTransaction) (float64, error) {
|
||||
totalOuts := 0
|
||||
for _, output := range psTx.Tx.Outputs {
|
||||
totalOuts += int(output.Value)
|
||||
}
|
||||
|
||||
totalIns := 0
|
||||
for _, input := range psTx.PartiallySignedInputs {
|
||||
totalIns += int(input.PrevOutput.Value)
|
||||
}
|
||||
|
||||
if totalIns < totalOuts {
|
||||
return 0, errors.Errorf("Transaction don't have enough funds to pay for the outputs")
|
||||
}
|
||||
fee := totalIns - totalOuts
|
||||
mass, err := s.estimateComputeMassAfterSignatures(psTx)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return float64(fee) / float64(mass), nil
|
||||
}
|
||||
|
||||
func (s *server) checkTransactionFeeRate(psTx *serialization.PartiallySignedTransaction, maxFee uint64) error {
|
||||
feeRate, err := s.transactionFeeRate(psTx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if feeRate < 1 {
|
||||
return errors.Errorf("setting --max-fee to %d results in a fee rate of %f, which is below the minimum allowed fee rate of 1 sompi/gram", maxFee, feeRate)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *server) maybeSplitAndMergeTransaction(transaction *serialization.PartiallySignedTransaction, toAddress util.Address,
|
||||
changeAddress util.Address, changeWalletAddress *walletAddress, feeRate float64, maxFee uint64) ([]*serialization.PartiallySignedTransaction, error) {
|
||||
|
||||
err := s.checkTransactionFeeRate(transaction, maxFee)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return serialization.DeserializePartiallySignedTransaction(mergeTransactionBytes)
|
||||
}
|
||||
|
||||
func (s *server) maybeSplitAndMergeTransaction(transaction *serialization.PartiallySignedTransaction, toAddress util.Address,
|
||||
changeAddress util.Address, changeWalletAddress *walletAddress) ([]*serialization.PartiallySignedTransaction, error) {
|
||||
|
||||
transactionMass, err := s.estimateMassAfterSignatures(transaction)
|
||||
transactionMass, err := s.estimateComputeMassAfterSignatures(transaction)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -117,7 +157,7 @@ func (s *server) maybeSplitAndMergeTransaction(transaction *serialization.Partia
|
||||
return []*serialization.PartiallySignedTransaction{transaction}, nil
|
||||
}
|
||||
|
||||
splitCount, inputCountPerSplit, err := s.splitAndInputPerSplitCounts(transaction, transactionMass, changeAddress)
|
||||
splitCount, inputCountPerSplit, err := s.splitAndInputPerSplitCounts(transaction, transactionMass, changeAddress, feeRate, maxFee)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -127,19 +167,24 @@ func (s *server) maybeSplitAndMergeTransaction(transaction *serialization.Partia
|
||||
startIndex := i * inputCountPerSplit
|
||||
endIndex := startIndex + inputCountPerSplit
|
||||
var err error
|
||||
splitTransactions[i], err = s.createSplitTransaction(transaction, changeAddress, startIndex, endIndex)
|
||||
splitTransactions[i], err = s.createSplitTransaction(transaction, changeAddress, startIndex, endIndex, feeRate, maxFee)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = s.checkTransactionFeeRate(splitTransactions[i], maxFee)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if len(splitTransactions) > 1 {
|
||||
mergeTransaction, err := s.mergeTransaction(splitTransactions, transaction, toAddress, changeAddress, changeWalletAddress)
|
||||
mergeTransaction, err := s.mergeTransaction(splitTransactions, transaction, toAddress, changeAddress, changeWalletAddress, feeRate, maxFee)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Recursion will be 2-3 iterations deep even in the rarest` cases, so considered safe..
|
||||
splitMergeTransaction, err := s.maybeSplitAndMergeTransaction(mergeTransaction, toAddress, changeAddress, changeWalletAddress)
|
||||
splitMergeTransaction, err := s.maybeSplitAndMergeTransaction(mergeTransaction, toAddress, changeAddress, changeWalletAddress, feeRate, maxFee)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -152,7 +197,7 @@ func (s *server) maybeSplitAndMergeTransaction(transaction *serialization.Partia
|
||||
|
||||
// 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) {
|
||||
changeAddress util.Address, feeRate float64, maxFee uint64) (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
|
||||
@ -172,7 +217,7 @@ func (s *server) splitAndInputPerSplitCounts(transaction *serialization.Partiall
|
||||
|
||||
// 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)
|
||||
splitTransactionWithoutInputs, err := s.createSplitTransaction(transaction, changeAddress, 0, 0, feeRate, maxFee)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
@ -190,7 +235,7 @@ func (s *server) splitAndInputPerSplitCounts(transaction *serialization.Partiall
|
||||
}
|
||||
|
||||
func (s *server) createSplitTransaction(transaction *serialization.PartiallySignedTransaction,
|
||||
changeAddress util.Address, startIndex int, endIndex int) (*serialization.PartiallySignedTransaction, error) {
|
||||
changeAddress util.Address, startIndex int, endIndex int, feeRate float64, maxFee uint64) (*serialization.PartiallySignedTransaction, error) {
|
||||
|
||||
selectedUTXOs := make([]*libkaspawallet.UTXO, 0, endIndex-startIndex)
|
||||
totalSompi := uint64(0)
|
||||
@ -206,25 +251,36 @@ func (s *server) createSplitTransaction(transaction *serialization.PartiallySign
|
||||
})
|
||||
|
||||
totalSompi += selectedUTXOs[i-startIndex].UTXOEntry.Amount()
|
||||
totalSompi -= feePerInput
|
||||
}
|
||||
unsignedTransactionBytes, err := libkaspawallet.CreateUnsignedTransaction(s.keysFile.ExtendedPublicKeys,
|
||||
if len(selectedUTXOs) != 0 {
|
||||
fee, err := s.estimateFee(selectedUTXOs, feeRate, maxFee, totalSompi)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
totalSompi -= fee
|
||||
}
|
||||
|
||||
return 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) {
|
||||
return EstimateMassAfterSignatures(transaction, s.keysFile.ECDSA, s.keysFile.MinimumSignatures, s.txMassCalculator)
|
||||
}
|
||||
|
||||
func (s *server) estimateComputeMassAfterSignatures(transaction *serialization.PartiallySignedTransaction) (uint64, error) {
|
||||
return estimateComputeMassAfterSignatures(transaction, s.keysFile.ECDSA, s.keysFile.MinimumSignatures, s.txMassCalculator)
|
||||
}
|
||||
|
||||
func createTransactionWithJunkFieldsForMassCalculation(transaction *serialization.PartiallySignedTransaction, ecdsa bool, minimumSignatures uint32, txMassCalculator *txmass.Calculator) (*externalapi.DomainTransaction, error) {
|
||||
transaction = transaction.Clone()
|
||||
var signatureSize uint64
|
||||
if s.keysFile.ECDSA {
|
||||
if ecdsa {
|
||||
signatureSize = secp256k1.SerializedECDSASignatureSize
|
||||
} else {
|
||||
signatureSize = secp256k1.SerializedSchnorrSignatureSize
|
||||
@ -232,7 +288,7 @@ func (s *server) estimateMassAfterSignatures(transaction *serialization.Partiall
|
||||
|
||||
for i, input := range transaction.PartiallySignedInputs {
|
||||
for j, pubKeyPair := range input.PubKeySignaturePairs {
|
||||
if uint32(j) >= s.keysFile.MinimumSignatures {
|
||||
if uint32(j) >= minimumSignatures {
|
||||
break
|
||||
}
|
||||
pubKeyPair.Signature = make([]byte, signatureSize+1) // +1 for SigHashType
|
||||
@ -240,15 +296,28 @@ func (s *server) estimateMassAfterSignatures(transaction *serialization.Partiall
|
||||
transaction.Tx.Inputs[i].SigOpCount = byte(len(input.PubKeySignaturePairs))
|
||||
}
|
||||
|
||||
transactionWithSignatures, err := libkaspawallet.ExtractTransactionDeserialized(transaction, s.keysFile.ECDSA)
|
||||
return libkaspawallet.ExtractTransactionDeserialized(transaction, ecdsa)
|
||||
}
|
||||
|
||||
func estimateComputeMassAfterSignatures(transaction *serialization.PartiallySignedTransaction, ecdsa bool, minimumSignatures uint32, txMassCalculator *txmass.Calculator) (uint64, error) {
|
||||
transactionWithSignatures, err := createTransactionWithJunkFieldsForMassCalculation(transaction, ecdsa, minimumSignatures, txMassCalculator)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return s.txMassCalculator.CalculateTransactionMass(transactionWithSignatures), nil
|
||||
return txMassCalculator.CalculateTransactionMass(transactionWithSignatures), nil
|
||||
}
|
||||
|
||||
func (s *server) moreUTXOsForMergeTransaction(alreadySelectedUTXOs []*libkaspawallet.UTXO, requiredAmount uint64) (
|
||||
func EstimateMassAfterSignatures(transaction *serialization.PartiallySignedTransaction, ecdsa bool, minimumSignatures uint32, txMassCalculator *txmass.Calculator) (uint64, error) {
|
||||
transactionWithSignatures, err := createTransactionWithJunkFieldsForMassCalculation(transaction, ecdsa, minimumSignatures, txMassCalculator)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return txMassCalculator.CalculateTransactionOverallMass(transactionWithSignatures), nil
|
||||
}
|
||||
|
||||
func (s *server) moreUTXOsForMergeTransaction(alreadySelectedUTXOs []*libkaspawallet.UTXO, requiredAmount uint64, feeRate float64) (
|
||||
additionalUTXOs []*libkaspawallet.UTXO, totalValueAdded uint64, err error) {
|
||||
|
||||
dagInfo, err := s.rpcClient.GetBlockDAGInfo()
|
||||
@ -260,11 +329,16 @@ func (s *server) moreUTXOsForMergeTransaction(alreadySelectedUTXOs []*libkaspawa
|
||||
alreadySelectedUTXOsMap[*alreadySelectedUTXO.Outpoint] = struct{}{}
|
||||
}
|
||||
|
||||
feePerInput, err := s.estimateFeePerInput(feeRate)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
for _, utxo := range s.utxosSortedByAmount {
|
||||
if _, ok := alreadySelectedUTXOsMap[*utxo.Outpoint]; ok {
|
||||
continue
|
||||
}
|
||||
if !isUTXOSpendable(utxo, dagInfo.VirtualDAAScore, s.params.BlockCoinbaseMaturity) {
|
||||
if !s.isUTXOSpendable(utxo, dagInfo.VirtualDAAScore) {
|
||||
continue
|
||||
}
|
||||
additionalUTXOs = append(additionalUTXOs, &libkaspawallet.UTXO{
|
||||
|
@ -20,9 +20,9 @@ import (
|
||||
"github.com/kaspanet/kaspad/domain/consensus/utils/testutils"
|
||||
)
|
||||
|
||||
func TestEstimateMassAfterSignatures(t *testing.T) {
|
||||
func TestEstimateComputeMassAfterSignatures(t *testing.T) {
|
||||
testutils.ForAllNets(t, true, func(t *testing.T, consensusConfig *consensus.Config) {
|
||||
unsignedTransactionBytes, mnemonics, params, teardown := testEstimateMassIncreaseForSignaturesSetUp(t, consensusConfig)
|
||||
unsignedTransaction, mnemonics, params, teardown := testEstimateMassIncreaseForSignaturesSetUp(t, consensusConfig)
|
||||
defer teardown(false)
|
||||
|
||||
serverInstance := &server{
|
||||
@ -33,14 +33,14 @@ func TestEstimateMassAfterSignatures(t *testing.T) {
|
||||
txMassCalculator: txmass.NewCalculator(params.MassPerTxByte, params.MassPerScriptPubKeyByte, params.MassPerSigOp),
|
||||
}
|
||||
|
||||
unsignedTransaction, err := serialization.DeserializePartiallySignedTransaction(unsignedTransactionBytes)
|
||||
estimatedMassAfterSignatures, err := serverInstance.estimateComputeMassAfterSignatures(unsignedTransaction)
|
||||
if err != nil {
|
||||
t.Fatalf("Error deserializing unsignedTransaction: %s", err)
|
||||
t.Fatalf("Error from estimateComputeMassAfterSignatures: %s", err)
|
||||
}
|
||||
|
||||
estimatedMassAfterSignatures, err := serverInstance.estimateMassAfterSignatures(unsignedTransaction)
|
||||
unsignedTransactionBytes, err := serialization.SerializePartiallySignedTransaction(unsignedTransaction)
|
||||
if err != nil {
|
||||
t.Fatalf("Error from estimateMassAfterSignatures: %s", err)
|
||||
t.Fatalf("Error deserializing unsignedTransaction: %s", err)
|
||||
}
|
||||
|
||||
signedTxStep1Bytes, err := libkaspawallet.Sign(params, mnemonics[:1], unsignedTransactionBytes, false)
|
||||
@ -67,8 +67,67 @@ func TestEstimateMassAfterSignatures(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestEstimateMassAfterSignatures(t *testing.T) {
|
||||
testutils.ForAllNets(t, true, func(t *testing.T, consensusConfig *consensus.Config) {
|
||||
unsignedTransaction, mnemonics, params, teardown := testEstimateMassIncreaseForSignaturesSetUp(t, consensusConfig)
|
||||
defer teardown(false)
|
||||
|
||||
for i := range unsignedTransaction.Tx.Inputs {
|
||||
unsignedTransaction.Tx.Inputs[i].UTXOEntry = utxo.NewUTXOEntry(1, &externalapi.ScriptPublicKey{}, false, 0)
|
||||
unsignedTransaction.PartiallySignedInputs[i].PrevOutput = &externalapi.DomainTransactionOutput{
|
||||
Value: 1,
|
||||
ScriptPublicKey: &externalapi.ScriptPublicKey{},
|
||||
}
|
||||
}
|
||||
|
||||
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),
|
||||
}
|
||||
|
||||
estimatedMassAfterSignatures, err := serverInstance.estimateMassAfterSignatures(unsignedTransaction)
|
||||
if err != nil {
|
||||
t.Fatalf("Error from estimateMassAfterSignatures: %s", err)
|
||||
}
|
||||
|
||||
unsignedTransactionBytes, err := serialization.SerializePartiallySignedTransaction(unsignedTransaction)
|
||||
if err != nil {
|
||||
t.Fatalf("Error deserializing unsignedTransaction: %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)
|
||||
}
|
||||
|
||||
for i := range extractedSignedTx.Inputs {
|
||||
extractedSignedTx.Inputs[i].UTXOEntry = utxo.NewUTXOEntry(1, &externalapi.ScriptPublicKey{}, false, 0)
|
||||
}
|
||||
|
||||
actualMassAfterSignatures := serverInstance.txMassCalculator.CalculateTransactionOverallMass(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)) {
|
||||
*serialization.PartiallySignedTransaction, []string, *dagconfig.Params, func(keepDataDir bool)) {
|
||||
|
||||
consensusConfig.BlockCoinbaseMaturity = 0
|
||||
params := &consensusConfig.Params
|
||||
|
@ -6,6 +6,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/kaspanet/kaspad/cmd/kaspawallet/libkaspawallet"
|
||||
"github.com/kaspanet/kaspad/domain/consensus/model/externalapi"
|
||||
|
||||
"github.com/kaspanet/kaspad/app/appmessage"
|
||||
"github.com/pkg/errors"
|
||||
@ -23,7 +24,7 @@ func (was walletAddressSet) strings() []string {
|
||||
return addresses
|
||||
}
|
||||
|
||||
func (s *server) sync() error {
|
||||
func (s *server) syncLoop() error {
|
||||
ticker := time.NewTicker(time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
@ -32,29 +33,39 @@ func (s *server) sync() error {
|
||||
return err
|
||||
}
|
||||
|
||||
err = s.refreshExistingUTXOsWithLock()
|
||||
err = s.refreshUTXOs()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for range ticker.C {
|
||||
err = s.collectFarAddresses()
|
||||
if err != nil {
|
||||
return err
|
||||
s.firstSyncDone.Store(true)
|
||||
log.Infof("Wallet is synced and ready for operation")
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
case <-s.forceSyncChan:
|
||||
}
|
||||
|
||||
err = s.collectRecentAddresses()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = s.refreshExistingUTXOsWithLock()
|
||||
err := s.sync()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
func (s *server) sync() error {
|
||||
err := s.collectFarAddresses()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = s.collectRecentAddresses()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.refreshUTXOs()
|
||||
}
|
||||
|
||||
const (
|
||||
@ -158,7 +169,7 @@ func (s *server) collectAddresses(start, end uint32) error {
|
||||
return err
|
||||
}
|
||||
|
||||
getBalancesByAddressesResponse, err := s.rpcClient.GetBalancesByAddresses(addressSet.strings())
|
||||
getBalancesByAddressesResponse, err := s.backgroundRPCClient.GetBalancesByAddresses(addressSet.strings())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -208,15 +219,17 @@ func (s *server) updateAddressesAndLastUsedIndexes(requestedAddressSet walletAdd
|
||||
return s.keysFile.SetLastUsedInternalIndex(lastUsedInternalIndex)
|
||||
}
|
||||
|
||||
func (s *server) refreshExistingUTXOsWithLock() error {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
|
||||
return s.refreshUTXOs()
|
||||
func (s *server) usedOutpointHasExpired(outpointBroadcastTime time.Time) bool {
|
||||
// If the node returns a UTXO we previously attempted to spend and enough time has passed, we assume
|
||||
// that the network rejected or lost the previous transaction and allow a reuse. We set this time
|
||||
// interval to a minute.
|
||||
// We also verify that a full refresh UTXO operation started after this time point and has already
|
||||
// completed, in order to make sure that indeed this state reflects a state obtained following the required wait time.
|
||||
return s.startTimeOfLastCompletedRefresh.After(outpointBroadcastTime.Add(time.Minute))
|
||||
}
|
||||
|
||||
// updateUTXOSet clears the current UTXO set, and re-fills it with the given entries
|
||||
func (s *server) updateUTXOSet(entries []*appmessage.UTXOsByAddressesEntry, mempoolEntries []*appmessage.MempoolEntryByAddress) error {
|
||||
func (s *server) updateUTXOSet(entries []*appmessage.UTXOsByAddressesEntry, mempoolEntries []*appmessage.MempoolEntryByAddress, refreshStart time.Time) error {
|
||||
utxos := make([]*walletUTXO, 0, len(entries))
|
||||
|
||||
exclude := make(map[appmessage.RPCOutpoint]struct{})
|
||||
@ -228,11 +241,8 @@ func (s *server) updateUTXOSet(entries []*appmessage.UTXOsByAddressesEntry, memp
|
||||
}
|
||||
}
|
||||
|
||||
mempoolExcludedUTXOs := make(map[externalapi.DomainOutpoint]*walletUTXO)
|
||||
for _, entry := range entries {
|
||||
if _, ok := exclude[*entry.Outpoint]; ok {
|
||||
continue
|
||||
}
|
||||
|
||||
outpoint, err := appmessage.RPCOutpointToDomainOutpoint(entry.Outpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -243,45 +253,82 @@ func (s *server) updateUTXOSet(entries []*appmessage.UTXOsByAddressesEntry, memp
|
||||
return err
|
||||
}
|
||||
|
||||
// No need to lock for reading since the only writer of this set is on `syncLoop` on the same goroutine.
|
||||
address, ok := s.addressSet[entry.Address]
|
||||
if !ok {
|
||||
return errors.Errorf("Got result from address %s even though it wasn't requested", entry.Address)
|
||||
}
|
||||
utxos = append(utxos, &walletUTXO{
|
||||
|
||||
utxo := &walletUTXO{
|
||||
Outpoint: outpoint,
|
||||
UTXOEntry: utxoEntry,
|
||||
address: address,
|
||||
})
|
||||
}
|
||||
|
||||
if _, ok := exclude[*entry.Outpoint]; ok {
|
||||
mempoolExcludedUTXOs[*outpoint] = utxo
|
||||
} else {
|
||||
utxos = append(utxos, &walletUTXO{
|
||||
Outpoint: outpoint,
|
||||
UTXOEntry: utxoEntry,
|
||||
address: address,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
sort.Slice(utxos, func(i, j int) bool { return utxos[i].UTXOEntry.Amount() > utxos[j].UTXOEntry.Amount() })
|
||||
|
||||
s.lock.Lock()
|
||||
s.startTimeOfLastCompletedRefresh = refreshStart
|
||||
s.utxosSortedByAmount = utxos
|
||||
s.mempoolExcludedUTXOs = mempoolExcludedUTXOs
|
||||
|
||||
// Cleanup expired used outpoints to avoid a memory leak
|
||||
for outpoint, broadcastTime := range s.usedOutpoints {
|
||||
if s.usedOutpointHasExpired(broadcastTime) {
|
||||
delete(s.usedOutpoints, outpoint)
|
||||
}
|
||||
}
|
||||
s.lock.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *server) refreshUTXOs() error {
|
||||
refreshStart := time.Now()
|
||||
|
||||
// No need to lock for reading since the only writer of this set is on `syncLoop` on the same goroutine.
|
||||
addresses := s.addressSet.strings()
|
||||
// It's important to check the mempool before calling `GetUTXOsByAddresses`:
|
||||
// If we would do it the other way around an output can be spent in the mempool
|
||||
// and not in consensus, and between the calls its spending transaction will be
|
||||
// added to consensus and removed from the mempool, so `getUTXOsByAddressesResponse`
|
||||
// will include an obsolete output.
|
||||
mempoolEntriesByAddresses, err := s.rpcClient.GetMempoolEntriesByAddresses(s.addressSet.strings(), true, true)
|
||||
mempoolEntriesByAddresses, err := s.backgroundRPCClient.GetMempoolEntriesByAddresses(addresses, true, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
getUTXOsByAddressesResponse, err := s.rpcClient.GetUTXOsByAddresses(s.addressSet.strings())
|
||||
getUTXOsByAddressesResponse, err := s.backgroundRPCClient.GetUTXOsByAddresses(addresses)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.updateUTXOSet(getUTXOsByAddressesResponse.Entries, mempoolEntriesByAddresses.Entries)
|
||||
return s.updateUTXOSet(getUTXOsByAddressesResponse.Entries, mempoolEntriesByAddresses.Entries, refreshStart)
|
||||
}
|
||||
|
||||
func (s *server) forceSync() {
|
||||
// Technically if two callers check the `if` simultaneously they will both spawn a
|
||||
// goroutine, but we don't care about the small redundancy in such a rare case.
|
||||
if len(s.forceSyncChan) == 0 {
|
||||
go func() {
|
||||
s.forceSyncChan <- struct{}{}
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
func (s *server) isSynced() bool {
|
||||
return s.nextSyncStartIndex > s.maxUsedIndex()
|
||||
return s.nextSyncStartIndex > s.maxUsedIndex() && s.firstSyncDone.Load()
|
||||
}
|
||||
|
||||
func (s *server) formatSyncStateReport() string {
|
||||
@ -291,8 +338,11 @@ func (s *server) formatSyncStateReport() string {
|
||||
maxUsedIndex = s.nextSyncStartIndex
|
||||
}
|
||||
|
||||
return fmt.Sprintf("scanned %d out of %d addresses (%.2f%%)",
|
||||
s.nextSyncStartIndex, maxUsedIndex, float64(s.nextSyncStartIndex)*100.0/float64(maxUsedIndex))
|
||||
if s.nextSyncStartIndex < s.maxUsedIndex() {
|
||||
return fmt.Sprintf("scanned %d out of %d addresses (%.2f%%)",
|
||||
s.nextSyncStartIndex, maxUsedIndex, float64(s.nextSyncStartIndex)*100.0/float64(maxUsedIndex))
|
||||
}
|
||||
return "loading the wallet UTXO set"
|
||||
}
|
||||
|
||||
func (s *server) updateSyncingProgressLog(currProcessedAddresses, currMaxUsedAddresses uint32) {
|
||||
@ -311,7 +361,7 @@ func (s *server) updateSyncingProgressLog(currProcessedAddresses, currMaxUsedAdd
|
||||
|
||||
if s.maxProcessedAddressesForLog >= s.maxUsedAddressesForLog {
|
||||
if !s.isLogFinalProgressLineShown {
|
||||
log.Infof("Wallet is synced, ready for queries")
|
||||
log.Infof("Finished scanning recent addresses")
|
||||
s.isLogFinalProgressLineShown = true
|
||||
}
|
||||
} else {
|
||||
|
@ -1,4 +1,4 @@
|
||||
package main
|
||||
package server
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
@ -9,7 +9,7 @@ import (
|
||||
// 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 {
|
||||
func EncodeTransactionsToHex(transactions [][]byte) string {
|
||||
transactionsInHex := make([]string, len(transactions))
|
||||
for i, transaction := range transactions {
|
||||
transactionsInHex[i] = hex.EncodeToString(transaction)
|
||||
@ -17,7 +17,7 @@ func encodeTransactionsToHex(transactions [][]byte) string {
|
||||
return strings.Join(transactionsInHex, hexTransactionsSeparator)
|
||||
}
|
||||
|
||||
func decodeTransactionsFromHex(transactionsHex string) ([][]byte, error) {
|
||||
func DecodeTransactionsFromHex(transactionsHex string) ([][]byte, error) {
|
||||
splitTransactionsHexes := strings.Split(transactionsHex, hexTransactionsSeparator)
|
||||
transactions := make([][]byte, len(splitTransactionsHexes))
|
||||
|
16
cmd/kaspawallet/daemon/server/version.go
Normal file
16
cmd/kaspawallet/daemon/server/version.go
Normal file
@ -0,0 +1,16 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/pb"
|
||||
"github.com/kaspanet/kaspad/version"
|
||||
)
|
||||
|
||||
func (s *server) GetVersion(_ context.Context, _ *pb.GetVersionRequest) (*pb.GetVersionResponse, error) {
|
||||
s.lock.RLock()
|
||||
defer s.lock.RUnlock()
|
||||
|
||||
return &pb.GetVersionResponse{
|
||||
Version: version.Version(),
|
||||
}, nil
|
||||
}
|
29
cmd/kaspawallet/docker/Dockerfile
Normal file
29
cmd/kaspawallet/docker/Dockerfile
Normal file
@ -0,0 +1,29 @@
|
||||
# -- multistage docker build: stage #1: build stage
|
||||
FROM golang:1.23-alpine AS build
|
||||
|
||||
RUN mkdir -p /go/src/github.com/kaspanet/kaspad
|
||||
|
||||
WORKDIR /go/src/github.com/kaspanet/kaspad
|
||||
|
||||
RUN apk add --no-cache curl git openssh binutils gcc musl-dev
|
||||
|
||||
COPY go.mod .
|
||||
COPY go.sum .
|
||||
|
||||
RUN go mod download
|
||||
|
||||
COPY . .
|
||||
|
||||
WORKDIR /go/src/github.com/kaspanet/kaspad/cmd/kaspawallet
|
||||
RUN GOOS=linux go build -a -installsuffix cgo -o kaspawallet .
|
||||
|
||||
# --- multistage docker build: stage #2: runtime image
|
||||
FROM alpine
|
||||
WORKDIR /app
|
||||
|
||||
RUN apk add --no-cache ca-certificates tini
|
||||
|
||||
COPY --from=build /go/src/github.com/kaspanet/kaspad/cmd/kaspawallet/kaspawallet /app/
|
||||
|
||||
USER nobody
|
||||
ENTRYPOINT [ "/sbin/tini", "--" ]
|
26
cmd/kaspawallet/get_daemon_version.go
Normal file
26
cmd/kaspawallet/get_daemon_version.go
Normal file
@ -0,0 +1,26 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/client"
|
||||
"github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/pb"
|
||||
)
|
||||
|
||||
func getDaemonVersion(conf *getDaemonVersionConfig) error {
|
||||
daemonClient, tearDown, err := client.Connect(conf.DaemonAddress)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tearDown()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), daemonTimeout)
|
||||
defer cancel()
|
||||
response, err := daemonClient.GetVersion(ctx, &pb.GetVersionRequest{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println(response.Version)
|
||||
|
||||
return nil
|
||||
}
|
@ -6,7 +6,7 @@
|
||||
Package base58 provides an API for working with modified base58 and Base58Check
|
||||
encodings.
|
||||
|
||||
Modified Base58 Encoding
|
||||
# Modified Base58 Encoding
|
||||
|
||||
Standard base58 encoding is similar to standard base64 encoding except, as the
|
||||
name implies, it uses a 58 character alphabet which results in an alphanumeric
|
||||
@ -17,7 +17,7 @@ The modified base58 alphabet used by Bitcoin, and hence this package, omits the
|
||||
0, O, I, and l characters that look the same in many fonts and are therefore
|
||||
hard to humans to distinguish.
|
||||
|
||||
Base58Check Encoding Scheme
|
||||
# Base58Check Encoding Scheme
|
||||
|
||||
The Base58Check encoding scheme is primarily used for Bitcoin addresses at the
|
||||
time of this writing, however it can be used to generically encode arbitrary
|
||||
|
@ -10,7 +10,7 @@ import (
|
||||
"github.com/kaspanet/kaspad/domain/consensus/utils/utxo"
|
||||
)
|
||||
|
||||
//KaspawalletdUTXOsTolibkaspawalletUTXOs converts a []*pb.UtxosByAddressesEntry to a []*libkaspawallet.UTXO
|
||||
// KaspawalletdUTXOsTolibkaspawalletUTXOs converts a []*pb.UtxosByAddressesEntry to a []*libkaspawallet.UTXO
|
||||
func KaspawalletdUTXOsTolibkaspawalletUTXOs(kaspawalletdUtxoEntires []*pb.UtxosByAddressesEntry) ([]*UTXO, error) {
|
||||
UTXOs := make([]*UTXO, len(kaspawalletdUtxoEntires))
|
||||
for i, entry := range kaspawalletdUtxoEntires {
|
||||
|
@ -1,7 +1,7 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.28.0
|
||||
// protoc v3.17.2
|
||||
// protoc-gen-go v1.26.0
|
||||
// protoc v3.21.12
|
||||
// source: wallet.proto
|
||||
|
||||
package protoserialization
|
||||
|
@ -88,7 +88,7 @@ func SerializePartiallySignedTransaction(partiallySignedTransaction *PartiallySi
|
||||
return proto.Marshal(partiallySignedTransactionToProto(partiallySignedTransaction))
|
||||
}
|
||||
|
||||
//DeserializeDomainTransaction Deserialize a Transaction to an *externalapi.DomainTransaction
|
||||
// DeserializeDomainTransaction Deserialize a Transaction to an *externalapi.DomainTransaction
|
||||
func DeserializeDomainTransaction(serializedTransactionMessage []byte) (*externalapi.DomainTransaction, error) {
|
||||
protoTransactionMessage := &protoserialization.TransactionMessage{}
|
||||
err := proto.Unmarshal(serializedTransactionMessage, protoTransactionMessage)
|
||||
|
@ -7,6 +7,7 @@ import (
|
||||
"github.com/kaspanet/kaspad/domain/consensus/utils/constants"
|
||||
"github.com/kaspanet/kaspad/domain/consensus/utils/subnetworks"
|
||||
"github.com/kaspanet/kaspad/domain/consensus/utils/txscript"
|
||||
"github.com/kaspanet/kaspad/domain/consensus/utils/utxo"
|
||||
"github.com/kaspanet/kaspad/util"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
@ -31,15 +32,10 @@ func CreateUnsignedTransaction(
|
||||
extendedPublicKeys []string,
|
||||
minimumSignatures uint32,
|
||||
payments []*Payment,
|
||||
selectedUTXOs []*UTXO) ([]byte, error) {
|
||||
selectedUTXOs []*UTXO) (*serialization.PartiallySignedTransaction, error) {
|
||||
|
||||
sortPublicKeys(extendedPublicKeys)
|
||||
unsignedTransaction, err := createUnsignedTransaction(extendedPublicKeys, minimumSignatures, payments, selectedUTXOs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return serialization.SerializePartiallySignedTransaction(unsignedTransaction)
|
||||
return createUnsignedTransaction(extendedPublicKeys, minimumSignatures, payments, selectedUTXOs)
|
||||
}
|
||||
|
||||
func multiSigRedeemScript(extendedPublicKeys []string, minimumSignatures uint32, path string, ecdsa bool) ([]byte, error) {
|
||||
@ -247,6 +243,12 @@ func ExtractTransactionDeserialized(partiallySignedTransaction *serialization.Pa
|
||||
}
|
||||
partiallySignedTransaction.Tx.Inputs[i].SignatureScript = sigScript
|
||||
}
|
||||
partiallySignedTransaction.Tx.Inputs[i].UTXOEntry = utxo.NewUTXOEntry(
|
||||
input.PrevOutput.Value,
|
||||
input.PrevOutput.ScriptPublicKey,
|
||||
false, // This is a fake value
|
||||
0, // This is a fake value
|
||||
)
|
||||
}
|
||||
return partiallySignedTransaction.Tx, nil
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ package libkaspawallet_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/kaspanet/kaspad/cmd/kaspawallet/libkaspawallet/serialization"
|
||||
"github.com/kaspanet/kaspad/domain/consensus/utils/constants"
|
||||
"strings"
|
||||
"testing"
|
||||
@ -26,6 +27,20 @@ func forSchnorrAndECDSA(t *testing.T, testFunc func(t *testing.T, ecdsa bool)) {
|
||||
})
|
||||
}
|
||||
|
||||
func createUnsignedTransactionSerialized(
|
||||
extendedPublicKeys []string,
|
||||
minimumSignatures uint32,
|
||||
payments []*libkaspawallet.Payment,
|
||||
selectedUTXOs []*libkaspawallet.UTXO) ([]byte, error) {
|
||||
|
||||
tx, err := libkaspawallet.CreateUnsignedTransaction(extendedPublicKeys, minimumSignatures, payments, selectedUTXOs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return serialization.SerializePartiallySignedTransaction(tx)
|
||||
}
|
||||
|
||||
func TestMultisig(t *testing.T) {
|
||||
testutils.ForAllNets(t, true, func(t *testing.T, consensusConfig *consensus.Config) {
|
||||
params := &consensusConfig.Params
|
||||
@ -102,7 +117,7 @@ func TestMultisig(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
unsignedTransaction, err := libkaspawallet.CreateUnsignedTransaction(publicKeys, minimumSignatures,
|
||||
unsignedTransaction, err := createUnsignedTransactionSerialized(publicKeys, minimumSignatures,
|
||||
[]*libkaspawallet.Payment{{
|
||||
Address: address,
|
||||
Amount: 10,
|
||||
@ -263,7 +278,7 @@ func TestP2PK(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
unsignedTransaction, err := libkaspawallet.CreateUnsignedTransaction(publicKeys, minimumSignatures,
|
||||
unsignedTransaction, err := createUnsignedTransactionSerialized(publicKeys, minimumSignatures,
|
||||
[]*libkaspawallet.Payment{{
|
||||
Address: address,
|
||||
Amount: 10,
|
||||
@ -425,7 +440,7 @@ func TestMaxSompi(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
unsignedTxWithLargeInputAmount, err := libkaspawallet.CreateUnsignedTransaction(publicKeys, minimumSignatures,
|
||||
unsignedTxWithLargeInputAmount, err := createUnsignedTransactionSerialized(publicKeys, minimumSignatures,
|
||||
[]*libkaspawallet.Payment{{
|
||||
Address: address,
|
||||
Amount: 10,
|
||||
@ -476,7 +491,7 @@ func TestMaxSompi(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
unsignedTxWithLargeInputAndOutputAmount, err := libkaspawallet.CreateUnsignedTransaction(publicKeys, minimumSignatures,
|
||||
unsignedTxWithLargeInputAndOutputAmount, err := createUnsignedTransactionSerialized(publicKeys, minimumSignatures,
|
||||
[]*libkaspawallet.Payment{{
|
||||
Address: address,
|
||||
Amount: 22e6 * constants.SompiPerKaspa,
|
||||
|
@ -1,10 +1,11 @@
|
||||
package main
|
||||
|
||||
import "github.com/pkg/errors"
|
||||
import (
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func main() {
|
||||
subCmd, config := parseCommandLine()
|
||||
|
||||
var err error
|
||||
switch subCmd {
|
||||
case createSubCmd:
|
||||
@ -19,6 +20,8 @@ func main() {
|
||||
err = sign(config.(*signConfig))
|
||||
case broadcastSubCmd:
|
||||
err = broadcast(config.(*broadcastConfig))
|
||||
case broadcastReplacementSubCmd:
|
||||
err = broadcastReplacement(config.(*broadcastConfig))
|
||||
case parseSubCmd:
|
||||
err = parse(config.(*parseConfig))
|
||||
case showAddressesSubCmd:
|
||||
@ -31,6 +34,14 @@ func main() {
|
||||
err = startDaemon(config.(*startDaemonConfig))
|
||||
case sweepSubCmd:
|
||||
err = sweep(config.(*sweepConfig))
|
||||
case versionSubCmd:
|
||||
showVersion()
|
||||
case getDaemonVersionSubCmd:
|
||||
err = getDaemonVersion(config.(*getDaemonVersionConfig))
|
||||
case bumpFeeSubCmd:
|
||||
err = bumpFee(config.(*bumpFeeConfig))
|
||||
case bumpFeeUnsignedSubCmd:
|
||||
err = bumpFeeUnsigned(config.(*bumpFeeUnsignedConfig))
|
||||
default:
|
||||
err = errors.Errorf("Unknown sub-command '%s'\n", subCmd)
|
||||
}
|
||||
|
@ -3,13 +3,17 @@ package main
|
||||
import (
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
|
||||
"github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/server"
|
||||
"github.com/kaspanet/kaspad/cmd/kaspawallet/keys"
|
||||
"github.com/kaspanet/kaspad/cmd/kaspawallet/libkaspawallet/serialization"
|
||||
"github.com/kaspanet/kaspad/domain/consensus/utils/consensushashing"
|
||||
"github.com/kaspanet/kaspad/domain/consensus/utils/constants"
|
||||
"github.com/kaspanet/kaspad/domain/consensus/utils/txscript"
|
||||
"github.com/kaspanet/kaspad/util/txmass"
|
||||
"github.com/pkg/errors"
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func parse(conf *parseConfig) error {
|
||||
@ -20,6 +24,11 @@ func parse(conf *parseConfig) error {
|
||||
return errors.Errorf("Both --transaction and --transaction-file cannot be passed at the same time")
|
||||
}
|
||||
|
||||
keysFile, err := keys.ReadKeysFile(conf.NetParams(), conf.KeysFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
transactionHex := conf.Transaction
|
||||
if conf.TransactionFile != "" {
|
||||
transactionHexBytes, err := ioutil.ReadFile(conf.TransactionFile)
|
||||
@ -29,10 +38,12 @@ func parse(conf *parseConfig) error {
|
||||
transactionHex = strings.TrimSpace(string(transactionHexBytes))
|
||||
}
|
||||
|
||||
transactions, err := decodeTransactionsFromHex(transactionHex)
|
||||
transactions, err := server.DecodeTransactionsFromHex(transactionHex)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
txMassCalculator := txmass.NewCalculator(conf.NetParams().MassPerTxByte, conf.NetParams().MassPerScriptPubKeyByte, conf.NetParams().MassPerSigOp)
|
||||
for i, transaction := range transactions {
|
||||
|
||||
partiallySignedTransaction, err := serialization.DeserializePartiallySignedTransaction(transaction)
|
||||
@ -78,7 +89,16 @@ func parse(conf *parseConfig) error {
|
||||
}
|
||||
fmt.Println()
|
||||
|
||||
fmt.Printf("Fee:\t%d Sompi\n\n", allInputSompi-allOutputSompi)
|
||||
fee := allInputSompi - allOutputSompi
|
||||
fmt.Printf("Fee:\t%d Sompi (%f KAS)\n", fee, float64(fee)/float64(constants.SompiPerKaspa))
|
||||
mass, err := server.EstimateMassAfterSignatures(partiallySignedTransaction, keysFile.ECDSA, keysFile.MinimumSignatures, txMassCalculator)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("Mass: %d grams\n", mass)
|
||||
feeRate := float64(fee) / float64(mass)
|
||||
fmt.Printf("Fee rate: %.2f Sompi/Gram\n", feeRate)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
@ -10,7 +10,7 @@ import (
|
||||
"github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/pb"
|
||||
"github.com/kaspanet/kaspad/cmd/kaspawallet/keys"
|
||||
"github.com/kaspanet/kaspad/cmd/kaspawallet/libkaspawallet"
|
||||
"github.com/kaspanet/kaspad/domain/consensus/utils/constants"
|
||||
"github.com/kaspanet/kaspad/cmd/kaspawallet/utils"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
@ -33,14 +33,40 @@ func send(conf *sendConfig) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), daemonTimeout)
|
||||
defer cancel()
|
||||
|
||||
sendAmountSompi := uint64(conf.SendAmount * constants.SompiPerKaspa)
|
||||
var sendAmountSompi uint64
|
||||
if !conf.IsSendAll {
|
||||
sendAmountSompi, err = utils.KasToSompi(conf.SendAmount)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
var feePolicy *pb.FeePolicy
|
||||
if conf.FeeRate > 0 {
|
||||
feePolicy = &pb.FeePolicy{
|
||||
FeePolicy: &pb.FeePolicy_ExactFeeRate{
|
||||
ExactFeeRate: conf.FeeRate,
|
||||
},
|
||||
}
|
||||
} else if conf.MaxFeeRate > 0 {
|
||||
feePolicy = &pb.FeePolicy{
|
||||
FeePolicy: &pb.FeePolicy_MaxFeeRate{MaxFeeRate: conf.MaxFeeRate},
|
||||
}
|
||||
} else if conf.MaxFee > 0 {
|
||||
feePolicy = &pb.FeePolicy{
|
||||
FeePolicy: &pb.FeePolicy_MaxFee{MaxFee: conf.MaxFee},
|
||||
}
|
||||
}
|
||||
|
||||
createUnsignedTransactionsResponse, err :=
|
||||
daemonClient.CreateUnsignedTransactions(ctx, &pb.CreateUnsignedTransactionsRequest{
|
||||
From: conf.FromAddresses,
|
||||
Address: conf.ToAddress,
|
||||
Amount: sendAmountSompi,
|
||||
IsSendAll: conf.IsSendAll,
|
||||
UseExistingChangeAddress: conf.UseExistingChangeAddress,
|
||||
FeePolicy: feePolicy,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
@ -67,23 +93,29 @@ func send(conf *sendConfig) error {
|
||||
signedTransactions[i] = signedTransaction
|
||||
}
|
||||
|
||||
if len(signedTransactions) > 1 {
|
||||
fmt.Printf("Broadcasting %d transactions\n", len(signedTransactions))
|
||||
}
|
||||
|
||||
fmt.Printf("Broadcasting %d transaction(s)\n", len(signedTransactions))
|
||||
// Since we waited for user input when getting the password, which could take unbound amount of time -
|
||||
// create a new context for broadcast, to reset the timeout.
|
||||
broadcastCtx, broadcastCancel := context.WithTimeout(context.Background(), daemonTimeout)
|
||||
defer broadcastCancel()
|
||||
|
||||
response, err := daemonClient.Broadcast(broadcastCtx, &pb.BroadcastRequest{Transactions: signedTransactions})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println("Transactions were sent successfully")
|
||||
fmt.Println("Transaction ID(s): ")
|
||||
for _, txID := range response.TxIDs {
|
||||
fmt.Printf("\t%s\n", txID)
|
||||
const chunkSize = 100 // To avoid sending a message bigger than the gRPC max message size, we split it to chunks
|
||||
for offset := 0; offset < len(signedTransactions); offset += chunkSize {
|
||||
end := len(signedTransactions)
|
||||
if offset+chunkSize <= len(signedTransactions) {
|
||||
end = offset + chunkSize
|
||||
}
|
||||
|
||||
chunk := signedTransactions[offset:end]
|
||||
response, err := daemonClient.Broadcast(broadcastCtx, &pb.BroadcastRequest{Transactions: chunk})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Printf("Broadcasted %d transaction(s) (broadcasted %.2f%% of the transactions so far)\n", len(chunk), 100*float64(end)/float64(len(signedTransactions)))
|
||||
fmt.Println("Broadcasted Transaction ID(s): ")
|
||||
for _, txID := range response.TxIDs {
|
||||
fmt.Printf("\t%s\n", txID)
|
||||
}
|
||||
}
|
||||
|
||||
if conf.Verbose {
|
||||
|
@ -6,6 +6,7 @@ import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/server"
|
||||
"github.com/kaspanet/kaspad/cmd/kaspawallet/keys"
|
||||
"github.com/kaspanet/kaspad/cmd/kaspawallet/libkaspawallet"
|
||||
"github.com/pkg/errors"
|
||||
@ -40,7 +41,7 @@ func sign(conf *signConfig) error {
|
||||
}
|
||||
transactionsHex = strings.TrimSpace(string(transactionHexBytes))
|
||||
}
|
||||
partiallySignedTransactions, err := decodeTransactionsFromHex(transactionsHex)
|
||||
partiallySignedTransactions, err := server.DecodeTransactionsFromHex(transactionsHex)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -72,6 +73,6 @@ func sign(conf *signConfig) error {
|
||||
fmt.Fprintln(os.Stderr, "Successfully signed transaction")
|
||||
}
|
||||
|
||||
fmt.Println(encodeTransactionsToHex(updatedPartiallySignedTransactions))
|
||||
fmt.Println(server.EncodeTransactionsToHex(updatedPartiallySignedTransactions))
|
||||
return nil
|
||||
}
|
||||
|
@ -2,8 +2,13 @@ package utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/kaspanet/kaspad/domain/consensus/utils/constants"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// FormatKas takes the amount of sompis as uint64, and returns amount of KAS with 8 decimal places
|
||||
@ -14,3 +19,50 @@ func FormatKas(amount uint64) string {
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// KasToSompi takes in a string representation of the Kas value to convert to Sompi
|
||||
func KasToSompi(amount string) (uint64, error) {
|
||||
err := validateKASAmountFormat(amount)
|
||||
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
// after validation, amount can only be either an int OR
|
||||
// a float with an int component and decimal places
|
||||
parts := strings.Split(amount, ".")
|
||||
amountStr := ""
|
||||
|
||||
if constants.SompiPerKaspa%10 != 0 {
|
||||
return 0, errors.Errorf("Unable to convert to sompi when SompiPerKaspa is not a multiple of 10")
|
||||
}
|
||||
|
||||
decimalPlaces := int(math.Log10(constants.SompiPerKaspa))
|
||||
decimalStr := ""
|
||||
|
||||
if len(parts) == 2 {
|
||||
decimalStr = parts[1]
|
||||
}
|
||||
|
||||
amountStr = fmt.Sprintf("%s%-*s", parts[0], decimalPlaces, decimalStr) // Padded with spaces at the end to fill for missing decimals: Sample "0.01234 "
|
||||
amountStr = strings.ReplaceAll(amountStr, " ", "0") // Make the spaces be 0s. Sample "0.012340000"
|
||||
|
||||
convertedAmount, err := strconv.ParseUint(amountStr, 10, 64)
|
||||
|
||||
return convertedAmount, err
|
||||
}
|
||||
|
||||
func validateKASAmountFormat(amount string) error {
|
||||
// Check whether it's an integer, or a float with max 8 digits
|
||||
match, err := regexp.MatchString("^([1-9]\\d{0,11}|0)(\\.\\d{0,8})?$", amount)
|
||||
|
||||
if !match {
|
||||
return errors.Errorf("Invalid amount")
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
90
cmd/kaspawallet/utils/format_kas_test.go
Normal file
90
cmd/kaspawallet/utils/format_kas_test.go
Normal file
@ -0,0 +1,90 @@
|
||||
package utils
|
||||
|
||||
import "testing"
|
||||
|
||||
// Takes in a string representation of the Kas value to convert to Sompi
|
||||
func TestKasToSompi(t *testing.T) {
|
||||
type testVector struct {
|
||||
originalAmount string
|
||||
convertedAmount uint64
|
||||
}
|
||||
|
||||
validCases := []testVector{
|
||||
{originalAmount: "0", convertedAmount: 0},
|
||||
{originalAmount: "1", convertedAmount: 100000000},
|
||||
{originalAmount: "33184.1489732", convertedAmount: 3318414897320},
|
||||
{originalAmount: "21.35808032", convertedAmount: 2135808032},
|
||||
{originalAmount: "184467440737.09551615", convertedAmount: 18446744073709551615},
|
||||
}
|
||||
|
||||
for _, currentTestVector := range validCases {
|
||||
convertedAmount, err := KasToSompi(currentTestVector.originalAmount)
|
||||
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
} else if convertedAmount != currentTestVector.convertedAmount {
|
||||
t.Errorf("Expected %s, to convert to %d. Got: %d", currentTestVector.originalAmount, currentTestVector.convertedAmount, convertedAmount)
|
||||
}
|
||||
}
|
||||
|
||||
invalidCases := []string{
|
||||
"184467440737.09551616", // Bigger than max uint64
|
||||
"-1",
|
||||
"a",
|
||||
"",
|
||||
}
|
||||
|
||||
for _, currentTestVector := range invalidCases {
|
||||
_, err := KasToSompi(currentTestVector)
|
||||
|
||||
if err == nil {
|
||||
t.Errorf("Expected an error but succeeded validation for test case %s", currentTestVector)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateAmountFormat(t *testing.T) {
|
||||
validCases := []string{
|
||||
"0",
|
||||
"1",
|
||||
"1.0",
|
||||
"0.1",
|
||||
"0.12345678",
|
||||
"111111111111.11111111", // 12 digits to the left of decimal, 8 digits to the right
|
||||
"184467440737.09551615", // Maximum input that can be represented in sompi later
|
||||
"184467440737.09551616", // Cannot be represented in sompi, but we'll acccept for "correct format"
|
||||
"999999999999.99999999", // Cannot be represented in sompi, but we'll acccept for "correct format"
|
||||
}
|
||||
|
||||
for _, testCase := range validCases {
|
||||
err := validateKASAmountFormat(testCase)
|
||||
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
invalidCases := []string{
|
||||
"",
|
||||
"a",
|
||||
"-1",
|
||||
"0.123456789", // 9 decimal digits
|
||||
".1", // decimal but no integer component
|
||||
"0a", // Extra character
|
||||
"0000000000000", // 13 zeros
|
||||
"012", // Int padded with zero
|
||||
"00.1", // Decimal padded with zeros
|
||||
"111111111111111111111", // all digits
|
||||
"111111111111A11111111", // non-period/non-digit where decimal would be
|
||||
"000000000000.00000000", // all zeros
|
||||
"kaspa", // all text
|
||||
}
|
||||
|
||||
for _, testCase := range invalidCases {
|
||||
err := validateKASAmountFormat(testCase)
|
||||
|
||||
if err == nil {
|
||||
t.Errorf("Expected an error but succeeded validation for test case %s", testCase)
|
||||
}
|
||||
}
|
||||
}
|
15
cmd/kaspawallet/version.go
Normal file
15
cmd/kaspawallet/version.go
Normal file
@ -0,0 +1,15 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/kaspanet/kaspad/version"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func showVersion() {
|
||||
appName := filepath.Base(os.Args[0])
|
||||
appName = strings.TrimSuffix(appName, filepath.Ext(appName))
|
||||
fmt.Println(appName, "version", version.Version())
|
||||
}
|
6
doc.go
6
doc.go
@ -13,10 +13,12 @@ the box' for most users. However, there are also a wide variety of flags that
|
||||
can be used to control it.
|
||||
|
||||
Usage:
|
||||
kaspad [OPTIONS]
|
||||
|
||||
kaspad [OPTIONS]
|
||||
|
||||
For an up-to-date help message:
|
||||
kaspad --help
|
||||
|
||||
kaspad --help
|
||||
|
||||
The long form of all option flags (except -C) can be specified in a configuration
|
||||
file that is automatically parsed when kaspad starts up. By default, the
|
||||
|
@ -1,5 +1,5 @@
|
||||
# -- multistage docker build: stage #1: build stage
|
||||
FROM golang:1.18-alpine AS build
|
||||
FROM golang:1.23-alpine AS build
|
||||
|
||||
RUN mkdir -p /go/src/github.com/kaspanet/kaspad
|
||||
|
||||
|
@ -329,6 +329,7 @@ func initTestBlockAcceptanceDataForClone() []*externalapi.BlockAcceptanceData {
|
||||
[]byte{0x01},
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
externalapi.NewDomainTransactionIDFromByteArray(&[externalapi.DomainHashSize]byte{
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
@ -377,6 +378,7 @@ func iniBlockAcceptanceDataForEqual() []testBlockAcceptanceDataStruct {
|
||||
[]byte{0x01},
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
externalapi.NewDomainTransactionIDFromByteArray(&[externalapi.DomainHashSize]byte{
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
@ -410,6 +412,7 @@ func iniBlockAcceptanceDataForEqual() []testBlockAcceptanceDataStruct {
|
||||
[]byte{0x01},
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
externalapi.NewDomainTransactionIDFromByteArray(&[externalapi.DomainHashSize]byte{
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
@ -443,6 +446,7 @@ func iniBlockAcceptanceDataForEqual() []testBlockAcceptanceDataStruct {
|
||||
[]byte{0x01},
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
externalapi.NewDomainTransactionIDFromByteArray(&[externalapi.DomainHashSize]byte{
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
@ -476,6 +480,7 @@ func iniBlockAcceptanceDataForEqual() []testBlockAcceptanceDataStruct {
|
||||
[]byte{0x01},
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
externalapi.NewDomainTransactionIDFromByteArray(&[externalapi.DomainHashSize]byte{
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
@ -510,6 +515,7 @@ func iniBlockAcceptanceDataForEqual() []testBlockAcceptanceDataStruct {
|
||||
[]byte{0x01},
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
externalapi.NewDomainTransactionIDFromByteArray(&[externalapi.DomainHashSize]byte{
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
@ -614,6 +620,7 @@ func initTestAcceptanceDataForClone() []externalapi.AcceptanceData {
|
||||
[]byte{0x01},
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
externalapi.NewDomainTransactionIDFromByteArray(&[externalapi.DomainHashSize]byte{
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
@ -664,6 +671,7 @@ func initAcceptanceDataForEqual() []testAcceptanceDataStruct {
|
||||
[]byte{0x01},
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
externalapi.NewDomainTransactionIDFromByteArray(&[externalapi.DomainHashSize]byte{
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
@ -697,6 +705,7 @@ func initAcceptanceDataForEqual() []testAcceptanceDataStruct {
|
||||
[]byte{0x01},
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
externalapi.NewDomainTransactionIDFromByteArray(&[externalapi.DomainHashSize]byte{
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
@ -730,6 +739,7 @@ func initAcceptanceDataForEqual() []testAcceptanceDataStruct {
|
||||
[]byte{0x01},
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
externalapi.NewDomainTransactionIDFromByteArray(&[externalapi.DomainHashSize]byte{
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
@ -763,6 +773,7 @@ func initAcceptanceDataForEqual() []testAcceptanceDataStruct {
|
||||
[]byte{0x01},
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
externalapi.NewDomainTransactionIDFromByteArray(&[externalapi.DomainHashSize]byte{
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
|
@ -10,10 +10,12 @@ package externalapi
|
||||
//
|
||||
// For example, assume a selected parent chain with IDs as depicted below, and the
|
||||
// stop block is genesis:
|
||||
// genesis -> 1 -> 2 -> ... -> 15 -> 16 -> 17 -> 18
|
||||
//
|
||||
// genesis -> 1 -> 2 -> ... -> 15 -> 16 -> 17 -> 18
|
||||
//
|
||||
// The block locator for block 17 would be the hashes of blocks:
|
||||
// [17 16 14 11 7 2 genesis]
|
||||
//
|
||||
// [17 16 14 11 7 2 genesis]
|
||||
type BlockLocator []*DomainHash
|
||||
|
||||
// Clone returns a clone of BlockLocator
|
||||
|
@ -18,8 +18,9 @@ type DomainTransaction struct {
|
||||
Gas uint64
|
||||
Payload []byte
|
||||
|
||||
Fee uint64
|
||||
Mass uint64
|
||||
Fee uint64
|
||||
Mass uint64
|
||||
MassCommitment uint64
|
||||
|
||||
// ID is a field that is used to cache the transaction ID.
|
||||
// Always use consensushashing.TransactionID instead of accessing this field directly
|
||||
@ -47,23 +48,24 @@ func (tx *DomainTransaction) Clone() *DomainTransaction {
|
||||
}
|
||||
|
||||
return &DomainTransaction{
|
||||
Version: tx.Version,
|
||||
Inputs: inputsClone,
|
||||
Outputs: outputsClone,
|
||||
LockTime: tx.LockTime,
|
||||
SubnetworkID: *tx.SubnetworkID.Clone(),
|
||||
Gas: tx.Gas,
|
||||
Payload: payloadClone,
|
||||
Fee: tx.Fee,
|
||||
Mass: tx.Mass,
|
||||
ID: idClone,
|
||||
Version: tx.Version,
|
||||
Inputs: inputsClone,
|
||||
Outputs: outputsClone,
|
||||
LockTime: tx.LockTime,
|
||||
SubnetworkID: *tx.SubnetworkID.Clone(),
|
||||
Gas: tx.Gas,
|
||||
Payload: payloadClone,
|
||||
Fee: tx.Fee,
|
||||
Mass: tx.Mass,
|
||||
MassCommitment: tx.MassCommitment,
|
||||
ID: idClone,
|
||||
}
|
||||
}
|
||||
|
||||
// If this doesn't compile, it means the type definition has been changed, so it's
|
||||
// an indication to update Equal and Clone accordingly.
|
||||
var _ = DomainTransaction{0, []*DomainTransactionInput{}, []*DomainTransactionOutput{}, 0,
|
||||
DomainSubnetworkID{}, 0, []byte{}, 0, 0,
|
||||
DomainSubnetworkID{}, 0, []byte{}, 0, 0, 0,
|
||||
&DomainTransactionID{}}
|
||||
|
||||
// Equal returns whether tx equals to other
|
||||
@ -112,6 +114,10 @@ func (tx *DomainTransaction) Equal(other *DomainTransaction) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
if tx.MassCommitment != other.MassCommitment {
|
||||
return false
|
||||
}
|
||||
|
||||
if tx.Fee != 0 && other.Fee != 0 && tx.Fee != other.Fee {
|
||||
panic(errors.New("identical transactions should always have the same fee"))
|
||||
}
|
||||
|
@ -83,6 +83,7 @@ func initTestBaseTransaction() *externalapi.DomainTransaction {
|
||||
[]byte{0x01},
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
externalapi.NewDomainTransactionIDFromByteArray(&[externalapi.DomainHashSize]byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
@ -112,6 +113,7 @@ func initTestTransactionToCompare() []*transactionToCompare {
|
||||
[]byte{0x01},
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
externalapi.NewDomainTransactionIDFromByteArray(&[externalapi.DomainHashSize]byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
@ -137,6 +139,7 @@ func initTestTransactionToCompare() []*transactionToCompare {
|
||||
[]byte{0x01},
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
externalapi.NewDomainTransactionIDFromByteArray(&[externalapi.DomainHashSize]byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
@ -162,6 +165,7 @@ func initTestTransactionToCompare() []*transactionToCompare {
|
||||
[]byte{0x01, 0x02}, //Changed
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
externalapi.NewDomainTransactionIDFromByteArray(&[externalapi.DomainHashSize]byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
@ -186,6 +190,7 @@ func initTestTransactionToCompare() []*transactionToCompare {
|
||||
[]byte{0x01},
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
externalapi.NewDomainTransactionIDFromByteArray(&[externalapi.DomainHashSize]byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
@ -213,6 +218,7 @@ func initTestTransactionToCompare() []*transactionToCompare {
|
||||
[]byte{0x01},
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
externalapi.NewDomainTransactionIDFromByteArray(&[externalapi.DomainHashSize]byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
@ -239,6 +245,7 @@ func initTestTransactionToCompare() []*transactionToCompare {
|
||||
[]byte{0x01},
|
||||
1000000000, //Changed
|
||||
1,
|
||||
0,
|
||||
externalapi.NewDomainTransactionIDFromByteArray(&[externalapi.DomainHashSize]byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
@ -263,6 +270,7 @@ func initTestTransactionToCompare() []*transactionToCompare {
|
||||
[]byte{0x01},
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
externalapi.NewDomainTransactionIDFromByteArray(&[externalapi.DomainHashSize]byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
@ -287,6 +295,7 @@ func initTestTransactionToCompare() []*transactionToCompare {
|
||||
[]byte{0x01},
|
||||
0,
|
||||
2, //Changed
|
||||
0,
|
||||
externalapi.NewDomainTransactionIDFromByteArray(&[externalapi.DomainHashSize]byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
@ -311,6 +320,7 @@ func initTestTransactionToCompare() []*transactionToCompare {
|
||||
[]byte{0x01},
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
externalapi.NewDomainTransactionIDFromByteArray(&[externalapi.DomainHashSize]byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
@ -341,6 +351,7 @@ func initTestTransactionToCompare() []*transactionToCompare {
|
||||
[]byte{0x01},
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
externalapi.NewDomainTransactionIDFromByteArray(&[externalapi.DomainHashSize]byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
@ -366,6 +377,7 @@ func initTestTransactionToCompare() []*transactionToCompare {
|
||||
[]byte{0x01},
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
externalapi.NewDomainTransactionIDFromByteArray(&[externalapi.DomainHashSize]byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
@ -390,6 +402,7 @@ func initTestTransactionToCompare() []*transactionToCompare {
|
||||
[]byte{0x01},
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
nil, //changed
|
||||
},
|
||||
expectedResult: true,
|
||||
@ -411,6 +424,7 @@ func initTestTransactionToCompare() []*transactionToCompare {
|
||||
[]byte{0x01},
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
externalapi.NewDomainTransactionIDFromByteArray(&[externalapi.DomainHashSize]byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
@ -435,6 +449,7 @@ func initTestTransactionToCompare() []*transactionToCompare {
|
||||
[]byte{0x01},
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
externalapi.NewDomainTransactionIDFromByteArray(&[externalapi.DomainHashSize]byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
@ -461,6 +476,34 @@ func initTestTransactionToCompare() []*transactionToCompare {
|
||||
[]byte{0x01},
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
externalapi.NewDomainTransactionIDFromByteArray(&[externalapi.DomainHashSize]byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02}),
|
||||
},
|
||||
expectedResult: false,
|
||||
},
|
||||
{
|
||||
tx: &externalapi.DomainTransaction{
|
||||
1,
|
||||
[]*externalapi.DomainTransactionInput{{externalapi.DomainOutpoint{
|
||||
*externalapi.NewDomainTransactionIDFromByteArray(&[externalapi.DomainHashSize]byte{0x01}), 0xFFFF},
|
||||
[]byte{1, 2, 3},
|
||||
uint64(0xFFFFFFFF),
|
||||
1,
|
||||
utxo.NewUTXOEntry(1, &externalapi.ScriptPublicKey{Script: []byte{0, 1, 2, 3}, Version: 0}, true, 2)}},
|
||||
[]*externalapi.DomainTransactionOutput{{uint64(0xFFFF),
|
||||
&externalapi.ScriptPublicKey{Script: []byte{1, 2}, Version: 0}},
|
||||
{uint64(0xFFFF),
|
||||
&externalapi.ScriptPublicKey{Script: []byte{1, 3}, Version: 0}}},
|
||||
1,
|
||||
externalapi.DomainSubnetworkID{0x01},
|
||||
1,
|
||||
[]byte{0x01},
|
||||
0,
|
||||
1,
|
||||
1, // Changed
|
||||
externalapi.NewDomainTransactionIDFromByteArray(&[externalapi.DomainHashSize]byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
@ -536,6 +579,7 @@ func initTestDomainTransactionForEqual() []testDomainTransactionStruct {
|
||||
[]byte{0x01},
|
||||
1,
|
||||
1,
|
||||
0,
|
||||
externalapi.NewDomainTransactionIDFromByteArray(&[externalapi.DomainHashSize]byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
@ -555,6 +599,7 @@ func initTestDomainTransactionForEqual() []testDomainTransactionStruct {
|
||||
[]byte{0x01},
|
||||
1,
|
||||
1,
|
||||
0,
|
||||
nil,
|
||||
},
|
||||
expectedResult: false,
|
||||
@ -569,6 +614,7 @@ func initTestDomainTransactionForEqual() []testDomainTransactionStruct {
|
||||
[]byte{0x01},
|
||||
1,
|
||||
1,
|
||||
0,
|
||||
nil,
|
||||
},
|
||||
expectedResult: true,
|
||||
@ -583,6 +629,7 @@ func initTestDomainTransactionForEqual() []testDomainTransactionStruct {
|
||||
[]byte{0x01},
|
||||
2, // Changed fee
|
||||
1,
|
||||
0,
|
||||
nil,
|
||||
},
|
||||
expectsPanic: true,
|
||||
|
@ -43,6 +43,9 @@ type TestConsensus interface {
|
||||
AddBlock(parentHashes []*externalapi.DomainHash, coinbaseData *externalapi.DomainCoinbaseData,
|
||||
transactions []*externalapi.DomainTransaction) (*externalapi.DomainHash, *externalapi.VirtualChangeSet, error)
|
||||
|
||||
AddBlockOnTips(coinbaseData *externalapi.DomainCoinbaseData,
|
||||
transactions []*externalapi.DomainTransaction) (*externalapi.DomainHash, *externalapi.VirtualChangeSet, error)
|
||||
|
||||
AddUTXOInvalidHeader(parentHashes []*externalapi.DomainHash) (*externalapi.DomainHash, *externalapi.VirtualChangeSet, error)
|
||||
|
||||
AddUTXOInvalidBlock(parentHashes []*externalapi.DomainHash) (*externalapi.DomainHash,
|
||||
|
@ -131,11 +131,12 @@ func (bb *blockBuilder) validateTransactions(stagingArea *model.StagingArea,
|
||||
for _, transaction := range transactions {
|
||||
err := bb.validateTransaction(stagingArea, transaction)
|
||||
if err != nil {
|
||||
if !errors.As(err, &ruleerrors.RuleError{}) {
|
||||
ruleError := ruleerrors.RuleError{}
|
||||
if !errors.As(err, &ruleError) {
|
||||
return err
|
||||
}
|
||||
invalidTransactions = append(invalidTransactions,
|
||||
ruleerrors.InvalidTransaction{Transaction: transaction, Error: err})
|
||||
ruleerrors.InvalidTransaction{Transaction: transaction, Error: &ruleError})
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -146,8 +146,8 @@ func (v *blockValidator) validateDifficulty(stagingArea *model.StagingArea,
|
||||
// target difficulty as claimed.
|
||||
//
|
||||
// The flags modify the behavior of this function as follows:
|
||||
// - BFNoPoWCheck: The check to ensure the block hash is less than the target
|
||||
// difficulty is not performed.
|
||||
// - BFNoPoWCheck: The check to ensure the block hash is less than the target
|
||||
// difficulty is not performed.
|
||||
func (v *blockValidator) checkProofOfWork(header externalapi.BlockHeader) error {
|
||||
// The target difficulty must be larger than zero.
|
||||
state := pow.NewState(header.ToMutable())
|
||||
|
@ -204,8 +204,8 @@ func (c *coinbaseManager) calcDeflationaryPeriodBlockSubsidy(blockDaaScore uint6
|
||||
}
|
||||
|
||||
/*
|
||||
This table was pre-calculated by calling `calcDeflationaryPeriodBlockSubsidyFloatCalc` for all months until reaching 0 subsidy.
|
||||
To regenerate this table, run `TestBuildSubsidyTable` in coinbasemanager_test.go (note the `deflationaryPhaseBaseSubsidy` therein)
|
||||
This table was pre-calculated by calling `calcDeflationaryPeriodBlockSubsidyFloatCalc` for all months until reaching 0 subsidy.
|
||||
To regenerate this table, run `TestBuildSubsidyTable` in coinbasemanager_test.go (note the `deflationaryPhaseBaseSubsidy` therein)
|
||||
*/
|
||||
var subsidyByDeflationaryMonthTable = []uint64{
|
||||
44000000000, 41530469757, 39199543598, 36999442271, 34922823143, 32962755691, 31112698372, 29366476791, 27718263097, 26162556530, 24694165062, 23308188075, 22000000000, 20765234878, 19599771799, 18499721135, 17461411571, 16481377845, 15556349186, 14683238395, 13859131548, 13081278265, 12347082531, 11654094037, 11000000000,
|
||||
|
@ -159,8 +159,9 @@ func TestDoubleSpends(t *testing.T) {
|
||||
// TestTransactionAcceptance checks that block transactions are accepted correctly when the merge set is sorted topologically.
|
||||
// DAG diagram:
|
||||
// genesis <- blockA <- blockB <- blockC <- ..(chain of k-blocks).. lastBlockInChain <- blockD <- blockE <- blockF <- blockG
|
||||
// ^ ^ |
|
||||
// | redBlock <------------------------ blueChildOfRedBlock <-------------------------------
|
||||
//
|
||||
// ^ ^ |
|
||||
// | redBlock <------------------------ blueChildOfRedBlock <-------------------------------
|
||||
func TestTransactionAcceptance(t *testing.T) {
|
||||
testutils.ForAllNets(t, true, func(t *testing.T, consensusConfig *consensus.Config) {
|
||||
stagingArea := model.NewStagingArea()
|
||||
|
@ -30,14 +30,14 @@ func (bg *blockGHOSTDAGData) toModel() *externalapi.BlockGHOSTDAGData {
|
||||
//
|
||||
// 1) |anticone-of-candidate-block ∩ blue-set-of-newBlock| ≤ K
|
||||
//
|
||||
// 2) For every blue block in blue-set-of-newBlock:
|
||||
// |(anticone-of-blue-block ∩ blue-set-newBlock) ∪ {candidate-block}| ≤ K.
|
||||
// We validate this condition by maintaining a map BluesAnticoneSizes for
|
||||
// each block which holds all the blue anticone sizes that were affected by
|
||||
// the new added blue blocks.
|
||||
// So to find out what is |anticone-of-blue ∩ blue-set-of-newBlock| we just iterate in
|
||||
// the selected parent chain of the new block until we find an existing entry in
|
||||
// BluesAnticoneSizes.
|
||||
// 2. For every blue block in blue-set-of-newBlock:
|
||||
// |(anticone-of-blue-block ∩ blue-set-newBlock) ∪ {candidate-block}| ≤ K.
|
||||
// We validate this condition by maintaining a map BluesAnticoneSizes for
|
||||
// each block which holds all the blue anticone sizes that were affected by
|
||||
// the new added blue blocks.
|
||||
// So to find out what is |anticone-of-blue ∩ blue-set-of-newBlock| we just iterate in
|
||||
// the selected parent chain of the new block until we find an existing entry in
|
||||
// BluesAnticoneSizes.
|
||||
//
|
||||
// For further details see the article https://eprint.iacr.org/2018/104.pdf
|
||||
func (gm *ghostdagManager) GHOSTDAG(stagingArea *model.StagingArea, blockHash *externalapi.DomainHash) error {
|
||||
|
@ -12,13 +12,13 @@ import (
|
||||
// interval contains B's interval, it replaces it.
|
||||
//
|
||||
// Notes:
|
||||
// * Intervals never intersect unless one contains the other
|
||||
// (this follows from the tree structure and the indexing rule).
|
||||
// * Since node.FutureCoveringSet is kept ordered, a binary search can be
|
||||
// used for insertion/queries.
|
||||
// * Although reindexing may change a block's interval, the
|
||||
// is-superset relation will by definition
|
||||
// be always preserved.
|
||||
// - Intervals never intersect unless one contains the other
|
||||
// (this follows from the tree structure and the indexing rule).
|
||||
// - Since node.FutureCoveringSet is kept ordered, a binary search can be
|
||||
// used for insertion/queries.
|
||||
// - Although reindexing may change a block's interval, the
|
||||
// is-superset relation will by definition
|
||||
// be always preserved.
|
||||
func (rt *reachabilityManager) insertToFutureCoveringSet(stagingArea *model.StagingArea, node, futureNode *externalapi.DomainHash) error {
|
||||
reachabilityData, err := rt.reachabilityDataForInsertion(stagingArea, node)
|
||||
if err != nil {
|
||||
|
@ -161,7 +161,9 @@ func intervalSplitWithExponentialBias(ri *model.ReachabilityInterval, sizes []ui
|
||||
|
||||
// exponentialFractions returns a fraction of each size in sizes
|
||||
// as follows:
|
||||
// fraction[i] = 2^size[i] / sum_j(2^size[j])
|
||||
//
|
||||
// fraction[i] = 2^size[i] / sum_j(2^size[j])
|
||||
//
|
||||
// In the code below the above equation is divided by 2^max(size)
|
||||
// to avoid exploding numbers. Note that in 1 / 2^(max(size)-size[i])
|
||||
// we divide 1 by potentially a very large number, which will
|
||||
|
@ -42,13 +42,13 @@ Core (BFS) algorithms used during reindexing
|
||||
// and populates the provided subTreeSizeMap with the results.
|
||||
// It is equivalent to the following recursive implementation:
|
||||
//
|
||||
// func (rt *reachabilityManager) countSubtrees(node *model.ReachabilityTreeNode) uint64 {
|
||||
// subtreeSize := uint64(0)
|
||||
// for _, child := range node.children {
|
||||
// subtreeSize += child.countSubtrees()
|
||||
// }
|
||||
// return subtreeSize + 1
|
||||
// }
|
||||
// func (rt *reachabilityManager) countSubtrees(node *model.ReachabilityTreeNode) uint64 {
|
||||
// subtreeSize := uint64(0)
|
||||
// for _, child := range node.children {
|
||||
// subtreeSize += child.countSubtrees()
|
||||
// }
|
||||
// return subtreeSize + 1
|
||||
// }
|
||||
//
|
||||
// However, we are expecting (linearly) deep trees, and so a
|
||||
// recursive stack-based approach is inefficient and will hit
|
||||
|
@ -315,7 +315,7 @@ func NewErrMissingParents(missingParentHashes []*externalapi.DomainHash) error {
|
||||
// InvalidTransaction is a struct containing an invalid transaction, and the error explaining why it's invalid.
|
||||
type InvalidTransaction struct {
|
||||
Transaction *externalapi.DomainTransaction
|
||||
Error error
|
||||
Error *RuleError
|
||||
}
|
||||
|
||||
func (invalid InvalidTransaction) String() string {
|
||||
|
@ -3,9 +3,10 @@ package ruleerrors
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/kaspanet/kaspad/domain/consensus/utils/consensushashing"
|
||||
"testing"
|
||||
|
||||
"github.com/kaspanet/kaspad/domain/consensus/utils/consensushashing"
|
||||
|
||||
"github.com/kaspanet/kaspad/domain/consensus/model/externalapi"
|
||||
)
|
||||
|
||||
@ -49,7 +50,7 @@ func TestNewErrMissingTxOut(t *testing.T) {
|
||||
func TestNewErrInvalidTransactionsInNewBlock(t *testing.T) {
|
||||
tx := &externalapi.DomainTransaction{Fee: 1337}
|
||||
txID := consensushashing.TransactionID(tx)
|
||||
outer := NewErrInvalidTransactionsInNewBlock([]InvalidTransaction{{tx, ErrNoTxInputs}})
|
||||
outer := NewErrInvalidTransactionsInNewBlock([]InvalidTransaction{{tx, &ErrNoTxInputs}})
|
||||
//TODO: Implement Stringer for `DomainTransaction`
|
||||
expectedOuterErr := fmt.Sprintf("ErrInvalidTransactionsInNewBlock: [(%s: ErrNoTxInputs)]", txID)
|
||||
inner := &ErrInvalidTransactionsInNewBlock{}
|
||||
@ -60,7 +61,7 @@ func TestNewErrInvalidTransactionsInNewBlock(t *testing.T) {
|
||||
if len(inner.InvalidTransactions) != 1 {
|
||||
t.Fatalf("TestNewErrInvalidTransactionsInNewBlock: Expected len(inner.MissingOutpoints) 1, found: %d", len(inner.InvalidTransactions))
|
||||
}
|
||||
if inner.InvalidTransactions[0].Error != ErrNoTxInputs {
|
||||
if *inner.InvalidTransactions[0].Error != ErrNoTxInputs {
|
||||
t.Fatalf("TestNewErrInvalidTransactionsInNewBlock: Expected ErrNoTxInputs. found: %v", inner.InvalidTransactions[0].Error)
|
||||
}
|
||||
if inner.InvalidTransactions[0].Transaction.Fee != 1337 {
|
||||
|
@ -69,6 +69,17 @@ func (tc *testConsensus) AddBlock(parentHashes []*externalapi.DomainHash, coinba
|
||||
return consensushashing.BlockHash(block), virtualChangeSet, nil
|
||||
}
|
||||
|
||||
func (tc *testConsensus) AddBlockOnTips(coinbaseData *externalapi.DomainCoinbaseData,
|
||||
transactions []*externalapi.DomainTransaction) (*externalapi.DomainHash, *externalapi.VirtualChangeSet, error) {
|
||||
|
||||
tips, err := tc.Tips()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return tc.AddBlock(tips, coinbaseData, transactions)
|
||||
}
|
||||
|
||||
func (tc *testConsensus) AddUTXOInvalidHeader(parentHashes []*externalapi.DomainHash) (*externalapi.DomainHash,
|
||||
*externalapi.VirtualChangeSet, error) {
|
||||
|
||||
|
@ -27,7 +27,7 @@ func TransactionHash(tx *externalapi.DomainTransaction) *externalapi.DomainHash
|
||||
// Encode the header and hash everything prior to the number of
|
||||
// transactions.
|
||||
writer := hashes.NewTransactionHashWriter()
|
||||
err := serializeTransaction(writer, tx, txEncodingFull)
|
||||
err := serializeTransaction(writer, tx, txEncodingFull, true)
|
||||
if err != nil {
|
||||
// It seems like this could only happen if the writer returned an error.
|
||||
// and this writer should never return an error (no allocations or possible failures)
|
||||
@ -52,7 +52,7 @@ func TransactionID(tx *externalapi.DomainTransaction) *externalapi.DomainTransac
|
||||
encodingFlags = txEncodingExcludeSignatureScript
|
||||
}
|
||||
writer := hashes.NewTransactionIDWriter()
|
||||
err := serializeTransaction(writer, tx, encodingFlags)
|
||||
err := serializeTransaction(writer, tx, encodingFlags, false)
|
||||
if err != nil {
|
||||
// this writer never return errors (no allocations or possible failures) so errors can only come from validity checks,
|
||||
// and we assume we never construct malformed transactions.
|
||||
@ -74,7 +74,7 @@ func TransactionIDs(txs []*externalapi.DomainTransaction) []*externalapi.DomainT
|
||||
return txIDs
|
||||
}
|
||||
|
||||
func serializeTransaction(w io.Writer, tx *externalapi.DomainTransaction, encodingFlags txEncoding) error {
|
||||
func serializeTransaction(w io.Writer, tx *externalapi.DomainTransaction, encodingFlags txEncoding, includeMass bool) error {
|
||||
err := binaryserializer.PutUint16(w, tx.Version)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -126,6 +126,15 @@ func serializeTransaction(w io.Writer, tx *externalapi.DomainTransaction, encodi
|
||||
return err
|
||||
}
|
||||
|
||||
if includeMass {
|
||||
if tx.MassCommitment > 0 { // For backward compatibility, serialize MassCommitment only if it's not zero
|
||||
err = binaryserializer.PutUint64(w, tx.MassCommitment)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -272,7 +272,7 @@
|
||||
"P2SH"
|
||||
],
|
||||
[
|
||||
"Argument negative with by-blockheight nLockTime=0"
|
||||
"Argument negative with by-DAAScore nLockTime=0"
|
||||
],
|
||||
[
|
||||
[
|
||||
@ -564,7 +564,7 @@
|
||||
"P2SH"
|
||||
],
|
||||
[
|
||||
"Argument negative with by-blockheight txin.nSequence=0"
|
||||
"Argument negative with by-DAAScore txin.nSequence=0"
|
||||
],
|
||||
[
|
||||
[
|
||||
|
@ -4,7 +4,7 @@ Package txscript implements the kaspa transaction script language.
|
||||
This package provides data structures and functions to parse and execute
|
||||
kaspa transaction scripts.
|
||||
|
||||
Script Overview
|
||||
# Script Overview
|
||||
|
||||
Kaspa transaction scripts are written in a stack-base, FORTH-like language.
|
||||
|
||||
@ -22,7 +22,7 @@ is used to prove the the spender is authorized to perform the transaction.
|
||||
One benefit of using a scripting language is added flexibility in specifying
|
||||
what conditions must be met in order to spend kaspa.
|
||||
|
||||
Errors
|
||||
# Errors
|
||||
|
||||
Errors returned by this package are of type txscript.Error. This allows the
|
||||
caller to programmatically determine the specific error by examining the
|
||||
|
@ -272,10 +272,10 @@ func (e ErrorCode) String() string {
|
||||
|
||||
// Error identifies a script-related error. It is used to indicate three
|
||||
// classes of errors:
|
||||
// 1) Script execution failures due to violating one of the many requirements
|
||||
// imposed by the script engine or evaluating to false
|
||||
// 2) Improper API usage by callers
|
||||
// 3) Internal consistency check failures
|
||||
// 1. Script execution failures due to violating one of the many requirements
|
||||
// imposed by the script engine or evaluating to false
|
||||
// 2. Improper API usage by callers
|
||||
// 3. Internal consistency check failures
|
||||
//
|
||||
// The caller can use type assertions on the returned errors to access the
|
||||
// ErrorCode field to ascertain the specific reason for the error. As an
|
||||
|
@ -6,6 +6,7 @@ package txscript
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"github.com/kaspanet/kaspad/domain/consensus/model/externalapi"
|
||||
"reflect"
|
||||
"testing"
|
||||
@ -3581,50 +3582,92 @@ func TestHasCanonicalPush(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func hexDecode(src string) []byte {
|
||||
decoded, err := hex.DecodeString(src)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return decoded
|
||||
}
|
||||
|
||||
// TestGetPreciseSigOps ensures the more precise signature operation counting
|
||||
// mechanism which includes signatures in P2SH scripts works as expected.
|
||||
func TestGetPreciseSigOps(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
scriptSig []byte
|
||||
nSigOps int
|
||||
}{
|
||||
{
|
||||
name: "scriptSig doesn't parse",
|
||||
scriptSig: mustParseShortForm("PUSHDATA1 0x02", 0),
|
||||
},
|
||||
{
|
||||
name: "scriptSig isn't push only",
|
||||
scriptSig: mustParseShortForm("1 DUP", 0),
|
||||
nSigOps: 0,
|
||||
},
|
||||
{
|
||||
name: "scriptSig length 0",
|
||||
scriptSig: nil,
|
||||
nSigOps: 0,
|
||||
},
|
||||
{
|
||||
name: "No script at the end",
|
||||
// No script at end but still push only.
|
||||
scriptSig: mustParseShortForm("1 1", 0),
|
||||
nSigOps: 0,
|
||||
},
|
||||
{
|
||||
name: "pushed script doesn't parse",
|
||||
scriptSig: mustParseShortForm("DATA_2 PUSHDATA1 0x02", 0),
|
||||
},
|
||||
}
|
||||
|
||||
// The signature in the p2sh script is nonsensical for the tests since
|
||||
// The signature in the p2sh script is nonsensical for most tests since
|
||||
// this script will never be executed. What matters is that it matches
|
||||
// the right pattern.
|
||||
scriptOnly := mustParseShortForm("BLAKE2B DATA_32 0x433ec2ac1ffa1b7b7d0"+
|
||||
"27f564529c57197f9ae88 EQUAL", 0)
|
||||
scriptPubKey := &externalapi.ScriptPublicKey{scriptOnly, 0}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
scriptSig []byte
|
||||
scriptPublicKey *externalapi.ScriptPublicKey
|
||||
nSigOps int
|
||||
}{
|
||||
{
|
||||
name: "scriptSig doesn't parse",
|
||||
scriptSig: mustParseShortForm("PUSHDATA1 0x02", 0),
|
||||
scriptPublicKey: scriptPubKey,
|
||||
},
|
||||
{
|
||||
name: "scriptSig isn't push only",
|
||||
scriptSig: mustParseShortForm("1 DUP", 0),
|
||||
nSigOps: 0,
|
||||
scriptPublicKey: scriptPubKey,
|
||||
},
|
||||
{
|
||||
name: "scriptSig length 0",
|
||||
scriptSig: nil,
|
||||
nSigOps: 0,
|
||||
scriptPublicKey: scriptPubKey,
|
||||
},
|
||||
{
|
||||
name: "No script at the end",
|
||||
// No script at end but still push only.
|
||||
scriptSig: mustParseShortForm("1 1", 0),
|
||||
nSigOps: 0,
|
||||
scriptPublicKey: scriptPubKey,
|
||||
},
|
||||
{
|
||||
name: "pushed script doesn't parse",
|
||||
scriptSig: mustParseShortForm("DATA_2 PUSHDATA1 0x02", 0),
|
||||
scriptPublicKey: scriptPubKey,
|
||||
},
|
||||
{
|
||||
name: "mainnet multisig transaction 487f94ffa63106f72644068765b9dc629bb63e481210f382667d4a93b69af412",
|
||||
scriptSig: hexDecode("41eb577889fa28283709201ef5b056745c6cf0546dd31666cecd41c40a581b256e885d941b86b14d44efacec12d614e7fcabf7b341660f95bab16b71d766ab010501411c0eeef117ca485d34e4bc0cf6d5b578aa250c5d13ebff0882a7e2eeea1f31e8ecb6755696d194b1b0fcb853afab28b61f3f7cec487bd611df7e57252802f535014c875220ab64c7691713a32ea6dfced9155c5c26e8186426f0697af0db7a4b1340f992d12041ae738d66fe3d21105483e5851778ad73c5cddf0819c5e8fd8a589260d967e72065120722c36d3fac19646258481dd3661fa767da151304af514cb30af5cb5692203cd7690ecb67cbbe6cafad00a7c9133da535298ab164549e0cce2658f7b3032754ae"),
|
||||
scriptPublicKey: &externalapi.ScriptPublicKey{
|
||||
Script: hexDecode("aa20f38031f61ca23d70844f63a477d07f0b2c2decab907c2e096e548b0e08721c7987"),
|
||||
Version: 0,
|
||||
},
|
||||
nSigOps: 4,
|
||||
},
|
||||
{
|
||||
name: "a partially parseable script public key",
|
||||
scriptSig: nil,
|
||||
scriptPublicKey: &externalapi.ScriptPublicKey{
|
||||
Script: mustParseShortForm("CHECKSIG CHECKSIG DATA_1", 0),
|
||||
Version: 0,
|
||||
},
|
||||
nSigOps: 2,
|
||||
},
|
||||
{
|
||||
name: "p2pk",
|
||||
scriptSig: hexDecode("416db0c0ce824a6d076c8e73aae9987416933df768e07760829cb0685dc0a2bbb11e2c0ced0cab806e111a11cbda19784098fd25db176b6a9d7c93e5747674d32301"),
|
||||
scriptPublicKey: &externalapi.ScriptPublicKey{
|
||||
Script: hexDecode("208a457ca74ade0492c44c440da1cab5b008d8449150fe2794f0d8f4cce7e8aa27ac"),
|
||||
Version: 0,
|
||||
},
|
||||
nSigOps: 1,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
count := GetPreciseSigOpCount(test.scriptSig, scriptPubKey, true)
|
||||
count := GetPreciseSigOpCount(test.scriptSig, test.scriptPublicKey, true)
|
||||
if count != test.nSigOps {
|
||||
t.Errorf("%s: expected count of %d, got %d", test.name,
|
||||
test.nSigOps, count)
|
||||
|
@ -37,16 +37,17 @@ func (e ErrScriptNotCanonical) Error() string {
|
||||
// For example, the following would build a 2-of-3 multisig script for usage in
|
||||
// a pay-to-script-hash (although in this situation MultiSigScript() would be a
|
||||
// better choice to generate the script):
|
||||
// builder := txscript.NewScriptBuilder()
|
||||
// builder.AddOp(txscript.OP_2).AddData(pubKey1).AddData(pubKey2)
|
||||
// builder.AddData(pubKey3).AddOp(txscript.OP_3)
|
||||
// builder.AddOp(txscript.OP_CHECKMULTISIG)
|
||||
// script, err := builder.Script()
|
||||
// if err != nil {
|
||||
// // Handle the error.
|
||||
// return
|
||||
// }
|
||||
// fmt.Printf("Final multi-sig script: %x\n", script)
|
||||
//
|
||||
// builder := txscript.NewScriptBuilder()
|
||||
// builder.AddOp(txscript.OP_2).AddData(pubKey1).AddData(pubKey2)
|
||||
// builder.AddData(pubKey3).AddOp(txscript.OP_3)
|
||||
// builder.AddOp(txscript.OP_CHECKMULTISIG)
|
||||
// script, err := builder.Script()
|
||||
// if err != nil {
|
||||
// // Handle the error.
|
||||
// return
|
||||
// }
|
||||
// fmt.Printf("Final multi-sig script: %x\n", script)
|
||||
type ScriptBuilder struct {
|
||||
script []byte
|
||||
err error
|
||||
|
@ -82,18 +82,19 @@ func checkMinimalDataEncoding(v []byte) error {
|
||||
// Bytes returns the number serialized as a little endian with a sign bit.
|
||||
//
|
||||
// Example encodings:
|
||||
// 127 -> [0x7f]
|
||||
// -127 -> [0xff]
|
||||
// 128 -> [0x80 0x00]
|
||||
// -128 -> [0x80 0x80]
|
||||
// 129 -> [0x81 0x00]
|
||||
// -129 -> [0x81 0x80]
|
||||
// 256 -> [0x00 0x01]
|
||||
// -256 -> [0x00 0x81]
|
||||
// 32767 -> [0xff 0x7f]
|
||||
// -32767 -> [0xff 0xff]
|
||||
// 32768 -> [0x00 0x80 0x00]
|
||||
// -32768 -> [0x00 0x80 0x80]
|
||||
//
|
||||
// 127 -> [0x7f]
|
||||
// -127 -> [0xff]
|
||||
// 128 -> [0x80 0x00]
|
||||
// -128 -> [0x80 0x80]
|
||||
// 129 -> [0x81 0x00]
|
||||
// -129 -> [0x81 0x80]
|
||||
// 256 -> [0x00 0x01]
|
||||
// -256 -> [0x00 0x81]
|
||||
// 32767 -> [0xff 0x7f]
|
||||
// -32767 -> [0xff 0xff]
|
||||
// 32768 -> [0x00 0x80 0x00]
|
||||
// -32768 -> [0x00 0x80 0x80]
|
||||
func (n scriptNum) Bytes() []byte {
|
||||
// Zero encodes as an empty byte slice.
|
||||
if n == 0 {
|
||||
|
@ -387,7 +387,7 @@ func ExtractAtomicSwapDataPushes(version uint16, scriptPubKey []byte) (*AtomicSw
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(pops) != 20 {
|
||||
if len(pops) != 19 {
|
||||
return nil, nil
|
||||
}
|
||||
isAtomicSwap := pops[0].opcode.value == OpIf &&
|
||||
@ -403,13 +403,12 @@ func ExtractAtomicSwapDataPushes(version uint16, scriptPubKey []byte) (*AtomicSw
|
||||
pops[10].opcode.value == OpElse &&
|
||||
canonicalPush(pops[11]) &&
|
||||
pops[12].opcode.value == OpCheckLockTimeVerify &&
|
||||
pops[13].opcode.value == OpDrop &&
|
||||
pops[14].opcode.value == OpDup &&
|
||||
pops[15].opcode.value == OpBlake2b &&
|
||||
pops[16].opcode.value == OpData32 &&
|
||||
pops[17].opcode.value == OpEndIf &&
|
||||
pops[18].opcode.value == OpEqualVerify &&
|
||||
pops[19].opcode.value == OpCheckSig
|
||||
pops[13].opcode.value == OpDup &&
|
||||
pops[14].opcode.value == OpBlake2b &&
|
||||
pops[15].opcode.value == OpData32 &&
|
||||
pops[16].opcode.value == OpEndIf &&
|
||||
pops[17].opcode.value == OpEqualVerify &&
|
||||
pops[18].opcode.value == OpCheckSig
|
||||
if !isAtomicSwap {
|
||||
return nil, nil
|
||||
}
|
||||
@ -417,9 +416,9 @@ func ExtractAtomicSwapDataPushes(version uint16, scriptPubKey []byte) (*AtomicSw
|
||||
pushes := new(AtomicSwapDataPushes)
|
||||
copy(pushes.SecretHash[:], pops[5].data)
|
||||
copy(pushes.RecipientBlake2b[:], pops[9].data)
|
||||
copy(pushes.RefundBlake2b[:], pops[16].data)
|
||||
copy(pushes.RefundBlake2b[:], pops[15].data)
|
||||
if pops[2].data != nil {
|
||||
locktime, err := makeScriptNum(pops[2].data, 5)
|
||||
locktime, err := makeScriptNum(pops[2].data, 8)
|
||||
if err != nil {
|
||||
return nil, nil
|
||||
}
|
||||
@ -430,7 +429,7 @@ func ExtractAtomicSwapDataPushes(version uint16, scriptPubKey []byte) (*AtomicSw
|
||||
return nil, nil
|
||||
}
|
||||
if pops[11].data != nil {
|
||||
locktime, err := makeScriptNum(pops[11].data, 5)
|
||||
locktime, err := makeScriptNum(pops[11].data, 8)
|
||||
if err != nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
@ -75,15 +75,21 @@ func subtractionWithRemainderHavingDAAScoreInPlace(collection1, collection2, res
|
||||
//
|
||||
// diffFrom follows a set of rules represented by the following 3 by 3 table:
|
||||
//
|
||||
// | | this | |
|
||||
// | | this | |
|
||||
//
|
||||
// ---------+-----------+-----------+-----------+-----------
|
||||
// | | toAdd | toRemove | None
|
||||
//
|
||||
// | | toAdd | toRemove | None
|
||||
//
|
||||
// ---------+-----------+-----------+-----------+-----------
|
||||
// other | toAdd | - | X | toAdd
|
||||
// ---------+-----------+-----------+-----------+-----------
|
||||
// | toRemove | X | - | toRemove
|
||||
//
|
||||
// | toRemove | X | - | toRemove
|
||||
//
|
||||
// ---------+-----------+-----------+-----------+-----------
|
||||
// | None | toRemove | toAdd | -
|
||||
//
|
||||
// | None | toRemove | toAdd | -
|
||||
//
|
||||
// Key:
|
||||
// - Don't add anything to the result
|
||||
@ -92,10 +98,10 @@ func subtractionWithRemainderHavingDAAScoreInPlace(collection1, collection2, res
|
||||
// toRemove Add the UTXO into the toRemove collection of the result
|
||||
//
|
||||
// Examples:
|
||||
// 1. This diff contains a UTXO in toAdd, and the other diff contains it in toRemove
|
||||
// diffFrom results in an error
|
||||
// 2. This diff contains a UTXO in toRemove, and the other diff does not contain it
|
||||
// diffFrom results in the UTXO being added to toAdd
|
||||
// 1. This diff contains a UTXO in toAdd, and the other diff contains it in toRemove
|
||||
// diffFrom results in an error
|
||||
// 2. This diff contains a UTXO in toRemove, and the other diff does not contain it
|
||||
// diffFrom results in the UTXO being added to toAdd
|
||||
func diffFrom(this, other *mutableUTXODiff) (*mutableUTXODiff, error) {
|
||||
// Note that the following cases are not accounted for, as they are impossible
|
||||
// as long as the base utxoSet is the same:
|
||||
|
@ -3,9 +3,10 @@ Package dagconfig defines DAG configuration parameters.
|
||||
|
||||
In addition to the main Kaspa network, which is intended for the transfer
|
||||
of monetary value, there also exists the following standard networks:
|
||||
* testnet
|
||||
* simnet
|
||||
* devnet
|
||||
- testnet
|
||||
- simnet
|
||||
- devnet
|
||||
|
||||
These networks are incompatible with each other (each sharing a different
|
||||
genesis block) and software should handle errors where input intended for
|
||||
one network is used on an application instance running on a different
|
||||
@ -19,40 +20,40 @@ one of the standard Param vars for use as the application's "active" network.
|
||||
When a network parameter is needed, it may then be looked up through this
|
||||
variable (either directly, or hidden in a library call).
|
||||
|
||||
package main
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/kaspanet/kaspad/util"
|
||||
"github.com/kaspanet/kaspad/domain/dagconfig"
|
||||
)
|
||||
"github.com/kaspanet/kaspad/util"
|
||||
"github.com/kaspanet/kaspad/domain/dagconfig"
|
||||
)
|
||||
|
||||
var testnet = flag.Bool("testnet", false, "operate on the testnet Kaspa network")
|
||||
var testnet = flag.Bool("testnet", false, "operate on the testnet Kaspa network")
|
||||
|
||||
// By default (without --testnet), use mainnet.
|
||||
var dagParams = &dagconfig.MainnetParams
|
||||
// By default (without --testnet), use mainnet.
|
||||
var dagParams = &dagconfig.MainnetParams
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
func main() {
|
||||
flag.Parse()
|
||||
|
||||
// Modify active network parameters if operating on testnet.
|
||||
if *testnet {
|
||||
dagParams = &dagconfig.TestnetParams
|
||||
}
|
||||
// Modify active network parameters if operating on testnet.
|
||||
if *testnet {
|
||||
dagParams = &dagconfig.TestnetParams
|
||||
}
|
||||
|
||||
// later...
|
||||
// later...
|
||||
|
||||
// Create and print new payment address, specific to the active network.
|
||||
pubKey := make([]byte, 32)
|
||||
addr, err := util.NewAddressPubKey(pubKey, dagParams)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Println(addr)
|
||||
}
|
||||
// Create and print new payment address, specific to the active network.
|
||||
pubKey := make([]byte, 32)
|
||||
addr, err := util.NewAddressPubKey(pubKey, dagParams)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Println(addr)
|
||||
}
|
||||
|
||||
If an application does not use one of the standard Kaspa networks, a new
|
||||
Params struct may be created which defines the parameters for the non-
|
||||
|
@ -232,6 +232,8 @@ var MainnetParams = Params{
|
||||
"seeder4.kaspad.net",
|
||||
// This DNS seeder is run by Tim
|
||||
"kaspadns.kaspacalc.net",
|
||||
// This DNS seeder is run by supertypo
|
||||
"n-mainnet.kaspa.ws",
|
||||
},
|
||||
|
||||
// DAG parameters
|
||||
|
@ -147,16 +147,12 @@ func (btb *blockTemplateBuilder) BuildBlockTemplate(
|
||||
invalidTxsErr := ruleerrors.ErrInvalidTransactionsInNewBlock{}
|
||||
if errors.As(err, &invalidTxsErr) {
|
||||
log.Criticalf("consensusReference.Consensus().BuildBlock returned invalid txs in BuildBlockTemplate")
|
||||
invalidTxs := make([]*consensusexternalapi.DomainTransaction, 0, len(invalidTxsErr.InvalidTransactions))
|
||||
for _, tx := range invalidTxsErr.InvalidTransactions {
|
||||
invalidTxs = append(invalidTxs, tx.Transaction)
|
||||
}
|
||||
err = btb.mempool.RemoveTransactions(invalidTxs, true)
|
||||
err = btb.mempool.RemoveInvalidTransactions(&invalidTxsErr)
|
||||
if err != nil {
|
||||
// mempool.RemoveTransactions might return errors in situations that are perfectly fine in this context.
|
||||
// mempool.RemoveInvalidTransactions might return errors in situations that are perfectly fine in this context.
|
||||
// TODO: Once the mempool invariants are clear, this should be converted back `return nil, err`:
|
||||
// https://github.com/kaspanet/kaspad/issues/1553
|
||||
log.Criticalf("Error from mempool.RemoveTransactions: %+v", err)
|
||||
log.Criticalf("Error from mempool.RemoveInvalidTransactions: %+v", err)
|
||||
}
|
||||
// We can call this recursively without worry because this should almost never happen
|
||||
return btb.BuildBlockTemplate(coinbaseData)
|
||||
|
@ -51,6 +51,7 @@ const (
|
||||
RejectDifficulty RejectCode = 0x44
|
||||
RejectImmatureSpend RejectCode = 0x45
|
||||
RejectBadOrphan RejectCode = 0x64
|
||||
RejectSpamTx RejectCode = 0x65
|
||||
)
|
||||
|
||||
// Map of reject codes back strings for pretty printing.
|
||||
|
@ -3,6 +3,11 @@ package mempool
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/kaspanet/kaspad/domain/consensus/ruleerrors"
|
||||
"github.com/kaspanet/kaspad/domain/consensus/utils/consensushashing"
|
||||
"github.com/kaspanet/kaspad/domain/consensus/utils/constants"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/kaspanet/kaspad/domain/consensusreference"
|
||||
|
||||
"github.com/kaspanet/kaspad/domain/consensus/model/externalapi"
|
||||
@ -141,7 +146,57 @@ func (mp *mempool) BlockCandidateTransactions() []*externalapi.DomainTransaction
|
||||
mp.mtx.RLock()
|
||||
defer mp.mtx.RUnlock()
|
||||
|
||||
return mp.transactionsPool.allReadyTransactions()
|
||||
readyTxs := mp.transactionsPool.allReadyTransactions()
|
||||
var candidateTxs []*externalapi.DomainTransaction
|
||||
var spamTx *externalapi.DomainTransaction
|
||||
var spamTxNewestUTXODaaScore uint64
|
||||
for _, tx := range readyTxs {
|
||||
if len(tx.Outputs) > len(tx.Inputs) {
|
||||
hasCoinbaseInput := false
|
||||
for _, input := range tx.Inputs {
|
||||
if input.UTXOEntry.IsCoinbase() {
|
||||
hasCoinbaseInput = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
numExtraOuts := len(tx.Outputs) - len(tx.Inputs)
|
||||
if !hasCoinbaseInput && numExtraOuts > 2 && tx.Fee < uint64(numExtraOuts)*constants.SompiPerKaspa {
|
||||
log.Debugf("Filtered spam tx %s", consensushashing.TransactionID(tx))
|
||||
continue
|
||||
}
|
||||
|
||||
if hasCoinbaseInput || tx.Fee > uint64(numExtraOuts)*constants.SompiPerKaspa {
|
||||
candidateTxs = append(candidateTxs, tx)
|
||||
} else {
|
||||
txNewestUTXODaaScore := tx.Inputs[0].UTXOEntry.BlockDAAScore()
|
||||
for _, input := range tx.Inputs {
|
||||
if input.UTXOEntry.BlockDAAScore() > txNewestUTXODaaScore {
|
||||
txNewestUTXODaaScore = input.UTXOEntry.BlockDAAScore()
|
||||
}
|
||||
}
|
||||
|
||||
if spamTx != nil {
|
||||
if txNewestUTXODaaScore < spamTxNewestUTXODaaScore {
|
||||
spamTx = tx
|
||||
spamTxNewestUTXODaaScore = txNewestUTXODaaScore
|
||||
}
|
||||
} else {
|
||||
spamTx = tx
|
||||
spamTxNewestUTXODaaScore = txNewestUTXODaaScore
|
||||
}
|
||||
}
|
||||
} else {
|
||||
candidateTxs = append(candidateTxs, tx)
|
||||
}
|
||||
}
|
||||
|
||||
if spamTx != nil {
|
||||
log.Debugf("Adding spam tx candidate %s", consensushashing.TransactionID(spamTx))
|
||||
candidateTxs = append(candidateTxs, spamTx)
|
||||
}
|
||||
|
||||
return candidateTxs
|
||||
}
|
||||
|
||||
func (mp *mempool) RevalidateHighPriorityTransactions() (validTransactions []*externalapi.DomainTransaction, err error) {
|
||||
@ -151,11 +206,19 @@ func (mp *mempool) RevalidateHighPriorityTransactions() (validTransactions []*ex
|
||||
return mp.revalidateHighPriorityTransactions()
|
||||
}
|
||||
|
||||
func (mp *mempool) RemoveTransactions(transactions []*externalapi.DomainTransaction, removeRedeemers bool) error {
|
||||
func (mp *mempool) RemoveInvalidTransactions(err *ruleerrors.ErrInvalidTransactionsInNewBlock) error {
|
||||
mp.mtx.Lock()
|
||||
defer mp.mtx.Unlock()
|
||||
|
||||
return mp.removeTransactions(transactions, removeRedeemers)
|
||||
for _, tx := range err.InvalidTransactions {
|
||||
removeRedeemers := !errors.As(tx.Error, &ruleerrors.ErrMissingTxOut{})
|
||||
err := mp.removeTransaction(consensushashing.TransactionID(tx.Transaction), removeRedeemers)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (mp *mempool) RemoveTransaction(transactionID *externalapi.DomainTransactionID, removeRedeemers bool) error {
|
||||
|
@ -2,20 +2,9 @@ package mempool
|
||||
|
||||
import (
|
||||
"github.com/kaspanet/kaspad/domain/consensus/model/externalapi"
|
||||
"github.com/kaspanet/kaspad/domain/consensus/utils/consensushashing"
|
||||
"github.com/kaspanet/kaspad/domain/miningmanager/mempool/model"
|
||||
)
|
||||
|
||||
func (mp *mempool) removeTransactions(transactions []*externalapi.DomainTransaction, removeRedeemers bool) error {
|
||||
for _, transaction := range transactions {
|
||||
err := mp.removeTransaction(consensushashing.TransactionID(transaction), removeRedeemers)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (mp *mempool) removeTransaction(transactionID *externalapi.DomainTransactionID, removeRedeemers bool) error {
|
||||
if _, ok := mp.orphansPool.allOrphans[*transactionID]; ok {
|
||||
return mp.orphansPool.removeOrphan(transactionID, true)
|
||||
|
@ -7,20 +7,85 @@ import (
|
||||
)
|
||||
|
||||
func (mp *mempool) revalidateHighPriorityTransactions() ([]*externalapi.DomainTransaction, error) {
|
||||
type txNode struct {
|
||||
children map[externalapi.DomainTransactionID]struct{}
|
||||
nonVisitedParents int
|
||||
tx *model.MempoolTransaction
|
||||
visited bool
|
||||
}
|
||||
|
||||
onEnd := logger.LogAndMeasureExecutionTime(log, "revalidateHighPriorityTransactions")
|
||||
defer onEnd()
|
||||
|
||||
// We revalidate transactions in topological order in case there are dependencies between them
|
||||
|
||||
// Naturally transactions point to their dependencies, but since we want to start processing the dependencies
|
||||
// first, we build the opposite DAG. We initially fill `queue` with transactions with no dependencies.
|
||||
txDAG := make(map[externalapi.DomainTransactionID]*txNode)
|
||||
|
||||
maybeAddNode := func(txID externalapi.DomainTransactionID) *txNode {
|
||||
if node, ok := txDAG[txID]; ok {
|
||||
return node
|
||||
}
|
||||
|
||||
node := &txNode{
|
||||
children: make(map[externalapi.DomainTransactionID]struct{}),
|
||||
nonVisitedParents: 0,
|
||||
tx: mp.transactionsPool.highPriorityTransactions[txID],
|
||||
}
|
||||
txDAG[txID] = node
|
||||
return node
|
||||
}
|
||||
|
||||
queue := make([]*txNode, 0, len(mp.transactionsPool.highPriorityTransactions))
|
||||
for id, transaction := range mp.transactionsPool.highPriorityTransactions {
|
||||
node := maybeAddNode(id)
|
||||
|
||||
parents := make(map[externalapi.DomainTransactionID]struct{})
|
||||
for _, input := range transaction.Transaction().Inputs {
|
||||
if _, ok := mp.transactionsPool.highPriorityTransactions[input.PreviousOutpoint.TransactionID]; !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
parents[input.PreviousOutpoint.TransactionID] = struct{}{} // To avoid duplicate parents, we first add it to a set and then count it
|
||||
maybeAddNode(input.PreviousOutpoint.TransactionID).children[id] = struct{}{}
|
||||
}
|
||||
node.nonVisitedParents = len(parents)
|
||||
|
||||
if node.nonVisitedParents == 0 {
|
||||
queue = append(queue, node)
|
||||
}
|
||||
}
|
||||
|
||||
validTransactions := []*externalapi.DomainTransaction{}
|
||||
for _, transaction := range mp.transactionsPool.highPriorityTransactions {
|
||||
|
||||
// Now we iterate the DAG in topological order using BFS
|
||||
for len(queue) > 0 {
|
||||
var node *txNode
|
||||
node, queue = queue[0], queue[1:]
|
||||
|
||||
if node.visited {
|
||||
continue
|
||||
}
|
||||
node.visited = true
|
||||
|
||||
transaction := node.tx
|
||||
isValid, err := mp.revalidateTransaction(transaction)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !isValid {
|
||||
continue
|
||||
|
||||
for child := range node.children {
|
||||
childNode := txDAG[child]
|
||||
childNode.nonVisitedParents--
|
||||
if childNode.nonVisitedParents == 0 {
|
||||
queue = append(queue, txDAG[child])
|
||||
}
|
||||
}
|
||||
|
||||
validTransactions = append(validTransactions, transaction.Transaction().Clone())
|
||||
if isValid {
|
||||
validTransactions = append(validTransactions, transaction.Transaction().Clone())
|
||||
}
|
||||
}
|
||||
|
||||
return validTransactions, nil
|
||||
@ -35,7 +100,7 @@ func (mp *mempool) revalidateTransaction(transaction *model.MempoolTransaction)
|
||||
}
|
||||
if len(missingParents) > 0 {
|
||||
log.Debugf("Removing transaction %s, it failed revalidation", transaction.TransactionID())
|
||||
err := mp.removeTransaction(transaction.TransactionID(), true)
|
||||
err := mp.removeTransaction(transaction.TransactionID(), false)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
@ -2,7 +2,6 @@ package mempool
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/kaspanet/kaspad/infrastructure/logger"
|
||||
|
||||
"github.com/kaspanet/kaspad/domain/consensus/model/externalapi"
|
||||
|
@ -2,6 +2,7 @@ package mempool
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/kaspanet/kaspad/domain/consensus/utils/constants"
|
||||
|
||||
"github.com/kaspanet/kaspad/domain/consensus/model/externalapi"
|
||||
"github.com/kaspanet/kaspad/domain/consensus/utils/consensushashing"
|
||||
@ -44,6 +45,20 @@ func (mp *mempool) validateTransactionInIsolation(transaction *externalapi.Domai
|
||||
}
|
||||
|
||||
func (mp *mempool) validateTransactionInContext(transaction *externalapi.DomainTransaction) error {
|
||||
hasCoinbaseInput := false
|
||||
for _, input := range transaction.Inputs {
|
||||
if input.UTXOEntry.IsCoinbase() {
|
||||
hasCoinbaseInput = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
numExtraOuts := len(transaction.Outputs) - len(transaction.Inputs)
|
||||
if !hasCoinbaseInput && numExtraOuts > 2 && transaction.Fee < uint64(numExtraOuts)*constants.SompiPerKaspa {
|
||||
log.Warnf("Rejected spam tx %s from mempool (%d outputs)", consensushashing.TransactionID(transaction), len(transaction.Outputs))
|
||||
return transactionRuleError(RejectSpamTx, fmt.Sprintf("Rejected spam tx %s from mempool", consensushashing.TransactionID(transaction)))
|
||||
}
|
||||
|
||||
if !mp.config.AcceptNonStandard {
|
||||
err := mp.checkTransactionStandardInContext(transaction)
|
||||
if err != nil {
|
||||
|
@ -577,6 +577,72 @@ func TestRevalidateHighPriorityTransactions(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestRevalidateHighPriorityTransactionsWithChain(t *testing.T) {
|
||||
testutils.ForAllNets(t, true, func(t *testing.T, consensusConfig *consensus.Config) {
|
||||
consensusConfig.BlockCoinbaseMaturity = 0
|
||||
factory := consensus.NewFactory()
|
||||
tc, teardown, err := factory.NewTestConsensus(consensusConfig, "TestRevalidateHighPriorityTransactions")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed setting up TestConsensus: %+v", err)
|
||||
}
|
||||
defer teardown(false)
|
||||
|
||||
miningFactory := miningmanager.NewFactory()
|
||||
mempoolConfig := mempool.DefaultConfig(&consensusConfig.Params)
|
||||
tcAsConsensus := tc.(externalapi.Consensus)
|
||||
tcAsConsensusPointer := &tcAsConsensus
|
||||
consensusReference := consensusreference.NewConsensusReference(&tcAsConsensusPointer)
|
||||
miningManager := miningFactory.NewMiningManager(consensusReference, &consensusConfig.Params, mempoolConfig)
|
||||
|
||||
const chainSize = 10
|
||||
chain, err := createTxChain(tc, chainSize)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
_, err = miningManager.ValidateAndInsertTransaction(chain[0], true, false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
blockHash, _, err := tc.AddBlockOnTips(nil, []*externalapi.DomainTransaction{chain[0].Clone()})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
block, _, err := tc.GetBlock(blockHash)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
_, err = miningManager.HandleNewBlockTransactions(block.Transactions)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
for _, transaction := range chain[1:] {
|
||||
_, err = miningManager.ValidateAndInsertTransaction(transaction, true, false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
_, _, err = tc.AddBlockOnTips(nil, []*externalapi.DomainTransaction{chain[1].Clone()})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
revalidated, err := miningManager.RevalidateHighPriorityTransactions()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(revalidated) != chainSize-2 {
|
||||
t.Fatalf("expected %d transactions to revalidate but instead only %d revalidated", chainSize-2, len(revalidated))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestModifyBlockTemplate verifies that modifying a block template changes coinbase data correctly.
|
||||
func TestModifyBlockTemplate(t *testing.T) {
|
||||
testutils.ForAllNets(t, true, func(t *testing.T, consensusConfig *consensus.Config) {
|
||||
@ -904,40 +970,58 @@ func createArraysOfParentAndChildrenTransactions(tc testapi.TestConsensus) ([]*e
|
||||
func createParentAndChildrenTransactions(tc testapi.TestConsensus) (txParent *externalapi.DomainTransaction,
|
||||
txChild *externalapi.DomainTransaction, err error) {
|
||||
|
||||
chain, err := createTxChain(tc, 2)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return chain[0], chain[1], nil
|
||||
}
|
||||
|
||||
func createTxChain(tc testapi.TestConsensus, numTxs int) ([]*externalapi.DomainTransaction, error) {
|
||||
// We will add two blocks by consensus before the parent transactions, in order to fund the parent transactions.
|
||||
tips, err := tc.Tips()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, _, err = tc.AddBlock(tips, nil, nil)
|
||||
if err != nil {
|
||||
return nil, nil, errors.Wrapf(err, "AddBlock: %v", err)
|
||||
return nil, errors.Wrapf(err, "AddBlock: %v", err)
|
||||
}
|
||||
|
||||
tips, err = tc.Tips()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fundingBlockHashForParent, _, err := tc.AddBlock(tips, nil, nil)
|
||||
if err != nil {
|
||||
return nil, nil, errors.Wrap(err, "AddBlock: ")
|
||||
return nil, errors.Wrap(err, "AddBlock: ")
|
||||
}
|
||||
fundingBlockForParent, _, err := tc.GetBlock(fundingBlockHashForParent)
|
||||
if err != nil {
|
||||
return nil, nil, errors.Wrap(err, "GetBlock: ")
|
||||
return nil, errors.Wrap(err, "GetBlock: ")
|
||||
}
|
||||
fundingTransactionForParent := fundingBlockForParent.Transactions[transactionhelper.CoinbaseTransactionIndex]
|
||||
txParent, err = testutils.CreateTransaction(fundingTransactionForParent, 1000)
|
||||
|
||||
transactions := make([]*externalapi.DomainTransaction, numTxs)
|
||||
transactions[0], err = testutils.CreateTransaction(fundingTransactionForParent, 1000)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
return nil, err
|
||||
}
|
||||
txChild, err = testutils.CreateTransaction(txParent, 1000)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
|
||||
txParent := transactions[0]
|
||||
for i := 1; i < numTxs; i++ {
|
||||
transactions[i], err = testutils.CreateTransaction(txParent, 1000)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
txParent = transactions[i]
|
||||
}
|
||||
return txParent, txChild, nil
|
||||
|
||||
return transactions, nil
|
||||
}
|
||||
|
||||
func createChildAndParentTxsAndAddParentToConsensus(tc testapi.TestConsensus) (*externalapi.DomainTransaction, error) {
|
||||
|
@ -2,6 +2,7 @@ package model
|
||||
|
||||
import (
|
||||
"github.com/kaspanet/kaspad/domain/consensus/model/externalapi"
|
||||
"github.com/kaspanet/kaspad/domain/consensus/ruleerrors"
|
||||
)
|
||||
|
||||
// Mempool maintains a set of known transactions that
|
||||
@ -11,7 +12,7 @@ type Mempool interface {
|
||||
BlockCandidateTransactions() []*externalapi.DomainTransaction
|
||||
ValidateAndInsertTransaction(transaction *externalapi.DomainTransaction, isHighPriority bool, allowOrphan bool) (
|
||||
acceptedTransactions []*externalapi.DomainTransaction, err error)
|
||||
RemoveTransactions(txs []*externalapi.DomainTransaction, removeRedeemers bool) error
|
||||
RemoveInvalidTransactions(err *ruleerrors.ErrInvalidTransactionsInNewBlock) error
|
||||
GetTransaction(
|
||||
transactionID *externalapi.DomainTransactionID,
|
||||
includeTransactionPool bool,
|
||||
|
@ -52,6 +52,8 @@ func (ui *UTXOIndex) Reset() error {
|
||||
ui.mutex.Lock()
|
||||
defer ui.mutex.Unlock()
|
||||
|
||||
log.Infof("Starting UTXO index reset")
|
||||
|
||||
err := ui.store.deleteAll()
|
||||
if err != nil {
|
||||
return err
|
||||
@ -88,7 +90,13 @@ func (ui *UTXOIndex) Reset() error {
|
||||
}
|
||||
|
||||
// This has to be done last to mark that the reset went smoothly and no reset has to be called next time.
|
||||
return ui.store.updateAndCommitVirtualParentsWithoutTransaction(virtualInfo.ParentHashes)
|
||||
err = ui.store.updateAndCommitVirtualParentsWithoutTransaction(virtualInfo.ParentHashes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Infof("Finished UTXO index reset")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ui *UTXOIndex) isSynced() (bool, error) {
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user