Compare commits

...

137 Commits

Author SHA1 Message Date
Michael Sutton
4bb5bf25d3
Bump version to 0.12.22 (#2307) 2025-05-08 11:47:23 +03:00
Michael Sutton
25c2dd8670
apply post-crescendo coinbase maturity to all nets (#2306) 2025-05-05 15:01:13 +03:00
Sergi Rene
c93100ccd0
getPayloadHash returns not zero when payload included (#2305) 2025-04-23 17:24:35 +03:00
Ori Newman
03cc7dfc19 Update version to v0.12.20 2025-03-18 11:06:56 +02:00
Toni Lukkaroinen
ed745a9acb
Fix block verbosedata population… (#2275)
* Fix block verbosedata population from skipping transaction verbosedata population when BlockInfo errorenously reports block as Header only, but domainBlock is still fount with GetBlockEvenIfHeaderOnly.

* Update app/rpc/rpccontext/verbosedata.go

---------

Co-authored-by: Toni Lukkaroinen <toni.lukkaroinen@hoosat.fi>
Co-authored-by: Michael Sutton <mikisiton2@gmail.com>
Co-authored-by: Ori Newman <orinewman1@gmail.com>
2025-03-18 11:03:47 +02:00
Ori Newman
c23c1d141c Update gh actions 2025-03-18 10:59:21 +02:00
Ori Newman
352d261fd6
Fix serializeTransaction (#2302) 2025-02-23 15:03:16 +02:00
Veronica
43b9523919
Adding support for mass. (#2301)
* Adding support for mass.

* update grpc v1.69.2

* Upgrade protos using latest protoc

* Use MassCommitment instead of Mass

* Fix DomainTransaction clone, equal and tests

---------

Co-authored-by: veronica <jimjouny@gmail.com>
Co-authored-by: Ori Newman <orinewman1@gmail.com>
2025-01-21 10:57:28 +02:00
Michael Sutton
6085d1fc84
bump version to 0.12.19 (#2295) 2024-10-22 13:09:31 +03:00
Ori Newman
1e9ddc42d0
Add fee estimation to wallet (#2291)
* Add fee estimation to wallet

* Add fee rate to kaspawallet parse

* Update go version

* Get rid of golint

* Add RBF support to wallet

* Fix bump_fee UTXO lookup and fix wrong change address

* impl storage mass as per KIP9

* Use CalculateTransactionOverallMass where needed

* Some fixes

* Minor typos

* Fix test

* update version

* BroadcastRBF -> BroadcastReplacement

* rc3

* align proto files to only use camel case (fixed on RK as well)

* Rename to FeePolicy and add MaxFee option + todo

* apply max fee constrains

* increase minChangeTarget to 10kas

* fmt

* Some fixes

* fix description: maximum -> minimum

* put min feerate check in the correct location

* Fix calculateFeeLimits nil handling

* Add validations to CLI flags

* Change to rc6

* Add checkTransactionFeeRate

* Add failed broadcast transactions on send error`

* Fix estimateFee change value

* Estimate fee correctly for --send-all

* On estimateFee always assume that the recipient has ECDSA address

* remove patch version

---------

Co-authored-by: Michael Sutton <msutton@cs.huji.ac.il>
2024-10-22 12:34:54 +03:00
Ori Newman
48a142e12f
Add deprecated message (#2282)
* Add deprecated message

* fix readme
2024-08-15 08:15:30 +03:00
Michael Sutton
86b89065cf
KIP9 basic wallet compatibility (#2276)
* introduce min change target

* clarify wallet help messages for from-address and send-all
2024-05-09 18:53:21 +03:00
Michael Sutton
f41dc7fa0b
Bump version to 0.12.17 (#2268)
* Bump version to 0.12.17

* changelog
2024-02-19 11:58:40 +02:00
Michael Sutton
6b38bf7069
RPC SubmitTransaction: Dequeue old responses from previous requests (#2262) 2024-01-05 14:58:19 +02:00
Ori Newman
d2453f8e7b
Lazy wallet utxo sync after broadcasting a tx (#2258)
* Lazy wallet utxo sync after broadcasting a tx

* Make a more granular lock for refreshUTXOs

* Don't push to forceSyncChan if it has an element

* Better policy for used outpoints and wait for first sync when creating an unsigned tx

* fix expire condition

* lock address reading

* fix small memory leak

* add an rpc client dedicated for background ops

* rename to follow conventions

* one more rename

* Compare wallet addresses by value

* small fixes

* Add comment

---------

Co-authored-by: Michael Sutton <msutton@cs.huji.ac.il>
2023-12-27 18:10:16 +02:00
Ori Newman
629faa8436
Add options to see wallet and wallet daemon versions (#2257) 2023-12-26 16:00:44 +02:00
Michael Sutton
91e6c6b74b
version bump + changelog (#2255) 2023-12-25 10:22:29 +02:00
Michael Sutton
0819244ba1
if the tx has change and thus so 2 outputs, try having at least 2 inputs as well (in order to not be slowed down by dust patch) (#2254) 2023-12-25 09:19:04 +02:00
Ori Newman
a0149cd8d0
Broadcast wallet transactions in chunks (#2253) 2023-12-13 16:15:25 +02:00
Ori Newman
5a3b8a0066
Fix type detection in RemoveInvalidTransactions (#2252) 2023-12-12 17:13:27 +02:00
Michael Sutton
8e71f79f98
use rpc to identify testnet 11 (#2211)
Co-authored-by: Ori Newman <orinewman1@gmail.com>
2023-12-12 12:16:35 +02:00
Ori Newman
346341a709
Fix extract atomic swap data pushes (#2203)
* Remove unnecessary drop from ExtractAtomicSwapDataPushes

* fixed atomicswap 

fixed ExtractAtomicSwapDataPushes to extract the correct RefundBlake2b

* Fix message converters

* Fix locktime data size in ExtractAtomicSwapDataPushes

---------

Co-authored-by: pieroforfora <124444595+pieroforfora@users.noreply.github.com>
2023-12-11 16:24:16 +02:00
supertypo
8c881aea39
Added a mainnet dnsseeder (#2247)
Co-authored-by: supertypo <suprtypo@pm.me>
2023-12-07 21:14:25 +02:00
Ori Newman
40ec440dcf
Fix windows asset building by increasing go version (#2245) 2023-12-07 14:10:26 +02:00
Ori Newman
88bdcb43bc
Anti-spam measurements against dust attack (#2223) (#2244)
* BlockCandidateTransactions patch

* Fix condition

* Fix fee

* Fix bug

* Reject from mempool

* Fix hasCoinbaseInput

* Fix position

* Bump version to v0.12.14
2023-12-06 14:38:04 +02:00
Ori Newman
9d1e44673f
Use removeRedeemers correctly (#2235)
* Use removeRedeemers correctly

* Fix topological iteration

* Some fixes

* Ignore RejectDuplicate and swallow other rule errors

* Fix typo

* Don't remove redeemers and skip mempool full revalidation
2023-12-06 14:29:29 +02:00
coderofstuff
387fade044
Fix off by small amounts in sent amount kaspa (#2220)
* Fix off by small amounts in sent amount kaspa

Floating point math causes inconsistencies when converting kas to sompi.

Use a method that parses the amount as a string, the converts it to
sompi then parse back to uint64

* Deal with SendAmount as strings all the way

* Consistent config handling

* Set variables directly from utils.KasToSompi

Use = instead of := to ensure no shadowing

* Fix validate amount regex

* Use decimal places as defined by constants

Also check if SompiPerKaspa is multiple of 10

* Minor updates for context clarity
2023-09-23 11:28:38 +03:00
Ori Newman
c417c8b525
Update ECDSA address test to use a valid public key (#2202) 2023-04-09 14:40:01 +03:00
Ori Newman
bd1420220a
Bump version to v0.12.13 (#2196) 2023-03-06 17:12:35 +02:00
dependabot[bot]
5640ec4020
Bump golang.org/x/net from 0.0.0-20210405180319-a5a99cb37ef4 to 0.7.0 (#2194)
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.0.0-20210405180319-a5a99cb37ef4 to 0.7.0.
- [Release notes](https://github.com/golang/net/releases)
- [Commits](https://github.com/golang/net/commits/v0.7.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Ori Newman <orinewman1@gmail.com>
2023-03-06 14:56:21 +02:00
dependabot[bot]
1c0887ca60
Bump golang.org/x/crypto from 0.0.0-20210513164829-c07d793c2f9a to 0.1.0 (#2195)
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.0.0-20210513164829-c07d793c2f9a to 0.1.0.
- [Release notes](https://github.com/golang/crypto/releases)
- [Commits](https://github.com/golang/crypto/commits/v0.1.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-06 12:54:24 +02:00
Ori Newman
7be3f41aa7
Avoid sending transactions with no funds (#2193) 2023-03-06 12:20:42 +02:00
Ori Newman
26c4c73624
Update changelog.txt and version.go (#2192) 2023-02-28 18:14:01 +02:00
D-Stacks
880d917e58
Rename last references to blockheight - closes #1036 (#2089)
* Rename references of blockheight - closes #1036

* Update tx_invalid.json

* bluescore -> DAAScore

---------

Co-authored-by: Ori Newman <orinewman1@gmail.com>
2023-02-27 17:14:41 +02:00
Eyal Yablonka
3c53c6d8cd
Eyal (#2183)
* Create CODE_OF_CONDUCT.md

Code of conduct added

* changed discord to google form

* Update CODE_OF_CONDUCT.md

Updated code of conduct to match community project.

* Update CODE_OF_CONDUCT.md

Updated code of conduct to match community project.
Added google form.

---------

Co-authored-by: Ori Newman <orinewman1@gmail.com>
2023-02-27 16:10:27 +02:00
Ori Newman
3c4b973090
Extend TestGetPreciseSigOps with more tests (#2188) 2023-02-27 15:58:10 +02:00
Ori Newman
8aee8f81c5
Add Dockerfile to kaspawallet (#2187) 2023-02-27 13:11:52 +02:00
Svarog
ec3441e63f
Add --send-all to kaspawallet send command (#2181)
* Allow to send --all

* Fix a typo

---------

Co-authored-by: Ori Newman <orinewman1@gmail.com>
2023-02-27 12:39:01 +02:00
dependabot[bot]
e3ba1ca07e
Bump golang.org/x/text from 0.3.5 to 0.3.8 (#2190)
Bumps [golang.org/x/text](https://github.com/golang/text) from 0.3.5 to 0.3.8.
- [Release notes](https://github.com/golang/text/releases)
- [Commits](https://github.com/golang/text/compare/v0.3.5...v0.3.8)

---
updated-dependencies:
- dependency-name: golang.org/x/text
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Ori Newman <orinewman1@gmail.com>
2023-02-27 12:26:52 +02:00
Ori Newman
27fdbd9c88
Upgrade to go 1.19 (#2191)
* Upgrade to go 1.19

* fmt
2023-02-27 10:03:11 +02:00
Michael Sutton
377d9aaaeb
Update changelog.txt for v0.12.11 (#2176) 2022-12-01 12:33:48 +02:00
Ori Newman
beee947dda
Fix IBD sync conditions (#2174)
* Fix IBD sync conditions

* Fix syntax

* Fix Sprintf

* Bump version

* On negotiation check only blocks in future of PP

* Only log error and add comment

* Fix comment
2022-11-29 17:18:07 +02:00
Ori Newman
d4a27bf1c1
Update changelog.txt for v0.12.10 (#2172) 2022-11-23 12:26:47 +02:00
Ori Newman
eec6eb9669
Check rule errors when validating blocks with trusted data (#2171) 2022-11-21 23:06:00 +02:00
Ori Newman
d5c10832c2
Update README.md (#2163) 2022-11-20 14:50:47 +02:00
Ori Newman
9fbfba17b6
Compare blue score with selected tip when checking if a pruning point… (#2169)
* Compare blue score with selected tip when checking if a pruning point proof is needed

* Don't redeclare err

Co-authored-by: Michael Sutton <mikisiton2@gmail.com>
2022-11-20 13:35:00 +02:00
Ori Newman
09d698dd0e
Add note about change addresses to 'show-addresses' (#2170)
* Add note about change addresses to 'show-addresses'

* Use stdout
2022-11-20 12:12:09 +02:00
Ori Newman
ec51c6926a
Use one of the From addresses as a change address (#2164)
* Use one of the From addresses as a change address

* Use change address from fromAddress only if useExisting is set to true

* Change FromAddresses description
2022-11-17 15:31:18 +02:00
Ori Newman
7d44275eb1
Add found to GetBlock (#2165) 2022-11-16 23:48:05 +02:00
Ori Newman
a3387a56b3
Increase devnet's initial difficulty (#2167) 2022-11-13 14:01:29 +02:00
Ori Newman
c2ae03fc89
Fix nativeTx to be actually native (#2166) 2022-11-13 13:15:18 +02:00
Ori Newman
6c774c966b
Changelog for v0.12.9 (#2161) 2022-10-24 00:50:38 +03:00
Ori Newman
2d54c9693b
Create directory before locking lock file (#2160) 2022-10-24 00:41:59 +03:00
Ori Newman
d8350d62b0
v0.12.8 changelog.txt (#2159) 2022-10-23 16:29:26 +03:00
Ori Newman
26c7db251f
Make more checks if status is invalid even if the block exists (#2158)
* Make more checks if status is invalid even if the block exists

* Use HasHeader
2022-10-13 19:22:00 +03:00
Michael Sutton
4d435f2b3a
Use utxo diff algo for pruning point move and use acceptance data method only as a fall-back (#2157)
Co-authored-by: Ori Newman <orinewman1@gmail.com>
2022-10-12 12:37:13 +03:00
Tiram
067688f549
Add a new testnet dnsseeder (#2156)
* Add a new testnet dnsseeder

* Apply code format

Co-authored-by: Ori Newman <orinewman1@gmail.com>
2022-10-05 09:23:35 +03:00
Ori Newman
3a3fa0d3f0
Add lock file to kaspawallet (#2154)
* Add lock file to kaspawallet

* Add a possible explanation to password error

Co-authored-by: msutton <mikisiton2@gmail.com>
2022-10-02 23:42:14 +03:00
Ori Newman
cf4073b773
Remove HF activation code (#2152)
* Remove HF activation code

* Remove unused var totalInputs
2022-10-02 19:17:33 +03:00
Michael Sutton
6a5e7c9e3f
Add IBD error details to the log (#2150) 2022-09-30 13:23:54 +03:00
Ori Newman
7e9b5b9010
Security Patch + HF (#2142)
* HF

* Fix lint
2022-09-21 18:58:32 +03:00
D-Stacks
953838e0d8
Allow mismatched versioning connections with rpc_client, with a warning. (#2137) 2022-09-14 09:53:30 +03:00
Ori Newman
a1dcb34c29
Update changelog for v0.12.6 (#2136) 2022-09-09 15:57:43 +03:00
Ori Newman
23764e1b0b
Optionally show serialized transactions on send (#2135)
* Optionally show serialized transactions on send

* Explain more about serialized transactions

* Increase grpc server send message size
2022-09-09 15:51:20 +03:00
Ori Newman
0838cc8e32
Update virtual on IBD if nearly synced (#2134)
* Update virtual on IBD if nearly synced

* Don't resolve virtual if updateVirtual
2022-09-09 00:52:08 +03:00
Ori Newman
9f51330f38
Remove tests from docker files (#2133)
* Remove tests from docker files

* Remove unused interface method
2022-09-01 14:14:37 +03:00
Ori Newman
f6d46fd23f
Update changelog.txt for v0.12.5 (#2130)
* Update changelog.txt for v0.12.5

* Update version and add #2131 to changelog.txt
2022-08-28 13:53:33 +03:00
Svarog
2a7e03e232
Kaspawallet.send(): Make separate context for Broadcast, to prolong timeout (#2131)
* Kaspawallet.send(): Make separate context for Broadcast, to prolong timeout

* Amend comment

Co-authored-by: Michael Sutton <mikisiton2@gmail.com>
2022-08-28 13:17:58 +03:00
Ori Newman
3286a7d010
Call update pruning point if required on resolve virtual (#2129)
* Call UpdatePruningPointIfRequired when resolving virtual

* Don't sanity check pruning point UTXO set if it's genesis

* Add UpdatePruningPointIfRequired on init
2022-08-24 13:32:39 +03:00
Ori Newman
aabbc741d7
Add UseExistingChangeAddress option to the wallet (#2127) 2022-08-21 17:12:05 +03:00
Elichai Turkel
20b7ab89f9
Add tests for hash writers (#2120)
Co-authored-by: Michael Sutton <mikisiton2@gmail.com>
2022-08-21 09:08:20 +03:00
Ori Newman
10f1e7e3f4
Replace daglabs's dnsseeder with Wolfie's (#2119)
Co-authored-by: Michael Sutton <mikisiton2@gmail.com>
2022-08-21 03:10:59 +03:00
Ori Newman
d941c73701
Change testnet dnsseeder (#2126)
Co-authored-by: Michael Sutton <mikisiton2@gmail.com>
2022-08-21 02:32:40 +03:00
Michael Sutton
3f80638c86
Add missing locks to notification listener modifications (#2124) 2022-08-21 01:26:03 +03:00
Michael Sutton
266ec6c270
Calculate pruning point utxo set from acceptance data (#2123)
* Calc new pruning point utxo diff through chain acceptance data

* Fix daa score to chain block
2022-08-17 15:56:53 +03:00
Michael Sutton
9ee409afaa
Fix RPC client memory/goroutine leak (#2122)
* Showcase the RPC client memory leak

* Fixes an RPC client goroutine leak by properly closing the underlying connection
2022-08-09 16:30:24 +03:00
Michael Sutton
715cb3b1ac
Fix a subtle lock sync issue in consensus insert block (#2121)
* Manage the locks more tightly and fix a slight and rare sync issue

* Extract virtualResolveChunk constant
2022-08-02 10:33:39 +03:00
D-Stacks
eb693c4a86
Mempool: Retrive stable state of the mempool. optimze get mempool entries by addresses (#2111)
* fix mempool accessing, rewrite get_mempool_entries_by_addresses

* fix counter, add verbose

* fmt

* addresses as string

* Define error in case utxoEntry is missing.

* fix error variable to string

* stop tests from failing (see in code comment)

* access both pools in the same state via parameters

* get rid of todo message

* fmt - very important!

* perf: scriptpublickey in mempool, no txscript.

* address reveiw

* fmt fix

* mixed up isorphan bool, pass tests now

* do map preallocation in mempoolbyaddresses

* no proallocation for orphanpool sending.

Co-authored-by: Ori Newman <orinewman1@gmail.com>
2022-07-26 23:06:08 +03:00
Aleoami
7a61c637b0
Add RPC timeout parameter to wallet daemon (#2104)
Co-authored-by: Ori Newman <orinewman1@gmail.com>
2022-07-21 13:55:12 +03:00
Michael Sutton
c7bd84ef9d
Bump version and changelog for v0.12.4 (#2118)
* Bump version and changelog
2022-07-17 03:07:51 +03:00
Svarog
b26b9f6c4b
Implement multi-layer auto-compound (#2115)
Co-authored-by: Michael Sutton <mikisiton2@gmail.com>
2022-07-15 14:12:34 +03:00
Michael Sutton
1c9bb54cc2
Crucial fix for the UTXO difference mechanism (#2114)
* Illustrate the bug through prints

* Change consensus API to a single ResolveVirtual call

* nil changeset is not expected when err=nil

* Fixes a deep bug in the resolve virtual process

* Be more defensive at resolving virtual when adding a block

* When finally resolved, set virtual parents properly

* Return nil changeset when nothing happened

* Make sure the block at the split point is reversed to new chain as well

* bump to version 0.12.4

* Avoid locking consensus twice in the common case of adding block with updateVirtual=true

* check err

* Parents must be picked first before set as virtual parents

* Keep the flag for tracking virtual state, since tip sorting perf is high with many tips

* Improve and clarify resolve virtual tests

* Addressing minor review comments

* Fixed a bug in the reported virtual changeset, and modified the test to verify it

* Addressing review comments
2022-07-15 12:35:20 +03:00
Michael Sutton
b9093d59eb
Bump version and add change log (#2110) 2022-06-29 16:24:56 +03:00
D-Stacks
18d000f625
Logger: change log level for "Couldn't find UTXO entry" to debug (and ignore message on orphan transactions) (#2108)
* Logger: change log level for "Couldn't find UTXO entry" to debug

* ignore error for orphans

* continue also if orphans

* check if blocks exist

* safe rpc mode

* limit window size

* verify block status depending on context

* allow a 2 factor gap in expected mergeset size

Co-authored-by: msutton <mikisiton2@gmail.com>
2022-06-29 15:54:40 +03:00
Ori Newman
c5aade7e7f
Update changelog to v0.12.2 (#2091) 2022-06-17 18:24:21 +03:00
Ori Newman
d4b741fd7c
Bump version to v0.12.2 (#2086) 2022-06-16 12:54:38 +03:00
D-Stacks
74a4f927e9
RPC: include orphans into mempool entries (#2046)
* RPC: include orphans into mempool entries

* no need for + 1

* give request option to choose mempool pool(s)

* add to wallet, fix bg

* use orphanpool rpc to test for orphans

* fix fmt

* fix crash when quering orphan pool in get_mempool_entries

* pass the tests, fix fromAppMessage bug

* Update config_test.go

don't think this is needed

* needed for tests to pass

* inverse to transactionpoolfilter, cut down code to two ifs.

* fmt

* update test to true false, forgot one includetransactionpool renaming

* update and simplyfiy get_mempool_entry handler

* comment outdated

* i think we usually use make instead of var.

* Fix some leftovers of includeTransactionPool

Co-authored-by: Ori Newman <orinewman1@gmail.com>
2022-06-15 23:46:44 +03:00
biospb
847aafc91f
Fix RPC connections counting (#2026)
* Fix RPC connections counting

* show incomming connections count

* Use the flag RPCMaxClients instead of the const RPCMaxInboundConnections

* Add grpc server name to log message

Co-authored-by: Michael Sutton <mikisiton2@gmail.com>
2022-06-15 22:49:36 +03:00
Aleoami
c87e541570
Clarify wallet message concerning a wallet daemon sync state (#2045)
* upd clarified wallet daemon syncronization state log message

* Update address.go

* Update create_unsigned_transaction.go

* Update external_spendable_utxos.go

* Update sync.go

Co-authored-by: Michael Sutton <mikisiton2@gmail.com>
2022-06-15 16:05:01 +03:00
Aleoami
2ea1c4f922
Change the way the miner executable reports execution errors (closes issue #1677) (#2048)
* fix changed the way the miner executable reports execution errors

* Update main.go

* Update main.go

* Update main.go

* Update main.go

* Rolled back

Co-authored-by: Ori Newman <orinewman1@gmail.com>
Co-authored-by: Michael Sutton <mikisiton2@gmail.com>
2022-06-15 14:05:05 +03:00
Aleoami
5e9c28b77b
fix the confusing term in the getHashrate() (#2081)
Co-authored-by: Michael Sutton <mikisiton2@gmail.com>
2022-06-15 03:24:10 +03:00
Michael Sutton
d957a6d93a
Fix UTXO diff child error (#2084)
* Avoid creating the chain iterator if high hash is actually low hash

* Always use iterator in nextPruningPointAndCandidateByBlockHash

* Initial failing test

* Minimal failing test + some comments

* go lint

* Add simpler tests with two different errors

* Missed some error checks

* Minor

* A workaround patch for preventing the missing utxo child diff bug

* Make sure we fully resolve virtual

* Move ResolveVirtualWithMaxParam to test consensus

* Mark virtual not updated and loop in batches

* Refactor: remove VirtualChangeSet from functions return values

* Remove workaround comments

* If block has no body, virtual is still considered updated

* Remove special error ErrReverseUTXODiffsUTXODiffChildNotFound

Co-authored-by: Ori Newman <orinewman1@gmail.com>
2022-06-15 02:52:14 +03:00
Michael Sutton
b2648aa5bd
Fix not in selected chain crash (#2082)
* Avoid creating the chain iterator if high hash is actually low hash

* Always use iterator in nextPruningPointAndCandidateByBlockHash

Co-authored-by: Ori Newman <orinewman1@gmail.com>
2022-06-15 02:32:47 +03:00
D-Stacks
3908f274ae
[Ready] - RPC & UtxoIndex: keep track of, query and test circulating supply. (#2070)
* start

* pass tests, (cannot get before put!) and clean up.

* add rpc call.

* create and pass tests, fix bugs.  fully implement rpc

* As always fmt

* remover old test

* clean up proto comment

* put the logger back in place.

* revert back to 10 sec limit.

* migration, change utxoChanged removal to whole utxoEntryPair, add methods to update circulating supply, intialize circulating supply from reset.

* Update utxoindex.go

* ad

* rename to max, change comment

* one more total to max

Co-authored-by: Ori Newman <orinewman1@gmail.com>
2022-06-05 16:42:08 +03:00
Aleoami
fa7ea121ff
fix kaspawallet help messages, clarify sweep command help string (#2067)
* fix kaspawallet help messages, clarify sweep command help string

* fix sweep cmd help string phrasing

Co-authored-by: Ori Newman <orinewman1@gmail.com>
2022-06-05 15:39:56 +03:00
biospb
24848da895
Wallet parse/send/create commands improvement (#2024)
* Fix wallet parsing of multi tx data

* Output wallet msgs to stderr when creating/signing tx

* Fix go fmt

Co-authored-by: Ori Newman <orinewman1@gmail.com>
2022-06-03 19:06:51 +03:00
Michael Sutton
b200b77541
Use chunks for GetBlocksAcceptanceData calls in order to avoid blocking consensus for too long (#2075) 2022-06-03 18:25:02 +03:00
Michael Sutton
d50ad0667c
Unite multiple GetBlockAcceptanceData consensus calls to one (#2074)
* Unite multiple `GetBlockAcceptanceData` consensus calls to one

* Variable rename
2022-06-01 12:19:21 +03:00
Ori Newman
5cea285960
Update many-small-chains-and-one-big-chain DAG to not fail merge depth limit (#2072)
Co-authored-by: Michael Sutton <mikisiton2@gmail.com>
2022-05-31 16:13:43 +03:00
Michael Sutton
7eb5085f6b
Update changelog with v0.12.1 changes (#2071)
* Update changelog with v0.12.1 changes

* Remove full links and credits
2022-05-31 15:00:13 +03:00
D-Stacks
491e3569d2
RPC: include isSynced and isUtxoIndexed in GetInfoResponse (#2068)
* include utxoIndex and isNearlySynced in GetInfo

* fmt fixing

* add missing IsNearlySynced

* change `isNearlySynced` -> `isSynced` & `isUtxoIndexSet` ->`isUtxoIndexed`

* Add a check for `HasPeers` to make `isSynced` identical to `GetBlockTemplate.isSynced`

Co-authored-by: msutton <mikisiton2@gmail.com>
2022-05-30 13:12:00 +03:00
Aleoami
440aea19b0
Add wallet daemon state messages to a terminal window log (#2062)
* add wallet daemon state messages to a terminal window log

* refactoring changed var name to harmonize it

* fix global vars made the part of the "sever" class

* fix log message phrasing

Co-authored-by: Michael Sutton <mikisiton2@gmail.com>
2022-05-25 22:55:53 +03:00
Aleoami
968d47c3e6
add explanatory message about the mnemonic at the wallet creation (#2047)
Co-authored-by: Michael Sutton <mikisiton2@gmail.com>
2022-05-25 18:36:38 +03:00
D-Stacks
052193865e
populate with verbos (#2064)
Co-authored-by: Ori Newman <orinewman1@gmail.com>
Co-authored-by: Michael Sutton <mikisiton2@gmail.com>
2022-05-25 18:23:44 +03:00
D-Stacks
85febcb551
update rpc.md with includeAcceptedTransactionIds (#2065)
Co-authored-by: Michael Sutton <mikisiton2@gmail.com>
2022-05-25 18:13:35 +03:00
D-Stacks
a4d9fa10bf
fix typo (#2060)
Co-authored-by: Ori Newman <orinewman1@gmail.com>
2022-05-25 10:17:58 +03:00
Michael Sutton
cd5fd86ad3
Wrap the entire wallet send operation with a lock (#2063) 2022-05-25 08:55:35 +03:00
Ori Newman
b84d6fed2c
broadcast is using refreshUTXOs, so it should be protected by lock (#2061) 2022-05-25 01:16:54 +03:00
Svarog
24c94b38be
Move OnBlockAdded event to the channel that was only used by virtualChanged events (#2059)
* Move OnBlockAdded event to the channel that was only used by virtualChanged events

* Don't send notifications for header-only and invalid blocks

* Return block status from block processor and use it for event raising decision

* Use MaybeEnqueue for consensus events

* go lint

* Fix RPC call to actually include tx ids

Co-authored-by: msutton <mikisiton2@gmail.com>
2022-05-24 01:11:01 +03:00
Ori Newman
4dd7113dc5
Increase virtualChangeChan to 100e3 (#2056)
* Increase virtualChangeChan to 100e3
Don't crash when sending UTXO RPC notification to a closed route
Throw error if virtualChangeChan is full

* Use MaybeEnqueue in more places

* Remove comment

* Ignore capacity reached errors on MaybeEnqueue
2022-05-20 19:31:13 +03:00
biospb
48c7fa0104
Wallet synchronization improvement (#2025)
* Wallet synchronization improvement

* Much faster sync on startup
* Avoid double scan of the same address range

* Eliminate 1 sec delay on start

* Rename constant and add numIndexesToQueryForRecentAddresses const

Co-authored-by: Ori Newman <orinewman1@gmail.com>
2022-05-19 17:58:33 +03:00
Ori Newman
4d0cf2169a
Bumpt to v0.12.1 (#2054) 2022-05-19 16:45:35 +03:00
Ori Newman
5f7cc079e9
Make kaspawallet ignore outputs that exist in the mempool (#2053)
* Make kaspawallet ignore outputs that exist in the mempool

* Make kaspawallet ignore outputs that exist in the mempool

* Add comment
2022-05-19 16:12:17 +03:00
Michael Sutton
016ddfdfce
Use a channel for utxo change events (#2052)
* Use a channel from within consensus in order to raise change events in order -- note that this is only a draft commit for discussion

* Fix compilation

* Check for nil

* Allow nil virtualChangeChan

* Remove redundant comments

* Call notifyVirtualChange instead of notifyUTXOsChanged

* Remove redundant comment

* Add a separate function for initVirtualChangeHandler

* Remove redundant type

* Check for nil in the right place

* Fix integration test

* Add data to virtual changeset and cleanup block added event logic

* Renames

* Comment

Co-authored-by: Ori Newman <orinewman1@gmail.com>
2022-05-19 14:07:48 +03:00
Svarog
5d24e2afbc
Allow blank address in NotifyUTXOsChanged to get all updates (#2027)
* Allow blank address in NotifyUTXOsChanged to get all updates

* To see if address is possible to extract: Check for NonStandardTy rather than error

* Don't swallow errors

Co-authored-by: Ori Newman <orinewman1@gmail.com>
2022-05-18 22:28:33 +03:00
biospb
8735da045f
Block template cache improvement (#2023)
* Block template cache improvement

* Avoid concurrent calls to template builder
* Clear cache on new block event
* Move IsNearlySynced logic to within consensus and cache it for each template 
* Use a single consensus call for building template and checking synced

Co-authored-by: msutton <mikisiton2@gmail.com>
2022-05-06 11:36:07 +03:00
Ori Newman
c839337425
Add finality check to ResolveVirtual (#2041)
* Add finality check to ResolveVirtual

* Warn on finality conflict only if needed
2022-05-06 00:29:48 +03:00
Ori Newman
7390651072
Remove HF1 activation code (#2042)
* Remove HF1 activation code

* Remove test with an overflow

* Fix "max sompi amount is never dust"

* Remove estimatedHeaderUpperBound logic
2022-05-05 20:35:17 +03:00
Svarog
52fbeedf20
Add AcceptedTransactionIDs to ChainChanged notification and VirtualSelectedParentChain RPC (#2036)
* Add acceptedTransactionIds to GetVirtualSelectedParentChainFromBlockResponseMessage and VirtualSelectedParentChainChangedNotificationMessage

* Modify appmessage structs to include new fields

* Implement the functionality for acceptedTransactionID notifications

* Add missing field for IncludeAcceptedTransactionIds

* Notify of block added before notifying that chain changed

* Use consensushashing instead of Transaction.ID

* Don't notify of empty virtual changes

* Don't generate virtualChainChanged notification if there's nobody subscribed

* Fix test to not expect empty notifications

* Don't generate acceptedTransactionIDs if they were not requested by anyone

Co-authored-by: Ori Newman <orinewman1@gmail.com>
2022-05-05 12:35:02 +03:00
biospb
1660cf0cf1
Improved staging shard performance (#2034)
using map instead of growing array as shard id can be 1000+ in some cases

Co-authored-by: Ori Newman <orinewman1@gmail.com>
2022-05-04 21:24:25 +03:00
Ori Newman
2b5202be7a
Update Dockerfile for go 1.18 (#2038) 2022-05-03 00:22:47 +03:00
Ori Newman
9ffbb15160
Use double pointer when passing rpcError to errors.As (#2039) 2022-04-29 13:23:17 +03:00
tmrlvi
540b0d3a22
Add support for from address in kaspawallet send (#1964)
* Added option to choose from address in kaspawallet + fixed a bug with 0 change

* Checking if From is missing

* Fixed after merge to kaspad0.12

* Applying changes also to send command

* Better help description

* Fixed bug where we take all utxos except the one we wanted

* Swallow the parsing error only when we want to filter

* checking for wallet address directly

* go fmt

* Changing to `--from-address`

Co-authored-by: Ori Newman <orinewman1@gmail.com>
2022-04-27 22:51:10 +03:00
biospb
8d5faee53a
Fix wallet output in send/broadcast cmd (#2032)
Co-authored-by: Ori Newman <orinewman1@gmail.com>
2022-04-27 01:41:02 +03:00
D-Stacks
6e2fd0633b
add "GetMempoolEntriesByAddresses" to kaspad RPC. (#2022)
* add "GetMempoolEntriesByAddresses" to kaspad RPC.

* fmt formatting.

* some things I forgot

* update rpc.md

* forgot to add handler

* fix fmt

* bug fix, implicat testing & error handling

* address reveiw

* address reveiw
2022-04-26 12:31:31 +03:00
Ori Newman
beb038c815
Update changelog.txt for v0.12.0 (#2021)
* Update changelog.txt for v0.12.0

* Fix typo
2022-04-14 13:28:19 +03:00
Ori Newman
35a959b56f
Making a workaround for the UTXO diff child bug (#2020)
* Making a workaround for the UTXO diff child bug

* Fix error message

* Fix error message
2022-04-14 12:55:20 +03:00
D-Stacks
57c6118be8
Adds a "sweep" command to kaspawallet (#2018)
* Add "sweep" command to kaspawallet

* minor linting & layout improvements.

* contain code in "sweep"

* update to sweep.go

* formatting and bugs

* clean up renaming of ExtractTransaction functions

* Nicer formating of display

* address reveiw.

* one more sweeped -> swept

* missed one reveiw comment. (extra mass).

* remove redundant code from split function.

Co-authored-by: Ori Newman <orinewman1@gmail.com>
2022-04-14 02:16:16 +03:00
Michael Sutton
723aebbec9
Do not apply merge depth root heuristic on orphan roots (#2019) 2022-04-13 21:06:08 +03:00
Ori Newman
2b395e34b1
Use separate depth than finality depth for merge set calculations after HF (#2013)
* Use separate than finality depth for merge set calculations after HF

* Add comments and edit error messages

* Fix TestValidateTransactionInContextAndPopulateFee

* Don't disconnect from node if isViolatingBoundedMergeDepth

* Use new merge root for virtual pick parents; apply HF1 daa score split for validation only

* Use `blue work` heuristic to skip irrelevant relay blocks

* Minor

* Make sure virtual's merge depth root is a real block

* For ghostdag data we always use the non-trusted data

* Fix TestBoundedMergeDepth and in IBD use VirtualMergeDepthRoot instead of MergeDepthRoot

* Update HF1DAAScore

* Make sure merge root and finality are called + avoid calculating virtual root twice

* Update block version to 1 after HF

* Update to v0.12.0

Co-authored-by: msutton <mikisiton2@gmail.com>
2022-04-12 00:26:44 +03:00
Svarog
ada559f007
Kaspawallet daemon: Add Send and Sign commands (#2016)
* Add send and sign commands to protobuf

* Added Send and Sign stubs in kaspawalletd server

* Implement Sign

* Implemented Send

* Allow Broadcast command to supply multiple transactions

* No longer prompt for password in DecryptMnemonics

* Rename TransactionIDs -> TxIDs to keep consistency with Broadcast

* Add some comments and formatting

Co-authored-by: Ori Newman <orinewman1@gmail.com>
2022-04-11 23:42:01 +03:00
Ori Newman
357e8ce73c
Use cosigner index 0 for read only wallets (#2014) 2022-04-11 02:35:47 +03:00
Ori Newman
6725902663
Bump version and update changelog (#2011) 2022-04-06 22:06:57 +03:00
Ori Newman
99bb21c512
Decrement estimatedHeaderUpperBound from mempool's MaxBlockMass (#2009) 2022-04-06 21:53:00 +03:00
Ori Newman
a4669f3fb5
Bump version and update changelog (#2008) 2022-04-06 02:01:08 +03:00
Ori Newman
e8f40bdff9
Don't skip wallet address with different cosigner index (#2007) 2022-04-06 01:51:13 +03:00
Michael Sutton
68a407ea37
Update changelog for v0.11.15 (#2006)
* Update changelog for v0.11.15

* Print full json of rule violating header
2022-04-05 16:13:11 +03:00
322 changed files with 18332 additions and 8380 deletions

View File

@ -1,7 +1,7 @@
name: Build and upload assets name: Build and upload assets
on: on:
release: release:
types: [ published ] types: [published]
jobs: jobs:
build: build:
@ -9,7 +9,7 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
os: [ ubuntu-latest, windows-latest, macos-latest ] os: [ubuntu-latest, windows-latest, macos-latest]
name: Building, ${{ matrix.os }} name: Building, ${{ matrix.os }}
steps: steps:
- name: Fix CRLF on Windows - name: Fix CRLF on Windows
@ -17,13 +17,12 @@ jobs:
run: git config --global core.autocrlf false run: git config --global core.autocrlf false
- name: Check out code into the Go module directory - name: Check out code into the Go module directory
uses: actions/checkout@v2 uses: actions/checkout@v4
- name: Setup Go - name: Setup Go
uses: actions/setup-go@v2 uses: actions/setup-go@v5
with: with:
go-version: 1.18 go-version: 1.21
- name: Build on Linux - name: Build on Linux
if: runner.os == 'Linux' if: runner.os == 'Linux'
@ -31,7 +30,7 @@ jobs:
# `-tags netgo,osusergo` means use pure go replacements for "os/user" and "net" # `-tags netgo,osusergo` means use pure go replacements for "os/user" and "net"
# `-s -w` strips the binary to produce smaller size binaries # `-s -w` strips the binary to produce smaller size binaries
run: | 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" archive="bin/kaspad-${{ github.event.release.tag_name }}-linux.zip"
asset_name="kaspad-${{ github.event.release.tag_name }}-linux.zip" asset_name="kaspad-${{ github.event.release.tag_name }}-linux.zip"
zip -r "${archive}" ./bin/* zip -r "${archive}" ./bin/*
@ -42,7 +41,7 @@ jobs:
if: runner.os == 'Windows' if: runner.os == 'Windows'
shell: bash shell: bash
run: | 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" archive="bin/kaspad-${{ github.event.release.tag_name }}-win64.zip"
asset_name="kaspad-${{ github.event.release.tag_name }}-win64.zip" asset_name="kaspad-${{ github.event.release.tag_name }}-win64.zip"
powershell "Compress-Archive bin/* \"${archive}\"" powershell "Compress-Archive bin/* \"${archive}\""
@ -52,14 +51,13 @@ jobs:
- name: Build on MacOS - name: Build on MacOS
if: runner.os == 'macOS' if: runner.os == 'macOS'
run: | 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" archive="bin/kaspad-${{ github.event.release.tag_name }}-osx.zip"
asset_name="kaspad-${{ github.event.release.tag_name }}-osx.zip" asset_name="kaspad-${{ github.event.release.tag_name }}-osx.zip"
zip -r "${archive}" ./bin/* zip -r "${archive}" ./bin/*
echo "archive=${archive}" >> $GITHUB_ENV echo "archive=${archive}" >> $GITHUB_ENV
echo "asset_name=${asset_name}" >> $GITHUB_ENV echo "asset_name=${asset_name}" >> $GITHUB_ENV
- name: Upload release asset - name: Upload release asset
uses: actions/upload-release-asset@v1 uses: actions/upload-release-asset@v1
env: env:

View File

@ -11,18 +11,18 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
branch: [ master, latest ] branch: [master, latest]
name: Race detection on ${{ matrix.branch }} name: Race detection on ${{ matrix.branch }}
steps: steps:
- name: Check out code into the Go module directory - name: Check out code into the Go module directory
uses: actions/checkout@v2 uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Setup Go - name: Setup Go
uses: actions/setup-go@v2 uses: actions/setup-go@v5
with: with:
go-version: 1.18 go-version: 1.23
- name: Set scheduled branch name - name: Set scheduled branch name
shell: bash shell: bash

View File

@ -8,22 +8,20 @@ on:
types: [opened, synchronize, edited] types: [opened, synchronize, edited]
jobs: jobs:
build: build:
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
os: [ ubuntu-latest, macos-latest ] os: [ubuntu-latest, macos-latest]
name: Tests, ${{ matrix.os }} name: Tests, ${{ matrix.os }}
steps: steps:
- name: Fix CRLF on Windows - name: Fix CRLF on Windows
if: runner.os == 'Windows' if: runner.os == 'Windows'
run: git config --global core.autocrlf false run: git config --global core.autocrlf false
- name: Check out code into the Go module directory - 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 # Increase the pagefile size on Windows to aviod running out of memory
- name: Increase pagefile size on Windows - name: Increase pagefile size on Windows
@ -31,14 +29,13 @@ jobs:
run: powershell -command .github\workflows\SetPageFileSize.ps1 run: powershell -command .github\workflows\SetPageFileSize.ps1
- name: Setup Go - name: Setup Go
uses: actions/setup-go@v2 uses: actions/setup-go@v5
with: with:
go-version: 1.18 go-version: 1.23
# Source: https://github.com/actions/cache/blob/main/examples.md#go---modules # Source: https://github.com/actions/cache/blob/main/examples.md#go---modules
- name: Go Cache - name: Go Cache
uses: actions/cache@v2 uses: actions/cache@v4
with: with:
path: ~/go/pkg/mod path: ~/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
@ -49,19 +46,17 @@ jobs:
shell: bash shell: bash
run: ./build_and_test.sh -v run: ./build_and_test.sh -v
stability-test-fast: stability-test-fast:
runs-on: ubuntu-latest runs-on: ubuntu-latest
name: Fast stability tests, ${{ github.head_ref }} name: Fast stability tests, ${{ github.head_ref }}
steps: steps:
- name: Setup Go - name: Setup Go
uses: actions/setup-go@v2 uses: actions/setup-go@v5
with: with:
go-version: 1.18 go-version: 1.23
- name: Checkout - name: Checkout
uses: actions/checkout@v2 uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
@ -75,18 +70,17 @@ jobs:
working-directory: stability-tests working-directory: stability-tests
run: ./install_and_test.sh run: ./install_and_test.sh
coverage: coverage:
runs-on: ubuntu-latest runs-on: ubuntu-latest
name: Produce code coverage name: Produce code coverage
steps: steps:
- name: Check out code into the Go module directory - name: Check out code into the Go module directory
uses: actions/checkout@v2 uses: actions/checkout@v4
- name: Setup Go - name: Setup Go
uses: actions/setup-go@v2 uses: actions/setup-go@v5
with: with:
go-version: 1.18 go-version: 1.23
- name: Delete the stability tests from coverage - name: Delete the stability tests from coverage
run: rm -r stability-tests run: rm -r stability-tests

1
.gitignore vendored
View File

@ -53,6 +53,7 @@ _testmain.go
debug debug
debug.test debug.test
__debug_bin __debug_bin
*__debug_*
# CI # CI
version.txt version.txt

43
CODE_OF_CONDUCT.md Normal file
View 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/

View File

@ -1,13 +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
[![ISC License](http://img.shields.io/badge/license-ISC-blue.svg)](https://choosealicense.com/licenses/isc/) [![ISC License](http://img.shields.io/badge/license-ISC-blue.svg)](https://choosealicense.com/licenses/isc/)
[![GoDoc](https://img.shields.io/badge/godoc-reference-blue.svg)](http://godoc.org/github.com/kaspanet/kaspad) [![GoDoc](https://img.shields.io/badge/godoc-reference-blue.svg)](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).
This project is currently under active development and is in Beta state.
## What is kaspa ## What is kaspa
@ -15,7 +17,7 @@ Kaspa is an attempt at a proof-of-work cryptocurrency with instant confirmations
## Requirements ## Requirements
Go 1.18 or later. Go 1.23 or later.
## Installation ## Installation
@ -42,7 +44,6 @@ $ go install . ./cmd/...
not already add the bin directory to your system path during Go installation, not already add the bin directory to your system path during Go installation,
you are encouraged to do so now. you are encouraged to do so now.
## Getting Started ## Getting Started
Kaspad has several configuration options available to tweak how it runs, but all Kaspad has several configuration options available to tweak how it runs, but all
@ -53,6 +54,7 @@ $ kaspad
``` ```
## Discord ## Discord
Join our discord server using the following link: https://discord.gg/YNYnNN5Pf2 Join our discord server using the following link: https://discord.gg/YNYnNN5Pf2
## Issue Tracker ## Issue Tracker

View File

@ -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 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. 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 The kaspa protocol consists of exchanging messages between peers. Each
message is preceded by a header which identifies information about it such as 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 appmessage using kaspa encoding are handled so the caller doesn't have to concern
themselves with the specifics. themselves with the specifics.
Message Interaction # Message Interaction
The following provides a quick summary of how the kaspa messages are intended 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 to interact with one another. As stated above, these interactions are not
@ -45,13 +45,13 @@ interactions in no particular order.
notfound message (MsgNotFound) notfound message (MsgNotFound)
ping message (MsgPing) pong message (MsgPong) ping message (MsgPing) pong message (MsgPong)
Common Parameters # Common Parameters
There are several common parameters that arise when using this package to read 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 and write kaspa messages. The following sections provide a quick overview of
these parameters so the next sections can build on them. 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 The protocol version should be negotiated with the remote peer at a higher
level than this package via the version (MsgVersion) message exchange, however, 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 for all outbound connections before a potentially lower protocol version is
negotiated. negotiated.
Kaspa Network # Kaspa Network
The kaspa network is a magic number which is used to identify the start of a 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 message and which kaspa network the message applies to. This package provides
the following constants: the following constants:
appmessage.Mainnet appmessage.Mainnet
appmessage.Testnet (Test network) appmessage.Testnet (Test network)
appmessage.Simnet (Simulation test network) appmessage.Simnet (Simulation test network)
appmessage.Devnet (Development network) appmessage.Devnet (Development network)
Determining Message Type # Determining Message Type
As discussed in the kaspa message overview section, this package reads As discussed in the kaspa message overview section, this package reads
and writes kaspa messages using a generic interface named Message. In 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) 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 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 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 // Log and handle the error
} }
Writing Messages # Writing Messages
In order to marshall kaspa messages to the appmessage, use the WriteMessage 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 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 // Log and handle the error
} }
Errors # Errors
Errors returned by this package are either the raw errors provided by underlying 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 calls to read/write from streams such as io.EOF, io.ErrUnexpectedEOF, and

View File

@ -2,9 +2,10 @@ package appmessage
import ( import (
"encoding/hex" "encoding/hex"
"github.com/pkg/errors"
"math/big" "math/big"
"github.com/pkg/errors"
"github.com/kaspanet/kaspad/domain/consensus/utils/blockheader" "github.com/kaspanet/kaspad/domain/consensus/utils/blockheader"
"github.com/kaspanet/kaspad/domain/consensus/utils/hashes" "github.com/kaspanet/kaspad/domain/consensus/utils/hashes"
"github.com/kaspanet/kaspad/domain/consensus/utils/utxo" "github.com/kaspanet/kaspad/domain/consensus/utils/utxo"
@ -213,13 +214,14 @@ func RPCTransactionToDomainTransaction(rpcTransaction *RPCTransaction) (*externa
} }
return &externalapi.DomainTransaction{ return &externalapi.DomainTransaction{
Version: rpcTransaction.Version, Version: rpcTransaction.Version,
Inputs: inputs, Inputs: inputs,
Outputs: outputs, Outputs: outputs,
LockTime: rpcTransaction.LockTime, LockTime: rpcTransaction.LockTime,
SubnetworkID: *subnetworkID, SubnetworkID: *subnetworkID,
Gas: rpcTransaction.LockTime, Gas: rpcTransaction.Gas,
Payload: payload, MassCommitment: rpcTransaction.Mass,
Payload: payload,
}, nil }, nil
} }
@ -286,7 +288,8 @@ func DomainTransactionToRPCTransaction(transaction *externalapi.DomainTransactio
Outputs: outputs, Outputs: outputs,
LockTime: transaction.LockTime, LockTime: transaction.LockTime,
SubnetworkID: subnetworkID, SubnetworkID: subnetworkID,
Gas: transaction.LockTime, Gas: transaction.Gas,
Mass: transaction.MassCommitment,
Payload: payload, Payload: payload,
} }
} }

View File

@ -159,6 +159,14 @@ const (
CmdNotifyNewBlockTemplateRequestMessage CmdNotifyNewBlockTemplateRequestMessage
CmdNotifyNewBlockTemplateResponseMessage CmdNotifyNewBlockTemplateResponseMessage
CmdNewBlockTemplateNotificationMessage CmdNewBlockTemplateNotificationMessage
CmdGetMempoolEntriesByAddressesRequestMessage
CmdGetMempoolEntriesByAddressesResponseMessage
CmdGetCoinSupplyRequestMessage
CmdGetCoinSupplyResponseMessage
CmdGetFeeEstimateRequestMessage
CmdGetFeeEstimateResponseMessage
CmdSubmitTransactionReplacementRequestMessage
CmdSubmitTransactionReplacementResponseMessage
) )
// ProtocolMessageCommandToString maps all MessageCommands to their string representation // ProtocolMessageCommandToString maps all MessageCommands to their string representation
@ -292,6 +300,14 @@ var RPCMessageCommandToString = map[MessageCommand]string{
CmdNotifyNewBlockTemplateRequestMessage: "NotifyNewBlockTemplateRequest", CmdNotifyNewBlockTemplateRequestMessage: "NotifyNewBlockTemplateRequest",
CmdNotifyNewBlockTemplateResponseMessage: "NotifyNewBlockTemplateResponse", CmdNotifyNewBlockTemplateResponseMessage: "NotifyNewBlockTemplateResponse",
CmdNewBlockTemplateNotificationMessage: "NewBlockTemplateNotification", CmdNewBlockTemplateNotificationMessage: "NewBlockTemplateNotification",
CmdGetMempoolEntriesByAddressesRequestMessage: "GetMempoolEntriesByAddressesRequest",
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 // Message is an interface that describes a kaspa message. A type that

View File

@ -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{ var blockOne = MsgBlock{
Header: MsgBlockHeader{ Header: MsgBlockHeader{
Version: 0, Version: 0,

View File

@ -133,8 +133,8 @@ func TestTx(t *testing.T) {
// TestTxHash tests the ability to generate the hash of a transaction accurately. // TestTxHash tests the ability to generate the hash of a transaction accurately.
func TestTxHashAndID(t *testing.T) { func TestTxHashAndID(t *testing.T) {
txHash1Str := "93663e597f6c968d32d229002f76408edf30d6a0151ff679fc729812d8cb2acc" txHash1Str := "b06f8b650115b5cf4d59499e10764a9312742930cb43c9b4ff6495d76f332ed7"
txID1Str := "24079c6d2bdf602fc389cc307349054937744a9c8dc0f07c023e6af0e949a4e7" txID1Str := "e20225c3d065ee41743607ee627db44d01ef396dc9779b05b2caf55bac50e12d"
wantTxID1, err := transactionid.FromString(txID1Str) wantTxID1, err := transactionid.FromString(txID1Str)
if err != nil { if err != nil {
t.Fatalf("NewTxIDFromStr: %v", err) t.Fatalf("NewTxIDFromStr: %v", err)
@ -185,7 +185,7 @@ func TestTxHashAndID(t *testing.T) {
spew.Sprint(tx1ID), spew.Sprint(wantTxID1)) spew.Sprint(tx1ID), spew.Sprint(wantTxID1))
} }
hash2Str := "8dafd1bec24527d8e3b443ceb0a3b92fffc0d60026317f890b2faf5e9afc177a" hash2Str := "fa16a8ce88d52ca1ff45187bbba0d33044e9f5fe309e8d0b22d4812dcf1782b7"
wantHash2, err := externalapi.NewDomainHashFromString(hash2Str) wantHash2, err := externalapi.NewDomainHashFromString(hash2Str)
if err != nil { if err != nil {
t.Errorf("NewTxIDFromStr: %v", err) t.Errorf("NewTxIDFromStr: %v", err)

View 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{}
}

View File

@ -0,0 +1,40 @@
package appmessage
// GetCoinSupplyRequestMessage is an appmessage corresponding to
// its respective RPC message
type GetCoinSupplyRequestMessage struct {
baseMessage
}
// Command returns the protocol command string for the message
func (msg *GetCoinSupplyRequestMessage) Command() MessageCommand {
return CmdGetCoinSupplyRequestMessage
}
// NewGetCoinSupplyRequestMessage returns a instance of the message
func NewGetCoinSupplyRequestMessage() *GetCoinSupplyRequestMessage {
return &GetCoinSupplyRequestMessage{}
}
// GetCoinSupplyResponseMessage is an appmessage corresponding to
// its respective RPC message
type GetCoinSupplyResponseMessage struct {
baseMessage
MaxSompi uint64
CirculatingSompi uint64
Error *RPCError
}
// Command returns the protocol command string for the message
func (msg *GetCoinSupplyResponseMessage) Command() MessageCommand {
return CmdGetCoinSupplyResponseMessage
}
// NewGetCoinSupplyResponseMessage returns a instance of the message
func NewGetCoinSupplyResponseMessage(maxSompi uint64, circulatingSompi uint64) *GetCoinSupplyResponseMessage {
return &GetCoinSupplyResponseMessage{
MaxSompi: maxSompi,
CirculatingSompi: circulatingSompi,
}
}

View File

@ -23,6 +23,8 @@ type GetInfoResponseMessage struct {
P2PID string P2PID string
MempoolSize uint64 MempoolSize uint64
ServerVersion string ServerVersion string
IsUtxoIndexed bool
IsSynced bool
Error *RPCError Error *RPCError
} }
@ -33,10 +35,12 @@ func (msg *GetInfoResponseMessage) Command() MessageCommand {
} }
// NewGetInfoResponseMessage returns a instance of the message // NewGetInfoResponseMessage returns a instance of the message
func NewGetInfoResponseMessage(p2pID string, mempoolSize uint64, serverVersion string) *GetInfoResponseMessage { func NewGetInfoResponseMessage(p2pID string, mempoolSize uint64, serverVersion string, isUtxoIndexed bool, isSynced bool) *GetInfoResponseMessage {
return &GetInfoResponseMessage{ return &GetInfoResponseMessage{
P2PID: p2pID, P2PID: p2pID,
MempoolSize: mempoolSize, MempoolSize: mempoolSize,
ServerVersion: serverVersion, ServerVersion: serverVersion,
IsUtxoIndexed: isUtxoIndexed,
IsSynced: isSynced,
} }
} }

View File

@ -4,6 +4,8 @@ package appmessage
// its respective RPC message // its respective RPC message
type GetMempoolEntriesRequestMessage struct { type GetMempoolEntriesRequestMessage struct {
baseMessage baseMessage
IncludeOrphanPool bool
FilterTransactionPool bool
} }
// Command returns the protocol command string for the message // Command returns the protocol command string for the message
@ -12,8 +14,11 @@ func (msg *GetMempoolEntriesRequestMessage) Command() MessageCommand {
} }
// NewGetMempoolEntriesRequestMessage returns a instance of the message // NewGetMempoolEntriesRequestMessage returns a instance of the message
func NewGetMempoolEntriesRequestMessage() *GetMempoolEntriesRequestMessage { func NewGetMempoolEntriesRequestMessage(includeOrphanPool bool, filterTransactionPool bool) *GetMempoolEntriesRequestMessage {
return &GetMempoolEntriesRequestMessage{} return &GetMempoolEntriesRequestMessage{
IncludeOrphanPool: includeOrphanPool,
FilterTransactionPool: filterTransactionPool,
}
} }
// GetMempoolEntriesResponseMessage is an appmessage corresponding to // GetMempoolEntriesResponseMessage is an appmessage corresponding to

View File

@ -0,0 +1,52 @@
package appmessage
// MempoolEntryByAddress represents MempoolEntries associated with some address
type MempoolEntryByAddress struct {
Address string
Receiving []*MempoolEntry
Sending []*MempoolEntry
}
// GetMempoolEntriesByAddressesRequestMessage is an appmessage corresponding to
// its respective RPC message
type GetMempoolEntriesByAddressesRequestMessage struct {
baseMessage
Addresses []string
IncludeOrphanPool bool
FilterTransactionPool bool
}
// Command returns the protocol command string for the message
func (msg *GetMempoolEntriesByAddressesRequestMessage) Command() MessageCommand {
return CmdGetMempoolEntriesByAddressesRequestMessage
}
// NewGetMempoolEntriesByAddressesRequestMessage returns a instance of the message
func NewGetMempoolEntriesByAddressesRequestMessage(addresses []string, includeOrphanPool bool, filterTransactionPool bool) *GetMempoolEntriesByAddressesRequestMessage {
return &GetMempoolEntriesByAddressesRequestMessage{
Addresses: addresses,
IncludeOrphanPool: includeOrphanPool,
FilterTransactionPool: filterTransactionPool,
}
}
// GetMempoolEntriesByAddressesResponseMessage is an appmessage corresponding to
// its respective RPC message
type GetMempoolEntriesByAddressesResponseMessage struct {
baseMessage
Entries []*MempoolEntryByAddress
Error *RPCError
}
// Command returns the protocol command string for the message
func (msg *GetMempoolEntriesByAddressesResponseMessage) Command() MessageCommand {
return CmdGetMempoolEntriesByAddressesResponseMessage
}
// NewGetMempoolEntriesByAddressesResponseMessage returns a instance of the message
func NewGetMempoolEntriesByAddressesResponseMessage(entries []*MempoolEntryByAddress) *GetMempoolEntriesByAddressesResponseMessage {
return &GetMempoolEntriesByAddressesResponseMessage{
Entries: entries,
}
}

View File

@ -4,7 +4,9 @@ package appmessage
// its respective RPC message // its respective RPC message
type GetMempoolEntryRequestMessage struct { type GetMempoolEntryRequestMessage struct {
baseMessage baseMessage
TxID string TxID string
IncludeOrphanPool bool
FilterTransactionPool bool
} }
// Command returns the protocol command string for the message // Command returns the protocol command string for the message
@ -13,8 +15,12 @@ func (msg *GetMempoolEntryRequestMessage) Command() MessageCommand {
} }
// NewGetMempoolEntryRequestMessage returns a instance of the message // NewGetMempoolEntryRequestMessage returns a instance of the message
func NewGetMempoolEntryRequestMessage(txID string) *GetMempoolEntryRequestMessage { func NewGetMempoolEntryRequestMessage(txID string, includeOrphanPool bool, filterTransactionPool bool) *GetMempoolEntryRequestMessage {
return &GetMempoolEntryRequestMessage{TxID: txID} return &GetMempoolEntryRequestMessage{
TxID: txID,
IncludeOrphanPool: includeOrphanPool,
FilterTransactionPool: filterTransactionPool,
}
} }
// GetMempoolEntryResponseMessage is an appmessage corresponding to // GetMempoolEntryResponseMessage is an appmessage corresponding to
@ -30,6 +36,7 @@ type GetMempoolEntryResponseMessage struct {
type MempoolEntry struct { type MempoolEntry struct {
Fee uint64 Fee uint64
Transaction *RPCTransaction Transaction *RPCTransaction
IsOrphan bool
} }
// Command returns the protocol command string for the message // Command returns the protocol command string for the message
@ -38,11 +45,12 @@ func (msg *GetMempoolEntryResponseMessage) Command() MessageCommand {
} }
// NewGetMempoolEntryResponseMessage returns a instance of the message // NewGetMempoolEntryResponseMessage returns a instance of the message
func NewGetMempoolEntryResponseMessage(fee uint64, transaction *RPCTransaction) *GetMempoolEntryResponseMessage { func NewGetMempoolEntryResponseMessage(fee uint64, transaction *RPCTransaction, isOrphan bool) *GetMempoolEntryResponseMessage {
return &GetMempoolEntryResponseMessage{ return &GetMempoolEntryResponseMessage{
Entry: &MempoolEntry{ Entry: &MempoolEntry{
Fee: fee, Fee: fee,
Transaction: transaction, Transaction: transaction,
IsOrphan: isOrphan,
}, },
} }
} }

View File

@ -4,7 +4,8 @@ package appmessage
// its respective RPC message // its respective RPC message
type GetVirtualSelectedParentChainFromBlockRequestMessage struct { type GetVirtualSelectedParentChainFromBlockRequestMessage struct {
baseMessage baseMessage
StartHash string StartHash string
IncludeAcceptedTransactionIDs bool
} }
// Command returns the protocol command string for the message // Command returns the protocol command string for the message
@ -13,18 +14,29 @@ func (msg *GetVirtualSelectedParentChainFromBlockRequestMessage) Command() Messa
} }
// NewGetVirtualSelectedParentChainFromBlockRequestMessage returns a instance of the message // NewGetVirtualSelectedParentChainFromBlockRequestMessage returns a instance of the message
func NewGetVirtualSelectedParentChainFromBlockRequestMessage(startHash string) *GetVirtualSelectedParentChainFromBlockRequestMessage { func NewGetVirtualSelectedParentChainFromBlockRequestMessage(
startHash string, includeAcceptedTransactionIDs bool) *GetVirtualSelectedParentChainFromBlockRequestMessage {
return &GetVirtualSelectedParentChainFromBlockRequestMessage{ return &GetVirtualSelectedParentChainFromBlockRequestMessage{
StartHash: startHash, StartHash: startHash,
IncludeAcceptedTransactionIDs: includeAcceptedTransactionIDs,
} }
} }
// AcceptedTransactionIDs is a part of the GetVirtualSelectedParentChainFromBlockResponseMessage and
// VirtualSelectedParentChainChangedNotificationMessage appmessages
type AcceptedTransactionIDs struct {
AcceptingBlockHash string
AcceptedTransactionIDs []string
}
// GetVirtualSelectedParentChainFromBlockResponseMessage is an appmessage corresponding to // GetVirtualSelectedParentChainFromBlockResponseMessage is an appmessage corresponding to
// its respective RPC message // its respective RPC message
type GetVirtualSelectedParentChainFromBlockResponseMessage struct { type GetVirtualSelectedParentChainFromBlockResponseMessage struct {
baseMessage baseMessage
RemovedChainBlockHashes []string RemovedChainBlockHashes []string
AddedChainBlockHashes []string AddedChainBlockHashes []string
AcceptedTransactionIDs []*AcceptedTransactionIDs
Error *RPCError Error *RPCError
} }
@ -36,10 +48,11 @@ func (msg *GetVirtualSelectedParentChainFromBlockResponseMessage) Command() Mess
// NewGetVirtualSelectedParentChainFromBlockResponseMessage returns a instance of the message // NewGetVirtualSelectedParentChainFromBlockResponseMessage returns a instance of the message
func NewGetVirtualSelectedParentChainFromBlockResponseMessage(removedChainBlockHashes, func NewGetVirtualSelectedParentChainFromBlockResponseMessage(removedChainBlockHashes,
addedChainBlockHashes []string) *GetVirtualSelectedParentChainFromBlockResponseMessage { addedChainBlockHashes []string, acceptedTransactionIDs []*AcceptedTransactionIDs) *GetVirtualSelectedParentChainFromBlockResponseMessage {
return &GetVirtualSelectedParentChainFromBlockResponseMessage{ return &GetVirtualSelectedParentChainFromBlockResponseMessage{
RemovedChainBlockHashes: removedChainBlockHashes, RemovedChainBlockHashes: removedChainBlockHashes,
AddedChainBlockHashes: addedChainBlockHashes, AddedChainBlockHashes: addedChainBlockHashes,
AcceptedTransactionIDs: acceptedTransactionIDs,
} }
} }

View File

@ -4,6 +4,7 @@ package appmessage
// its respective RPC message // its respective RPC message
type NotifyVirtualSelectedParentChainChangedRequestMessage struct { type NotifyVirtualSelectedParentChainChangedRequestMessage struct {
baseMessage baseMessage
IncludeAcceptedTransactionIDs bool
} }
// Command returns the protocol command string for the message // Command returns the protocol command string for the message
@ -11,9 +12,13 @@ func (msg *NotifyVirtualSelectedParentChainChangedRequestMessage) Command() Mess
return CmdNotifyVirtualSelectedParentChainChangedRequestMessage return CmdNotifyVirtualSelectedParentChainChangedRequestMessage
} }
// NewNotifyVirtualSelectedParentChainChangedRequestMessage returns a instance of the message // NewNotifyVirtualSelectedParentChainChangedRequestMessage returns an instance of the message
func NewNotifyVirtualSelectedParentChainChangedRequestMessage() *NotifyVirtualSelectedParentChainChangedRequestMessage { func NewNotifyVirtualSelectedParentChainChangedRequestMessage(
return &NotifyVirtualSelectedParentChainChangedRequestMessage{} includeAcceptedTransactionIDs bool) *NotifyVirtualSelectedParentChainChangedRequestMessage {
return &NotifyVirtualSelectedParentChainChangedRequestMessage{
IncludeAcceptedTransactionIDs: includeAcceptedTransactionIDs,
}
} }
// NotifyVirtualSelectedParentChainChangedResponseMessage is an appmessage corresponding to // NotifyVirtualSelectedParentChainChangedResponseMessage is an appmessage corresponding to
@ -39,6 +44,7 @@ type VirtualSelectedParentChainChangedNotificationMessage struct {
baseMessage baseMessage
RemovedChainBlockHashes []string RemovedChainBlockHashes []string
AddedChainBlockHashes []string AddedChainBlockHashes []string
AcceptedTransactionIDs []*AcceptedTransactionIDs
} }
// Command returns the protocol command string for the message // Command returns the protocol command string for the message
@ -48,10 +54,11 @@ func (msg *VirtualSelectedParentChainChangedNotificationMessage) Command() Messa
// NewVirtualSelectedParentChainChangedNotificationMessage returns a instance of the message // NewVirtualSelectedParentChainChangedNotificationMessage returns a instance of the message
func NewVirtualSelectedParentChainChangedNotificationMessage(removedChainBlockHashes, func NewVirtualSelectedParentChainChangedNotificationMessage(removedChainBlockHashes,
addedChainBlocks []string) *VirtualSelectedParentChainChangedNotificationMessage { addedChainBlocks []string, acceptedTransactionIDs []*AcceptedTransactionIDs) *VirtualSelectedParentChainChangedNotificationMessage {
return &VirtualSelectedParentChainChangedNotificationMessage{ return &VirtualSelectedParentChainChangedNotificationMessage{
RemovedChainBlockHashes: removedChainBlockHashes, RemovedChainBlockHashes: removedChainBlockHashes,
AddedChainBlockHashes: addedChainBlocks, AddedChainBlockHashes: addedChainBlocks,
AcceptedTransactionIDs: acceptedTransactionIDs,
} }
} }

View File

@ -52,6 +52,7 @@ type RPCTransaction struct {
SubnetworkID string SubnetworkID string
Gas uint64 Gas uint64
Payload string Payload string
Mass uint64
VerboseData *RPCTransactionVerboseData VerboseData *RPCTransactionVerboseData
} }

View 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,
}
}

View File

@ -4,6 +4,8 @@ import (
"fmt" "fmt"
"sync/atomic" "sync/atomic"
"github.com/kaspanet/kaspad/domain/consensus/model/externalapi"
"github.com/kaspanet/kaspad/domain/miningmanager/mempool" "github.com/kaspanet/kaspad/domain/miningmanager/mempool"
"github.com/kaspanet/kaspad/app/protocol" "github.com/kaspanet/kaspad/app/protocol"
@ -67,6 +69,7 @@ func (a *ComponentManager) Stop() {
} }
a.protocolManager.Close() a.protocolManager.Close()
close(a.protocolManager.Context().Domain().ConsensusEventsChannel())
return return
} }
@ -118,7 +121,7 @@ func NewComponentManager(cfg *config.Config, db infrastructuredatabase.Database,
if err != nil { if err != nil {
return nil, err return nil, err
} }
rpcManager := setupRPC(cfg, domain, netAdapter, protocolManager, connectionManager, addressManager, utxoIndex, interrupt) rpcManager := setupRPC(cfg, domain, netAdapter, protocolManager, connectionManager, addressManager, utxoIndex, domain.ConsensusEventsChannel(), interrupt)
return &ComponentManager{ return &ComponentManager{
cfg: cfg, cfg: cfg,
@ -139,6 +142,7 @@ func setupRPC(
connectionManager *connmanager.ConnectionManager, connectionManager *connmanager.ConnectionManager,
addressManager *addressmanager.AddressManager, addressManager *addressmanager.AddressManager,
utxoIndex *utxoindex.UTXOIndex, utxoIndex *utxoindex.UTXOIndex,
consensusEventsChan chan externalapi.ConsensusEvent,
shutDownChan chan<- struct{}, shutDownChan chan<- struct{},
) *rpc.Manager { ) *rpc.Manager {
@ -150,11 +154,10 @@ func setupRPC(
connectionManager, connectionManager,
addressManager, addressManager,
utxoIndex, utxoIndex,
consensusEventsChan,
shutDownChan, shutDownChan,
) )
protocolManager.SetOnVirtualChange(rpcManager.NotifyVirtualChange)
protocolManager.SetOnNewBlockTemplateHandler(rpcManager.NotifyNewBlockTemplate) protocolManager.SetOnNewBlockTemplateHandler(rpcManager.NotifyNewBlockTemplate)
protocolManager.SetOnBlockAddedToDAGHandler(rpcManager.NotifyBlockAddedToDAG)
protocolManager.SetOnPruningPointUTXOSetOverrideHandler(rpcManager.NotifyPruningPointUTXOSetOverride) protocolManager.SetOnPruningPointUTXOSetOverrideHandler(rpcManager.NotifyPruningPointUTXOSetOverride)
return rpcManager return rpcManager

View File

@ -16,60 +16,40 @@ import (
// OnNewBlock updates the mempool after a new block arrival, and // OnNewBlock updates the mempool after a new block arrival, and
// relays newly unorphaned transactions and possibly rebroadcast // relays newly unorphaned transactions and possibly rebroadcast
// manually added transactions when not in IBD. // manually added transactions when not in IBD.
func (f *FlowContext) OnNewBlock(block *externalapi.DomainBlock, func (f *FlowContext) OnNewBlock(block *externalapi.DomainBlock) error {
virtualChangeSet *externalapi.VirtualChangeSet) error {
hash := consensushashing.BlockHash(block) hash := consensushashing.BlockHash(block)
log.Tracef("OnNewBlock start for block %s", hash) log.Tracef("OnNewBlock start for block %s", hash)
defer log.Tracef("OnNewBlock end for block %s", hash) defer log.Tracef("OnNewBlock end for block %s", hash)
unorphaningResults, err := f.UnorphanBlocks(block) unorphanedBlocks, err := f.UnorphanBlocks(block)
if err != nil { if err != nil {
return err return err
} }
log.Debugf("OnNewBlock: block %s unorphaned %d blocks", hash, len(unorphaningResults)) log.Debugf("OnNewBlock: block %s unorphaned %d blocks", hash, len(unorphanedBlocks))
newBlocks := []*externalapi.DomainBlock{block} newBlocks := []*externalapi.DomainBlock{block}
newVirtualChangeSets := []*externalapi.VirtualChangeSet{virtualChangeSet} newBlocks = append(newBlocks, unorphanedBlocks...)
for _, unorphaningResult := range unorphaningResults {
newBlocks = append(newBlocks, unorphaningResult.block)
newVirtualChangeSets = append(newVirtualChangeSets, unorphaningResult.virtualChangeSet)
}
allAcceptedTransactions := make([]*externalapi.DomainTransaction, 0) allAcceptedTransactions := make([]*externalapi.DomainTransaction, 0)
for i, newBlock := range newBlocks { for _, newBlock := range newBlocks {
log.Debugf("OnNewBlock: passing block %s transactions to mining manager", hash) log.Debugf("OnNewBlock: passing block %s transactions to mining manager", hash)
acceptedTransactions, err := f.Domain().MiningManager().HandleNewBlockTransactions(newBlock.Transactions) acceptedTransactions, err := f.Domain().MiningManager().HandleNewBlockTransactions(newBlock.Transactions)
if err != nil { if err != nil {
return err return err
} }
allAcceptedTransactions = append(allAcceptedTransactions, acceptedTransactions...) allAcceptedTransactions = append(allAcceptedTransactions, acceptedTransactions...)
if f.onBlockAddedToDAGHandler != nil {
log.Debugf("OnNewBlock: calling f.onBlockAddedToDAGHandler for block %s", hash)
virtualChangeSet = newVirtualChangeSets[i]
err := f.onBlockAddedToDAGHandler(newBlock, virtualChangeSet)
if err != nil {
return err
}
}
} }
return f.broadcastTransactionsAfterBlockAdded(newBlocks, allAcceptedTransactions) return f.broadcastTransactionsAfterBlockAdded(newBlocks, allAcceptedTransactions)
} }
// OnVirtualChange calls the handler function whenever the virtual block changes.
func (f *FlowContext) OnVirtualChange(virtualChangeSet *externalapi.VirtualChangeSet) error {
if f.onVirtualChangeHandler != nil && virtualChangeSet != nil {
return f.onVirtualChangeHandler(virtualChangeSet)
}
return nil
}
// OnNewBlockTemplate calls the handler function whenever a new block template is available for miners. // OnNewBlockTemplate calls the handler function whenever a new block template is available for miners.
func (f *FlowContext) OnNewBlockTemplate() error { func (f *FlowContext) OnNewBlockTemplate() error {
// Clear current template cache. Note we call this even if the handler is nil, in order to keep the
// state consistent without dependency on external event registration
f.Domain().MiningManager().ClearBlockTemplate()
if f.onNewBlockTemplateHandler != nil { if f.onNewBlockTemplateHandler != nil {
return f.onNewBlockTemplateHandler() return f.onNewBlockTemplateHandler()
} }
@ -127,7 +107,7 @@ func (f *FlowContext) AddBlock(block *externalapi.DomainBlock) error {
return protocolerrors.Errorf(false, "cannot add header only block") return protocolerrors.Errorf(false, "cannot add header only block")
} }
virtualChangeSet, err := f.Domain().Consensus().ValidateAndInsertBlock(block, true) err := f.Domain().Consensus().ValidateAndInsertBlock(block, true)
if err != nil { if err != nil {
if errors.As(err, &ruleerrors.RuleError{}) { if errors.As(err, &ruleerrors.RuleError{}) {
log.Warnf("Validation failed for block %s: %s", consensushashing.BlockHash(block), err) log.Warnf("Validation failed for block %s: %s", consensushashing.BlockHash(block), err)
@ -138,7 +118,7 @@ func (f *FlowContext) AddBlock(block *externalapi.DomainBlock) error {
if err != nil { if err != nil {
return err return err
} }
err = f.OnNewBlock(block, virtualChangeSet) err = f.OnNewBlock(block)
if err != nil { if err != nil {
return err return err
} }

View File

@ -18,13 +18,6 @@ import (
"github.com/kaspanet/kaspad/infrastructure/network/netadapter/id" "github.com/kaspanet/kaspad/infrastructure/network/netadapter/id"
) )
// OnBlockAddedToDAGHandler is a handler function that's triggered
// when a block is added to the DAG
type OnBlockAddedToDAGHandler func(block *externalapi.DomainBlock, virtualChangeSet *externalapi.VirtualChangeSet) error
// OnVirtualChangeHandler is a handler function that's triggered when the virtual changes
type OnVirtualChangeHandler func(virtualChangeSet *externalapi.VirtualChangeSet) error
// OnNewBlockTemplateHandler is a handler function that's triggered when a new block template is available // OnNewBlockTemplateHandler is a handler function that's triggered when a new block template is available
type OnNewBlockTemplateHandler func() error type OnNewBlockTemplateHandler func() error
@ -47,15 +40,12 @@ type FlowContext struct {
timeStarted int64 timeStarted int64
onVirtualChangeHandler OnVirtualChangeHandler
onBlockAddedToDAGHandler OnBlockAddedToDAGHandler
onNewBlockTemplateHandler OnNewBlockTemplateHandler onNewBlockTemplateHandler OnNewBlockTemplateHandler
onPruningPointUTXOSetOverrideHandler OnPruningPointUTXOSetOverrideHandler onPruningPointUTXOSetOverrideHandler OnPruningPointUTXOSetOverrideHandler
onTransactionAddedToMempoolHandler OnTransactionAddedToMempoolHandler onTransactionAddedToMempoolHandler OnTransactionAddedToMempoolHandler
expectedDAAWindowDurationInMilliseconds int64 lastRebroadcastTime time.Time
lastRebroadcastTime time.Time sharedRequestedTransactions *SharedRequestedTransactions
sharedRequestedTransactions *SharedRequestedTransactions
sharedRequestedBlocks *SharedRequestedBlocks sharedRequestedBlocks *SharedRequestedBlocks
@ -93,8 +83,6 @@ func New(cfg *config.Config, domain domain.Domain, addressManager *addressmanage
transactionIDsToPropagate: []*externalapi.DomainTransactionID{}, transactionIDsToPropagate: []*externalapi.DomainTransactionID{},
lastTransactionIDPropagationTime: time.Now(), lastTransactionIDPropagationTime: time.Now(),
shutdownChan: make(chan struct{}), shutdownChan: make(chan struct{}),
expectedDAAWindowDurationInMilliseconds: cfg.NetParams().TargetTimePerBlock.Milliseconds() *
int64(cfg.NetParams().DifficultyAdjustmentWindowSize),
} }
} }
@ -109,14 +97,9 @@ func (f *FlowContext) ShutdownChan() <-chan struct{} {
return f.shutdownChan return f.shutdownChan
} }
// SetOnVirtualChangeHandler sets the onVirtualChangeHandler handler // IsNearlySynced returns whether current consensus is considered synced or close to being synced.
func (f *FlowContext) SetOnVirtualChangeHandler(onVirtualChangeHandler OnVirtualChangeHandler) { func (f *FlowContext) IsNearlySynced() (bool, error) {
f.onVirtualChangeHandler = onVirtualChangeHandler return f.Domain().Consensus().IsNearlySynced()
}
// SetOnBlockAddedToDAGHandler sets the onBlockAddedToDAG handler
func (f *FlowContext) SetOnBlockAddedToDAGHandler(onBlockAddedToDAGHandler OnBlockAddedToDAGHandler) {
f.onBlockAddedToDAGHandler = onBlockAddedToDAGHandler
} }
// SetOnNewBlockTemplateHandler sets the onNewBlockTemplateHandler handler // SetOnNewBlockTemplateHandler sets the onNewBlockTemplateHandler handler

View File

@ -72,3 +72,10 @@ func (f *FlowContext) Peers() []*peerpkg.Peer {
} }
return peers return peers
} }
// HasPeers returns whether there are currently active peers
func (f *FlowContext) HasPeers() bool {
f.peersMutex.RLock()
defer f.peersMutex.RUnlock()
return len(f.peers) > 0
}

View File

@ -15,12 +15,6 @@ import (
// on: 2^orphanResolutionRange * PHANTOM K. // on: 2^orphanResolutionRange * PHANTOM K.
const maxOrphans = 600 const maxOrphans = 600
// UnorphaningResult is the result of unorphaning a block
type UnorphaningResult struct {
block *externalapi.DomainBlock
virtualChangeSet *externalapi.VirtualChangeSet
}
// AddOrphan adds the block to the orphan set // AddOrphan adds the block to the orphan set
func (f *FlowContext) AddOrphan(orphanBlock *externalapi.DomainBlock) { func (f *FlowContext) AddOrphan(orphanBlock *externalapi.DomainBlock) {
f.orphansMutex.Lock() f.orphansMutex.Lock()
@ -57,7 +51,7 @@ func (f *FlowContext) IsOrphan(blockHash *externalapi.DomainHash) bool {
} }
// UnorphanBlocks removes the block from the orphan set, and remove all of the blocks that are not orphans anymore. // UnorphanBlocks removes the block from the orphan set, and remove all of the blocks that are not orphans anymore.
func (f *FlowContext) UnorphanBlocks(rootBlock *externalapi.DomainBlock) ([]*UnorphaningResult, error) { func (f *FlowContext) UnorphanBlocks(rootBlock *externalapi.DomainBlock) ([]*externalapi.DomainBlock, error) {
f.orphansMutex.Lock() f.orphansMutex.Lock()
defer f.orphansMutex.Unlock() defer f.orphansMutex.Unlock()
@ -66,7 +60,7 @@ func (f *FlowContext) UnorphanBlocks(rootBlock *externalapi.DomainBlock) ([]*Uno
rootBlockHash := consensushashing.BlockHash(rootBlock) rootBlockHash := consensushashing.BlockHash(rootBlock)
processQueue := f.addChildOrphansToProcessQueue(rootBlockHash, []externalapi.DomainHash{}) processQueue := f.addChildOrphansToProcessQueue(rootBlockHash, []externalapi.DomainHash{})
var unorphaningResults []*UnorphaningResult var unorphanedBlocks []*externalapi.DomainBlock
for len(processQueue) > 0 { for len(processQueue) > 0 {
var orphanHash externalapi.DomainHash var orphanHash externalapi.DomainHash
orphanHash, processQueue = processQueue[0], processQueue[1:] orphanHash, processQueue = processQueue[0], processQueue[1:]
@ -90,21 +84,18 @@ func (f *FlowContext) UnorphanBlocks(rootBlock *externalapi.DomainBlock) ([]*Uno
} }
} }
if canBeUnorphaned { if canBeUnorphaned {
virtualChangeSet, unorphaningSucceeded, err := f.unorphanBlock(orphanHash) unorphaningSucceeded, err := f.unorphanBlock(orphanHash)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if unorphaningSucceeded { if unorphaningSucceeded {
unorphaningResults = append(unorphaningResults, &UnorphaningResult{ unorphanedBlocks = append(unorphanedBlocks, orphanBlock)
block: orphanBlock,
virtualChangeSet: virtualChangeSet,
})
processQueue = f.addChildOrphansToProcessQueue(&orphanHash, processQueue) processQueue = f.addChildOrphansToProcessQueue(&orphanHash, processQueue)
} }
} }
} }
return unorphaningResults, nil return unorphanedBlocks, nil
} }
// addChildOrphansToProcessQueue finds all child orphans of `blockHash` // addChildOrphansToProcessQueue finds all child orphans of `blockHash`
@ -143,24 +134,24 @@ func (f *FlowContext) findChildOrphansOfBlock(blockHash *externalapi.DomainHash)
return childOrphans return childOrphans
} }
func (f *FlowContext) unorphanBlock(orphanHash externalapi.DomainHash) (*externalapi.VirtualChangeSet, bool, error) { func (f *FlowContext) unorphanBlock(orphanHash externalapi.DomainHash) (bool, error) {
orphanBlock, ok := f.orphans[orphanHash] orphanBlock, ok := f.orphans[orphanHash]
if !ok { if !ok {
return nil, false, errors.Errorf("attempted to unorphan a non-orphan block %s", orphanHash) return false, errors.Errorf("attempted to unorphan a non-orphan block %s", orphanHash)
} }
delete(f.orphans, orphanHash) delete(f.orphans, orphanHash)
virtualChangeSet, err := f.domain.Consensus().ValidateAndInsertBlock(orphanBlock, true) err := f.domain.Consensus().ValidateAndInsertBlock(orphanBlock, true)
if err != nil { if err != nil {
if errors.As(err, &ruleerrors.RuleError{}) { if errors.As(err, &ruleerrors.RuleError{}) {
log.Warnf("Validation failed for orphan block %s: %s", orphanHash, err) log.Warnf("Validation failed for orphan block %s: %s", orphanHash, err)
return nil, false, nil return false, nil
} }
return nil, false, err return false, err
} }
log.Infof("Unorphaned block %s", orphanHash) log.Infof("Unorphaned block %s", orphanHash)
return virtualChangeSet, true, nil return true, nil
} }
// GetOrphanRoots returns the roots of the missing ancestors DAG of the given orphan // GetOrphanRoots returns the roots of the missing ancestors DAG of the given orphan

View File

@ -1,40 +0,0 @@
package flowcontext
import "github.com/kaspanet/kaspad/util/mstime"
// IsNearlySynced returns whether this node is considered synced or close to being synced. This info
// is used to determine if it's ok to use a block template from this node for mining purposes.
func (f *FlowContext) IsNearlySynced() (bool, error) {
peers := f.Peers()
if len(peers) == 0 {
log.Debugf("The node is not connected to peers, so IsNearlySynced returns false")
return false, nil
}
virtualSelectedParent, err := f.domain.Consensus().GetVirtualSelectedParent()
if err != nil {
return false, err
}
if virtualSelectedParent.Equal(f.Config().NetParams().GenesisHash) {
return false, nil
}
virtualSelectedParentHeader, err := f.domain.Consensus().GetBlockHeader(virtualSelectedParent)
if err != nil {
return false, err
}
now := mstime.Now().UnixMilliseconds()
// As a heuristic, we allow the node to mine if he is likely to be within the current DAA window of fully synced nodes.
// Such blocks contribute to security by maintaining the current difficulty despite possibly being slightly out of sync.
if now-virtualSelectedParentHeader.TimeInMilliseconds() < f.expectedDAAWindowDurationInMilliseconds {
log.Debugf("The selected tip timestamp is recent (%d), so IsNearlySynced returns true",
virtualSelectedParentHeader.TimeInMilliseconds())
return true, nil
}
log.Debugf("The selected tip timestamp is old (%d), so IsNearlySynced returns false",
virtualSelectedParentHeader.TimeInMilliseconds())
return false, nil
}

View File

@ -21,7 +21,7 @@ func (flow *handleRelayInvsFlow) receiveBlockLocator() (blockLocatorHashes []*ex
switch message := message.(type) { switch message := message.(type) {
case *appmessage.MsgInvRelayBlock: case *appmessage.MsgInvRelayBlock:
flow.invsQueue = append(flow.invsQueue, message) flow.invsQueue = append(flow.invsQueue, invRelayBlock{Hash: message.Hash, IsOrphanRoot: false})
case *appmessage.MsgBlockLocator: case *appmessage.MsgBlockLocator:
return message.BlockLocatorHashes, nil return message.BlockLocatorHashes, nil
default: default:

View File

@ -5,7 +5,6 @@ import (
"github.com/kaspanet/kaspad/app/protocol/peer" "github.com/kaspanet/kaspad/app/protocol/peer"
"github.com/kaspanet/kaspad/app/protocol/protocolerrors" "github.com/kaspanet/kaspad/app/protocol/protocolerrors"
"github.com/kaspanet/kaspad/domain" "github.com/kaspanet/kaspad/domain"
"github.com/kaspanet/kaspad/domain/consensus/model/externalapi"
"github.com/kaspanet/kaspad/infrastructure/network/netadapter/router" "github.com/kaspanet/kaspad/infrastructure/network/netadapter/router"
) )
@ -34,7 +33,7 @@ func HandleIBDBlockLocator(context HandleIBDBlockLocatorContext, incomingRoute *
if err != nil { if err != nil {
return err return err
} }
if !blockInfo.Exists { if !blockInfo.HasHeader() {
return protocolerrors.Errorf(true, "received IBDBlockLocator "+ return protocolerrors.Errorf(true, "received IBDBlockLocator "+
"with an unknown targetHash %s", targetHash) "with an unknown targetHash %s", targetHash)
} }
@ -47,7 +46,7 @@ func HandleIBDBlockLocator(context HandleIBDBlockLocatorContext, incomingRoute *
} }
// The IBD block locator is checking only existing blocks with bodies. // The IBD block locator is checking only existing blocks with bodies.
if !blockInfo.Exists || blockInfo.BlockStatus == externalapi.StatusHeaderOnly { if !blockInfo.HasBody() {
continue continue
} }

View File

@ -4,7 +4,6 @@ import (
"github.com/kaspanet/kaspad/app/appmessage" "github.com/kaspanet/kaspad/app/appmessage"
"github.com/kaspanet/kaspad/app/protocol/protocolerrors" "github.com/kaspanet/kaspad/app/protocol/protocolerrors"
"github.com/kaspanet/kaspad/domain" "github.com/kaspanet/kaspad/domain"
"github.com/kaspanet/kaspad/domain/consensus/model/externalapi"
"github.com/kaspanet/kaspad/infrastructure/network/netadapter/router" "github.com/kaspanet/kaspad/infrastructure/network/netadapter/router"
"github.com/pkg/errors" "github.com/pkg/errors"
) )
@ -28,18 +27,15 @@ func HandleIBDBlockRequests(context HandleIBDBlockRequestsContext, incomingRoute
log.Debugf("Got request for %d ibd blocks", len(msgRequestIBDBlocks.Hashes)) log.Debugf("Got request for %d ibd blocks", len(msgRequestIBDBlocks.Hashes))
for i, hash := range msgRequestIBDBlocks.Hashes { for i, hash := range msgRequestIBDBlocks.Hashes {
// Fetch the block from the database. // Fetch the block from the database.
blockInfo, err := context.Domain().Consensus().GetBlockInfo(hash) block, found, err := context.Domain().Consensus().GetBlock(hash)
if err != nil {
return err
}
if !blockInfo.Exists || blockInfo.BlockStatus == externalapi.StatusHeaderOnly {
return protocolerrors.Errorf(true, "block %s not found (v5)", hash)
}
block, err := context.Domain().Consensus().GetBlock(hash)
if err != nil { if err != nil {
return errors.Wrapf(err, "unable to fetch requested block hash %s", hash) return errors.Wrapf(err, "unable to fetch requested block hash %s", hash)
} }
if !found {
return protocolerrors.Errorf(false, "IBD block %s not found", hash)
}
// TODO (Partial nodes): Convert block to partial block if needed // TODO (Partial nodes): Convert block to partial block if needed
blockMessage := appmessage.DomainBlockToMsgBlock(block) blockMessage := appmessage.DomainBlockToMsgBlock(block)

View File

@ -119,11 +119,15 @@ func HandlePruningPointAndItsAnticoneRequests(context PruningPointAndItsAnticone
} }
for i, blockHash := range pointAndItsAnticone { for i, blockHash := range pointAndItsAnticone {
block, err := context.Domain().Consensus().GetBlock(blockHash) block, found, err := context.Domain().Consensus().GetBlock(blockHash)
if err != nil { if err != nil {
return err return err
} }
if !found {
return protocolerrors.Errorf(false, "pruning point anticone block %s not found", blockHash)
}
err = outgoingRoute.Enqueue(appmessage.DomainBlockWithTrustedDataToBlockWithTrustedDataV4(block, trustedDataDAABlockIndexes[*blockHash], trustedDataGHOSTDAGDataIndexes[*blockHash])) err = outgoingRoute.Enqueue(appmessage.DomainBlockWithTrustedDataToBlockWithTrustedDataV4(block, trustedDataDAABlockIndexes[*blockHash], trustedDataGHOSTDAGDataIndexes[*blockHash]))
if err != nil { if err != nil {
return err return err

View File

@ -5,7 +5,6 @@ import (
peerpkg "github.com/kaspanet/kaspad/app/protocol/peer" peerpkg "github.com/kaspanet/kaspad/app/protocol/peer"
"github.com/kaspanet/kaspad/app/protocol/protocolerrors" "github.com/kaspanet/kaspad/app/protocol/protocolerrors"
"github.com/kaspanet/kaspad/domain" "github.com/kaspanet/kaspad/domain"
"github.com/kaspanet/kaspad/domain/consensus/model/externalapi"
"github.com/kaspanet/kaspad/infrastructure/network/netadapter/router" "github.com/kaspanet/kaspad/infrastructure/network/netadapter/router"
"github.com/pkg/errors" "github.com/pkg/errors"
) )
@ -29,18 +28,15 @@ func HandleRelayBlockRequests(context RelayBlockRequestsContext, incomingRoute *
log.Debugf("Got request for relay blocks with hashes %s", getRelayBlocksMessage.Hashes) log.Debugf("Got request for relay blocks with hashes %s", getRelayBlocksMessage.Hashes)
for _, hash := range getRelayBlocksMessage.Hashes { for _, hash := range getRelayBlocksMessage.Hashes {
// Fetch the block from the database. // Fetch the block from the database.
blockInfo, err := context.Domain().Consensus().GetBlockInfo(hash) block, found, err := context.Domain().Consensus().GetBlock(hash)
if err != nil {
return err
}
if !blockInfo.Exists || blockInfo.BlockStatus == externalapi.StatusHeaderOnly {
return protocolerrors.Errorf(true, "block %s not found", hash)
}
block, err := context.Domain().Consensus().GetBlock(hash)
if err != nil { if err != nil {
return errors.Wrapf(err, "unable to fetch requested block hash %s", hash) return errors.Wrapf(err, "unable to fetch requested block hash %s", hash)
} }
if !found {
return protocolerrors.Errorf(false, "Relay block %s not found", hash)
}
// TODO (Partial nodes): Convert block to partial block if needed // TODO (Partial nodes): Convert block to partial block if needed
err = outgoingRoute.Enqueue(appmessage.DomainBlockToMsgBlock(block)) err = outgoingRoute.Enqueue(appmessage.DomainBlockToMsgBlock(block))

View File

@ -7,6 +7,7 @@ import (
peerpkg "github.com/kaspanet/kaspad/app/protocol/peer" peerpkg "github.com/kaspanet/kaspad/app/protocol/peer"
"github.com/kaspanet/kaspad/app/protocol/protocolerrors" "github.com/kaspanet/kaspad/app/protocol/protocolerrors"
"github.com/kaspanet/kaspad/domain" "github.com/kaspanet/kaspad/domain"
"github.com/kaspanet/kaspad/domain/consensus/model"
"github.com/kaspanet/kaspad/domain/consensus/model/externalapi" "github.com/kaspanet/kaspad/domain/consensus/model/externalapi"
"github.com/kaspanet/kaspad/domain/consensus/ruleerrors" "github.com/kaspanet/kaspad/domain/consensus/ruleerrors"
"github.com/kaspanet/kaspad/domain/consensus/utils/consensushashing" "github.com/kaspanet/kaspad/domain/consensus/utils/consensushashing"
@ -25,8 +26,7 @@ var orphanResolutionRange uint32 = 5
type RelayInvsContext interface { type RelayInvsContext interface {
Domain() domain.Domain Domain() domain.Domain
Config() *config.Config Config() *config.Config
OnNewBlock(block *externalapi.DomainBlock, virtualChangeSet *externalapi.VirtualChangeSet) error OnNewBlock(block *externalapi.DomainBlock) error
OnVirtualChange(virtualChangeSet *externalapi.VirtualChangeSet) error
OnNewBlockTemplate() error OnNewBlockTemplate() error
OnPruningPointUTXOSetOverride() error OnPruningPointUTXOSetOverride() error
SharedRequestedBlocks() *flowcontext.SharedRequestedBlocks SharedRequestedBlocks() *flowcontext.SharedRequestedBlocks
@ -39,11 +39,16 @@ type RelayInvsContext interface {
IsNearlySynced() (bool, error) IsNearlySynced() (bool, error)
} }
type invRelayBlock struct {
Hash *externalapi.DomainHash
IsOrphanRoot bool
}
type handleRelayInvsFlow struct { type handleRelayInvsFlow struct {
RelayInvsContext RelayInvsContext
incomingRoute, outgoingRoute *router.Route incomingRoute, outgoingRoute *router.Route
peer *peerpkg.Peer peer *peerpkg.Peer
invsQueue []*appmessage.MsgInvRelayBlock invsQueue []invRelayBlock
} }
// HandleRelayInvs listens to appmessage.MsgInvRelayBlock messages, requests their corresponding blocks if they // HandleRelayInvs listens to appmessage.MsgInvRelayBlock messages, requests their corresponding blocks if they
@ -56,7 +61,7 @@ func HandleRelayInvs(context RelayInvsContext, incomingRoute *router.Route, outg
incomingRoute: incomingRoute, incomingRoute: incomingRoute,
outgoingRoute: outgoingRoute, outgoingRoute: outgoingRoute,
peer: peer, peer: peer,
invsQueue: make([]*appmessage.MsgInvRelayBlock, 0), invsQueue: make([]invRelayBlock, 0),
} }
err := flow.start() err := flow.start()
// Currently, HandleRelayInvs flow is the only place where IBD is triggered, so the channel can be closed now // Currently, HandleRelayInvs flow is the only place where IBD is triggered, so the channel can be closed now
@ -139,12 +144,36 @@ func (flow *handleRelayInvsFlow) start() error {
continue continue
} }
// Note we do not apply the heuristic below if inv was queued as an orphan root, since
// that means the process started by a proper and relevant relay block
if !inv.IsOrphanRoot {
// Check bounded merge depth to avoid requesting irrelevant data which cannot be merged under virtual
virtualMergeDepthRoot, err := flow.Domain().Consensus().VirtualMergeDepthRoot()
if err != nil {
return err
}
if !virtualMergeDepthRoot.Equal(model.VirtualGenesisBlockHash) {
mergeDepthRootHeader, err := flow.Domain().Consensus().GetBlockHeader(virtualMergeDepthRoot)
if err != nil {
return err
}
// Since `BlueWork` respects topology, this condition means that the relay
// block is not in the future of virtual's merge depth root, and thus cannot be merged unless
// other valid blocks Kosherize it, in which case it will be obtained once the merger is relayed
if block.Header.BlueWork().Cmp(mergeDepthRootHeader.BlueWork()) <= 0 {
log.Debugf("Block %s has lower blue work than virtual's merge root %s (%d <= %d), hence we are skipping it",
inv.Hash, virtualMergeDepthRoot, block.Header.BlueWork(), mergeDepthRootHeader.BlueWork())
continue
}
}
}
log.Debugf("Processing block %s", inv.Hash) log.Debugf("Processing block %s", inv.Hash)
oldVirtualInfo, err := flow.Domain().Consensus().GetVirtualInfo() oldVirtualInfo, err := flow.Domain().Consensus().GetVirtualInfo()
if err != nil { if err != nil {
return err return err
} }
missingParents, virtualChangeSet, err := flow.processBlock(block) missingParents, err := flow.processBlock(block)
if err != nil { if err != nil {
if errors.Is(err, ruleerrors.ErrPrunedBlock) { if errors.Is(err, ruleerrors.ErrPrunedBlock) {
log.Infof("Ignoring pruned block %s", inv.Hash) log.Infof("Ignoring pruned block %s", inv.Hash)
@ -182,10 +211,14 @@ func (flow *handleRelayInvsFlow) start() error {
continue continue
} }
virtualHasNewParents = true virtualHasNewParents = true
block, err := flow.Domain().Consensus().GetBlock(parent) block, found, err := flow.Domain().Consensus().GetBlock(parent)
if err != nil { if err != nil {
return err return err
} }
if !found {
return protocolerrors.Errorf(false, "Virtual parent %s not found", parent)
}
blockHash := consensushashing.BlockHash(block) blockHash := consensushashing.BlockHash(block)
log.Debugf("Relaying block %s", blockHash) log.Debugf("Relaying block %s", blockHash)
err = flow.relayBlock(block) err = flow.relayBlock(block)
@ -203,7 +236,7 @@ func (flow *handleRelayInvsFlow) start() error {
} }
log.Infof("Accepted block %s via relay", inv.Hash) log.Infof("Accepted block %s via relay", inv.Hash)
err = flow.OnNewBlock(block, virtualChangeSet) err = flow.OnNewBlock(block)
if err != nil { if err != nil {
return err return err
} }
@ -219,24 +252,24 @@ func (flow *handleRelayInvsFlow) banIfBlockIsHeaderOnly(block *externalapi.Domai
return nil return nil
} }
func (flow *handleRelayInvsFlow) readInv() (*appmessage.MsgInvRelayBlock, error) { func (flow *handleRelayInvsFlow) readInv() (invRelayBlock, error) {
if len(flow.invsQueue) > 0 { if len(flow.invsQueue) > 0 {
var inv *appmessage.MsgInvRelayBlock var inv invRelayBlock
inv, flow.invsQueue = flow.invsQueue[0], flow.invsQueue[1:] inv, flow.invsQueue = flow.invsQueue[0], flow.invsQueue[1:]
return inv, nil return inv, nil
} }
msg, err := flow.incomingRoute.Dequeue() msg, err := flow.incomingRoute.Dequeue()
if err != nil { if err != nil {
return nil, err return invRelayBlock{}, err
} }
inv, ok := msg.(*appmessage.MsgInvRelayBlock) msgInv, ok := msg.(*appmessage.MsgInvRelayBlock)
if !ok { if !ok {
return nil, protocolerrors.Errorf(true, "unexpected %s message in the block relay handleRelayInvsFlow while "+ return invRelayBlock{}, protocolerrors.Errorf(true, "unexpected %s message in the block relay handleRelayInvsFlow while "+
"expecting an inv message", msg.Command()) "expecting an inv message", msg.Command())
} }
return inv, nil return invRelayBlock{Hash: msgInv.Hash, IsOrphanRoot: false}, nil
} }
func (flow *handleRelayInvsFlow) requestBlock(requestHash *externalapi.DomainHash) (*externalapi.DomainBlock, bool, error) { func (flow *handleRelayInvsFlow) requestBlock(requestHash *externalapi.DomainHash) (*externalapi.DomainBlock, bool, error) {
@ -281,7 +314,7 @@ func (flow *handleRelayInvsFlow) readMsgBlock() (msgBlock *appmessage.MsgBlock,
switch message := message.(type) { switch message := message.(type) {
case *appmessage.MsgInvRelayBlock: case *appmessage.MsgInvRelayBlock:
flow.invsQueue = append(flow.invsQueue, message) flow.invsQueue = append(flow.invsQueue, invRelayBlock{Hash: message.Hash, IsOrphanRoot: false})
case *appmessage.MsgBlock: case *appmessage.MsgBlock:
return message, nil return message, nil
default: default:
@ -290,25 +323,25 @@ func (flow *handleRelayInvsFlow) readMsgBlock() (msgBlock *appmessage.MsgBlock,
} }
} }
func (flow *handleRelayInvsFlow) processBlock(block *externalapi.DomainBlock) ([]*externalapi.DomainHash, *externalapi.VirtualChangeSet, error) { func (flow *handleRelayInvsFlow) processBlock(block *externalapi.DomainBlock) ([]*externalapi.DomainHash, error) {
blockHash := consensushashing.BlockHash(block) blockHash := consensushashing.BlockHash(block)
virtualChangeSet, err := flow.Domain().Consensus().ValidateAndInsertBlock(block, true) err := flow.Domain().Consensus().ValidateAndInsertBlock(block, true)
if err != nil { if err != nil {
if !errors.As(err, &ruleerrors.RuleError{}) { if !errors.As(err, &ruleerrors.RuleError{}) {
return nil, nil, errors.Wrapf(err, "failed to process block %s", blockHash) return nil, errors.Wrapf(err, "failed to process block %s", blockHash)
} }
missingParentsError := &ruleerrors.ErrMissingParents{} missingParentsError := &ruleerrors.ErrMissingParents{}
if errors.As(err, missingParentsError) { if errors.As(err, missingParentsError) {
return missingParentsError.MissingParentHashes, nil, nil return missingParentsError.MissingParentHashes, nil
} }
// A duplicate block should not appear to the user as a warning and is already reported in the calling function // A duplicate block should not appear to the user as a warning and is already reported in the calling function
if !errors.Is(err, ruleerrors.ErrDuplicateBlock) { if !errors.Is(err, ruleerrors.ErrDuplicateBlock) {
log.Warnf("Rejected block %s from %s: %s", blockHash, flow.peer, err) log.Warnf("Rejected block %s from %s: %s", blockHash, flow.peer, err)
} }
return nil, nil, protocolerrors.Wrapf(true, err, "got invalid block %s from relay", blockHash) return nil, protocolerrors.Wrapf(true, err, "got invalid block %s from relay", blockHash)
} }
return nil, virtualChangeSet, nil return nil, nil
} }
func (flow *handleRelayInvsFlow) relayBlock(block *externalapi.DomainBlock) error { func (flow *handleRelayInvsFlow) relayBlock(block *externalapi.DomainBlock) error {
@ -422,10 +455,10 @@ func (flow *handleRelayInvsFlow) AddOrphanRootsToQueue(orphan *externalapi.Domai
} }
log.Infof("Block %s has %d missing ancestors. Adding them to the invs queue...", orphan, len(orphanRoots)) log.Infof("Block %s has %d missing ancestors. Adding them to the invs queue...", orphan, len(orphanRoots))
invMessages := make([]*appmessage.MsgInvRelayBlock, len(orphanRoots)) invMessages := make([]invRelayBlock, len(orphanRoots))
for i, root := range orphanRoots { for i, root := range orphanRoots {
log.Debugf("Adding block %s missing ancestor %s to the invs queue", orphan, root) log.Debugf("Adding block %s missing ancestor %s to the invs queue", orphan, root)
invMessages[i] = appmessage.NewMsgInvBlock(root) invMessages[i] = invRelayBlock{Hash: root, IsOrphanRoot: true}
} }
flow.invsQueue = append(invMessages, flow.invsQueue...) flow.invsQueue = append(invMessages, flow.invsQueue...)

View File

@ -47,9 +47,9 @@ func (flow *handleRequestAnticoneFlow) start() error {
// GetAnticone is expected to be called by the syncee for getting the anticone of the header selected tip // GetAnticone is expected to be called by the syncee for getting the anticone of the header selected tip
// intersected by past of relayed block, and is thus expected to be bounded by mergeset limit since // intersected by past of relayed block, and is thus expected to be bounded by mergeset limit since
// we relay blocks only if they enter virtual's mergeset. We add 2 for a small margin error. // we relay blocks only if they enter virtual's mergeset. We add a 2 factor for possible sync gaps.
blockHashes, err := flow.Domain().Consensus().GetAnticone(blockHash, contextHash, blockHashes, err := flow.Domain().Consensus().GetAnticone(blockHash, contextHash,
flow.Config().ActiveNetParams.MergeSetSizeLimit+2) flow.Config().ActiveNetParams.MergeSetSizeLimit*2)
if err != nil { if err != nil {
return protocolerrors.Wrap(true, err, "Failed querying anticone") return protocolerrors.Wrap(true, err, "Failed querying anticone")
} }

View File

@ -46,7 +46,25 @@ func (flow *handleRequestHeadersFlow) start() error {
} }
log.Debugf("Received requestHeaders with lowHash: %s, highHash: %s", lowHash, highHash) log.Debugf("Received requestHeaders with lowHash: %s, highHash: %s", lowHash, highHash)
isLowSelectedAncestorOfHigh, err := flow.Domain().Consensus().IsInSelectedParentChainOf(lowHash, highHash) consensus := flow.Domain().Consensus()
lowHashInfo, err := consensus.GetBlockInfo(lowHash)
if err != nil {
return err
}
if !lowHashInfo.HasHeader() {
return protocolerrors.Errorf(true, "Block %s does not exist", lowHash)
}
highHashInfo, err := consensus.GetBlockInfo(highHash)
if err != nil {
return err
}
if !highHashInfo.HasHeader() {
return protocolerrors.Errorf(true, "Block %s does not exist", highHash)
}
isLowSelectedAncestorOfHigh, err := consensus.IsInSelectedParentChainOf(lowHash, highHash)
if err != nil { if err != nil {
return err return err
} }
@ -62,7 +80,7 @@ func (flow *handleRequestHeadersFlow) start() error {
// in order to avoid locking the consensus for too long // in order to avoid locking the consensus for too long
// maxBlocks MUST be >= MergeSetSizeLimit + 1 // maxBlocks MUST be >= MergeSetSizeLimit + 1
const maxBlocks = 1 << 10 const maxBlocks = 1 << 10
blockHashes, _, err := flow.Domain().Consensus().GetHashesBetween(lowHash, highHash, maxBlocks) blockHashes, _, err := consensus.GetHashesBetween(lowHash, highHash, maxBlocks)
if err != nil { if err != nil {
return err return err
} }
@ -70,7 +88,7 @@ func (flow *handleRequestHeadersFlow) start() error {
blockHeaders := make([]*appmessage.MsgBlockHeader, len(blockHashes)) blockHeaders := make([]*appmessage.MsgBlockHeader, len(blockHashes))
for i, blockHash := range blockHashes { for i, blockHash := range blockHashes {
blockHeader, err := flow.Domain().Consensus().GetBlockHeader(blockHash) blockHeader, err := consensus.GetBlockHeader(blockHash)
if err != nil { if err != nil {
return err return err
} }

View File

@ -1,6 +1,7 @@
package blockrelay package blockrelay
import ( import (
"fmt"
"github.com/kaspanet/kaspad/app/appmessage" "github.com/kaspanet/kaspad/app/appmessage"
"github.com/kaspanet/kaspad/app/protocol/common" "github.com/kaspanet/kaspad/app/protocol/common"
peerpkg "github.com/kaspanet/kaspad/app/protocol/peer" peerpkg "github.com/kaspanet/kaspad/app/protocol/peer"
@ -20,8 +21,7 @@ import (
type IBDContext interface { type IBDContext interface {
Domain() domain.Domain Domain() domain.Domain
Config() *config.Config Config() *config.Config
OnNewBlock(block *externalapi.DomainBlock, virtualChangeSet *externalapi.VirtualChangeSet) error OnNewBlock(block *externalapi.DomainBlock) error
OnVirtualChange(virtualChangeSet *externalapi.VirtualChangeSet) error
OnNewBlockTemplate() error OnNewBlockTemplate() error
OnPruningPointUTXOSetOverride() error OnPruningPointUTXOSetOverride() error
IsIBDRunning() bool IsIBDRunning() bool
@ -71,16 +71,17 @@ func (flow *handleIBDFlow) runIBDIfNotRunning(block *externalapi.DomainBlock) er
} }
isFinishedSuccessfully := false isFinishedSuccessfully := false
var err error
defer func() { defer func() {
flow.UnsetIBDRunning() flow.UnsetIBDRunning()
flow.logIBDFinished(isFinishedSuccessfully) flow.logIBDFinished(isFinishedSuccessfully, err)
}() }()
relayBlockHash := consensushashing.BlockHash(block) relayBlockHash := consensushashing.BlockHash(block)
log.Debugf("IBD started with peer %s and relayBlockHash %s", flow.peer, relayBlockHash) log.Infof("IBD started with peer %s and relayBlockHash %s", flow.peer, relayBlockHash)
log.Debugf("Syncing blocks up to %s", relayBlockHash) log.Infof("Syncing blocks up to %s", relayBlockHash)
log.Debugf("Trying to find highest known syncer chain block from peer %s with relay hash %s", flow.peer, relayBlockHash) log.Infof("Trying to find highest known syncer chain block from peer %s with relay hash %s", flow.peer, relayBlockHash)
syncerHeaderSelectedTipHash, highestKnownSyncerChainHash, err := flow.negotiateMissingSyncerChainSegment() syncerHeaderSelectedTipHash, highestKnownSyncerChainHash, err := flow.negotiateMissingSyncerChainSegment()
if err != nil { if err != nil {
@ -99,7 +100,7 @@ func (flow *handleIBDFlow) runIBDIfNotRunning(block *externalapi.DomainBlock) er
if shouldDownloadHeadersProof { if shouldDownloadHeadersProof {
log.Infof("Starting IBD with headers proof") log.Infof("Starting IBD with headers proof")
err := flow.ibdWithHeadersProof(syncerHeaderSelectedTipHash, relayBlockHash, block.Header.DAAScore()) err = flow.ibdWithHeadersProof(syncerHeaderSelectedTipHash, relayBlockHash, block.Header.DAAScore())
if err != nil { if err != nil {
return err return err
} }
@ -174,6 +175,11 @@ func (flow *handleIBDFlow) negotiateMissingSyncerChainSegment() (*externalapi.Do
chainNegotiationRestartCounter := 0 chainNegotiationRestartCounter := 0
chainNegotiationZoomCounts := 0 chainNegotiationZoomCounts := 0
initialLocatorLen := len(locatorHashes) initialLocatorLen := len(locatorHashes)
pruningPoint, err := flow.Domain().Consensus().PruningPoint()
if err != nil {
return nil, nil, err
}
for { for {
var lowestUnknownSyncerChainHash, currentHighestKnownSyncerChainHash *externalapi.DomainHash var lowestUnknownSyncerChainHash, currentHighestKnownSyncerChainHash *externalapi.DomainHash
for _, syncerChainHash := range locatorHashes { for _, syncerChainHash := range locatorHashes {
@ -182,8 +188,25 @@ func (flow *handleIBDFlow) negotiateMissingSyncerChainSegment() (*externalapi.Do
return nil, nil, err return nil, nil, err
} }
if info.Exists { if info.Exists {
currentHighestKnownSyncerChainHash = syncerChainHash if info.BlockStatus == externalapi.StatusInvalid {
break return nil, nil, protocolerrors.Errorf(true, "Sent invalid chain block %s", syncerChainHash)
}
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 lowestUnknownSyncerChainHash = syncerChainHash
} }
@ -262,7 +285,7 @@ func (flow *handleIBDFlow) negotiateMissingSyncerChainSegment() (*externalapi.Do
} }
} }
log.Debugf("Found highest known syncer chain block %s from peer %s", log.Infof("Found highest known syncer chain block %s from peer %s",
highestKnownSyncerChainHash, flow.peer) highestKnownSyncerChainHash, flow.peer)
return syncerHeaderSelectedTipHash, highestKnownSyncerChainHash, nil return syncerHeaderSelectedTipHash, highestKnownSyncerChainHash, nil
@ -277,10 +300,14 @@ func (flow *handleIBDFlow) isGenesisVirtualSelectedParent() (bool, error) {
return virtualSelectedParent.Equal(flow.Config().NetParams().GenesisHash), nil return virtualSelectedParent.Equal(flow.Config().NetParams().GenesisHash), nil
} }
func (flow *handleIBDFlow) logIBDFinished(isFinishedSuccessfully bool) { func (flow *handleIBDFlow) logIBDFinished(isFinishedSuccessfully bool, err error) {
successString := "successfully" successString := "successfully"
if !isFinishedSuccessfully { if !isFinishedSuccessfully {
successString = "(interrupted)" 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) log.Infof("IBD with peer %s finished %s", flow.peer, successString)
} }
@ -489,7 +516,7 @@ func (flow *handleIBDFlow) processHeader(consensus externalapi.Consensus, msgBlo
log.Debugf("Block header %s is already in the DAG. Skipping...", blockHash) log.Debugf("Block header %s is already in the DAG. Skipping...", blockHash)
return nil return nil
} }
_, err = consensus.ValidateAndInsertBlock(block, false) err = consensus.ValidateAndInsertBlock(block, false)
if err != nil { if err != nil {
if !errors.As(err, &ruleerrors.RuleError{}) { if !errors.As(err, &ruleerrors.RuleError{}) {
return errors.Wrapf(err, "failed to process header %s during IBD", blockHash) return errors.Wrapf(err, "failed to process header %s during IBD", blockHash)
@ -567,7 +594,7 @@ func (flow *handleIBDFlow) receiveAndInsertPruningPointUTXOSet(
receivedChunkCount++ receivedChunkCount++
if receivedChunkCount%ibdBatchSize == 0 { if receivedChunkCount%ibdBatchSize == 0 {
log.Debugf("Received %d UTXO set chunks so far, totaling in %d UTXOs", log.Infof("Received %d UTXO set chunks so far, totaling in %d UTXOs",
receivedChunkCount, receivedUTXOCount) receivedChunkCount, receivedUTXOCount)
requestNextPruningPointUTXOSetChunkMessage := appmessage.NewMsgRequestNextPruningPointUTXOSetChunk() requestNextPruningPointUTXOSetChunkMessage := appmessage.NewMsgRequestNextPruningPointUTXOSetChunk()
@ -619,6 +646,12 @@ func (flow *handleIBDFlow) syncMissingBlockBodies(highHash *externalapi.DomainHa
progressReporter := newIBDProgressReporter(lowBlockHeader.DAAScore(), highBlockHeader.DAAScore(), "blocks") progressReporter := newIBDProgressReporter(lowBlockHeader.DAAScore(), highBlockHeader.DAAScore(), "blocks")
highestProcessedDAAScore := lowBlockHeader.DAAScore() highestProcessedDAAScore := lowBlockHeader.DAAScore()
// If the IBD is small, we want to update the virtual after each block in order to avoid complications and possible bugs.
updateVirtual, err := flow.Domain().Consensus().IsNearlySynced()
if err != nil {
return err
}
for offset := 0; offset < len(hashes); offset += ibdBatchSize { for offset := 0; offset < len(hashes); offset += ibdBatchSize {
var hashesToRequest []*externalapi.DomainHash var hashesToRequest []*externalapi.DomainHash
if offset+ibdBatchSize < len(hashes) { if offset+ibdBatchSize < len(hashes) {
@ -655,7 +688,7 @@ func (flow *handleIBDFlow) syncMissingBlockBodies(highHash *externalapi.DomainHa
return err return err
} }
virtualChangeSet, err := flow.Domain().Consensus().ValidateAndInsertBlock(block, false) err = flow.Domain().Consensus().ValidateAndInsertBlock(block, updateVirtual)
if err != nil { if err != nil {
if errors.Is(err, ruleerrors.ErrDuplicateBlock) { if errors.Is(err, ruleerrors.ErrDuplicateBlock) {
log.Debugf("Skipping IBD Block %s as it has already been added to the DAG", blockHash) log.Debugf("Skipping IBD Block %s as it has already been added to the DAG", blockHash)
@ -663,7 +696,7 @@ func (flow *handleIBDFlow) syncMissingBlockBodies(highHash *externalapi.DomainHa
} }
return protocolerrors.ConvertToBanningProtocolErrorIfRuleError(err, "invalid block %s", blockHash) return protocolerrors.ConvertToBanningProtocolErrorIfRuleError(err, "invalid block %s", blockHash)
} }
err = flow.OnNewBlock(block, virtualChangeSet) err = flow.OnNewBlock(block)
if err != nil { if err != nil {
return err return err
} }
@ -674,7 +707,15 @@ func (flow *handleIBDFlow) syncMissingBlockBodies(highHash *externalapi.DomainHa
progressReporter.reportProgress(len(hashesToRequest), highestProcessedDAAScore) progressReporter.reportProgress(len(hashesToRequest), highestProcessedDAAScore)
} }
return flow.resolveVirtual(highestProcessedDAAScore) // We need to resolve virtual only if it wasn't updated while syncing block bodies
if !updateVirtual {
err := flow.resolveVirtual(highestProcessedDAAScore)
if err != nil {
return err
}
}
return flow.OnNewBlockTemplate()
} }
func (flow *handleIBDFlow) banIfBlockIsHeaderOnly(block *externalapi.DomainBlock) error { func (flow *handleIBDFlow) banIfBlockIsHeaderOnly(block *externalapi.DomainBlock) error {
@ -687,42 +728,24 @@ func (flow *handleIBDFlow) banIfBlockIsHeaderOnly(block *externalapi.DomainBlock
} }
func (flow *handleIBDFlow) resolveVirtual(estimatedVirtualDAAScoreTarget uint64) error { func (flow *handleIBDFlow) resolveVirtual(estimatedVirtualDAAScoreTarget uint64) error {
virtualDAAScoreStart, err := flow.Domain().Consensus().GetVirtualDAAScore() err := flow.Domain().Consensus().ResolveVirtual(func(virtualDAAScoreStart uint64, virtualDAAScore uint64) {
var percents int
if estimatedVirtualDAAScoreTarget-virtualDAAScoreStart <= 0 {
percents = 100
} else {
percents = int(float64(virtualDAAScore-virtualDAAScoreStart) / float64(estimatedVirtualDAAScoreTarget-virtualDAAScoreStart) * 100)
}
if percents < 0 {
percents = 0
} else if percents > 100 {
percents = 100
}
log.Infof("Resolving virtual. Estimated progress: %d%%", percents)
})
if err != nil { if err != nil {
return err return err
} }
for i := 0; ; i++ { log.Infof("Resolved virtual")
if i%10 == 0 { return nil
virtualDAAScore, err := flow.Domain().Consensus().GetVirtualDAAScore()
if err != nil {
return err
}
var percents int
if estimatedVirtualDAAScoreTarget-virtualDAAScoreStart <= 0 {
percents = 100
} else {
percents = int(float64(virtualDAAScore-virtualDAAScoreStart) / float64(estimatedVirtualDAAScoreTarget-virtualDAAScoreStart) * 100)
}
log.Infof("Resolving virtual. Estimated progress: %d%%", percents)
}
virtualChangeSet, isCompletelyResolved, err := flow.Domain().Consensus().ResolveVirtual()
if err != nil {
return err
}
err = flow.OnVirtualChange(virtualChangeSet)
if err != nil {
return err
}
if isCompletelyResolved {
log.Infof("Resolved virtual")
err = flow.OnNewBlockTemplate()
if err != nil {
return err
}
return nil
}
}
} }

View File

@ -25,7 +25,7 @@ func (flow *handleIBDFlow) ibdWithHeadersProof(
return err return err
} }
log.Infof("IBD with pruning proof from %s was unsuccessful. Deleting the staging consensus.", flow.peer) log.Infof("IBD with pruning proof from %s was unsuccessful. Deleting the staging consensus. (%s)", flow.peer, err)
deleteStagingConsensusErr := flow.Domain().DeleteStagingConsensus() deleteStagingConsensusErr := flow.Domain().DeleteStagingConsensus()
if deleteStagingConsensusErr != nil { if deleteStagingConsensusErr != nil {
return deleteStagingConsensusErr return deleteStagingConsensusErr
@ -55,7 +55,12 @@ func (flow *handleIBDFlow) shouldSyncAndShouldDownloadHeadersProof(
var highestSharedBlockFound, isPruningPointInSharedBlockChain bool var highestSharedBlockFound, isPruningPointInSharedBlockChain bool
if highestKnownSyncerChainHash != nil { if highestKnownSyncerChainHash != nil {
highestSharedBlockFound = true blockInfo, err := flow.Domain().Consensus().GetBlockInfo(highestKnownSyncerChainHash)
if err != nil {
return false, false, err
}
highestSharedBlockFound = blockInfo.HasBody()
pruningPoint, err := flow.Domain().Consensus().PruningPoint() pruningPoint, err := flow.Domain().Consensus().PruningPoint()
if err != nil { if err != nil {
return false, false, err return false, false, err
@ -80,28 +85,33 @@ func (flow *handleIBDFlow) shouldSyncAndShouldDownloadHeadersProof(
return true, true, nil 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 return false, true, nil
} }
func (flow *handleIBDFlow) checkIfHighHashHasMoreBlueWorkThanSelectedTipAndPruningDepthMoreBlueScore(relayBlock *externalapi.DomainBlock) (bool, error) { func (flow *handleIBDFlow) checkIfHighHashHasMoreBlueWorkThanSelectedTipAndPruningDepthMoreBlueScore(relayBlock *externalapi.DomainBlock) (bool, error) {
headersSelectedTip, err := flow.Domain().Consensus().GetHeadersSelectedTip() virtualSelectedParent, err := flow.Domain().Consensus().GetVirtualSelectedParent()
if err != nil { if err != nil {
return false, err return false, err
} }
headersSelectedTipInfo, err := flow.Domain().Consensus().GetBlockInfo(headersSelectedTip) virtualSelectedTipInfo, err := flow.Domain().Consensus().GetBlockInfo(virtualSelectedParent)
if err != nil { if err != nil {
return false, err return false, err
} }
if relayBlock.Header.BlueScore() < headersSelectedTipInfo.BlueScore+flow.Config().NetParams().PruningDepth() { if relayBlock.Header.BlueScore() < virtualSelectedTipInfo.BlueScore+flow.Config().NetParams().PruningDepth() {
return false, nil return false, nil
} }
return relayBlock.Header.BlueWork().Cmp(headersSelectedTipInfo.BlueWork) > 0, nil return relayBlock.Header.BlueWork().Cmp(virtualSelectedTipInfo.BlueWork) > 0, nil
} }
func (flow *handleIBDFlow) syncAndValidatePruningPointProof() (*externalapi.DomainHash, error) { func (flow *handleIBDFlow) syncAndValidatePruningPointProof() (*externalapi.DomainHash, error) {
@ -280,8 +290,14 @@ func (flow *handleIBDFlow) processBlockWithTrustedData(
blockWithTrustedData.GHOSTDAGData = append(blockWithTrustedData.GHOSTDAGData, appmessage.GHOSTDAGHashPairToDomainGHOSTDAGHashPair(data.GHOSTDAGData[index])) blockWithTrustedData.GHOSTDAGData = append(blockWithTrustedData.GHOSTDAGData, appmessage.GHOSTDAGHashPairToDomainGHOSTDAGHashPair(data.GHOSTDAGData[index]))
} }
_, err := consensus.ValidateAndInsertBlockWithTrustedData(blockWithTrustedData, false) err := consensus.ValidateAndInsertBlockWithTrustedData(blockWithTrustedData, false)
return err if err != nil {
if errors.As(err, &ruleerrors.RuleError{}) {
return protocolerrors.Wrapf(true, err, "failed validating block with trusted data")
}
return err
}
return nil
} }
func (flow *handleIBDFlow) receiveBlockWithTrustedData() (*appmessage.MsgBlockWithTrustedDataV4, bool, error) { func (flow *handleIBDFlow) receiveBlockWithTrustedData() (*appmessage.MsgBlockWithTrustedDataV4, bool, error) {

View File

@ -102,7 +102,7 @@ func (flow *handleRelayedTransactionsFlow) requestInvTransactions(
func (flow *handleRelayedTransactionsFlow) isKnownTransaction(txID *externalapi.DomainTransactionID) bool { func (flow *handleRelayedTransactionsFlow) isKnownTransaction(txID *externalapi.DomainTransactionID) bool {
// Ask the transaction memory pool if the transaction is known // Ask the transaction memory pool if the transaction is known
// to it in any form (main pool or orphan). // to it in any form (main pool or orphan).
if _, ok := flow.Domain().MiningManager().GetTransaction(txID); ok { if _, _, ok := flow.Domain().MiningManager().GetTransaction(txID, true, true); ok {
return true return true
} }

View File

@ -30,7 +30,7 @@ func (flow *handleRequestedTransactionsFlow) start() error {
} }
for _, transactionID := range msgRequestTransactions.IDs { for _, transactionID := range msgRequestTransactions.IDs {
tx, ok := flow.Domain().MiningManager().GetTransaction(transactionID) tx, _, ok := flow.Domain().MiningManager().GetTransaction(transactionID, true, false)
if !ok { if !ok {
msgTransactionNotFound := appmessage.NewMsgTransactionNotFound(transactionID) msgTransactionNotFound := appmessage.NewMsgTransactionNotFound(transactionID)
@ -40,7 +40,6 @@ func (flow *handleRequestedTransactionsFlow) start() error {
} }
continue continue
} }
err := flow.outgoingRoute.Enqueue(appmessage.DomainTransactionToMsgTx(tx)) err := flow.outgoingRoute.Enqueue(appmessage.DomainTransactionToMsgTx(tx))
if err != nil { if err != nil {
return err return err

View File

@ -2,10 +2,11 @@ package protocol
import ( import (
"fmt" "fmt"
"github.com/kaspanet/kaspad/app/protocol/common"
"sync" "sync"
"sync/atomic" "sync/atomic"
"github.com/kaspanet/kaspad/app/protocol/common"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/kaspanet/kaspad/domain" "github.com/kaspanet/kaspad/domain"
@ -90,16 +91,6 @@ func (m *Manager) runFlows(flows []*common.Flow, peer *peerpkg.Peer, errChan <-c
return <-errChan return <-errChan
} }
// SetOnVirtualChange sets the onVirtualChangeHandler handler
func (m *Manager) SetOnVirtualChange(onVirtualChangeHandler flowcontext.OnVirtualChangeHandler) {
m.context.SetOnVirtualChangeHandler(onVirtualChangeHandler)
}
// SetOnBlockAddedToDAGHandler sets the onBlockAddedToDAG handler
func (m *Manager) SetOnBlockAddedToDAGHandler(onBlockAddedToDAGHandler flowcontext.OnBlockAddedToDAGHandler) {
m.context.SetOnBlockAddedToDAGHandler(onBlockAddedToDAGHandler)
}
// SetOnNewBlockTemplateHandler sets the onNewBlockTemplate handler // SetOnNewBlockTemplateHandler sets the onNewBlockTemplate handler
func (m *Manager) SetOnNewBlockTemplateHandler(onNewBlockTemplateHandler flowcontext.OnNewBlockTemplateHandler) { func (m *Manager) SetOnNewBlockTemplateHandler(onNewBlockTemplateHandler flowcontext.OnNewBlockTemplateHandler) {
m.context.SetOnNewBlockTemplateHandler(onNewBlockTemplateHandler) m.context.SetOnNewBlockTemplateHandler(onNewBlockTemplateHandler)
@ -115,12 +106,6 @@ func (m *Manager) SetOnTransactionAddedToMempoolHandler(onTransactionAddedToMemp
m.context.SetOnTransactionAddedToMempoolHandler(onTransactionAddedToMempoolHandler) m.context.SetOnTransactionAddedToMempoolHandler(onTransactionAddedToMempoolHandler)
} }
// ShouldMine returns whether it's ok to use block template from this node
// for mining purposes.
func (m *Manager) ShouldMine() (bool, error) {
return m.context.IsNearlySynced()
}
// IsIBDRunning returns true if IBD is currently marked as running // IsIBDRunning returns true if IBD is currently marked as running
func (m *Manager) IsIBDRunning() bool { func (m *Manager) IsIBDRunning() bool {
return m.context.IsIBDRunning() return m.context.IsIBDRunning()

View File

@ -23,7 +23,7 @@ func (m *Manager) routerInitializer(router *routerpkg.Router, netConnection *net
// errChan is used by the flow goroutines to return to runFlows when an error occurs. // errChan is used by the flow goroutines to return to runFlows when an error occurs.
// They are both initialized here and passed to register flows. // They are both initialized here and passed to register flows.
isStopping := uint32(0) isStopping := uint32(0)
errChan := make(chan error) errChan := make(chan error, 1)
receiveVersionRoute, sendVersionRoute, receiveReadyRoute := registerHandshakeRoutes(router) receiveVersionRoute, sendVersionRoute, receiveReadyRoute := registerHandshakeRoutes(router)

View File

@ -12,6 +12,7 @@ import (
"github.com/kaspanet/kaspad/infrastructure/network/addressmanager" "github.com/kaspanet/kaspad/infrastructure/network/addressmanager"
"github.com/kaspanet/kaspad/infrastructure/network/connmanager" "github.com/kaspanet/kaspad/infrastructure/network/connmanager"
"github.com/kaspanet/kaspad/infrastructure/network/netadapter" "github.com/kaspanet/kaspad/infrastructure/network/netadapter"
"github.com/pkg/errors"
) )
// Manager is an RPC manager // Manager is an RPC manager
@ -28,6 +29,7 @@ func NewManager(
connectionManager *connmanager.ConnectionManager, connectionManager *connmanager.ConnectionManager,
addressManager *addressmanager.AddressManager, addressManager *addressmanager.AddressManager,
utxoIndex *utxoindex.UTXOIndex, utxoIndex *utxoindex.UTXOIndex,
consensusEventsChan chan externalapi.ConsensusEvent,
shutDownChan chan<- struct{}) *Manager { shutDownChan chan<- struct{}) *Manager {
manager := Manager{ manager := Manager{
@ -44,50 +46,90 @@ func NewManager(
} }
netAdapter.SetRPCRouterInitializer(manager.routerInitializer) netAdapter.SetRPCRouterInitializer(manager.routerInitializer)
manager.initConsensusEventsHandler(consensusEventsChan)
return &manager return &manager
} }
// NotifyBlockAddedToDAG notifies the manager that a block has been added to the DAG func (m *Manager) initConsensusEventsHandler(consensusEventsChan chan externalapi.ConsensusEvent) {
func (m *Manager) NotifyBlockAddedToDAG(block *externalapi.DomainBlock, virtualChangeSet *externalapi.VirtualChangeSet) error { spawn("consensusEventsHandler", func() {
onEnd := logger.LogAndMeasureExecutionTime(log, "RPCManager.NotifyBlockAddedToDAG") for {
consensusEvent, ok := <-consensusEventsChan
if !ok {
return
}
switch event := consensusEvent.(type) {
case *externalapi.VirtualChangeSet:
err := m.notifyVirtualChange(event)
if err != nil {
panic(err)
}
case *externalapi.BlockAdded:
err := m.notifyBlockAddedToDAG(event.Block)
if err != nil {
panic(err)
}
default:
panic(errors.Errorf("Got event of unsupported type %T", consensusEvent))
}
}
})
}
// notifyBlockAddedToDAG notifies the manager that a block has been added to the DAG
func (m *Manager) notifyBlockAddedToDAG(block *externalapi.DomainBlock) error {
onEnd := logger.LogAndMeasureExecutionTime(log, "RPCManager.notifyBlockAddedToDAG")
defer onEnd() defer onEnd()
err := m.NotifyVirtualChange(virtualChangeSet) // Before converting the block and populating it, we check if any listeners are interested.
if err != nil { // This is done since most nodes do not use this event.
return err if !m.context.NotificationManager.HasBlockAddedListeners() {
return nil
} }
rpcBlock := appmessage.DomainBlockToRPCBlock(block) rpcBlock := appmessage.DomainBlockToRPCBlock(block)
err = m.context.PopulateBlockWithVerboseData(rpcBlock, block.Header, block, false) err := m.context.PopulateBlockWithVerboseData(rpcBlock, block.Header, block, true)
if err != nil { if err != nil {
return err return err
} }
blockAddedNotification := appmessage.NewBlockAddedNotificationMessage(rpcBlock) blockAddedNotification := appmessage.NewBlockAddedNotificationMessage(rpcBlock)
return m.context.NotificationManager.NotifyBlockAdded(blockAddedNotification) err = m.context.NotificationManager.NotifyBlockAdded(blockAddedNotification)
if err != nil {
return err
}
return nil
} }
// NotifyVirtualChange notifies the manager that the virtual block has been changed. // notifyVirtualChange notifies the manager that the virtual block has been changed.
func (m *Manager) NotifyVirtualChange(virtualChangeSet *externalapi.VirtualChangeSet) error { func (m *Manager) notifyVirtualChange(virtualChangeSet *externalapi.VirtualChangeSet) error {
onEnd := logger.LogAndMeasureExecutionTime(log, "RPCManager.NotifyVirtualChange") onEnd := logger.LogAndMeasureExecutionTime(log, "RPCManager.NotifyVirtualChange")
defer onEnd() defer onEnd()
if m.context.Config.UTXOIndex { if m.context.Config.UTXOIndex && virtualChangeSet.VirtualUTXODiff != nil {
err := m.notifyUTXOsChanged(virtualChangeSet) err := m.notifyUTXOsChanged(virtualChangeSet)
if err != nil { if err != nil {
return err return err
} }
} }
err := m.notifyVirtualSelectedParentBlueScoreChanged() err := m.notifyVirtualSelectedParentBlueScoreChanged(virtualChangeSet.VirtualSelectedParentBlueScore)
if err != nil { if err != nil {
return err return err
} }
err = m.notifyVirtualDaaScoreChanged() err = m.notifyVirtualDaaScoreChanged(virtualChangeSet.VirtualDAAScore)
if err != nil { if err != nil {
return err return err
} }
if virtualChangeSet.VirtualSelectedParentChainChanges == nil ||
(len(virtualChangeSet.VirtualSelectedParentChainChanges.Added) == 0 &&
len(virtualChangeSet.VirtualSelectedParentChainChanges.Removed) == 0) {
return nil
}
err = m.notifyVirtualSelectedParentChainChanged(virtualChangeSet) err = m.notifyVirtualSelectedParentChainChanged(virtualChangeSet)
if err != nil { if err != nil {
return err return err
@ -145,6 +187,7 @@ func (m *Manager) notifyUTXOsChanged(virtualChangeSet *externalapi.VirtualChange
if err != nil { if err != nil {
return err return err
} }
return m.context.NotificationManager.NotifyUTXOsChanged(utxoIndexChanges) return m.context.NotificationManager.NotifyUTXOsChanged(utxoIndexChanges)
} }
@ -160,33 +203,18 @@ func (m *Manager) notifyPruningPointUTXOSetOverride() error {
return m.context.NotificationManager.NotifyPruningPointUTXOSetOverride() return m.context.NotificationManager.NotifyPruningPointUTXOSetOverride()
} }
func (m *Manager) notifyVirtualSelectedParentBlueScoreChanged() error { func (m *Manager) notifyVirtualSelectedParentBlueScoreChanged(virtualSelectedParentBlueScore uint64) error {
onEnd := logger.LogAndMeasureExecutionTime(log, "RPCManager.NotifyVirtualSelectedParentBlueScoreChanged") onEnd := logger.LogAndMeasureExecutionTime(log, "RPCManager.NotifyVirtualSelectedParentBlueScoreChanged")
defer onEnd() defer onEnd()
virtualSelectedParent, err := m.context.Domain.Consensus().GetVirtualSelectedParent() notification := appmessage.NewVirtualSelectedParentBlueScoreChangedNotificationMessage(virtualSelectedParentBlueScore)
if err != nil {
return err
}
blockInfo, err := m.context.Domain.Consensus().GetBlockInfo(virtualSelectedParent)
if err != nil {
return err
}
notification := appmessage.NewVirtualSelectedParentBlueScoreChangedNotificationMessage(blockInfo.BlueScore)
return m.context.NotificationManager.NotifyVirtualSelectedParentBlueScoreChanged(notification) return m.context.NotificationManager.NotifyVirtualSelectedParentBlueScoreChanged(notification)
} }
func (m *Manager) notifyVirtualDaaScoreChanged() error { func (m *Manager) notifyVirtualDaaScoreChanged(virtualDAAScore uint64) error {
onEnd := logger.LogAndMeasureExecutionTime(log, "RPCManager.NotifyVirtualDaaScoreChanged") onEnd := logger.LogAndMeasureExecutionTime(log, "RPCManager.NotifyVirtualDaaScoreChanged")
defer onEnd() defer onEnd()
virtualDAAScore, err := m.context.Domain.Consensus().GetVirtualDAAScore()
if err != nil {
return err
}
notification := appmessage.NewVirtualDaaScoreChangedNotificationMessage(virtualDAAScore) notification := appmessage.NewVirtualDaaScoreChangedNotificationMessage(virtualDAAScore)
return m.context.NotificationManager.NotifyVirtualDaaScoreChanged(notification) return m.context.NotificationManager.NotifyVirtualDaaScoreChanged(notification)
} }
@ -195,10 +223,16 @@ func (m *Manager) notifyVirtualSelectedParentChainChanged(virtualChangeSet *exte
onEnd := logger.LogAndMeasureExecutionTime(log, "RPCManager.NotifyVirtualSelectedParentChainChanged") onEnd := logger.LogAndMeasureExecutionTime(log, "RPCManager.NotifyVirtualSelectedParentChainChanged")
defer onEnd() defer onEnd()
notification, err := m.context.ConvertVirtualSelectedParentChainChangesToChainChangedNotificationMessage( hasListeners, includeAcceptedTransactionIDs := m.context.NotificationManager.HasListenersThatPropagateVirtualSelectedParentChainChanged()
virtualChangeSet.VirtualSelectedParentChainChanges)
if err != nil { if hasListeners {
return err notification, err := m.context.ConvertVirtualSelectedParentChainChangesToChainChangedNotificationMessage(
virtualChangeSet.VirtualSelectedParentChainChanges, includeAcceptedTransactionIDs)
if err != nil {
return err
}
return m.context.NotificationManager.NotifyVirtualSelectedParentChainChanged(notification)
} }
return m.context.NotificationManager.NotifyVirtualSelectedParentChainChanged(notification)
return nil
} }

View File

@ -49,6 +49,8 @@ var handlers = map[appmessage.MessageCommand]handler{
appmessage.CmdEstimateNetworkHashesPerSecondRequestMessage: rpchandlers.HandleEstimateNetworkHashesPerSecond, appmessage.CmdEstimateNetworkHashesPerSecondRequestMessage: rpchandlers.HandleEstimateNetworkHashesPerSecond,
appmessage.CmdNotifyVirtualDaaScoreChangedRequestMessage: rpchandlers.HandleNotifyVirtualDaaScoreChanged, appmessage.CmdNotifyVirtualDaaScoreChangedRequestMessage: rpchandlers.HandleNotifyVirtualDaaScoreChanged,
appmessage.CmdNotifyNewBlockTemplateRequestMessage: rpchandlers.HandleNotifyNewBlockTemplate, appmessage.CmdNotifyNewBlockTemplateRequestMessage: rpchandlers.HandleNotifyNewBlockTemplate,
appmessage.CmdGetCoinSupplyRequestMessage: rpchandlers.HandleGetCoinSupply,
appmessage.CmdGetMempoolEntriesByAddressesRequestMessage: rpchandlers.HandleGetMempoolEntriesByAddresses,
} }
func (m *Manager) routerInitializer(router *router.Router, netConnection *netadapter.NetConnection) { func (m *Manager) routerInitializer(router *router.Router, netConnection *netadapter.NetConnection) {

View File

@ -3,12 +3,14 @@ package rpccontext
import ( import (
"github.com/kaspanet/kaspad/app/appmessage" "github.com/kaspanet/kaspad/app/appmessage"
"github.com/kaspanet/kaspad/domain/consensus/model/externalapi" "github.com/kaspanet/kaspad/domain/consensus/model/externalapi"
"github.com/kaspanet/kaspad/domain/consensus/utils/consensushashing"
) )
// ConvertVirtualSelectedParentChainChangesToChainChangedNotificationMessage converts // ConvertVirtualSelectedParentChainChangesToChainChangedNotificationMessage converts
// VirtualSelectedParentChainChanges to VirtualSelectedParentChainChangedNotificationMessage // VirtualSelectedParentChainChanges to VirtualSelectedParentChainChangedNotificationMessage
func (ctx *Context) ConvertVirtualSelectedParentChainChangesToChainChangedNotificationMessage( func (ctx *Context) ConvertVirtualSelectedParentChainChangesToChainChangedNotificationMessage(
selectedParentChainChanges *externalapi.SelectedChainPath) (*appmessage.VirtualSelectedParentChainChangedNotificationMessage, error) { selectedParentChainChanges *externalapi.SelectedChainPath, includeAcceptedTransactionIDs bool) (
*appmessage.VirtualSelectedParentChainChangedNotificationMessage, error) {
removedChainBlockHashes := make([]string, len(selectedParentChainChanges.Removed)) removedChainBlockHashes := make([]string, len(selectedParentChainChanges.Removed))
for i, removed := range selectedParentChainChanges.Removed { for i, removed := range selectedParentChainChanges.Removed {
@ -20,5 +22,58 @@ func (ctx *Context) ConvertVirtualSelectedParentChainChangesToChainChangedNotifi
addedChainBlocks[i] = added.String() addedChainBlocks[i] = added.String()
} }
return appmessage.NewVirtualSelectedParentChainChangedNotificationMessage(removedChainBlockHashes, addedChainBlocks), nil var acceptedTransactionIDs []*appmessage.AcceptedTransactionIDs
if includeAcceptedTransactionIDs {
var err error
acceptedTransactionIDs, err = ctx.getAndConvertAcceptedTransactionIDs(selectedParentChainChanges)
if err != nil {
return nil, err
}
}
return appmessage.NewVirtualSelectedParentChainChangedNotificationMessage(
removedChainBlockHashes, addedChainBlocks, acceptedTransactionIDs), nil
}
func (ctx *Context) getAndConvertAcceptedTransactionIDs(selectedParentChainChanges *externalapi.SelectedChainPath) (
[]*appmessage.AcceptedTransactionIDs, error) {
acceptedTransactionIDs := make([]*appmessage.AcceptedTransactionIDs, len(selectedParentChainChanges.Added))
const chunk = 1000
position := 0
for position < len(selectedParentChainChanges.Added) {
var chainBlocksChunk []*externalapi.DomainHash
if position+chunk > len(selectedParentChainChanges.Added) {
chainBlocksChunk = selectedParentChainChanges.Added[position:]
} else {
chainBlocksChunk = selectedParentChainChanges.Added[position : position+chunk]
}
// We use chunks in order to avoid blocking consensus for too long
chainBlocksAcceptanceData, err := ctx.Domain.Consensus().GetBlocksAcceptanceData(chainBlocksChunk)
if err != nil {
return nil, err
}
for i, addedChainBlock := range chainBlocksChunk {
chainBlockAcceptanceData := chainBlocksAcceptanceData[i]
acceptedTransactionIDs[position+i] = &appmessage.AcceptedTransactionIDs{
AcceptingBlockHash: addedChainBlock.String(),
AcceptedTransactionIDs: nil,
}
for _, blockAcceptanceData := range chainBlockAcceptanceData {
for _, transactionAcceptanceData := range blockAcceptanceData.TransactionAcceptanceData {
if transactionAcceptanceData.IsAccepted {
acceptedTransactionIDs[position+i].AcceptedTransactionIDs =
append(acceptedTransactionIDs[position+i].AcceptedTransactionIDs,
consensushashing.TransactionID(transactionAcceptanceData.Transaction).String())
}
}
}
}
position += chunk
}
return acceptedTransactionIDs, nil
} }

View File

@ -44,7 +44,7 @@ func NewContext(cfg *config.Config,
UTXOIndex: utxoIndex, UTXOIndex: utxoIndex,
ShutDownChan: shutDownChan, ShutDownChan: shutDownChan,
} }
context.NotificationManager = NewNotificationManager() context.NotificationManager = NewNotificationManager(cfg.ActiveNetParams)
return context return context
} }

View File

@ -3,6 +3,11 @@ package rpccontext
import ( import (
"sync" "sync"
"github.com/kaspanet/kaspad/domain/dagconfig"
"github.com/kaspanet/kaspad/domain/consensus/model/externalapi"
"github.com/kaspanet/kaspad/domain/consensus/utils/txscript"
"github.com/kaspanet/kaspad/app/appmessage" "github.com/kaspanet/kaspad/app/appmessage"
"github.com/kaspanet/kaspad/domain/utxoindex" "github.com/kaspanet/kaspad/domain/utxoindex"
routerpkg "github.com/kaspanet/kaspad/infrastructure/network/netadapter/router" routerpkg "github.com/kaspanet/kaspad/infrastructure/network/netadapter/router"
@ -13,6 +18,7 @@ import (
type NotificationManager struct { type NotificationManager struct {
sync.RWMutex sync.RWMutex
listeners map[*routerpkg.Router]*NotificationListener listeners map[*routerpkg.Router]*NotificationListener
params *dagconfig.Params
} }
// UTXOsChangedNotificationAddress represents a kaspad address. // UTXOsChangedNotificationAddress represents a kaspad address.
@ -24,6 +30,8 @@ type UTXOsChangedNotificationAddress struct {
// NotificationListener represents a registered RPC notification listener // NotificationListener represents a registered RPC notification listener
type NotificationListener struct { type NotificationListener struct {
params *dagconfig.Params
propagateBlockAddedNotifications bool propagateBlockAddedNotifications bool
propagateVirtualSelectedParentChainChangedNotifications bool propagateVirtualSelectedParentChainChangedNotifications bool
propagateFinalityConflictNotifications bool propagateFinalityConflictNotifications bool
@ -34,12 +42,14 @@ type NotificationListener struct {
propagatePruningPointUTXOSetOverrideNotifications bool propagatePruningPointUTXOSetOverrideNotifications bool
propagateNewBlockTemplateNotifications bool propagateNewBlockTemplateNotifications bool
propagateUTXOsChangedNotificationAddresses map[utxoindex.ScriptPublicKeyString]*UTXOsChangedNotificationAddress propagateUTXOsChangedNotificationAddresses map[utxoindex.ScriptPublicKeyString]*UTXOsChangedNotificationAddress
includeAcceptedTransactionIDsInVirtualSelectedParentChainChangedNotifications bool
} }
// NewNotificationManager creates a new NotificationManager // NewNotificationManager creates a new NotificationManager
func NewNotificationManager() *NotificationManager { func NewNotificationManager(params *dagconfig.Params) *NotificationManager {
return &NotificationManager{ return &NotificationManager{
params: params,
listeners: make(map[*routerpkg.Router]*NotificationListener), listeners: make(map[*routerpkg.Router]*NotificationListener),
} }
} }
@ -49,7 +59,7 @@ func (nm *NotificationManager) AddListener(router *routerpkg.Router) {
nm.Lock() nm.Lock()
defer nm.Unlock() defer nm.Unlock()
listener := newNotificationListener() listener := newNotificationListener(nm.params)
nm.listeners[router] = listener nm.listeners[router] = listener
} }
@ -73,6 +83,19 @@ func (nm *NotificationManager) Listener(router *routerpkg.Router) (*Notification
return listener, nil return listener, nil
} }
// HasBlockAddedListeners indicates if the notification manager has any listeners for `BlockAdded` events
func (nm *NotificationManager) HasBlockAddedListeners() bool {
nm.RLock()
defer nm.RUnlock()
for _, listener := range nm.listeners {
if listener.propagateBlockAddedNotifications {
return true
}
}
return false
}
// NotifyBlockAdded notifies the notification manager that a block has been added to the DAG // NotifyBlockAdded notifies the notification manager that a block has been added to the DAG
func (nm *NotificationManager) NotifyBlockAdded(notification *appmessage.BlockAddedNotificationMessage) error { func (nm *NotificationManager) NotifyBlockAdded(notification *appmessage.BlockAddedNotificationMessage) error {
nm.RLock() nm.RLock()
@ -80,10 +103,8 @@ func (nm *NotificationManager) NotifyBlockAdded(notification *appmessage.BlockAd
for router, listener := range nm.listeners { for router, listener := range nm.listeners {
if listener.propagateBlockAddedNotifications { if listener.propagateBlockAddedNotifications {
err := router.OutgoingRoute().Enqueue(notification) err := router.OutgoingRoute().MaybeEnqueue(notification)
if errors.Is(err, routerpkg.ErrRouteClosed) { if err != nil {
log.Warnf("Couldn't send notification: %s", err)
} else if err != nil {
return err return err
} }
} }
@ -92,13 +113,27 @@ func (nm *NotificationManager) NotifyBlockAdded(notification *appmessage.BlockAd
} }
// NotifyVirtualSelectedParentChainChanged notifies the notification manager that the DAG's selected parent chain has changed // NotifyVirtualSelectedParentChainChanged notifies the notification manager that the DAG's selected parent chain has changed
func (nm *NotificationManager) NotifyVirtualSelectedParentChainChanged(notification *appmessage.VirtualSelectedParentChainChangedNotificationMessage) error { func (nm *NotificationManager) NotifyVirtualSelectedParentChainChanged(
notification *appmessage.VirtualSelectedParentChainChangedNotificationMessage) error {
nm.RLock() nm.RLock()
defer nm.RUnlock() defer nm.RUnlock()
notificationWithoutAcceptedTransactionIDs := &appmessage.VirtualSelectedParentChainChangedNotificationMessage{
RemovedChainBlockHashes: notification.RemovedChainBlockHashes,
AddedChainBlockHashes: notification.AddedChainBlockHashes,
}
for router, listener := range nm.listeners { for router, listener := range nm.listeners {
if listener.propagateVirtualSelectedParentChainChangedNotifications { if listener.propagateVirtualSelectedParentChainChangedNotifications {
err := router.OutgoingRoute().Enqueue(notification) var err error
if listener.includeAcceptedTransactionIDsInVirtualSelectedParentChainChangedNotifications {
err = router.OutgoingRoute().MaybeEnqueue(notification)
} else {
err = router.OutgoingRoute().MaybeEnqueue(notificationWithoutAcceptedTransactionIDs)
}
if err != nil { if err != nil {
return err return err
} }
@ -107,6 +142,31 @@ func (nm *NotificationManager) NotifyVirtualSelectedParentChainChanged(notificat
return nil return nil
} }
// HasListenersThatPropagateVirtualSelectedParentChainChanged returns whether there's any listener that is
// subscribed to VirtualSelectedParentChainChanged notifications as well as checks if any such listener requested
// to include AcceptedTransactionIDs.
func (nm *NotificationManager) HasListenersThatPropagateVirtualSelectedParentChainChanged() (hasListeners, hasListenersThatRequireAcceptedTransactionIDs bool) {
nm.RLock()
defer nm.RUnlock()
hasListeners = false
hasListenersThatRequireAcceptedTransactionIDs = false
for _, listener := range nm.listeners {
if listener.propagateVirtualSelectedParentChainChangedNotifications {
hasListeners = true
// Generating acceptedTransactionIDs is a heavy operation, so we check if it's needed by any listener.
if listener.includeAcceptedTransactionIDsInVirtualSelectedParentChainChangedNotifications {
hasListenersThatRequireAcceptedTransactionIDs = true
break
}
}
}
return hasListeners, hasListenersThatRequireAcceptedTransactionIDs
}
// NotifyFinalityConflict notifies the notification manager that there's a finality conflict in the DAG // NotifyFinalityConflict notifies the notification manager that there's a finality conflict in the DAG
func (nm *NotificationManager) NotifyFinalityConflict(notification *appmessage.FinalityConflictNotificationMessage) error { func (nm *NotificationManager) NotifyFinalityConflict(notification *appmessage.FinalityConflictNotificationMessage) error {
nm.RLock() nm.RLock()
@ -147,7 +207,10 @@ func (nm *NotificationManager) NotifyUTXOsChanged(utxoChanges *utxoindex.UTXOCha
for router, listener := range nm.listeners { for router, listener := range nm.listeners {
if listener.propagateUTXOsChangedNotifications { if listener.propagateUTXOsChangedNotifications {
// Filter utxoChanges and create a notification // Filter utxoChanges and create a notification
notification := listener.convertUTXOChangesToUTXOsChangedNotification(utxoChanges) notification, err := listener.convertUTXOChangesToUTXOsChangedNotification(utxoChanges)
if err != nil {
return err
}
// Don't send the notification if it's empty // Don't send the notification if it's empty
if len(notification.Added) == 0 && len(notification.Removed) == 0 { if len(notification.Added) == 0 && len(notification.Removed) == 0 {
@ -155,7 +218,7 @@ func (nm *NotificationManager) NotifyUTXOsChanged(utxoChanges *utxoindex.UTXOCha
} }
// Enqueue the notification // Enqueue the notification
err := router.OutgoingRoute().Enqueue(notification) err = router.OutgoingRoute().MaybeEnqueue(notification)
if err != nil { if err != nil {
return err return err
} }
@ -174,7 +237,7 @@ func (nm *NotificationManager) NotifyVirtualSelectedParentBlueScoreChanged(
for router, listener := range nm.listeners { for router, listener := range nm.listeners {
if listener.propagateVirtualSelectedParentBlueScoreChangedNotifications { if listener.propagateVirtualSelectedParentBlueScoreChangedNotifications {
err := router.OutgoingRoute().Enqueue(notification) err := router.OutgoingRoute().MaybeEnqueue(notification)
if err != nil { if err != nil {
return err return err
} }
@ -193,7 +256,7 @@ func (nm *NotificationManager) NotifyVirtualDaaScoreChanged(
for router, listener := range nm.listeners { for router, listener := range nm.listeners {
if listener.propagateVirtualDaaScoreChangedNotifications { if listener.propagateVirtualDaaScoreChangedNotifications {
err := router.OutgoingRoute().Enqueue(notification) err := router.OutgoingRoute().MaybeEnqueue(notification)
if err != nil { if err != nil {
return err return err
} }
@ -238,8 +301,10 @@ func (nm *NotificationManager) NotifyPruningPointUTXOSetOverride() error {
return nil return nil
} }
func newNotificationListener() *NotificationListener { func newNotificationListener(params *dagconfig.Params) *NotificationListener {
return &NotificationListener{ return &NotificationListener{
params: params,
propagateBlockAddedNotifications: false, propagateBlockAddedNotifications: false,
propagateVirtualSelectedParentChainChangedNotifications: false, propagateVirtualSelectedParentChainChangedNotifications: false,
propagateFinalityConflictNotifications: false, propagateFinalityConflictNotifications: false,
@ -251,6 +316,12 @@ func newNotificationListener() *NotificationListener {
} }
} }
// IncludeAcceptedTransactionIDsInVirtualSelectedParentChainChangedNotifications returns true if this listener
// includes accepted transaction IDs in it's virtual-selected-parent-chain-changed notifications
func (nl *NotificationListener) IncludeAcceptedTransactionIDsInVirtualSelectedParentChainChangedNotifications() bool {
return nl.includeAcceptedTransactionIDsInVirtualSelectedParentChainChangedNotifications
}
// PropagateBlockAddedNotifications instructs the listener to send block added notifications // PropagateBlockAddedNotifications instructs the listener to send block added notifications
// to the remote listener // to the remote listener
func (nl *NotificationListener) PropagateBlockAddedNotifications() { func (nl *NotificationListener) PropagateBlockAddedNotifications() {
@ -259,8 +330,9 @@ func (nl *NotificationListener) PropagateBlockAddedNotifications() {
// PropagateVirtualSelectedParentChainChangedNotifications instructs the listener to send chain changed notifications // PropagateVirtualSelectedParentChainChangedNotifications instructs the listener to send chain changed notifications
// to the remote listener // to the remote listener
func (nl *NotificationListener) PropagateVirtualSelectedParentChainChangedNotifications() { func (nl *NotificationListener) PropagateVirtualSelectedParentChainChangedNotifications(includeAcceptedTransactionIDs bool) {
nl.propagateVirtualSelectedParentChainChangedNotifications = true nl.propagateVirtualSelectedParentChainChangedNotifications = true
nl.includeAcceptedTransactionIDsInVirtualSelectedParentChainChangedNotifications = includeAcceptedTransactionIDs
} }
// PropagateFinalityConflictNotifications instructs the listener to send finality conflict notifications // PropagateFinalityConflictNotifications instructs the listener to send finality conflict notifications
@ -279,7 +351,11 @@ func (nl *NotificationListener) PropagateFinalityConflictResolvedNotifications()
// to the remote listener for the given addresses. Subsequent calls instruct the listener to // to the remote listener for the given addresses. Subsequent calls instruct the listener to
// send UTXOs changed notifications for those addresses along with the old ones. Duplicate addresses // send UTXOs changed notifications for those addresses along with the old ones. Duplicate addresses
// are ignored. // are ignored.
func (nl *NotificationListener) PropagateUTXOsChangedNotifications(addresses []*UTXOsChangedNotificationAddress) { func (nm *NotificationManager) PropagateUTXOsChangedNotifications(nl *NotificationListener, addresses []*UTXOsChangedNotificationAddress) {
// Apply a write-lock since the internal listener address map is modified
nm.Lock()
defer nm.Unlock()
if !nl.propagateUTXOsChangedNotifications { if !nl.propagateUTXOsChangedNotifications {
nl.propagateUTXOsChangedNotifications = true nl.propagateUTXOsChangedNotifications = true
nl.propagateUTXOsChangedNotificationAddresses = nl.propagateUTXOsChangedNotificationAddresses =
@ -294,7 +370,11 @@ func (nl *NotificationListener) PropagateUTXOsChangedNotifications(addresses []*
// StopPropagatingUTXOsChangedNotifications instructs the listener to stop sending UTXOs // StopPropagatingUTXOsChangedNotifications instructs the listener to stop sending UTXOs
// changed notifications to the remote listener for the given addresses. Addresses for which // changed notifications to the remote listener for the given addresses. Addresses for which
// notifications are not currently sent are ignored. // notifications are not currently sent are ignored.
func (nl *NotificationListener) StopPropagatingUTXOsChangedNotifications(addresses []*UTXOsChangedNotificationAddress) { func (nm *NotificationManager) StopPropagatingUTXOsChangedNotifications(nl *NotificationListener, addresses []*UTXOsChangedNotificationAddress) {
// Apply a write-lock since the internal listener address map is modified
nm.Lock()
defer nm.Unlock()
if !nl.propagateUTXOsChangedNotifications { if !nl.propagateUTXOsChangedNotifications {
return return
} }
@ -305,7 +385,7 @@ func (nl *NotificationListener) StopPropagatingUTXOsChangedNotifications(address
} }
func (nl *NotificationListener) convertUTXOChangesToUTXOsChangedNotification( func (nl *NotificationListener) convertUTXOChangesToUTXOsChangedNotification(
utxoChanges *utxoindex.UTXOChanges) *appmessage.UTXOsChangedNotificationMessage { utxoChanges *utxoindex.UTXOChanges) (*appmessage.UTXOsChangedNotificationMessage, error) {
// As an optimization, we iterate over the smaller set (O(n)) among the two below // As an optimization, we iterate over the smaller set (O(n)) among the two below
// and check existence over the larger set (O(1)) // and check existence over the larger set (O(1))
@ -320,27 +400,64 @@ func (nl *NotificationListener) convertUTXOChangesToUTXOsChangedNotification(
notification.Added = append(notification.Added, utxosByAddressesEntries...) notification.Added = append(notification.Added, utxosByAddressesEntries...)
} }
} }
for scriptPublicKeyString, removedOutpoints := range utxoChanges.Removed { for scriptPublicKeyString, removedPairs := range utxoChanges.Removed {
if listenerAddress, ok := nl.propagateUTXOsChangedNotificationAddresses[scriptPublicKeyString]; ok { if listenerAddress, ok := nl.propagateUTXOsChangedNotificationAddresses[scriptPublicKeyString]; ok {
utxosByAddressesEntries := convertUTXOOutpointsToUTXOsByAddressesEntries(listenerAddress.Address, removedOutpoints) utxosByAddressesEntries := ConvertUTXOOutpointEntryPairsToUTXOsByAddressesEntries(listenerAddress.Address, removedPairs)
notification.Removed = append(notification.Removed, utxosByAddressesEntries...) notification.Removed = append(notification.Removed, utxosByAddressesEntries...)
} }
} }
} else { } else if addressesSize > 0 {
for _, listenerAddress := range nl.propagateUTXOsChangedNotificationAddresses { for _, listenerAddress := range nl.propagateUTXOsChangedNotificationAddresses {
listenerScriptPublicKeyString := listenerAddress.ScriptPublicKeyString listenerScriptPublicKeyString := listenerAddress.ScriptPublicKeyString
if addedPairs, ok := utxoChanges.Added[listenerScriptPublicKeyString]; ok { if addedPairs, ok := utxoChanges.Added[listenerScriptPublicKeyString]; ok {
utxosByAddressesEntries := ConvertUTXOOutpointEntryPairsToUTXOsByAddressesEntries(listenerAddress.Address, addedPairs) utxosByAddressesEntries := ConvertUTXOOutpointEntryPairsToUTXOsByAddressesEntries(listenerAddress.Address, addedPairs)
notification.Added = append(notification.Added, utxosByAddressesEntries...) notification.Added = append(notification.Added, utxosByAddressesEntries...)
} }
if removedOutpoints, ok := utxoChanges.Removed[listenerScriptPublicKeyString]; ok { if removedPairs, ok := utxoChanges.Removed[listenerScriptPublicKeyString]; ok {
utxosByAddressesEntries := convertUTXOOutpointsToUTXOsByAddressesEntries(listenerAddress.Address, removedOutpoints) utxosByAddressesEntries := ConvertUTXOOutpointEntryPairsToUTXOsByAddressesEntries(listenerAddress.Address, removedPairs)
notification.Removed = append(notification.Removed, utxosByAddressesEntries...) notification.Removed = append(notification.Removed, utxosByAddressesEntries...)
} }
} }
} else {
for scriptPublicKeyString, addedPairs := range utxoChanges.Added {
addressString, err := nl.scriptPubKeyStringToAddressString(scriptPublicKeyString)
if err != nil {
return nil, err
}
utxosByAddressesEntries := ConvertUTXOOutpointEntryPairsToUTXOsByAddressesEntries(addressString, addedPairs)
notification.Added = append(notification.Added, utxosByAddressesEntries...)
}
for scriptPublicKeyString, removedPAirs := range utxoChanges.Removed {
addressString, err := nl.scriptPubKeyStringToAddressString(scriptPublicKeyString)
if err != nil {
return nil, err
}
utxosByAddressesEntries := ConvertUTXOOutpointEntryPairsToUTXOsByAddressesEntries(addressString, removedPAirs)
notification.Removed = append(notification.Removed, utxosByAddressesEntries...)
}
} }
return notification return notification, nil
}
func (nl *NotificationListener) scriptPubKeyStringToAddressString(scriptPublicKeyString utxoindex.ScriptPublicKeyString) (string, error) {
scriptPubKey := externalapi.NewScriptPublicKeyFromString(string(scriptPublicKeyString))
// ignore error because it is often returned when the script is of unknown type
scriptType, address, err := txscript.ExtractScriptPubKeyAddress(scriptPubKey, nl.params)
if err != nil {
return "", err
}
var addressString string
if scriptType == txscript.NonStandardTy {
addressString = ""
} else {
addressString = address.String()
}
return addressString, nil
} }
// PropagateVirtualSelectedParentBlueScoreChangedNotifications instructs the listener to send // PropagateVirtualSelectedParentBlueScoreChangedNotifications instructs the listener to send

View File

@ -32,22 +32,6 @@ func ConvertUTXOOutpointEntryPairsToUTXOsByAddressesEntries(address string, pair
return utxosByAddressesEntries return utxosByAddressesEntries
} }
// convertUTXOOutpointsToUTXOsByAddressesEntries converts
// UTXOOutpoints to a slice of UTXOsByAddressesEntry
func convertUTXOOutpointsToUTXOsByAddressesEntries(address string, outpoints utxoindex.UTXOOutpoints) []*appmessage.UTXOsByAddressesEntry {
utxosByAddressesEntries := make([]*appmessage.UTXOsByAddressesEntry, 0, len(outpoints))
for outpoint := range outpoints {
utxosByAddressesEntries = append(utxosByAddressesEntries, &appmessage.UTXOsByAddressesEntry{
Address: address,
Outpoint: &appmessage.RPCOutpoint{
TransactionID: outpoint.TransactionID.String(),
Index: outpoint.Index,
},
})
}
return utxosByAddressesEntries
}
// ConvertAddressStringsToUTXOsChangedNotificationAddresses converts address strings // ConvertAddressStringsToUTXOsChangedNotificationAddresses converts address strings
// to UTXOsChangedNotificationAddresses // to UTXOsChangedNotificationAddresses
func (ctx *Context) ConvertAddressStringsToUTXOsChangedNotificationAddresses( func (ctx *Context) ConvertAddressStringsToUTXOsChangedNotificationAddresses(
@ -63,7 +47,7 @@ func (ctx *Context) ConvertAddressStringsToUTXOsChangedNotificationAddresses(
if err != nil { if err != nil {
return nil, errors.Errorf("Could not create a scriptPublicKey for address '%s': %s", addressString, err) return nil, errors.Errorf("Could not create a scriptPublicKey for address '%s': %s", addressString, err)
} }
scriptPublicKeyString := utxoindex.ConvertScriptPublicKeyToString(scriptPublicKey) scriptPublicKeyString := utxoindex.ScriptPublicKeyString(scriptPublicKey.String())
addresses[i] = &UTXOsChangedNotificationAddress{ addresses[i] = &UTXOsChangedNotificationAddress{
Address: addressString, Address: addressString,
ScriptPublicKeyString: scriptPublicKeyString, ScriptPublicKeyString: scriptPublicKeyString,

View File

@ -81,10 +81,6 @@ func (ctx *Context) PopulateBlockWithVerboseData(block *appmessage.RPCBlock, dom
block.VerboseData.SelectedParentHash = blockInfo.SelectedParent.String() block.VerboseData.SelectedParentHash = blockInfo.SelectedParent.String()
} }
if blockInfo.BlockStatus == externalapi.StatusHeaderOnly {
return nil
}
// Get the block if we didn't receive it previously // Get the block if we didn't receive it previously
if domainBlock == nil { if domainBlock == nil {
domainBlock, err = ctx.Domain.Consensus().GetBlockEvenIfHeaderOnly(blockHash) 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)) transactionIDs := make([]string, len(domainBlock.Transactions))
for i, transaction := range domainBlock.Transactions { for i, transaction := range domainBlock.Transactions {
transactionIDs[i] = consensushashing.TransactionID(transaction).String() transactionIDs[i] = consensushashing.TransactionID(transaction).String()
@ -122,6 +122,7 @@ func (ctx *Context) PopulateTransactionWithVerboseData(
} }
ctx.Domain.Consensus().PopulateMass(domainTransaction) ctx.Domain.Consensus().PopulateMass(domainTransaction)
transaction.VerboseData = &appmessage.RPCTransactionVerboseData{ transaction.VerboseData = &appmessage.RPCTransactionVerboseData{
TransactionID: consensushashing.TransactionID(domainTransaction).String(), TransactionID: consensushashing.TransactionID(domainTransaction).String(),
Hash: consensushashing.TransactionHash(domainTransaction).String(), Hash: consensushashing.TransactionHash(domainTransaction).String(),

View File

@ -9,6 +9,14 @@ import (
// HandleAddPeer handles the respectively named RPC command // HandleAddPeer handles the respectively named RPC command
func HandleAddPeer(context *rpccontext.Context, _ *router.Router, request appmessage.Message) (appmessage.Message, error) { func HandleAddPeer(context *rpccontext.Context, _ *router.Router, request appmessage.Message) (appmessage.Message, error) {
if context.Config.SafeRPC {
log.Warn("AddPeer RPC command called while node in safe RPC mode -- ignoring.")
response := appmessage.NewAddPeerResponseMessage()
response.Error =
appmessage.RPCErrorf("AddPeer RPC command called while node in safe RPC mode")
return response, nil
}
AddPeerRequest := request.(*appmessage.AddPeerRequestMessage) AddPeerRequest := request.(*appmessage.AddPeerRequestMessage)
address, err := network.NormalizeAddress(AddPeerRequest.Address, context.Config.ActiveNetParams.DefaultPort) address, err := network.NormalizeAddress(AddPeerRequest.Address, context.Config.ActiveNetParams.DefaultPort)
if err != nil { if err != nil {

View File

@ -9,6 +9,14 @@ import (
// HandleBan handles the respectively named RPC command // HandleBan handles the respectively named RPC command
func HandleBan(context *rpccontext.Context, _ *router.Router, request appmessage.Message) (appmessage.Message, error) { func HandleBan(context *rpccontext.Context, _ *router.Router, request appmessage.Message) (appmessage.Message, error) {
if context.Config.SafeRPC {
log.Warn("Ban RPC command called while node in safe RPC mode -- ignoring.")
response := appmessage.NewBanResponseMessage()
response.Error =
appmessage.RPCErrorf("Ban RPC command called while node in safe RPC mode")
return response, nil
}
banRequest := request.(*appmessage.BanRequestMessage) banRequest := request.(*appmessage.BanRequestMessage)
ip := net.ParseIP(banRequest.IP) ip := net.ParseIP(banRequest.IP)
if ip == nil { if ip == nil {

View File

@ -27,6 +27,27 @@ func HandleEstimateNetworkHashesPerSecond(
} }
} }
if context.Config.SafeRPC {
const windowSizeLimit = 10000
if windowSize > windowSizeLimit {
response := &appmessage.EstimateNetworkHashesPerSecondResponseMessage{}
response.Error =
appmessage.RPCErrorf(
"Requested window size %d is larger than max allowed in RPC safe mode (%d)",
windowSize, windowSizeLimit)
return response, nil
}
}
if uint64(windowSize) > context.Config.ActiveNetParams.PruningDepth() {
response := &appmessage.EstimateNetworkHashesPerSecondResponseMessage{}
response.Error =
appmessage.RPCErrorf(
"Requested window size %d is larger than pruning point depth %d",
windowSize, context.Config.ActiveNetParams.PruningDepth())
return response, nil
}
networkHashesPerSecond, err := context.Domain.Consensus().EstimateNetworkHashesPerSecond(startHash, windowSize) networkHashesPerSecond, err := context.Domain.Consensus().EstimateNetworkHashesPerSecond(startHash, windowSize)
if err != nil { if err != nil {
response := &appmessage.EstimateNetworkHashesPerSecondResponseMessage{} response := &appmessage.EstimateNetworkHashesPerSecondResponseMessage{}

View File

@ -22,7 +22,7 @@ func HandleGetBalanceByAddress(context *rpccontext.Context, _ *router.Router, re
balance, err := getBalanceByAddress(context, getBalanceByAddressRequest.Address) balance, err := getBalanceByAddress(context, getBalanceByAddressRequest.Address)
if err != nil { if err != nil {
rpcError := &appmessage.RPCError{} rpcError := &appmessage.RPCError{}
if !errors.As(err, rpcError) { if !errors.As(err, &rpcError) {
return nil, err return nil, err
} }
errorMessage := &appmessage.GetUTXOsByAddressesResponseMessage{} errorMessage := &appmessage.GetUTXOsByAddressesResponseMessage{}

View File

@ -23,7 +23,7 @@ func HandleGetBalancesByAddresses(context *rpccontext.Context, _ *router.Router,
if err != nil { if err != nil {
rpcError := &appmessage.RPCError{} rpcError := &appmessage.RPCError{}
if !errors.As(err, rpcError) { if !errors.As(err, &rpcError) {
return nil, err return nil, err
} }
errorMessage := &appmessage.GetUTXOsByAddressesResponseMessage{} errorMessage := &appmessage.GetUTXOsByAddressesResponseMessage{}

View File

@ -28,7 +28,8 @@ func HandleGetBlockTemplate(context *rpccontext.Context, _ *router.Router, reque
} }
coinbaseData := &externalapi.DomainCoinbaseData{ScriptPublicKey: scriptPublicKey, ExtraData: []byte(version.Version() + "/" + getBlockTemplateRequest.ExtraData)} coinbaseData := &externalapi.DomainCoinbaseData{ScriptPublicKey: scriptPublicKey, ExtraData: []byte(version.Version() + "/" + getBlockTemplateRequest.ExtraData)}
templateBlock, err := context.Domain.MiningManager().GetBlockTemplate(coinbaseData)
templateBlock, isNearlySynced, err := context.Domain.MiningManager().GetBlockTemplate(coinbaseData)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -41,10 +42,5 @@ func HandleGetBlockTemplate(context *rpccontext.Context, _ *router.Router, reque
rpcBlock := appmessage.DomainBlockToRPCBlock(templateBlock) rpcBlock := appmessage.DomainBlockToRPCBlock(templateBlock)
isSynced, err := context.ProtocolManager.ShouldMine() return appmessage.NewGetBlockTemplateResponseMessage(rpcBlock, context.ProtocolManager.Context().HasPeers() && isNearlySynced), nil
if err != nil {
return nil, err
}
return appmessage.NewGetBlockTemplateResponseMessage(rpcBlock, isSynced), nil
} }

View File

@ -37,7 +37,7 @@ func HandleGetBlocks(context *rpccontext.Context, _ *router.Router, request appm
return nil, err return nil, err
} }
if !blockInfo.Exists { if !blockInfo.HasHeader() {
return &appmessage.GetBlocksResponseMessage{ return &appmessage.GetBlocksResponseMessage{
Error: appmessage.RPCErrorf("Could not find lowHash %s", getBlocksRequest.LowHash), Error: appmessage.RPCErrorf("Could not find lowHash %s", getBlocksRequest.LowHash),
}, nil }, nil

View File

@ -23,6 +23,10 @@ type fakeDomain struct {
testapi.TestConsensus testapi.TestConsensus
} }
func (d fakeDomain) ConsensusEventsChannel() chan externalapi.ConsensusEvent {
panic("implement me")
}
func (d fakeDomain) DeleteStagingConsensus() error { func (d fakeDomain) DeleteStagingConsensus() error {
panic("implement me") panic("implement me")
} }

View File

@ -0,0 +1,29 @@
package rpchandlers
import (
"github.com/kaspanet/kaspad/app/appmessage"
"github.com/kaspanet/kaspad/app/rpc/rpccontext"
"github.com/kaspanet/kaspad/domain/consensus/utils/constants"
"github.com/kaspanet/kaspad/infrastructure/network/netadapter/router"
)
// HandleGetCoinSupply handles the respectively named RPC command
func HandleGetCoinSupply(context *rpccontext.Context, _ *router.Router, _ appmessage.Message) (appmessage.Message, error) {
if !context.Config.UTXOIndex {
errorMessage := &appmessage.GetCoinSupplyResponseMessage{}
errorMessage.Error = appmessage.RPCErrorf("Method unavailable when kaspad is run without --utxoindex")
return errorMessage, nil
}
circulatingSompiSupply, err := context.UTXOIndex.GetCirculatingSompiSupply()
if err != nil {
return nil, err
}
response := appmessage.NewGetCoinSupplyResponseMessage(
constants.MaxSompi,
circulatingSompiSupply,
)
return response, nil
}

View File

@ -9,10 +9,17 @@ import (
// HandleGetInfo handles the respectively named RPC command // HandleGetInfo handles the respectively named RPC command
func HandleGetInfo(context *rpccontext.Context, _ *router.Router, _ appmessage.Message) (appmessage.Message, error) { func HandleGetInfo(context *rpccontext.Context, _ *router.Router, _ appmessage.Message) (appmessage.Message, error) {
isNearlySynced, err := context.Domain.Consensus().IsNearlySynced()
if err != nil {
return nil, err
}
response := appmessage.NewGetInfoResponseMessage( response := appmessage.NewGetInfoResponseMessage(
context.NetAdapter.ID().String(), context.NetAdapter.ID().String(),
uint64(context.Domain.MiningManager().TransactionCount()), uint64(context.Domain.MiningManager().TransactionCount(true, false)),
version.Version(), version.Version(),
context.Config.UTXOIndex,
context.ProtocolManager.Context().HasPeers() && isNearlySynced,
) )
return response, nil return response, nil

View File

@ -7,19 +7,40 @@ import (
) )
// HandleGetMempoolEntries handles the respectively named RPC command // HandleGetMempoolEntries handles the respectively named RPC command
func HandleGetMempoolEntries(context *rpccontext.Context, _ *router.Router, _ appmessage.Message) (appmessage.Message, error) { func HandleGetMempoolEntries(context *rpccontext.Context, _ *router.Router, request appmessage.Message) (appmessage.Message, error) {
transactions := context.Domain.MiningManager().AllTransactions() getMempoolEntriesRequest := request.(*appmessage.GetMempoolEntriesRequestMessage)
entries := make([]*appmessage.MempoolEntry, 0, len(transactions))
for _, transaction := range transactions { entries := make([]*appmessage.MempoolEntry, 0)
rpcTransaction := appmessage.DomainTransactionToRPCTransaction(transaction)
err := context.PopulateTransactionWithVerboseData(rpcTransaction, nil) transactionPoolTransactions, orphanPoolTransactions := context.Domain.MiningManager().AllTransactions(!getMempoolEntriesRequest.FilterTransactionPool, getMempoolEntriesRequest.IncludeOrphanPool)
if err != nil {
return nil, err if !getMempoolEntriesRequest.FilterTransactionPool {
for _, transaction := range transactionPoolTransactions {
rpcTransaction := appmessage.DomainTransactionToRPCTransaction(transaction)
err := context.PopulateTransactionWithVerboseData(rpcTransaction, nil)
if err != nil {
return nil, err
}
entries = append(entries, &appmessage.MempoolEntry{
Fee: transaction.Fee,
Transaction: rpcTransaction,
IsOrphan: false,
})
}
}
if getMempoolEntriesRequest.IncludeOrphanPool {
for _, transaction := range orphanPoolTransactions {
rpcTransaction := appmessage.DomainTransactionToRPCTransaction(transaction)
err := context.PopulateTransactionWithVerboseData(rpcTransaction, nil)
if err != nil {
return nil, err
}
entries = append(entries, &appmessage.MempoolEntry{
Fee: transaction.Fee,
Transaction: rpcTransaction,
IsOrphan: true,
})
} }
entries = append(entries, &appmessage.MempoolEntry{
Fee: transaction.Fee,
Transaction: rpcTransaction,
})
} }
return appmessage.NewGetMempoolEntriesResponseMessage(entries), nil return appmessage.NewGetMempoolEntriesResponseMessage(entries), nil

View File

@ -0,0 +1,122 @@
package rpchandlers
import (
"github.com/kaspanet/kaspad/app/appmessage"
"github.com/kaspanet/kaspad/app/rpc/rpccontext"
"github.com/kaspanet/kaspad/domain/consensus/utils/txscript"
"github.com/kaspanet/kaspad/infrastructure/network/netadapter/router"
"github.com/kaspanet/kaspad/util"
)
// HandleGetMempoolEntriesByAddresses handles the respectively named RPC command
func HandleGetMempoolEntriesByAddresses(context *rpccontext.Context, _ *router.Router, request appmessage.Message) (appmessage.Message, error) {
getMempoolEntriesByAddressesRequest := request.(*appmessage.GetMempoolEntriesByAddressesRequestMessage)
mempoolEntriesByAddresses := make([]*appmessage.MempoolEntryByAddress, 0)
sendingInTransactionPool, receivingInTransactionPool, sendingInOrphanPool, receivingInOrphanPool, err := context.Domain.MiningManager().GetTransactionsByAddresses(!getMempoolEntriesByAddressesRequest.FilterTransactionPool, getMempoolEntriesByAddressesRequest.IncludeOrphanPool)
if err != nil {
return nil, err
}
for _, addressString := range getMempoolEntriesByAddressesRequest.Addresses {
address, err := util.DecodeAddress(addressString, context.Config.NetParams().Prefix)
if err != nil {
errorMessage := &appmessage.GetMempoolEntriesByAddressesResponseMessage{}
errorMessage.Error = appmessage.RPCErrorf("Could not decode address '%s': %s", addressString, err)
return errorMessage, nil
}
sending := make([]*appmessage.MempoolEntry, 0)
receiving := make([]*appmessage.MempoolEntry, 0)
scriptPublicKey, err := txscript.PayToAddrScript(address)
if err != nil {
errorMessage := &appmessage.GetMempoolEntriesByAddressesResponseMessage{}
errorMessage.Error = appmessage.RPCErrorf("Could not extract scriptPublicKey from address '%s': %s", addressString, err)
return errorMessage, nil
}
if !getMempoolEntriesByAddressesRequest.FilterTransactionPool {
if transaction, found := sendingInTransactionPool[scriptPublicKey.String()]; found {
rpcTransaction := appmessage.DomainTransactionToRPCTransaction(transaction)
err := context.PopulateTransactionWithVerboseData(rpcTransaction, nil)
if err != nil {
return nil, err
}
sending = append(sending, &appmessage.MempoolEntry{
Fee: transaction.Fee,
Transaction: rpcTransaction,
IsOrphan: false,
},
)
}
if transaction, found := receivingInTransactionPool[scriptPublicKey.String()]; found {
rpcTransaction := appmessage.DomainTransactionToRPCTransaction(transaction)
err := context.PopulateTransactionWithVerboseData(rpcTransaction, nil)
if err != nil {
return nil, err
}
receiving = append(receiving, &appmessage.MempoolEntry{
Fee: transaction.Fee,
Transaction: rpcTransaction,
IsOrphan: false,
},
)
}
}
if getMempoolEntriesByAddressesRequest.IncludeOrphanPool {
if transaction, found := sendingInOrphanPool[scriptPublicKey.String()]; found {
rpcTransaction := appmessage.DomainTransactionToRPCTransaction(transaction)
err := context.PopulateTransactionWithVerboseData(rpcTransaction, nil)
if err != nil {
return nil, err
}
sending = append(sending, &appmessage.MempoolEntry{
Fee: transaction.Fee,
Transaction: rpcTransaction,
IsOrphan: true,
},
)
}
if transaction, found := receivingInOrphanPool[scriptPublicKey.String()]; found {
rpcTransaction := appmessage.DomainTransactionToRPCTransaction(transaction)
err := context.PopulateTransactionWithVerboseData(rpcTransaction, nil)
if err != nil {
return nil, err
}
receiving = append(receiving, &appmessage.MempoolEntry{
Fee: transaction.Fee,
Transaction: rpcTransaction,
IsOrphan: true,
},
)
}
}
if len(sending) > 0 || len(receiving) > 0 {
mempoolEntriesByAddresses = append(
mempoolEntriesByAddresses,
&appmessage.MempoolEntryByAddress{
Address: address.String(),
Sending: sending,
Receiving: receiving,
},
)
}
}
return appmessage.NewGetMempoolEntriesByAddressesResponseMessage(mempoolEntriesByAddresses), nil
}

View File

@ -3,12 +3,18 @@ package rpchandlers
import ( import (
"github.com/kaspanet/kaspad/app/appmessage" "github.com/kaspanet/kaspad/app/appmessage"
"github.com/kaspanet/kaspad/app/rpc/rpccontext" "github.com/kaspanet/kaspad/app/rpc/rpccontext"
"github.com/kaspanet/kaspad/domain/consensus/model/externalapi"
"github.com/kaspanet/kaspad/domain/consensus/utils/transactionid" "github.com/kaspanet/kaspad/domain/consensus/utils/transactionid"
"github.com/kaspanet/kaspad/infrastructure/network/netadapter/router" "github.com/kaspanet/kaspad/infrastructure/network/netadapter/router"
) )
// HandleGetMempoolEntry handles the respectively named RPC command // HandleGetMempoolEntry handles the respectively named RPC command
func HandleGetMempoolEntry(context *rpccontext.Context, _ *router.Router, request appmessage.Message) (appmessage.Message, error) { func HandleGetMempoolEntry(context *rpccontext.Context, _ *router.Router, request appmessage.Message) (appmessage.Message, error) {
transaction := &externalapi.DomainTransaction{}
var found bool
var isOrphan bool
getMempoolEntryRequest := request.(*appmessage.GetMempoolEntryRequestMessage) getMempoolEntryRequest := request.(*appmessage.GetMempoolEntryRequestMessage)
transactionID, err := transactionid.FromString(getMempoolEntryRequest.TxID) transactionID, err := transactionid.FromString(getMempoolEntryRequest.TxID)
@ -18,17 +24,18 @@ func HandleGetMempoolEntry(context *rpccontext.Context, _ *router.Router, reques
return errorMessage, nil return errorMessage, nil
} }
transaction, ok := context.Domain.MiningManager().GetTransaction(transactionID) mempoolTransaction, isOrphan, found := context.Domain.MiningManager().GetTransaction(transactionID, !getMempoolEntryRequest.FilterTransactionPool, getMempoolEntryRequest.IncludeOrphanPool)
if !ok {
if !found {
errorMessage := &appmessage.GetMempoolEntryResponseMessage{} errorMessage := &appmessage.GetMempoolEntryResponseMessage{}
errorMessage.Error = appmessage.RPCErrorf("Transaction %s was not found", transactionID) errorMessage.Error = appmessage.RPCErrorf("Transaction %s was not found", transactionID)
return errorMessage, nil return errorMessage, nil
} }
rpcTransaction := appmessage.DomainTransactionToRPCTransaction(transaction)
rpcTransaction := appmessage.DomainTransactionToRPCTransaction(mempoolTransaction)
err = context.PopulateTransactionWithVerboseData(rpcTransaction, nil) err = context.PopulateTransactionWithVerboseData(rpcTransaction, nil)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return appmessage.NewGetMempoolEntryResponseMessage(transaction.Fee, rpcTransaction, isOrphan), nil
return appmessage.NewGetMempoolEntryResponseMessage(transaction.Fee, rpcTransaction), nil
} }

View File

@ -26,12 +26,14 @@ func HandleGetVirtualSelectedParentChainFromBlock(context *rpccontext.Context, _
return response, nil return response, nil
} }
chainChangedNotification, err := context.ConvertVirtualSelectedParentChainChangesToChainChangedNotificationMessage(virtualSelectedParentChain) chainChangedNotification, err := context.ConvertVirtualSelectedParentChainChangesToChainChangedNotificationMessage(
virtualSelectedParentChain, getVirtualSelectedParentChainFromBlockRequest.IncludeAcceptedTransactionIDs)
if err != nil { if err != nil {
return nil, err return nil, err
} }
response := appmessage.NewGetVirtualSelectedParentChainFromBlockResponseMessage( response := appmessage.NewGetVirtualSelectedParentChainFromBlockResponseMessage(
chainChangedNotification.RemovedChainBlockHashes, chainChangedNotification.AddedChainBlockHashes) chainChangedNotification.RemovedChainBlockHashes, chainChangedNotification.AddedChainBlockHashes,
chainChangedNotification.AcceptedTransactionIDs)
return response, nil return response, nil
} }

View File

@ -26,7 +26,7 @@ func HandleNotifyUTXOsChanged(context *rpccontext.Context, router *router.Router
if err != nil { if err != nil {
return nil, err return nil, err
} }
listener.PropagateUTXOsChangedNotifications(addresses) context.NotificationManager.PropagateUTXOsChangedNotifications(listener, addresses)
response := appmessage.NewNotifyUTXOsChangedResponseMessage() response := appmessage.NewNotifyUTXOsChangedResponseMessage()
return response, nil return response, nil

View File

@ -7,12 +7,17 @@ import (
) )
// HandleNotifyVirtualSelectedParentChainChanged handles the respectively named RPC command // HandleNotifyVirtualSelectedParentChainChanged handles the respectively named RPC command
func HandleNotifyVirtualSelectedParentChainChanged(context *rpccontext.Context, router *router.Router, _ appmessage.Message) (appmessage.Message, error) { func HandleNotifyVirtualSelectedParentChainChanged(context *rpccontext.Context, router *router.Router,
request appmessage.Message) (appmessage.Message, error) {
notifyVirtualSelectedParentChainChangedRequest := request.(*appmessage.NotifyVirtualSelectedParentChainChangedRequestMessage)
listener, err := context.NotificationManager.Listener(router) listener, err := context.NotificationManager.Listener(router)
if err != nil { if err != nil {
return nil, err return nil, err
} }
listener.PropagateVirtualSelectedParentChainChangedNotifications() listener.PropagateVirtualSelectedParentChainChangedNotifications(
notifyVirtualSelectedParentChainChangedRequest.IncludeAcceptedTransactionIDs)
response := appmessage.NewNotifyVirtualSelectedParentChainChangedResponseMessage() response := appmessage.NewNotifyVirtualSelectedParentChainChangedResponseMessage()
return response, nil return response, nil

View File

@ -8,6 +8,14 @@ import (
// HandleResolveFinalityConflict handles the respectively named RPC command // HandleResolveFinalityConflict handles the respectively named RPC command
func HandleResolveFinalityConflict(context *rpccontext.Context, _ *router.Router, request appmessage.Message) (appmessage.Message, error) { func HandleResolveFinalityConflict(context *rpccontext.Context, _ *router.Router, request appmessage.Message) (appmessage.Message, error) {
if context.Config.SafeRPC {
log.Warn("ResolveFinalityConflict RPC command called while node in safe RPC mode -- ignoring.")
response := &appmessage.ResolveFinalityConflictResponseMessage{}
response.Error =
appmessage.RPCErrorf("ResolveFinalityConflict RPC command called while node in safe RPC mode")
return response, nil
}
response := &appmessage.ResolveFinalityConflictResponseMessage{} response := &appmessage.ResolveFinalityConflictResponseMessage{}
response.Error = appmessage.RPCErrorf("not implemented") response.Error = appmessage.RPCErrorf("not implemented")
return response, nil return response, nil

View File

@ -12,6 +12,14 @@ const pauseBeforeShutDown = time.Second
// HandleShutDown handles the respectively named RPC command // HandleShutDown handles the respectively named RPC command
func HandleShutDown(context *rpccontext.Context, _ *router.Router, _ appmessage.Message) (appmessage.Message, error) { func HandleShutDown(context *rpccontext.Context, _ *router.Router, _ appmessage.Message) (appmessage.Message, error) {
if context.Config.SafeRPC {
log.Warn("ShutDown RPC command called while node in safe RPC mode -- ignoring.")
response := appmessage.NewShutDownResponseMessage()
response.Error =
appmessage.RPCErrorf("ShutDown RPC command called while node in safe RPC mode")
return response, nil
}
log.Warn("ShutDown RPC called.") log.Warn("ShutDown RPC called.")
// Wait a second before shutting down, to allow time to return the response to the caller // Wait a second before shutting down, to allow time to return the response to the caller

View File

@ -26,7 +26,7 @@ func HandleStopNotifyingUTXOsChanged(context *rpccontext.Context, router *router
if err != nil { if err != nil {
return nil, err return nil, err
} }
listener.StopPropagatingUTXOsChangedNotifications(addresses) context.NotificationManager.StopPropagatingUTXOsChangedNotifications(listener, addresses)
response := appmessage.NewStopNotifyingUTXOsChangedResponseMessage() response := appmessage.NewStopNotifyingUTXOsChangedResponseMessage()
return response, nil return response, nil

View File

@ -1,6 +1,7 @@
package rpchandlers package rpchandlers
import ( import (
"encoding/json"
"github.com/kaspanet/kaspad/app/appmessage" "github.com/kaspanet/kaspad/app/appmessage"
"github.com/kaspanet/kaspad/app/protocol/protocolerrors" "github.com/kaspanet/kaspad/app/protocol/protocolerrors"
"github.com/kaspanet/kaspad/app/rpc/rpccontext" "github.com/kaspanet/kaspad/app/rpc/rpccontext"
@ -14,9 +15,14 @@ import (
func HandleSubmitBlock(context *rpccontext.Context, _ *router.Router, request appmessage.Message) (appmessage.Message, error) { func HandleSubmitBlock(context *rpccontext.Context, _ *router.Router, request appmessage.Message) (appmessage.Message, error) {
submitBlockRequest := request.(*appmessage.SubmitBlockRequestMessage) submitBlockRequest := request.(*appmessage.SubmitBlockRequestMessage)
isSynced, err := context.ProtocolManager.ShouldMine() var err error
if err != nil { isSynced := false
return nil, err // The node is considered synced if it has peers and consensus state is nearly synced
if context.ProtocolManager.Context().HasPeers() {
isSynced, err = context.ProtocolManager.Context().IsNearlySynced()
if err != nil {
return nil, err
}
} }
if !context.Config.AllowSubmitBlockWhenNotSynced && !isSynced { if !context.Config.AllowSubmitBlockWhenNotSynced && !isSynced {
@ -58,6 +64,12 @@ func HandleSubmitBlock(context *rpccontext.Context, _ *router.Router, request ap
return nil, err return nil, err
} }
jsonBytes, _ := json.MarshalIndent(submitBlockRequest.Block.Header, "", " ")
if jsonBytes != nil {
log.Warnf("The RPC submitted block triggered a rule/protocol error (%s), printing "+
"the full header for debug purposes: \n%s", err, string(jsonBytes))
}
return &appmessage.SubmitBlockResponseMessage{ return &appmessage.SubmitBlockResponseMessage{
Error: appmessage.RPCErrorf("Block rejected. Reason: %s", err), Error: appmessage.RPCErrorf("Block rejected. Reason: %s", err),
RejectReason: appmessage.RejectReasonBlockInvalid, RejectReason: appmessage.RejectReasonBlockInvalid,

View File

@ -28,7 +28,8 @@ func HandleSubmitTransaction(context *rpccontext.Context, _ *router.Router, requ
} }
log.Debugf("Rejected transaction %s: %s", transactionID, err) 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) errorMessage.Error = appmessage.RPCErrorf("Rejected transaction %s: %s", transactionID, err)
return errorMessage, nil return errorMessage, nil
} }

View File

@ -9,6 +9,14 @@ import (
// HandleUnban handles the respectively named RPC command // HandleUnban handles the respectively named RPC command
func HandleUnban(context *rpccontext.Context, _ *router.Router, request appmessage.Message) (appmessage.Message, error) { func HandleUnban(context *rpccontext.Context, _ *router.Router, request appmessage.Message) (appmessage.Message, error) {
if context.Config.SafeRPC {
log.Warn("Unban RPC command called while node in safe RPC mode -- ignoring.")
response := appmessage.NewUnbanResponseMessage()
response.Error =
appmessage.RPCErrorf("Unban RPC command called while node in safe RPC mode")
return response, nil
}
unbanRequest := request.(*appmessage.UnbanRequestMessage) unbanRequest := request.(*appmessage.UnbanRequestMessage)
ip := net.ParseIP(unbanRequest.IP) ip := net.ParseIP(unbanRequest.IP)
if ip == nil { if ip == nil {

View File

@ -5,13 +5,10 @@ FLAGS=$@
go version go version
go get $FLAGS -t -d ./... 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 go install $FLAGS honnef.co/go/tools/cmd/staticcheck@latest
test -z "$(go fmt ./...)" 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 ./... 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 . go build $FLAGS -o kaspad .

View File

@ -1,3 +1,219 @@
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
===========================
* Create directory before locking lock file (#2160)
Kaspad v0.12.8 - 2022-10-23
===========================
* Remove hard fork activation rules (#2152)
* Add lock file to kaspawallet (#2154)
* Add a new testnet DNS seeder (#2156)
* Use utxo diff algo for pruning point move and use acceptance data method only as a fall-back (#2157)
* Make more checks if status is invalid even if the block exists (#2158)
Kaspad v0.12.7 - 2022-09-21
===========================
* Security Fix + Hard fork - Full details can be seen here: https://medium.com/@michaelsuttonil/kaspa-security-patch-and-hard-fork-september-2022-12da617b0094
Kaspad v0.12.6 - 2022-09-09
===========================
* Remove tests from docker files (#2133)
Wallet new features:
* Optionally show serialized transactions on send (#2135)
Bug fixes:
* Update virtual on IBD if nearly synced (#2134)
Kaspad v0.12.5 - 2022-08-28
===========================
* Add tests for hash writers (#2120)
* Replace daglabs's dnsseeder with Wolfie's (#2119)
* Change testnet dnsseeder (#2126)
* Add RPC timeout parameter to wallet daemon (#2104)
Wallet new features:
* Add UseExistingChangeAddress option to the wallet (#2127)
Bug fixes:
* Call update pruning point if required on resolve virtual or startup (#2129)
* Add missing locks to notification listener modifications (#2124)
* Calculate pruning point utxo set from acceptance data (#2123)
* Fix RPC client memory/goroutine leak (#2122)
* Fix a subtle lock sync issue in consensus insert block (#2121)
* Mempool: Retrieve stable state of the mempool. Optimze get mempool entries by addresses (#2111)
* Kaspawallet.send(): Make separate context for Broadcast, to prolong timeout (#2131)
Kaspad v0.12.4 - 2022-07-17
===========================
* Crucial fix for the UTXO difference mechanism (#2114)
* Implement multi-layer auto-compound (#2115)
Kaspad v0.12.3 - 2022-06-29
===========================
* Fixes a few bugs which can lead to node crashes or out-of-memory errors
Kaspad v0.12.2 - 2022-06-17
===========================
* Clarify wallet message concerning a wallet daemon sync state (#2045)
* Change the way the miner executable reports execution errors (closes issue #1677) (#2048)
* Fix kaspawallet help messages, clarify sweep command help string (#2067)
* Wallet parse/send/create commands improvement (#2024)
* Use chunks for `GetBlocksAcceptanceData` calls in order to avoid blocking consensus for too long (#2075)
* Unite multiple `GetBlockAcceptanceData` consensus calls to one (#2074)
* Update many-small-chains-and-one-big-chain DAG to not fail merge depth limit (#2072)
RPC API Changes:
* RPC: include orphans into mempool entries (#2046)
* RPC & UtxoIndex: keep track of, query and test circulating supply. (#2070)
Bug Fixes:
* Fix RPC connections counting (#2026)
* Fix UTXO diff child error (#2084)
* Fix `not in selected chain` crash (#2082)
Kaspad v0.12.1 - 2022-05-31
===========================
* Fix utxoindex synchronization bug which resulted in kaspawallet orphan tx errors (#2052, #2056, #2059)
* Add a channel mechanism for consensus events to be processed in the order they were produced (#2052, #2056, #2059)
* Block template cache improvement (#2023)
* Improved staging shard performance (#2034)
* Add finality check to ResolveVirtual (#2041)
* Update Dockerfile for go 1.18 (#2038)
* Remove HF1 activation code (#2042)
Kaspa wallet:
* Various kaspawallet text fixes and log additions (#2032, #2047, #2062)
* Wallet address synchronization improvement (#2025)
* Add support for `from` address in `kaspawallet send` (#1964)
* Make kaspawallet ignore outputs that exist in the mempool (#2053)
* Wrap the entire wallet send operation with a lock (#2063)
RPC API:
* Add "GetMempoolEntriesByAddresses" to kaspad RPC (#2022)
* Make sure RPCErrors are returned and do not crash the system (#2039)
* Add AcceptedTransactionIDs to ChainChanged notification and VirtualSelectedParentChain RPC (#2036, for exchanges to track tx confirmations)
* Allow blank address in NotifyUTXOsChanged to get all updates (#2027)
* Include isSynced and isUtxoIndexed in GetInfoResponse (#2068)
Kaspad v0.12.0 - 2022-04-14
===========================
Breaking changes:
Hard-fork at DAA score 14687583 (estimated to be on 28/04 16:38 UTC) which includes:
* Using separate depth than finality depth for merge set calculations (#2013)
* Not counting the header size as part of the block mass (#2013)
* Increasing block version to 1 (#2013)
* Removing the limit on amount of KAS that can be sent in one transaction (#2013)
Bug fixes:
* Making a workaround for the UTXO diff child bug (#2020)
* Use cosigner index 0 for read only wallets (#2014)
Non-breaking changes:
* Adding a "sweep" command to `kaspawallet` (#2018)
* Use `blue work` heuristic to skip irrelevant relay blocks
* Kaspawallet daemon: Add Send and Sign commands (#2016)
Kaspad v0.11.17 - 2022-04-06
===========================
* Decrement estimatedHeaderUpperBound from mempool's MaxBlockMass (#2009)
Kaspad v0.11.16 - 2022-04-05
===========================
* Don't skip wallet address with different cosigner index (#2007)
Kaspad v0.11.15 - 2022-04-05
===========================
* Add support for auto-compound in `kaspawallet send` (#1951)
* Unite reachability stores (#1963, #1993, #2001)
* Add names to nameless routes (#1986)
* Optimize the miner-kaspad flow and latency (#1988)
* Upgrade to go 1.18 (#1992)
* Add package name to kaspawalletd .proto file (#1991)
* Block template cache (#1994)
* Add extra data to GetBlockTemplate request (#1995, #1997)
* New definition for "out of sync" (#1996)
* Remove v4 p2p version (#1998)
* Remove increase pagefile from deploy.yaml (#2000)
* Cache the pruning point anticone (#2002)
* Add DB compaction after the deletion of a DB prefix (#2003)
* Fixed a bug in staging of pruning point by index (#2005)
* Clean up debug log level by moving many frequent logs to trace level (#2004)
Kaspad v0.11.14 - 2022-03-20 Kaspad v0.11.14 - 2022-03-20
=========================== ===========================
* Fix a bug in the new p2p v5 IBD chain negotiation (#1981) * Fix a bug in the new p2p v5 IBD chain negotiation (#1981)

View File

@ -4,7 +4,7 @@ kaspactl is an RPC client for kaspad
## Requirements ## Requirements
Go 1.18 or later. Go 1.23 or later.
## Installation ## Installation

View File

@ -31,10 +31,13 @@ var commandTypes = []reflect.Type{
reflect.TypeOf(protowire.KaspadMessage_GetMempoolEntryRequest{}), reflect.TypeOf(protowire.KaspadMessage_GetMempoolEntryRequest{}),
reflect.TypeOf(protowire.KaspadMessage_GetMempoolEntriesRequest{}), reflect.TypeOf(protowire.KaspadMessage_GetMempoolEntriesRequest{}),
reflect.TypeOf(protowire.KaspadMessage_GetMempoolEntriesByAddressesRequest{}),
reflect.TypeOf(protowire.KaspadMessage_SubmitTransactionRequest{}), reflect.TypeOf(protowire.KaspadMessage_SubmitTransactionRequest{}),
reflect.TypeOf(protowire.KaspadMessage_GetUtxosByAddressesRequest{}), reflect.TypeOf(protowire.KaspadMessage_GetUtxosByAddressesRequest{}),
reflect.TypeOf(protowire.KaspadMessage_GetBalanceByAddressRequest{}), reflect.TypeOf(protowire.KaspadMessage_GetBalanceByAddressRequest{}),
reflect.TypeOf(protowire.KaspadMessage_GetCoinSupplyRequest{}),
reflect.TypeOf(protowire.KaspadMessage_BanRequest{}), reflect.TypeOf(protowire.KaspadMessage_BanRequest{}),
reflect.TypeOf(protowire.KaspadMessage_UnbanRequest{}), reflect.TypeOf(protowire.KaspadMessage_UnbanRequest{}),

View File

@ -1,13 +1,11 @@
# -- multistage docker build: stage #1: build stage # -- 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 RUN mkdir -p /go/src/github.com/kaspanet/kaspad
WORKDIR /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 RUN apk add --no-cache curl git openssh binutils gcc musl-dev
RUN go get -u golang.org/x/lint/golint \
honnef.co/go/tools/cmd/staticcheck
COPY go.mod . COPY go.mod .
COPY go.sum . COPY go.sum .
@ -18,10 +16,6 @@ COPY . .
WORKDIR /go/src/github.com/kaspanet/kaspad/cmd/kaspactl WORKDIR /go/src/github.com/kaspanet/kaspad/cmd/kaspactl
RUN GOFMT_RESULT=`go fmt ./...`; echo $GOFMT_RESULT; test -z "$GOFMT_RESULT"
RUN go vet ./...
RUN golint -set_exit_status ./...
RUN staticcheck -checks SA4006 ./...
RUN GOOS=linux go build -a -installsuffix cgo -o kaspactl . RUN GOOS=linux go build -a -installsuffix cgo -o kaspactl .
# --- multistage docker build: stage #2: runtime image # --- multistage docker build: stage #2: runtime image

View File

@ -4,7 +4,7 @@ Kaspaminer is a CPU-based miner for kaspad
## Requirements ## Requirements
Go 1.18 or later. Go 1.23 or later.
## Installation ## Installation
@ -40,6 +40,7 @@ $ kaspaminer --help
``` ```
But the minimum configuration needed to run it is: But the minimum configuration needed to run it is:
```bash ```bash
$ kaspaminer --miningaddr=<YOUR_MINING_ADDRESS> $ kaspaminer --miningaddr=<YOUR_MINING_ADDRESS>
``` ```

View File

@ -1,13 +1,11 @@
# -- multistage docker build: stage #1: build stage # -- 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 RUN mkdir -p /go/src/github.com/kaspanet/kaspad
WORKDIR /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 RUN apk add --no-cache curl git openssh binutils gcc musl-dev
RUN go get -u golang.org/x/lint/golint \
honnef.co/go/tools/cmd/staticcheck
COPY go.mod . COPY go.mod .
COPY go.sum . COPY go.sum .
@ -17,11 +15,6 @@ RUN go mod download
COPY . . COPY . .
WORKDIR /go/src/github.com/kaspanet/kaspad/cmd/kaspaminer WORKDIR /go/src/github.com/kaspanet/kaspad/cmd/kaspaminer
RUN GOFMT_RESULT=`go fmt ./...`; echo $GOFMT_RESULT; test -z "$GOFMT_RESULT"
RUN go vet ./...
RUN golint -set_exit_status ./...
RUN staticcheck -checks SA4006 ./...
RUN GOOS=linux go build -a -installsuffix cgo -o kaspaminer . RUN GOOS=linux go build -a -installsuffix cgo -o kaspaminer .
# --- multistage docker build: stage #2: runtime image # --- multistage docker build: stage #2: runtime image

View File

@ -23,8 +23,7 @@ func main() {
cfg, err := parseConfig() cfg, err := parseConfig()
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "Error parsing command-line arguments: %s\n", err) printErrorAndExit(errors.Errorf("Error parsing command-line arguments: %s", err))
os.Exit(1)
} }
defer backendLog.Close() defer backendLog.Close()
@ -44,7 +43,7 @@ func main() {
miningAddr, err := util.DecodeAddress(cfg.MiningAddr, cfg.ActiveNetParams.Prefix) miningAddr, err := util.DecodeAddress(cfg.MiningAddr, cfg.ActiveNetParams.Prefix)
if err != nil { if err != nil {
panic(errors.Wrap(err, "error decoding mining address")) printErrorAndExit(errors.Errorf("Error decoding mining address: %s", err))
} }
doneChan := make(chan struct{}) doneChan := make(chan struct{})
@ -61,3 +60,8 @@ func main() {
case <-interrupt: case <-interrupt:
} }
} }
func printErrorAndExit(err error) {
fmt.Fprintf(os.Stderr, "%+v\n", err)
os.Exit(1)
}

View File

@ -3,19 +3,12 @@ package main
import ( import (
"context" "context"
"fmt" "fmt"
"github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/client" "github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/client"
"github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/pb" "github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/pb"
"github.com/kaspanet/kaspad/domain/consensus/utils/constants" "github.com/kaspanet/kaspad/cmd/kaspawallet/utils"
) )
func formatKas(amount uint64) string {
res := " "
if amount > 0 {
res = fmt.Sprintf("%19.8f", float64(amount)/constants.SompiPerKaspa)
}
return res
}
func balance(conf *balanceConfig) error { func balance(conf *balanceConfig) error {
daemonClient, tearDown, err := client.Connect(conf.DaemonAddress) daemonClient, tearDown, err := client.Connect(conf.DaemonAddress)
if err != nil { if err != nil {
@ -39,12 +32,12 @@ func balance(conf *balanceConfig) error {
println("Address Available Pending") println("Address Available Pending")
println("-----------------------------------------------------------------------------------------------------------") println("-----------------------------------------------------------------------------------------------------------")
for _, addressBalance := range response.AddressBalances { for _, addressBalance := range response.AddressBalances {
fmt.Printf("%s %s %s\n", addressBalance.Address, formatKas(addressBalance.Available), formatKas(addressBalance.Pending)) fmt.Printf("%s %s %s\n", addressBalance.Address, utils.FormatKas(addressBalance.Available), utils.FormatKas(addressBalance.Pending))
} }
println("-----------------------------------------------------------------------------------------------------------") println("-----------------------------------------------------------------------------------------------------------")
print(" ") print(" ")
} }
fmt.Printf("Total balance, KAS %s %s%s\n", formatKas(response.Available), formatKas(response.Pending), pendingSuffix) fmt.Printf("Total balance, KAS %s %s%s\n", utils.FormatKas(response.Available), utils.FormatKas(response.Pending), pendingSuffix)
return nil return nil
} }

View File

@ -8,6 +8,7 @@ import (
"github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/client" "github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/client"
"github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/pb" "github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/pb"
"github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/server"
"github.com/pkg/errors" "github.com/pkg/errors"
) )
@ -37,23 +38,19 @@ func broadcast(conf *broadcastConfig) error {
transactionsHex = strings.TrimSpace(string(transactionHexBytes)) transactionsHex = strings.TrimSpace(string(transactionHexBytes))
} }
transactions, err := decodeTransactionsFromHex(transactionsHex) transactions, err := server.DecodeTransactionsFromHex(transactionsHex)
if err != nil { if err != nil {
return err return err
} }
transactionsCount := len(transactions) response, err := daemonClient.Broadcast(ctx, &pb.BroadcastRequest{Transactions: transactions})
for i, transaction := range transactions { if err != nil {
response, err := daemonClient.Broadcast(ctx, &pb.BroadcastRequest{Transaction: transaction}) return err
if err != nil { }
return err fmt.Println("Transactions were sent successfully")
} fmt.Println("Transaction ID(s): ")
if transactionsCount == 1 { for _, txID := range response.TxIDs {
fmt.Println("Transaction was sent successfully") fmt.Printf("\t%s\n", txID)
} else {
fmt.Printf("Transaction %d (out of %d) was sent successfully\n", i+1, transactionsCount)
}
fmt.Printf("Transaction ID: \t%s\n", response.TxID)
} }
return nil return nil

View 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
View 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
}

View 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
}

View File

@ -13,6 +13,7 @@ const (
createSubCmd = "create" createSubCmd = "create"
balanceSubCmd = "balance" balanceSubCmd = "balance"
sendSubCmd = "send" sendSubCmd = "send"
sweepSubCmd = "sweep"
createUnsignedTransactionSubCmd = "create-unsigned-transaction" createUnsignedTransactionSubCmd = "create-unsigned-transaction"
signSubCmd = "sign" signSubCmd = "sign"
broadcastSubCmd = "broadcast" broadcastSubCmd = "broadcast"
@ -21,6 +22,11 @@ const (
newAddressSubCmd = "new-address" newAddressSubCmd = "new-address"
dumpUnencryptedDataSubCmd = "dump-unencrypted-data" dumpUnencryptedDataSubCmd = "dump-unencrypted-data"
startDaemonSubCmd = "start-daemon" startDaemonSubCmd = "start-daemon"
versionSubCmd = "version"
getDaemonVersionSubCmd = "get-daemon-version"
bumpFeeSubCmd = "bump-fee"
bumpFeeUnsignedSubCmd = "bump-fee-unsigned"
broadcastReplacementSubCmd = "broadcast-replacement"
) )
const ( const (
@ -29,6 +35,7 @@ const (
) )
type configFlags struct { type configFlags struct {
ShowVersion bool `short:"V" long:"version" description:"Display version information and exit"`
config.NetworkFlags config.NetworkFlags
} }
@ -45,24 +52,43 @@ type createConfig struct {
} }
type balanceConfig struct { type balanceConfig struct {
DaemonAddress string `long:"daemonaddress" short:"d" description:"Wallet daemon server to connect to (default: localhost:8082)"` DaemonAddress string `long:"daemonaddress" short:"d" description:"Wallet daemon server to connect to"`
Verbose bool `long:"verbose" short:"v" description:"Verbose: show addresses with balance"` Verbose bool `long:"verbose" short:"v" description:"Verbose: show addresses with balance"`
config.NetworkFlags config.NetworkFlags
} }
type sendConfig struct { type sendConfig struct {
KeysFile string `long:"keys-file" short:"f" description:"Keys file location (default: ~/.kaspawallet/keys.json (*nix), %USERPROFILE%\\AppData\\Local\\Kaspawallet\\key.json (Windows))"` KeysFile string `long:"keys-file" short:"f" description:"Keys file location (default: ~/.kaspawallet/keys.json (*nix), %USERPROFILE%\\AppData\\Local\\Kaspawallet\\key.json (Windows))"`
Password string `long:"password" short:"p" description:"Wallet password"` Password string `long:"password" short:"p" description:"Wallet password"`
DaemonAddress string `long:"daemonaddress" short:"d" description:"Wallet daemon server to connect to (default: localhost:8082)"` DaemonAddress string `long:"daemonaddress" short:"d" description:"Wallet daemon server to connect to"`
ToAddress string `long:"to-address" short:"t" description:"The public address to send Kaspa to" required:"true"` ToAddress string `long:"to-address" short:"t" description:"The public address to send Kaspa to" required:"true"`
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
}
type sweepConfig struct {
PrivateKey string `long:"private-key" short:"k" description:"Private key in hex format"`
DaemonAddress string `long:"daemonaddress" short:"d" description:"Wallet daemon server to connect to"`
config.NetworkFlags config.NetworkFlags
} }
type createUnsignedTransactionConfig struct { type createUnsignedTransactionConfig struct {
DaemonAddress string `long:"daemonaddress" short:"d" description:"Wallet daemon server to connect to (default: localhost:8082)"` DaemonAddress string `long:"daemonaddress" short:"d" description:"Wallet daemon server to connect to"`
ToAddress string `long:"to-address" short:"t" description:"The public address to send Kaspa to" required:"true"` ToAddress string `long:"to-address" short:"t" description:"The public address to send Kaspa to" required:"true"`
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. Use multiple times 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)"`
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 config.NetworkFlags
} }
@ -75,13 +101,14 @@ type signConfig struct {
} }
type broadcastConfig struct { type broadcastConfig struct {
DaemonAddress string `long:"daemonaddress" short:"d" description:"Wallet daemon server to connect to (default: localhost:8082)"` DaemonAddress string `long:"daemonaddress" short:"d" description:"Wallet daemon server to connect to"`
Transactions string `long:"transaction" short:"t" description:"The signed transaction to broadcast (encoded in hex)"` Transactions string `long:"transaction" short:"t" description:"The signed transaction to broadcast (encoded in hex)"`
TransactionsFile string `long:"transaction-file" short:"F" description:"The file containing the unsigned transaction to sign on (encoded in hex)"` TransactionsFile string `long:"transaction-file" short:"F" description:"The file containing the unsigned transaction to sign on (encoded in hex)"`
config.NetworkFlags config.NetworkFlags
} }
type parseConfig 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)"` 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)"` 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"` Verbose bool `long:"verbose" short:"v" description:"Verbose: show transaction inputs"`
@ -89,12 +116,12 @@ type parseConfig struct {
} }
type showAddressesConfig struct { type showAddressesConfig struct {
DaemonAddress string `long:"daemonaddress" short:"d" description:"Wallet daemon server to connect to (default: localhost:8082)"` DaemonAddress string `long:"daemonaddress" short:"d" description:"Wallet daemon server to connect to"`
config.NetworkFlags config.NetworkFlags
} }
type newAddressConfig struct { type newAddressConfig struct {
DaemonAddress string `long:"daemonaddress" short:"d" description:"Wallet daemon server to connect to (default: localhost:8082)"` DaemonAddress string `long:"daemonaddress" short:"d" description:"Wallet daemon server to connect to"`
config.NetworkFlags config.NetworkFlags
} }
@ -102,7 +129,8 @@ type startDaemonConfig struct {
KeysFile string `long:"keys-file" short:"f" description:"Keys file location (default: ~/.kaspawallet/keys.json (*nix), %USERPROFILE%\\AppData\\Local\\Kaspawallet\\key.json (Windows))"` KeysFile string `long:"keys-file" short:"f" description:"Keys file location (default: ~/.kaspawallet/keys.json (*nix), %USERPROFILE%\\AppData\\Local\\Kaspawallet\\key.json (Windows))"`
Password string `long:"password" short:"p" description:"Wallet password"` Password string `long:"password" short:"p" description:"Wallet password"`
RPCServer string `long:"rpcserver" short:"s" description:"RPC server to connect to"` RPCServer string `long:"rpcserver" short:"s" description:"RPC server to connect to"`
Listen string `short:"l" long:"listen" description:"Address to listen on (default: 0.0.0.0:8082)"` Listen string `long:"listen" short:"l" description:"Address to listen on (default: 0.0.0.0:8082)"`
Timeout uint32 `long:"wait-timeout" short:"w" description:"Waiting timeout for RPC calls, seconds (default: 30 s)"`
Profile string `long:"profile" description:"Enable HTTP profiling on given port -- NOTE port must be between 1024 and 65536"` Profile string `long:"profile" description:"Enable HTTP profiling on given port -- NOTE port must be between 1024 and 65536"`
config.NetworkFlags config.NetworkFlags
} }
@ -114,6 +142,38 @@ type dumpUnencryptedDataConfig struct {
config.NetworkFlags 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{}) { func parseCommandLine() (subCommand string, config interface{}) {
cfg := &configFlags{} cfg := &configFlags{}
parser := flags.NewParser(cfg, flags.PrintErrors|flags.HelpFlag) parser := flags.NewParser(cfg, flags.PrintErrors|flags.HelpFlag)
@ -130,6 +190,12 @@ func parseCommandLine() (subCommand string, config interface{}) {
parser.AddCommand(sendSubCmd, "Sends a Kaspa transaction to a public address", parser.AddCommand(sendSubCmd, "Sends a Kaspa transaction to a public address",
"Sends a Kaspa transaction to a public address", sendConf) "Sends a Kaspa transaction to a public address", sendConf)
sweepConf := &sweepConfig{DaemonAddress: defaultListen}
parser.AddCommand(sweepSubCmd, "Sends all funds associated with the given schnorr private key to a new address of the current wallet",
"Sends all funds associated with the given schnorr private key to a newly created external (i.e. not a change) address of the "+
"keyfile that is under the daemon's contol. Can be used with a private key generated with the genkeypair utilily "+
"to send funds to your main wallet.", sweepConf)
createUnsignedTransactionConf := &createUnsignedTransactionConfig{DaemonAddress: defaultListen} createUnsignedTransactionConf := &createUnsignedTransactionConfig{DaemonAddress: defaultListen}
parser.AddCommand(createUnsignedTransactionSubCmd, "Create an unsigned Kaspa transaction", parser.AddCommand(createUnsignedTransactionSubCmd, "Create an unsigned Kaspa transaction",
"Create an unsigned Kaspa transaction", createUnsignedTransactionConf) "Create an unsigned Kaspa transaction", createUnsignedTransactionConf)
@ -164,9 +230,17 @@ func parseCommandLine() (subCommand string, config interface{}) {
Listen: defaultListen, Listen: defaultListen,
} }
parser.AddCommand(startDaemonSubCmd, "Start the wallet daemon", "Start the wallet daemon", startDaemonConf) 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() _, err := parser.Parse()
if err != nil { if err != nil {
var flagsErr *flags.Error var flagsErr *flags.Error
if ok := errors.As(err, &flagsErr); ok && flagsErr.Type == flags.ErrHelp { if ok := errors.As(err, &flagsErr); ok && flagsErr.Type == flags.ErrHelp {
@ -198,13 +272,28 @@ func parseCommandLine() (subCommand string, config interface{}) {
if err != nil { if err != nil {
printErrorAndExit(err) printErrorAndExit(err)
} }
err = validateSendConfig(sendConf)
if err != nil {
printErrorAndExit(err)
}
config = sendConf config = sendConf
case sweepSubCmd:
combineNetworkFlags(&sweepConf.NetworkFlags, &cfg.NetworkFlags)
err := sweepConf.ResolveNetwork(parser)
if err != nil {
printErrorAndExit(err)
}
config = sweepConf
case createUnsignedTransactionSubCmd: case createUnsignedTransactionSubCmd:
combineNetworkFlags(&createUnsignedTransactionConf.NetworkFlags, &cfg.NetworkFlags) combineNetworkFlags(&createUnsignedTransactionConf.NetworkFlags, &cfg.NetworkFlags)
err := createUnsignedTransactionConf.ResolveNetwork(parser) err := createUnsignedTransactionConf.ResolveNetwork(parser)
if err != nil { if err != nil {
printErrorAndExit(err) printErrorAndExit(err)
} }
err = validateCreateUnsignedTransactionConf(createUnsignedTransactionConf)
if err != nil {
printErrorAndExit(err)
}
config = createUnsignedTransactionConf config = createUnsignedTransactionConf
case signSubCmd: case signSubCmd:
combineNetworkFlags(&signConf.NetworkFlags, &cfg.NetworkFlags) combineNetworkFlags(&signConf.NetworkFlags, &cfg.NetworkFlags)
@ -220,6 +309,13 @@ func parseCommandLine() (subCommand string, config interface{}) {
printErrorAndExit(err) printErrorAndExit(err)
} }
config = broadcastConf config = broadcastConf
case broadcastReplacementSubCmd:
combineNetworkFlags(&broadcastConf.NetworkFlags, &cfg.NetworkFlags)
err := broadcastConf.ResolveNetwork(parser)
if err != nil {
printErrorAndExit(err)
}
config = broadcastConf
case parseSubCmd: case parseSubCmd:
combineNetworkFlags(&parseConf.NetworkFlags, &cfg.NetworkFlags) combineNetworkFlags(&parseConf.NetworkFlags, &cfg.NetworkFlags)
err := parseConf.ResolveNetwork(parser) err := parseConf.ResolveNetwork(parser)
@ -255,11 +351,123 @@ func parseCommandLine() (subCommand string, config interface{}) {
printErrorAndExit(err) printErrorAndExit(err)
} }
config = startDaemonConf 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 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) { func combineNetworkFlags(dst, src *config.NetworkFlags) {
dst.Testnet = dst.Testnet || src.Testnet dst.Testnet = dst.Testnet || src.Testnet
dst.Simnet = dst.Simnet || src.Simnet dst.Simnet = dst.Simnet || src.Simnet

View File

@ -3,11 +3,12 @@ package main
import ( import (
"bufio" "bufio"
"fmt" "fmt"
"os"
"github.com/kaspanet/kaspad/cmd/kaspawallet/libkaspawallet" "github.com/kaspanet/kaspad/cmd/kaspawallet/libkaspawallet"
"github.com/kaspanet/kaspad/cmd/kaspawallet/libkaspawallet/bip32" "github.com/kaspanet/kaspad/cmd/kaspawallet/libkaspawallet/bip32"
"github.com/kaspanet/kaspad/cmd/kaspawallet/utils" "github.com/kaspanet/kaspad/cmd/kaspawallet/utils"
"github.com/pkg/errors" "github.com/pkg/errors"
"os"
"github.com/kaspanet/kaspad/cmd/kaspawallet/keys" "github.com/kaspanet/kaspad/cmd/kaspawallet/keys"
) )
@ -30,6 +31,10 @@ func create(conf *createConfig) error {
fmt.Printf("Extended public key of mnemonic #%d:\n%s\n\n", i+1, extendedPublicKey) fmt.Printf("Extended public key of mnemonic #%d:\n%s\n\n", i+1, extendedPublicKey)
} }
fmt.Printf("Notice the above is neither a secret key to your wallet " +
"(use \"kaspawallet dump-unencrypted-data\" to see a secret seed phrase) " +
"nor a wallet public address (use \"kaspawallet new-address\" to create and see one)\n\n")
extendedPublicKeys := make([]string, conf.NumPrivateKeys, conf.NumPublicKeys) extendedPublicKeys := make([]string, conf.NumPrivateKeys, conf.NumPublicKeys)
copy(extendedPublicKeys, signerExtendedPublicKeys) copy(extendedPublicKeys, signerExtendedPublicKeys)
reader := bufio.NewReader(os.Stdin) reader := bufio.NewReader(os.Stdin)
@ -50,9 +55,13 @@ func create(conf *createConfig) error {
extendedPublicKeys = append(extendedPublicKeys, string(extendedPublicKey)) extendedPublicKeys = append(extendedPublicKeys, string(extendedPublicKey))
} }
cosignerIndex, err := libkaspawallet.MinimumCosignerIndex(signerExtendedPublicKeys, extendedPublicKeys) // For a read only wallet the cosigner index is 0
if err != nil { cosignerIndex := uint32(0)
return err if len(signerExtendedPublicKeys) > 0 {
cosignerIndex, err = libkaspawallet.MinimumCosignerIndex(signerExtendedPublicKeys, extendedPublicKeys)
if err != nil {
return err
}
} }
file := keys.File{ file := keys.File{
@ -69,6 +78,11 @@ func create(conf *createConfig) error {
return err return err
} }
err = file.TryLock()
if err != nil {
return err
}
err = file.Save() err = file.Save()
if err != nil { if err != nil {
return err return err

View File

@ -3,10 +3,12 @@ package main
import ( import (
"context" "context"
"fmt" "fmt"
"os"
"github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/client" "github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/client"
"github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/pb" "github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/pb"
"github.com/kaspanet/kaspad/domain/consensus/utils/constants" "github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/server"
"github.com/kaspanet/kaspad/cmd/kaspawallet/utils"
) )
func createUnsignedTransaction(conf *createUnsignedTransactionConfig) error { func createUnsignedTransaction(conf *createUnsignedTransactionConfig) error {
@ -19,16 +21,46 @@ func createUnsignedTransaction(conf *createUnsignedTransactionConfig) error {
ctx, cancel := context.WithTimeout(context.Background(), daemonTimeout) ctx, cancel := context.WithTimeout(context.Background(), daemonTimeout)
defer cancel() 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{ response, err := daemonClient.CreateUnsignedTransactions(ctx, &pb.CreateUnsignedTransactionsRequest{
Address: conf.ToAddress, From: conf.FromAddresses,
Amount: sendAmountSompi, Address: conf.ToAddress,
Amount: sendAmountSompi,
IsSendAll: conf.IsSendAll,
UseExistingChangeAddress: conf.UseExistingChangeAddress,
FeePolicy: feePolicy,
}) })
if err != nil { if err != nil {
return err return err
} }
fmt.Println("Created unsigned transaction") fmt.Fprintln(os.Stderr, "Created unsigned transaction")
fmt.Println(encodeTransactionsToHex(response.UnsignedTransactions)) fmt.Println(server.EncodeTransactionsToHex(response.UnsignedTransactions))
return nil return nil
} }

View File

@ -4,6 +4,8 @@ import (
"context" "context"
"time" "time"
"github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/server"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/pb" "github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/pb"
@ -16,7 +18,7 @@ func Connect(address string) (pb.KaspawalletdClient, func(), error) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second) ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel() defer cancel()
conn, err := grpc.DialContext(ctx, address, grpc.WithInsecure(), grpc.WithBlock()) conn, err := grpc.DialContext(ctx, address, grpc.WithInsecure(), grpc.WithBlock(), grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(server.MaxDaemonSendMsgSize)))
if err != nil { if err != nil {
if errors.Is(err, context.DeadlineExceeded) { if errors.Is(err, context.DeadlineExceeded) {
return nil, nil, errors.New("kaspawallet daemon is not running, start it with `kaspawallet start-daemon`") return nil, nil, errors.New("kaspawallet daemon is not running, start it with `kaspawallet start-daemon`")

File diff suppressed because it is too large Load Diff

View File

@ -4,16 +4,28 @@ option go_package = "github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/pb";
package kaspawalletd; package kaspawalletd;
service kaspawalletd { service kaspawalletd {
rpc GetBalance (GetBalanceRequest) returns (GetBalanceResponse) {} rpc GetBalance(GetBalanceRequest) returns (GetBalanceResponse) {}
rpc CreateUnsignedTransactions (CreateUnsignedTransactionsRequest) returns (CreateUnsignedTransactionsResponse) {} rpc GetExternalSpendableUTXOs(GetExternalSpendableUTXOsRequest)
rpc ShowAddresses (ShowAddressesRequest) returns (ShowAddressesResponse) {} returns (GetExternalSpendableUTXOsResponse) {}
rpc NewAddress (NewAddressRequest) returns (NewAddressResponse) {} rpc CreateUnsignedTransactions(CreateUnsignedTransactionsRequest)
rpc Shutdown (ShutdownRequest) returns (ShutdownResponse) {} returns (CreateUnsignedTransactionsResponse) {}
rpc Broadcast (BroadcastRequest) returns (BroadcastResponse) {} 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
rpc Sign(SignRequest) returns (SignResponse) {}
rpc GetVersion(GetVersionRequest) returns (GetVersionResponse) {}
rpc BumpFee(BumpFeeRequest) returns (BumpFeeResponse) {}
} }
message GetBalanceRequest { message GetBalanceRequest {}
}
message GetBalanceResponse { message GetBalanceResponse {
uint64 available = 1; uint64 available = 1;
@ -22,44 +34,118 @@ message GetBalanceResponse {
} }
message AddressBalances { message AddressBalances {
string address = 1; string address = 1;
uint64 available = 2; uint64 available = 2;
uint64 pending = 3; uint64 pending = 3;
}
message FeePolicy {
oneof feePolicy {
double maxFeeRate = 6;
double exactFeeRate = 7;
uint64 maxFee = 8;
}
} }
message CreateUnsignedTransactionsRequest { message CreateUnsignedTransactionsRequest {
string address = 1; string address = 1;
uint64 amount = 2; uint64 amount = 2;
repeated string from = 3;
bool useExistingChangeAddress = 4;
bool isSendAll = 5;
FeePolicy feePolicy = 6;
} }
message CreateUnsignedTransactionsResponse { message CreateUnsignedTransactionsResponse {
repeated bytes unsignedTransactions = 1; repeated bytes unsignedTransactions = 1;
} }
message ShowAddressesRequest { message ShowAddressesRequest {}
}
message ShowAddressesResponse { message ShowAddressesResponse { repeated string address = 1; }
repeated string address = 1;
}
message NewAddressRequest { message NewAddressRequest {}
}
message NewAddressResponse { message NewAddressResponse { string address = 1; }
string address = 1;
}
message BroadcastRequest { message BroadcastRequest {
bytes transaction = 1; bool isDomain = 1;
repeated bytes transactions = 2;
} }
message BroadcastResponse { message BroadcastResponse { repeated string txIDs = 1; }
string txID = 1;
message ShutdownRequest {}
message ShutdownResponse {}
message Outpoint {
string transactionId = 1;
uint32 index = 2;
} }
message ShutdownRequest { message UtxosByAddressesEntry {
string address = 1;
Outpoint outpoint = 2;
UtxoEntry utxoEntry = 3;
} }
message ShutdownResponse { message ScriptPublicKey {
uint32 version = 1;
string scriptPublicKey = 2;
}
message UtxoEntry {
uint64 amount = 1;
ScriptPublicKey scriptPublicKey = 2;
uint64 blockDaaScore = 3;
bool isCoinbase = 4;
}
message GetExternalSpendableUTXOsRequest { string address = 1; }
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 {
string toAddress = 1;
uint64 amount = 2;
string password = 3;
repeated string from = 4;
bool useExistingChangeAddress = 5;
bool isSendAll = 6;
FeePolicy feePolicy = 7;
}
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 {
repeated bytes unsignedTransactions = 1;
string password = 2;
}
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;
} }

View File

@ -1,4 +1,8 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT. // Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.2.0
// - protoc v3.12.3
// source: kaspawalletd.proto
package pb package pb
@ -19,11 +23,22 @@ const _ = grpc.SupportPackageIsVersion7
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
type KaspawalletdClient interface { type KaspawalletdClient interface {
GetBalance(ctx context.Context, in *GetBalanceRequest, opts ...grpc.CallOption) (*GetBalanceResponse, error) GetBalance(ctx context.Context, in *GetBalanceRequest, opts ...grpc.CallOption) (*GetBalanceResponse, error)
GetExternalSpendableUTXOs(ctx context.Context, in *GetExternalSpendableUTXOsRequest, opts ...grpc.CallOption) (*GetExternalSpendableUTXOsResponse, error)
CreateUnsignedTransactions(ctx context.Context, in *CreateUnsignedTransactionsRequest, opts ...grpc.CallOption) (*CreateUnsignedTransactionsResponse, error) CreateUnsignedTransactions(ctx context.Context, in *CreateUnsignedTransactionsRequest, opts ...grpc.CallOption) (*CreateUnsignedTransactionsResponse, error)
ShowAddresses(ctx context.Context, in *ShowAddressesRequest, opts ...grpc.CallOption) (*ShowAddressesResponse, error) ShowAddresses(ctx context.Context, in *ShowAddressesRequest, opts ...grpc.CallOption) (*ShowAddressesResponse, error)
NewAddress(ctx context.Context, in *NewAddressRequest, opts ...grpc.CallOption) (*NewAddressResponse, error) NewAddress(ctx context.Context, in *NewAddressRequest, opts ...grpc.CallOption) (*NewAddressResponse, error)
Shutdown(ctx context.Context, in *ShutdownRequest, opts ...grpc.CallOption) (*ShutdownResponse, error) Shutdown(ctx context.Context, in *ShutdownRequest, opts ...grpc.CallOption) (*ShutdownResponse, error)
Broadcast(ctx context.Context, in *BroadcastRequest, opts ...grpc.CallOption) (*BroadcastResponse, error) Broadcast(ctx context.Context, in *BroadcastRequest, opts ...grpc.CallOption) (*BroadcastResponse, error)
// 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
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 { type kaspawalletdClient struct {
@ -43,6 +58,15 @@ func (c *kaspawalletdClient) GetBalance(ctx context.Context, in *GetBalanceReque
return out, nil return out, nil
} }
func (c *kaspawalletdClient) GetExternalSpendableUTXOs(ctx context.Context, in *GetExternalSpendableUTXOsRequest, opts ...grpc.CallOption) (*GetExternalSpendableUTXOsResponse, error) {
out := new(GetExternalSpendableUTXOsResponse)
err := c.cc.Invoke(ctx, "/kaspawalletd.kaspawalletd/GetExternalSpendableUTXOs", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *kaspawalletdClient) CreateUnsignedTransactions(ctx context.Context, in *CreateUnsignedTransactionsRequest, opts ...grpc.CallOption) (*CreateUnsignedTransactionsResponse, error) { func (c *kaspawalletdClient) CreateUnsignedTransactions(ctx context.Context, in *CreateUnsignedTransactionsRequest, opts ...grpc.CallOption) (*CreateUnsignedTransactionsResponse, error) {
out := new(CreateUnsignedTransactionsResponse) out := new(CreateUnsignedTransactionsResponse)
err := c.cc.Invoke(ctx, "/kaspawalletd.kaspawalletd/CreateUnsignedTransactions", in, out, opts...) err := c.cc.Invoke(ctx, "/kaspawalletd.kaspawalletd/CreateUnsignedTransactions", in, out, opts...)
@ -88,16 +112,72 @@ func (c *kaspawalletdClient) Broadcast(ctx context.Context, in *BroadcastRequest
return out, nil 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...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *kaspawalletdClient) Sign(ctx context.Context, in *SignRequest, opts ...grpc.CallOption) (*SignResponse, error) {
out := new(SignResponse)
err := c.cc.Invoke(ctx, "/kaspawalletd.kaspawalletd/Sign", in, out, opts...)
if err != nil {
return nil, err
}
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. // KaspawalletdServer is the server API for Kaspawalletd service.
// All implementations must embed UnimplementedKaspawalletdServer // All implementations must embed UnimplementedKaspawalletdServer
// for forward compatibility // for forward compatibility
type KaspawalletdServer interface { type KaspawalletdServer interface {
GetBalance(context.Context, *GetBalanceRequest) (*GetBalanceResponse, error) GetBalance(context.Context, *GetBalanceRequest) (*GetBalanceResponse, error)
GetExternalSpendableUTXOs(context.Context, *GetExternalSpendableUTXOsRequest) (*GetExternalSpendableUTXOsResponse, error)
CreateUnsignedTransactions(context.Context, *CreateUnsignedTransactionsRequest) (*CreateUnsignedTransactionsResponse, error) CreateUnsignedTransactions(context.Context, *CreateUnsignedTransactionsRequest) (*CreateUnsignedTransactionsResponse, error)
ShowAddresses(context.Context, *ShowAddressesRequest) (*ShowAddressesResponse, error) ShowAddresses(context.Context, *ShowAddressesRequest) (*ShowAddressesResponse, error)
NewAddress(context.Context, *NewAddressRequest) (*NewAddressResponse, error) NewAddress(context.Context, *NewAddressRequest) (*NewAddressResponse, error)
Shutdown(context.Context, *ShutdownRequest) (*ShutdownResponse, error) Shutdown(context.Context, *ShutdownRequest) (*ShutdownResponse, error)
Broadcast(context.Context, *BroadcastRequest) (*BroadcastResponse, error) Broadcast(context.Context, *BroadcastRequest) (*BroadcastResponse, error)
// 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
Sign(context.Context, *SignRequest) (*SignResponse, error)
GetVersion(context.Context, *GetVersionRequest) (*GetVersionResponse, error)
BumpFee(context.Context, *BumpFeeRequest) (*BumpFeeResponse, error)
mustEmbedUnimplementedKaspawalletdServer() mustEmbedUnimplementedKaspawalletdServer()
} }
@ -108,6 +188,9 @@ type UnimplementedKaspawalletdServer struct {
func (UnimplementedKaspawalletdServer) GetBalance(context.Context, *GetBalanceRequest) (*GetBalanceResponse, error) { func (UnimplementedKaspawalletdServer) GetBalance(context.Context, *GetBalanceRequest) (*GetBalanceResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetBalance not implemented") return nil, status.Errorf(codes.Unimplemented, "method GetBalance not implemented")
} }
func (UnimplementedKaspawalletdServer) GetExternalSpendableUTXOs(context.Context, *GetExternalSpendableUTXOsRequest) (*GetExternalSpendableUTXOsResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetExternalSpendableUTXOs not implemented")
}
func (UnimplementedKaspawalletdServer) CreateUnsignedTransactions(context.Context, *CreateUnsignedTransactionsRequest) (*CreateUnsignedTransactionsResponse, error) { func (UnimplementedKaspawalletdServer) CreateUnsignedTransactions(context.Context, *CreateUnsignedTransactionsRequest) (*CreateUnsignedTransactionsResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method CreateUnsignedTransactions not implemented") return nil, status.Errorf(codes.Unimplemented, "method CreateUnsignedTransactions not implemented")
} }
@ -123,6 +206,21 @@ func (UnimplementedKaspawalletdServer) Shutdown(context.Context, *ShutdownReques
func (UnimplementedKaspawalletdServer) Broadcast(context.Context, *BroadcastRequest) (*BroadcastResponse, error) { func (UnimplementedKaspawalletdServer) Broadcast(context.Context, *BroadcastRequest) (*BroadcastResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method Broadcast not implemented") 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() {} func (UnimplementedKaspawalletdServer) mustEmbedUnimplementedKaspawalletdServer() {}
// UnsafeKaspawalletdServer may be embedded to opt out of forward compatibility for this service. // UnsafeKaspawalletdServer may be embedded to opt out of forward compatibility for this service.
@ -154,6 +252,24 @@ func _Kaspawalletd_GetBalance_Handler(srv interface{}, ctx context.Context, dec
return interceptor(ctx, in, info, handler) return interceptor(ctx, in, info, handler)
} }
func _Kaspawalletd_GetExternalSpendableUTXOs_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(GetExternalSpendableUTXOsRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(KaspawalletdServer).GetExternalSpendableUTXOs(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/kaspawalletd.kaspawalletd/GetExternalSpendableUTXOs",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(KaspawalletdServer).GetExternalSpendableUTXOs(ctx, req.(*GetExternalSpendableUTXOsRequest))
}
return interceptor(ctx, in, info, handler)
}
func _Kaspawalletd_CreateUnsignedTransactions_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { func _Kaspawalletd_CreateUnsignedTransactions_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(CreateUnsignedTransactionsRequest) in := new(CreateUnsignedTransactionsRequest)
if err := dec(in); err != nil { if err := dec(in); err != nil {
@ -244,6 +360,96 @@ func _Kaspawalletd_Broadcast_Handler(srv interface{}, ctx context.Context, dec f
return interceptor(ctx, in, info, handler) 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 {
return nil, err
}
if interceptor == nil {
return srv.(KaspawalletdServer).Send(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/kaspawalletd.kaspawalletd/Send",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(KaspawalletdServer).Send(ctx, req.(*SendRequest))
}
return interceptor(ctx, in, info, handler)
}
func _Kaspawalletd_Sign_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(SignRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(KaspawalletdServer).Sign(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/kaspawalletd.kaspawalletd/Sign",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(KaspawalletdServer).Sign(ctx, req.(*SignRequest))
}
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. // Kaspawalletd_ServiceDesc is the grpc.ServiceDesc for Kaspawalletd service.
// It's only intended for direct use with grpc.RegisterService, // It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy) // and not to be introspected or modified (even as a copy)
@ -255,6 +461,10 @@ var Kaspawalletd_ServiceDesc = grpc.ServiceDesc{
MethodName: "GetBalance", MethodName: "GetBalance",
Handler: _Kaspawalletd_GetBalance_Handler, Handler: _Kaspawalletd_GetBalance_Handler,
}, },
{
MethodName: "GetExternalSpendableUTXOs",
Handler: _Kaspawalletd_GetExternalSpendableUTXOs_Handler,
},
{ {
MethodName: "CreateUnsignedTransactions", MethodName: "CreateUnsignedTransactions",
Handler: _Kaspawalletd_CreateUnsignedTransactions_Handler, Handler: _Kaspawalletd_CreateUnsignedTransactions_Handler,
@ -275,6 +485,26 @@ var Kaspawalletd_ServiceDesc = grpc.ServiceDesc{
MethodName: "Broadcast", MethodName: "Broadcast",
Handler: _Kaspawalletd_Broadcast_Handler, Handler: _Kaspawalletd_Broadcast_Handler,
}, },
{
MethodName: "BroadcastReplacement",
Handler: _Kaspawalletd_BroadcastReplacement_Handler,
},
{
MethodName: "Send",
Handler: _Kaspawalletd_Send_Handler,
},
{
MethodName: "Sign",
Handler: _Kaspawalletd_Sign_Handler,
},
{
MethodName: "GetVersion",
Handler: _Kaspawalletd_GetVersion_Handler,
},
{
MethodName: "BumpFee",
Handler: _Kaspawalletd_BumpFee_Handler,
},
}, },
Streams: []grpc.StreamDesc{}, Streams: []grpc.StreamDesc{},
Metadata: "kaspawalletd.proto", Metadata: "kaspawalletd.proto",

View File

@ -10,22 +10,33 @@ import (
"github.com/pkg/errors" "github.com/pkg/errors"
) )
func (s *server) changeAddress() (util.Address, *walletAddress, error) { func (s *server) changeAddress(useExisting bool, fromAddresses []*walletAddress) (util.Address, *walletAddress, error) {
err := s.keysFile.SetLastUsedInternalIndex(s.keysFile.LastUsedInternalIndex() + 1) var walletAddr *walletAddress
if err != nil { if len(fromAddresses) != 0 && useExisting {
return nil, nil, err walletAddr = fromAddresses[0]
} else {
internalIndex := uint32(0)
if !useExisting {
err := s.keysFile.SetLastUsedInternalIndex(s.keysFile.LastUsedInternalIndex() + 1)
if err != nil {
return nil, nil, err
}
err = s.keysFile.Save()
if err != nil {
return nil, nil, err
}
internalIndex = s.keysFile.LastUsedInternalIndex()
}
walletAddr = &walletAddress{
index: internalIndex,
cosignerIndex: s.keysFile.CosignerIndex,
keyChain: libkaspawallet.InternalKeychain,
}
} }
err = s.keysFile.Save()
if err != nil {
return nil, nil, err
}
walletAddr := &walletAddress{
index: s.keysFile.LastUsedInternalIndex(),
cosignerIndex: s.keysFile.CosignerIndex,
keyChain: libkaspawallet.InternalKeychain,
}
path := s.walletAddressPath(walletAddr) path := s.walletAddressPath(walletAddr)
address, err := libkaspawallet.Address(s.params, s.keysFile.ExtendedPublicKeys, s.keysFile.MinimumSignatures, path, s.keysFile.ECDSA) address, err := libkaspawallet.Address(s.params, s.keysFile.ExtendedPublicKeys, s.keysFile.MinimumSignatures, path, s.keysFile.ECDSA)
if err != nil { if err != nil {
@ -39,10 +50,10 @@ func (s *server) ShowAddresses(_ context.Context, request *pb.ShowAddressesReque
defer s.lock.Unlock() defer s.lock.Unlock()
if !s.isSynced() { if !s.isSynced() {
return nil, errors.New("server is not synced") return nil, errors.Errorf("wallet daemon is not synced yet, %s", s.formatSyncStateReport())
} }
addresses := make([]string, 0) addresses := make([]string, s.keysFile.LastUsedExternalIndex())
for i := uint32(1); i <= s.keysFile.LastUsedExternalIndex(); i++ { for i := uint32(1); i <= s.keysFile.LastUsedExternalIndex(); i++ {
walletAddr := &walletAddress{ walletAddr := &walletAddress{
index: i, index: i,
@ -54,7 +65,7 @@ func (s *server) ShowAddresses(_ context.Context, request *pb.ShowAddressesReque
if err != nil { if err != nil {
return nil, err return nil, err
} }
addresses = append(addresses, address.String()) addresses[i-1] = address.String()
} }
return &pb.ShowAddressesResponse{Address: addresses}, nil return &pb.ShowAddressesResponse{Address: addresses}, nil
@ -65,7 +76,7 @@ func (s *server) NewAddress(_ context.Context, request *pb.NewAddressRequest) (*
defer s.lock.Unlock() defer s.lock.Unlock()
if !s.isSynced() { if !s.isSynced() {
return nil, errors.New("server is not synced") return nil, errors.Errorf("wallet daemon is not synced yet, %s", s.formatSyncStateReport())
} }
err := s.keysFile.SetLastUsedExternalIndex(s.keysFile.LastUsedExternalIndex() + 1) err := s.keysFile.SetLastUsedExternalIndex(s.keysFile.LastUsedExternalIndex() + 1)

View File

@ -2,6 +2,7 @@ package server
import ( import (
"context" "context"
"github.com/pkg/errors"
"github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/pb" "github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/pb"
"github.com/kaspanet/kaspad/cmd/kaspawallet/libkaspawallet" "github.com/kaspanet/kaspad/cmd/kaspawallet/libkaspawallet"
@ -14,13 +15,15 @@ func (s *server) GetBalance(_ context.Context, _ *pb.GetBalanceRequest) (*pb.Get
s.lock.RLock() s.lock.RLock()
defer s.lock.RUnlock() 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() dagInfo, err := s.rpcClient.GetBlockDAGInfo()
if err != nil { if err != nil {
return nil, err return nil, err
} }
daaScore := dagInfo.VirtualDAAScore daaScore := dagInfo.VirtualDAAScore
maturity := s.params.BlockCoinbaseMaturity
balancesMap := make(balancesMapType, 0) balancesMap := make(balancesMapType, 0)
for _, entry := range s.utxosSortedByAmount { for _, entry := range s.utxosSortedByAmount {
amount := entry.UTXOEntry.Amount() amount := entry.UTXOEntry.Amount()
@ -30,7 +33,7 @@ func (s *server) GetBalance(_ context.Context, _ *pb.GetBalanceRequest) (*pb.Get
balances = new(balancesType) balances = new(balancesType)
balancesMap[address] = balances balancesMap[address] = balances
} }
if isUTXOSpendable(entry, daaScore, maturity) { if s.isUTXOSpendable(entry, daaScore) {
balances.available += amount balances.available += amount
} else { } else {
balances.pending += amount balances.pending += amount
@ -55,6 +58,8 @@ func (s *server) GetBalance(_ context.Context, _ *pb.GetBalanceRequest) (*pb.Get
pending += balances.pending pending += balances.pending
} }
log.Infof("GetBalance request scanned %d UTXOs overall over %d addresses", len(s.utxosSortedByAmount), len(balancesMap))
return &pb.GetBalanceResponse{ return &pb.GetBalanceResponse{
Available: available, Available: available,
Pending: pending, Pending: pending,
@ -62,9 +67,9 @@ func (s *server) GetBalance(_ context.Context, _ *pb.GetBalanceRequest) (*pb.Get
}, nil }, nil
} }
func isUTXOSpendable(entry *walletUTXO, virtualDAAScore uint64, coinbaseMaturity uint64) bool { func (s *server) isUTXOSpendable(entry *walletUTXO, virtualDAAScore uint64) bool {
if !entry.UTXOEntry.IsCoinbase() { if !entry.UTXOEntry.IsCoinbase() {
return true return true
} }
return entry.UTXOEntry.BlockDAAScore()+coinbaseMaturity < virtualDAAScore return entry.UTXOEntry.BlockDAAScore()+s.coinbaseMaturity < virtualDAAScore
} }

View File

@ -2,31 +2,66 @@ package server
import ( import (
"context" "context"
"time"
"github.com/kaspanet/kaspad/app/appmessage" "github.com/kaspanet/kaspad/app/appmessage"
"github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/pb" "github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/pb"
"github.com/kaspanet/kaspad/cmd/kaspawallet/libkaspawallet" "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/model/externalapi"
"github.com/kaspanet/kaspad/domain/consensus/utils/consensushashing"
"github.com/kaspanet/kaspad/infrastructure/network/rpcclient" "github.com/kaspanet/kaspad/infrastructure/network/rpcclient"
"github.com/pkg/errors" "github.com/pkg/errors"
) )
func (s *server) Broadcast(_ context.Context, request *pb.BroadcastRequest) (*pb.BroadcastResponse, error) { func (s *server) Broadcast(_ context.Context, request *pb.BroadcastRequest) (*pb.BroadcastResponse, error) {
tx, err := libkaspawallet.ExtractTransaction(request.Transaction, s.keysFile.ECDSA) s.lock.Lock()
defer s.lock.Unlock()
txIDs, err := s.broadcast(request.Transactions, request.IsDomain)
if err != nil { if err != nil {
return nil, err return nil, err
} }
txID, err := sendTransaction(s.rpcClient, tx) return &pb.BroadcastResponse{TxIDs: txIDs}, nil
if err != nil { }
return nil, err
func (s *server) broadcast(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
}
}
txIDs[i], err = sendTransaction(s.rpcClient, tx)
if err != nil {
return nil, err
}
for _, input := range tx.Inputs {
s.usedOutpoints[input.PreviousOutpoint] = time.Now()
}
} }
return &pb.BroadcastResponse{TxID: txID}, nil s.forceSync()
return txIDs, nil
} }
func sendTransaction(client *rpcclient.RPCClient, tx *externalapi.DomainTransaction) (string, error) { 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 { if err != nil {
return "", errors.Wrapf(err, "error submitting transaction") return "", errors.Wrapf(err, "error submitting transaction")
} }

View 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
}

View 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
}

View File

@ -2,60 +2,37 @@ package server
import ( import (
"context" "context"
"fmt"
"math"
"github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/pb" "github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/pb"
"github.com/kaspanet/kaspad/cmd/kaspawallet/libkaspawallet" "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/constants"
"github.com/kaspanet/kaspad/domain/consensus/utils/utxo"
"github.com/kaspanet/kaspad/util" "github.com/kaspanet/kaspad/util"
"github.com/pkg/errors" "github.com/pkg/errors"
) )
// TODO: Implement a better fee estimation mechanism // The minimal change amount to target in order to avoid large storage mass (see KIP9 for more details).
const feePerInput = 10000 // 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) ( func (s *server) CreateUnsignedTransactions(_ context.Context, request *pb.CreateUnsignedTransactionsRequest) (
*pb.CreateUnsignedTransactionsResponse, error) { *pb.CreateUnsignedTransactionsResponse, error,
) {
s.lock.Lock() s.lock.Lock()
defer s.lock.Unlock() defer s.lock.Unlock()
if !s.isSynced() { unsignedTransactions, err := s.createUnsignedTransactions(request.Address, request.Amount, request.IsSendAll,
return nil, errors.New("server is not synced") request.From, request.UseExistingChangeAddress, request.FeePolicy)
}
err := s.refreshUTXOs()
if err != nil {
return nil, err
}
toAddress, err := util.DecodeAddress(request.Address, s.params.Prefix)
if err != nil {
return nil, err
}
selectedUTXOs, changeSompi, err := s.selectUTXOs(request.Amount, feePerInput)
if err != nil {
return nil, err
}
changeAddress, changeWalletAddress, err := s.changeAddress()
if err != nil {
return nil, err
}
unsignedTransaction, err := libkaspawallet.CreateUnsignedTransaction(s.keysFile.ExtendedPublicKeys,
s.keysFile.MinimumSignatures,
[]*libkaspawallet.Payment{{
Address: toAddress,
Amount: request.Amount,
}, {
Address: changeAddress,
Amount: changeSompi,
}}, selectedUTXOs)
if err != nil {
return nil, err
}
unsignedTransactions, err := s.maybeAutoCompoundTransaction(unsignedTransaction, toAddress, changeAddress, changeWalletAddress)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -63,20 +40,153 @@ func (s *server) CreateUnsignedTransactions(_ context.Context, request *pb.Creat
return &pb.CreateUnsignedTransactionsResponse{UnsignedTransactions: unsignedTransactions}, nil return &pb.CreateUnsignedTransactionsResponse{UnsignedTransactions: unsignedTransactions}, nil
} }
func (s *server) selectUTXOs(spendAmount uint64, feePerInput uint64) ( func (s *server) calculateFeeLimits(requestFeePolicy *pb.FeePolicy) (feeRate float64, maxFee uint64, err error) {
selectedUTXOs []*libkaspawallet.UTXO, changeSompi uint64, err error) { feeRate = minFeeRate
maxFee = math.MaxUint64
selectedUTXOs = []*libkaspawallet.UTXO{} 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)
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)
}
fromAddresses = append(fromAddresses, fromAddress)
}
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: spendValue,
}}
if changeSompi > 0 {
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, feeRate, maxFee)
if err != nil {
return nil, err
}
return unsignedTransactions, nil
}
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) totalValue := uint64(0)
dagInfo, err := s.rpcClient.GetBlockDAGInfo() dagInfo, err := s.rpcClient.GetBlockDAGInfo()
if err != nil { if err != nil {
return nil, 0, err return nil, 0, 0, err
} }
for _, utxo := range s.utxosSortedByAmount { var fee uint64
if !isUTXOSpendable(utxo, dagInfo.VirtualDAAScore, s.params.BlockCoinbaseMaturity) { iteration := func(utxo *walletUTXO, avoidPreselected bool) (bool, error) {
continue if (fromAddresses != nil && !walletAddressesContain(fromAddresses, utxo.address)) ||
!s.isUTXOSpendable(utxo, dagInfo.VirtualDAAScore) {
return true, nil
}
if broadcastTime, ok := s.usedOutpoints[*utxo.Outpoint]; ok {
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
}
} }
selectedUTXOs = append(selectedUTXOs, &libkaspawallet.UTXO{ selectedUTXOs = append(selectedUTXOs, &libkaspawallet.UTXO{
@ -84,21 +194,171 @@ func (s *server) selectUTXOs(spendAmount uint64, feePerInput uint64) (
UTXOEntry: utxo.UTXOEntry, UTXOEntry: utxo.UTXOEntry,
DerivationPath: s.walletAddressPath(utxo.address), 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 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 break
} }
} }
fee := feePerInput * uint64(len(selectedUTXOs)) if shouldContinue {
totalSpend := spendAmount + fee 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 { 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) 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
} }

View File

@ -0,0 +1,75 @@
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/util"
)
func (s *server) GetExternalSpendableUTXOs(_ context.Context, request *pb.GetExternalSpendableUTXOsRequest) (*pb.GetExternalSpendableUTXOsResponse, error) {
s.lock.RLock()
defer s.lock.RUnlock()
_, err := util.DecodeAddress(request.Address, s.params.Prefix)
if err != nil {
return nil, err
}
externalUTXOs, err := s.rpcClient.GetUTXOsByAddresses([]string{request.Address})
if err != nil {
return nil, err
}
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
}
return &pb.GetExternalSpendableUTXOsResponse{
Entries: selectedUTXOs,
}, nil
}
func (s *server) selectExternalSpendableUTXOs(externalUTXOs *appmessage.GetUTXOsByAddressesResponseMessage, feeRate float64) ([]*pb.UtxosByAddressesEntry, error) {
dagInfo, err := s.rpcClient.GetBlockDAGInfo()
if err != nil {
return nil, err
}
daaScore := dagInfo.VirtualDAAScore
maturity := s.params.BlockCoinbaseMaturity
//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, feePerInput) {
continue
}
selectedExternalUtxos = append(selectedExternalUtxos, libkaspawallet.AppMessageUTXOToKaspawalletdUTXO(entry))
}
return selectedExternalUtxos, nil
}
func isExternalUTXOSpendable(entry *appmessage.UTXOsByAddressesEntry, virtualDAAScore uint64, coinbaseMaturity uint64, feePerInput uint64) bool {
if !entry.UTXOEntry.IsCoinbase {
return true
} else if entry.UTXOEntry.Amount <= feePerInput {
return false
}
return entry.UTXOEntry.BlockDAAScore+coinbaseMaturity < virtualDAAScore
}

View File

@ -1,15 +1,26 @@
package server package server
import ( import (
"time"
"github.com/kaspanet/kaspad/domain/dagconfig" "github.com/kaspanet/kaspad/domain/dagconfig"
"github.com/kaspanet/kaspad/infrastructure/network/rpcclient" "github.com/kaspanet/kaspad/infrastructure/network/rpcclient"
) )
func connectToRPC(params *dagconfig.Params, rpcServer string) (*rpcclient.RPCClient, error) { func connectToRPC(params *dagconfig.Params, rpcServer string, timeout uint32) (*rpcclient.RPCClient, error) {
rpcAddress, err := params.NormalizeRPCServerAddress(rpcServer) rpcAddress, err := params.NormalizeRPCServerAddress(rpcServer)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return rpcclient.NewRPCClient(rpcAddress) rpcClient, err := rpcclient.NewRPCClient(rpcAddress)
if err != nil {
return nil, err
}
if timeout != 0 {
rpcClient.SetTimeout(time.Duration(timeout) * time.Second)
}
return rpcClient, err
} }

View File

@ -0,0 +1,32 @@
package server
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.IsSendAll,
request.From, request.UseExistingChangeAddress, request.FeePolicy)
if err != nil {
return nil, err
}
signedTransactions, err := s.signTransactions(unsignedTransactions, request.Password)
if err != nil {
return nil, err
}
txIDs, err := s.broadcast(signedTransactions, false)
if err != nil {
return nil, errors.Wrapf(err, "error broadcasting transactions %s", EncodeTransactionsToHex(signedTransactions))
}
return &pb.SendResponse{TxIDs: txIDs, SignedTransactions: signedTransactions}, nil
}

View File

@ -5,8 +5,13 @@ import (
"net" "net"
"os" "os"
"sync" "sync"
"sync/atomic"
"time" "time"
"github.com/kaspanet/kaspad/version"
"github.com/kaspanet/kaspad/domain/consensus/model/externalapi"
"github.com/kaspanet/kaspad/util/txmass" "github.com/kaspanet/kaspad/util/txmass"
"github.com/kaspanet/kaspad/util/profiling" "github.com/kaspanet/kaspad/util/profiling"
@ -25,20 +30,35 @@ import (
type server struct { type server struct {
pb.UnimplementedKaspawalletdServer pb.UnimplementedKaspawalletdServer
rpcClient *rpcclient.RPCClient rpcClient *rpcclient.RPCClient // RPC client for ongoing user requests
params *dagconfig.Params backgroundRPCClient *rpcclient.RPCClient // RPC client dedicated for address and UTXO background fetching
params *dagconfig.Params
coinbaseMaturity uint64 // Different from go-kaspad default following Crescendo
lock sync.RWMutex lock sync.RWMutex
utxosSortedByAmount []*walletUTXO utxosSortedByAmount []*walletUTXO
nextSyncStartIndex uint32 mempoolExcludedUTXOs map[externalapi.DomainOutpoint]*walletUTXO
keysFile *keys.File nextSyncStartIndex uint32
shutdown chan struct{} keysFile *keys.File
addressSet walletAddressSet shutdown chan struct{}
txMassCalculator *txmass.Calculator forceSyncChan chan struct{}
startTimeOfLastCompletedRefresh time.Time
addressSet walletAddressSet
txMassCalculator *txmass.Calculator
usedOutpoints map[externalapi.DomainOutpoint]time.Time
firstSyncDone atomic.Bool
isLogFinalProgressLineShown bool
maxUsedAddressesForLog uint32
maxProcessedAddressesForLog uint32
} }
// MaxDaemonSendMsgSize is the max send message size used for the daemon server.
// Currently, set to 100MB
const MaxDaemonSendMsgSize = 100_000_000
// Start starts the kaspawalletd server // Start starts the kaspawalletd server
func Start(params *dagconfig.Params, listen, rpcServer string, keysFilePath string, profile string) error { func Start(params *dagconfig.Params, listen, rpcServer string, keysFilePath string, profile string, timeout uint32) error {
initLog(defaultLogFile, defaultErrLogFile) initLog(defaultLogFile, defaultErrLogFile)
defer panics.HandlePanic(log, "MAIN", nil) defer panics.HandlePanic(log, "MAIN", nil)
@ -48,41 +68,65 @@ func Start(params *dagconfig.Params, listen, rpcServer string, keysFilePath stri
profiling.Start(profile, log) profiling.Start(profile, log)
} }
log.Infof("Version %s", version.Version())
listener, err := net.Listen("tcp", listen) listener, err := net.Listen("tcp", listen)
if err != nil { if err != nil {
return (errors.Wrapf(err, "Error listening to tcp at %s", listen)) return (errors.Wrapf(err, "Error listening to TCP on %s", listen))
} }
log.Infof("Listening on %s", listen) log.Infof("Listening to TCP on %s", listen)
rpcClient, err := connectToRPC(params, rpcServer) log.Infof("Connecting to a node at %s...", rpcServer)
rpcClient, err := connectToRPC(params, rpcServer, timeout)
if err != nil { if err != nil {
return (errors.Wrapf(err, "Error connecting to RPC server %s", rpcServer)) 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) keysFile, err := keys.ReadKeysFile(params, keysFilePath)
if err != nil { if err != nil {
return (errors.Wrapf(err, "Error connecting to RPC server %s", rpcServer)) return (errors.Wrapf(err, "Error reading keys file %s", keysFilePath))
} }
err = keysFile.TryLock()
if err != nil {
return err
}
// Post-Crescendo coinbase maturity
coinbaseMaturity := uint64(1000)
serverInstance := &server{ serverInstance := &server{
rpcClient: rpcClient, rpcClient: rpcClient,
params: params, backgroundRPCClient: backgroundRPCClient,
utxosSortedByAmount: []*walletUTXO{}, params: params,
nextSyncStartIndex: 0, coinbaseMaturity: coinbaseMaturity,
keysFile: keysFile, utxosSortedByAmount: []*walletUTXO{},
shutdown: make(chan struct{}), mempoolExcludedUTXOs: map[externalapi.DomainOutpoint]*walletUTXO{},
addressSet: make(walletAddressSet), nextSyncStartIndex: 0,
txMassCalculator: txmass.NewCalculator(params.MassPerTxByte, params.MassPerScriptPubKeyByte, params.MassPerSigOp), 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{},
isLogFinalProgressLineShown: false,
maxUsedAddressesForLog: 0,
maxProcessedAddressesForLog: 0,
} }
spawn("serverInstance.sync", func() { log.Infof("Read, syncing the wallet...")
err := serverInstance.sync() spawn("serverInstance.syncLoop", func() {
err := serverInstance.syncLoop()
if err != nil { if err != nil {
printErrorAndExit(errors.Wrap(err, "error syncing the wallet")) printErrorAndExit(errors.Wrap(err, "error syncing the wallet"))
} }
}) })
grpcServer := grpc.NewServer() grpcServer := grpc.NewServer(grpc.MaxSendMsgSize(MaxDaemonSendMsgSize))
pb.RegisterKaspawalletdServer(grpcServer, serverInstance) pb.RegisterKaspawalletdServer(grpcServer, serverInstance)
spawn("grpcServer.Serve", func() { spawn("grpcServer.Serve", func() {

Some files were not shown because too many files have changed in this diff Show More