Compare commits

..

61 Commits

Author SHA1 Message Date
Mike Zak
45ba3bec43 Merge remote-tracking branch 'upstream/v0.11.0-dev' into mempool-redesign 2021-06-09 15:05:14 +03:00
Mike Zak
befc0172ee Add allowOrphans to ValidateAndInsertTransaction stub 2021-06-09 14:56:27 +03:00
Mike Zak
82e807cf5a Use maximumOrphanTransactionCount 2021-06-09 14:53:31 +03:00
Mike Zak
bfa804b300 If a transaction was removed from the mempool - update it's redeemers in orphan pool as well 2021-06-09 12:13:39 +03:00
Constantine Bitensky
79c74c482b Get connections from Seeder when no connections left (#1742)
Co-authored-by: Constantine Bitensky <constantinebitensky@gmail.com>
2021-06-08 17:42:13 +03:00
Mike Zak
7115b54d3f Implement RemoveTransaction 2021-06-08 17:39:10 +03:00
Mike Zak
25cc6184d6 Implement transactionsPool.addTransaction 2021-06-08 16:30:53 +03:00
Mike Zak
4e46be67a4 Implemented processOrphansAfterAcceptedTransaction 2021-06-08 16:05:39 +03:00
Mike Zak
35eb4b9d87 Implement maybeAddOrphan and AddOrphan 2021-06-08 15:45:08 +03:00
Ori Newman
3b0394eefe Skip solving the block if SkipProofOfWork (#1741) 2021-06-08 15:44:27 +03:00
Mike Zak
2402d48c3e Implement removeOrphan 2021-06-08 14:35:54 +03:00
Mike Zak
9a1d548e7f Implemented removeOrphan 2021-06-08 14:00:26 +03:00
Mike Zak
65da2462b8 Implemented mempoolUTXOSet.checkDoubleSpends 2021-06-08 10:44:02 +03:00
Mike Zak
4335a8eaf8 Implement removeTransaction, remove sanity checks 2021-06-08 10:35:52 +03:00
Mike Zak
d88fe305c5 Implemented mempoolUTXOSet.addTransaction 2021-06-08 10:27:06 +03:00
Mike Zak
099f023b5b Implement getParentsInPool 2021-06-08 10:16:45 +03:00
Mike Zak
2294559198 Move all model objects to model package 2021-06-08 10:01:46 +03:00
Mike Zak
71875c99d6 Invert the condition for banning when mempool rejects a transaction 2021-06-07 14:07:25 +03:00
Mike Zak
c099fd2986 Add error.go to mempool 2021-06-07 11:46:31 +03:00
Mike Zak
17357a0fca Orphan maps should be idToOrphan 2021-06-07 10:21:36 +03:00
Mike Zak
01edc43e36 Imeplement transactionsOrderedByFeeRate 2021-06-07 10:08:33 +03:00
Mike Zak
b412cd9bbc Revert "Rename isHighPriority to neverExpires"
This reverts commit b2da9a4a00.
2021-06-07 09:50:26 +03:00
tal
43e6467ff1 Fix merge errors 2021-06-06 17:00:29 +03:00
stasatdaglabs
363494ef7a Implement NotifyVirtualDaaScoreChanged (#1737)
* Add notifyVirtualDaaScoreChanged to protowire.

* Add notifyVirtualDaaScoreChanged to the rest of kaspad.

* Add notifyVirtualDaaScoreChanged to the rest of kaspad.

* Test the DAA score notification in TestVirtualSelectedParentBlueScore.

* Rename TestVirtualSelectedParentBlueScore to TestVirtualSelectedParentBlueScoreAndVirtualDAAScore.

(cherry picked from commit 83e631548f)
2021-06-06 16:46:02 +03:00
Mike Zak
e5e266ff0b Add stub for checkDoubleSpends 2021-06-06 16:31:00 +03:00
Mike Zak
b2da9a4a00 Rename isHighPriority to neverExpires 2021-06-06 14:39:28 +03:00
Mike Zak
ff10ce145a implement expireOldTransactions and expireOrphanTransactions 2021-06-06 14:13:19 +03:00
Mike Zak
619b7ab8cd Implement BlockCandidateTransactions 2021-06-06 12:53:37 +03:00
Mike Zak
04c98ac6df Add constructors to all main objects 2021-06-06 12:49:52 +03:00
Mike Zak
94ddf0aab0 Added model and stubs for all main methods 2021-06-06 12:27:25 +03:00
cbitensky
d1df97c4c5 added --password and --yes cmdline parameters (#1735)
* added --password and --yes cmdline parameters

* Renamed:
confPassword -> cmdLinePassword
yes -> forceOverride

Added password in ImportMnemonics

Co-authored-by: Ori Newman <orinewman1@gmail.com>
2021-06-01 15:36:18 +03:00
Svarog
4f52a6de51 Check if err != nil returned from ConnectionManager.Ban() (#1736) 2021-05-31 10:50:12 +03:00
Svarog
4f4a8934e7 Add option to specify blockHash in EstimateNetworkHashesPerSecond (#1731)
* Add BlockHash optional parameter to EstimateNetworkBlockHashesPerSecond

* Allow to pass '-' for optional values in kaspactl

* Solve a division-by-zero in estimateNetworkHashesPerSecond

* Add BlockHash to toAppMessage/fromAppMessage functions

* Rename: topHash -> StartHash

* Return proper error message if provided startHash doesn't deserialize into a hash
2021-05-27 14:59:29 +03:00
Svarog
16ba2bd312 Export DefaultPath + Add logging to kaspawallet daemon (#1730)
* Export DefaultPath + Add logging to kaspawallet daemon

* Export purpose and CoinType instead of defaultPath

* Move TODO to correct place
2021-05-26 18:51:11 +03:00
Svarog
6613faee2d Invert coin-type and purpose in default derivation path (#1728) 2021-05-20 17:43:07 +03:00
Ori Newman
edc459ae1b Improve pickVirtualParents performance and add many-tips to the stability tests suite (#1725)
* First limit the candidates size to 3*csm.maxBlockParents before taking the bottom csm.maxBlockParents/2

* Change log level of printing all tips to Tracef

* Add many-tips to run-fast.sh and run-slow.sh

* Fix preallocation size

* Assign intermediate variables
2021-05-19 16:06:52 +03:00
talelbaz
d7f2cf81c0 Change merge set order to topological order (#1654)
* Change mergeSet to be ordered topologically.

* Add special condition for genesis.

* Add check that the coinbase is validated.

* Change names of variables(old: chainHash, blueHash).

* Fix the DAG diagram in the comment above the function.

* Fix variables names.

Co-authored-by: tal <tal@daglabs.com>
Co-authored-by: Ori Newman <orinewman1@gmail.com>
2021-05-19 14:40:55 +03:00
Ori Newman
4658f9d05c Implement BIP 39 and HD wallet features (#1705)
* Naive bip39 with address reuse

* Avoid address reuse in libkaspawallet

* Add wallet daemon

* Use daemon everywhere

* Add forceOverride

* Make CreateUnsignedTransaction endpoint receive amount in sompis

* Collect close UTXOs

* Filter out non-spendable UTXOs from selectUTXOs

* Use different paths for multisig and non multisig

* Fix tests to use non zero path

* Fix multisig cosigner index detection

* Add comments

* Fix dump_unencrypted_data.go according to bip39 and bip32

* Fix wrong derivation path for multisig on wallet creation

* Remove IsSynced endpoint and add validation if wallet is synced for the relevant endpoints

* Rename server address to daemon address

* Fix capacity for extendedPublicKeys

* Use ReadBytes instead of ReadLine

* Add validation when importing

* Increment before using index value, and use it as is

* Save keys file exactly where needed

* Use %+v printErrorAndExit

* Remove redundant consts

* Rnemae collectCloseUTXOs and collectFarUTXOs

* Move typedefs around

* Add comment to addressesToQuery

* Update collectUTXOsFromRecentAddresses comment about locks

* Split collectUTXOs to small functions

* Add sanity check

* Add addEntryToUTXOSet function

* Change validateIsSynced to isSynced

* Simplify createKeyPairsFromFunction logic

* Rename .Sync() to .Save()

* Fix typo

* Create bip39BitSize const

* Add consts to purposes

* Add multisig check for 'send'

* Rename updatedPSTxBytes to partiallySignedTransaction

* Change collectUTXOsFromFarAddresses's comment

* Use setters for last used indexes

* Don't use the pstx acronym

* Fix SetPath

* Remove spaces when reading lines

* Fix walletserver to daemonaddress

* Fix isUTXOSpendable to use DAA score

Co-authored-by: Svarog <feanorr@gmail.com>
2021-05-19 10:03:23 +03:00
Ori Newman
010df3b0d3 Rename IncludeTransactionVerboseData flag to IncludeTransactions (#1717)
* Rename IncludeTransactionVerboseData flag to IncludeTransactions

* Regenerate auto-generated files
2021-05-18 17:40:06 +03:00
stasatdaglabs
346598e67f Fix merge errors. 2021-05-18 17:09:11 +03:00
Ori Newman
268906a7ce Add VirtualDaaScore to GetBlockDagInfo (#1719)
Co-authored-by: Svarog <feanorr@gmail.com>

(cherry picked from commit 36c56f73bf)
2021-05-18 17:07:07 +03:00
stasatdaglabs
befc60b185 Update changelog.txt for v0.10.2.
(cherry picked from commit 91866dd61c)
2021-05-18 16:37:10 +03:00
Ori Newman
dd3e04e671 Fix overflow when checking coinbase maturity and don't ban peers that send transactions with immature spend (#1722)
* Fix overflow when checking coinbase maturity and don't ban peers that send transactions with immature spend

* Fix tests

Co-authored-by: Svarog <feanorr@gmail.com>
(cherry picked from commit a18f2f8802)
2021-05-18 16:33:29 +03:00
stasatdaglabs
9c743db4d6 Fix merge errors. 2021-05-18 16:33:01 +03:00
Ori Newman
eb3dba5c88 Fix calcTxSequenceLockFromReferencedUTXOEntries for loop break condition (#1723)
Co-authored-by: Svarog <feanorr@gmail.com>
(cherry picked from commit 04dc1947ff)
2021-05-18 16:30:26 +03:00
stasatdaglabs
e46e2580b1 Add VirtualDaaScore to GetBlockDagInfo (#1719)
Co-authored-by: Svarog <feanorr@gmail.com>
(cherry picked from commit 36c56f73bf)

# Conflicts:
#	infrastructure/network/netadapter/server/grpcserver/protowire/rpc.pb.go
2021-05-18 16:28:57 +03:00
Ori Newman
414f58fb90 serializeAddress should always serialize as IPv6, since it assumes the IP size is 16 bytes (#1720)
(cherry picked from commit b76ca41109)
2021-05-18 16:05:36 +03:00
Ori Newman
9df80957b1 Fix getBlock and getBlocks RPC commands to return blocks and transactions properly (#1716)
* Fix getBlock RPC command to return transactions

* Fix getBlocks RPC command to return transactions and blocks

* Add GetBlockEvenIfHeaderOnly and use it for getBlock and getBlocks

* Implement GetBlockEvenIfHeaderOnly for fakeRelayInvsContext

* Use less nested code

(cherry picked from commit 50fd86e287)
2021-05-13 16:00:42 +03:00
stasatdaglabs
268c9fa83c Update changelog for v0.10.1.
(cherry picked from commit 9e0b50c0dd)
2021-05-11 12:14:04 +03:00
Ori Newman
2e3592e351 Calculate virtual's acceptance data and multiset after importing a new pruning point (#1700)
(cherry picked from commit b405ea50e5)
2021-05-11 12:14:00 +03:00
talelbaz
19718ac102 Change removeTransactionAndItsChainedTransactions to be non-recursive (#1696)
* Change removeTransactionAndItsChainedTransactions to be non-recursive

* Split the variables assigning.

* Change names of function and variables.

* Append the correct queue.

Co-authored-by: tal <tal@daglabs.com>
Co-authored-by: stasatdaglabs <39559713+stasatdaglabs@users.noreply.github.com>
2021-05-10 15:21:48 +03:00
talelbaz
28a8e96e65 New stability test - many tips (#1694)
* Unfinished code.

* Update the testnet version to testnet-5. (#1683)

* Generalize stability-tests/docker/Dockerfile. (#1685)

* Committed for rebasing.

* Adds stability-test many-tips, which tests kaspad handling with many tips in the DAG.

* Delete manytips_test.go.

* Add timeout to the test and create only one RPC client.

* Place the spawn before the for loop and remove a redundant condition.

Co-authored-by: tal <tal@daglabs.com>
Co-authored-by: stasatdaglabs <39559713+stasatdaglabs@users.noreply.github.com>
2021-05-10 15:03:20 +03:00
Ori Newman
4df283934a Add a link to the kaspanet github project in the README (#1698)
* Add link to project

* Fix README.md
2021-05-04 11:56:51 +03:00
talelbaz
ab89efe3dc Update relayTransactions unit tests. (#1623)
* [NOD-1344] relaytransactions: simple unit tests

* [NOD-1344]  Add mid-complexity unit tests for relaytransactions
* Improve TestHandleRelayedTransactionssub tests
* Improve TestHandleRequestedTransactions sub tests

* [NOD-1344]  Fix Simple call test

* [NOD-1344]  Fix tests after redesign

* Divide transactionrelay_test.go to 2 separated tests and updates the tests.

* Changes due to review:change the test files name and the test function names, adds new comments and fix typo.

* Delete an unnecessary comparison to True in the if statement condition.

* Update the branch to v0.11.0-dev.

Co-authored-by: karim1king <karimkaspersky@yahoo.com>
Co-authored-by: tal <tal@daglabs.com>
Co-authored-by: Svarog <feanorr@gmail.com>
2021-05-02 16:16:57 +03:00
Ori Newman
fa16c30cf3 Implement bip32 (#1676)
* Implement bip32

* Unite private and public extended keys

* Change variable names

* Change test name and add comment

* Rename var name

* Rename ckd.go to child_key_derivation.go

* Rename ser32 -> serializeUint32

* Add PrivateKey method

* Rename Path -> DeriveFromPath

* Add comment to validateChecksum

* Remove redundant condition from parsePath

* Rename Fingerprint->ParentFingerprint

* Merge hardened and non-hardened paths in calcI

* Change fingerPrintFromPoint to method

* Move hash160 to hash.go

* Fix a bug in calcI

* Simplify doubleSha256

* Remove slice end bound

* Split long line

* Change KaspaMainnetPrivate/public to represent kprv/kpub

* Add comments

* Fix comment

* Copy base58 library to kaspad

* Add versions for all networks

* Change versions to hex

* Add comments

Co-authored-by: Svarog <feanorr@gmail.com>
Co-authored-by: Elichai Turkel <elichai.turkel@gmail.com>
2021-04-28 15:27:16 +03:00
stasatdaglabs
c28366eb50 Add v0.10.0 to the changelog. (#1692)
(cherry picked from commit 9dd8136e4b)
2021-04-26 15:12:50 +03:00
stasatdaglabs
dc0bf56bf3 Fix the mempool-limits stability test (#1690)
* Add -v to the `go test` command.

* Generate a new keypair for mempool-limits.

* Set mempool-limits to time out only after 24 hours.

(cherry picked from commit eb1703b948)
2021-04-26 15:12:46 +03:00
stasatdaglabs
91de1807ad Generalize stability-tests/docker/Dockerfile. (#1685)
(cherry picked from commit a6da3251d0)
2021-04-26 15:12:44 +03:00
stasatdaglabs
830684167c Update the testnet version to testnet-5. (#1683)
(cherry picked from commit bf198948c4)
2021-04-26 15:12:41 +03:00
stasatdaglabs
1f56a68a28 Add an RPC command: EstimateNetworkHashesPerSecond (#1686)
* Implement EstimateNetworkHashesPerSecond.

* Fix failing tests.

* Add request/response messages to the .proto files.

* Add the EstimateNetworkHashesPerSecond RPC command.

* Add the EstimateNetworkHashesPerSecond RPC client function.

* Add the EstimateNetworkHashesPerSecond RPC command to kaspactl.

* Disallow windowSize lesser than 2.

* Fix wrong scale (milliseconds instead of seconds).

* Handle windowHashes being 0.
2021-04-22 15:18:21 +03:00
stasatdaglabs
13a6b4cc51 Update to version 0.11.0 2021-04-20 13:40:11 +03:00
156 changed files with 9368 additions and 2639 deletions

View File

@@ -63,6 +63,8 @@ Join our discord server using the following link: https://discord.gg/YNYnNN5Pf2
The [integrated github issue tracker](https://github.com/kaspanet/kaspad/issues)
is used for this project.
Issue priorities may be seen at https://github.com/orgs/kaspanet/projects/4
## Documentation
The [documentation](https://github.com/kaspanet/docs) is a work-in-progress

View File

@@ -163,14 +163,10 @@ func outpointToDomainOutpoint(outpoint *Outpoint) *externalapi.DomainOutpoint {
func RPCTransactionToDomainTransaction(rpcTransaction *RPCTransaction) (*externalapi.DomainTransaction, error) {
inputs := make([]*externalapi.DomainTransactionInput, len(rpcTransaction.Inputs))
for i, input := range rpcTransaction.Inputs {
transactionID, err := transactionid.FromString(input.PreviousOutpoint.TransactionID)
previousOutpoint, err := RPCOutpointToDomainOutpoint(input.PreviousOutpoint)
if err != nil {
return nil, err
}
previousOutpoint := &externalapi.DomainOutpoint{
TransactionID: *transactionID,
Index: input.PreviousOutpoint.Index,
}
signatureScript, err := hex.DecodeString(input.SignatureScript)
if err != nil {
return nil, err
@@ -213,6 +209,36 @@ func RPCTransactionToDomainTransaction(rpcTransaction *RPCTransaction) (*externa
}, nil
}
// RPCOutpointToDomainOutpoint converts RPCOutpoint to DomainOutpoint
func RPCOutpointToDomainOutpoint(outpoint *RPCOutpoint) (*externalapi.DomainOutpoint, error) {
transactionID, err := transactionid.FromString(outpoint.TransactionID)
if err != nil {
return nil, err
}
return &externalapi.DomainOutpoint{
TransactionID: *transactionID,
Index: outpoint.Index,
}, nil
}
// RPCUTXOEntryToUTXOEntry converts RPCUTXOEntry to UTXOEntry
func RPCUTXOEntryToUTXOEntry(entry *RPCUTXOEntry) (externalapi.UTXOEntry, error) {
script, err := hex.DecodeString(entry.ScriptPublicKey.Script)
if err != nil {
return nil, err
}
return utxo.NewUTXOEntry(
entry.Amount,
&externalapi.ScriptPublicKey{
Script: script,
Version: entry.ScriptPublicKey.Version,
},
entry.IsCoinbase,
entry.BlockDAAScore,
), nil
}
// DomainTransactionToRPCTransaction converts DomainTransactions to RPCTransactions
func DomainTransactionToRPCTransaction(transaction *externalapi.DomainTransaction) *RPCTransaction {
inputs := make([]*RPCTransactionInput, len(transaction.Inputs))
@@ -257,22 +283,27 @@ func OutpointAndUTXOEntryPairsToDomainOutpointAndUTXOEntryPairs(
domainOutpointAndUTXOEntryPairs := make([]*externalapi.OutpointAndUTXOEntryPair, len(outpointAndUTXOEntryPairs))
for i, outpointAndUTXOEntryPair := range outpointAndUTXOEntryPairs {
domainOutpointAndUTXOEntryPairs[i] = &externalapi.OutpointAndUTXOEntryPair{
Outpoint: &externalapi.DomainOutpoint{
TransactionID: outpointAndUTXOEntryPair.Outpoint.TxID,
Index: outpointAndUTXOEntryPair.Outpoint.Index,
},
UTXOEntry: utxo.NewUTXOEntry(
outpointAndUTXOEntryPair.UTXOEntry.Amount,
outpointAndUTXOEntryPair.UTXOEntry.ScriptPublicKey,
outpointAndUTXOEntryPair.UTXOEntry.IsCoinbase,
outpointAndUTXOEntryPair.UTXOEntry.BlockDAAScore,
),
}
domainOutpointAndUTXOEntryPairs[i] = outpointAndUTXOEntryPairToDomainOutpointAndUTXOEntryPair(outpointAndUTXOEntryPair)
}
return domainOutpointAndUTXOEntryPairs
}
func outpointAndUTXOEntryPairToDomainOutpointAndUTXOEntryPair(
outpointAndUTXOEntryPair *OutpointAndUTXOEntryPair) *externalapi.OutpointAndUTXOEntryPair {
return &externalapi.OutpointAndUTXOEntryPair{
Outpoint: &externalapi.DomainOutpoint{
TransactionID: outpointAndUTXOEntryPair.Outpoint.TxID,
Index: outpointAndUTXOEntryPair.Outpoint.Index,
},
UTXOEntry: utxo.NewUTXOEntry(
outpointAndUTXOEntryPair.UTXOEntry.Amount,
outpointAndUTXOEntryPair.UTXOEntry.ScriptPublicKey,
outpointAndUTXOEntryPair.UTXOEntry.IsCoinbase,
outpointAndUTXOEntryPair.UTXOEntry.BlockDAAScore,
),
}
}
// DomainOutpointAndUTXOEntryPairsToOutpointAndUTXOEntryPairs converts
// domain OutpointAndUTXOEntryPairs to OutpointAndUTXOEntryPairs
func DomainOutpointAndUTXOEntryPairsToOutpointAndUTXOEntryPairs(

View File

@@ -137,6 +137,11 @@ const (
CmdPruningPointUTXOSetOverrideNotificationMessage
CmdStopNotifyingPruningPointUTXOSetOverrideRequestMessage
CmdStopNotifyingPruningPointUTXOSetOverrideResponseMessage
CmdEstimateNetworkHashesPerSecondRequestMessage
CmdEstimateNetworkHashesPerSecondResponseMessage
CmdNotifyVirtualDaaScoreChangedRequestMessage
CmdNotifyVirtualDaaScoreChangedResponseMessage
CmdVirtualDaaScoreChangedNotificationMessage
)
// ProtocolMessageCommandToString maps all MessageCommands to their string representation
@@ -248,6 +253,11 @@ var RPCMessageCommandToString = map[MessageCommand]string{
CmdPruningPointUTXOSetOverrideNotificationMessage: "PruningPointUTXOSetOverrideNotification",
CmdStopNotifyingPruningPointUTXOSetOverrideRequestMessage: "StopNotifyingPruningPointUTXOSetOverrideRequest",
CmdStopNotifyingPruningPointUTXOSetOverrideResponseMessage: "StopNotifyingPruningPointUTXOSetOverrideResponse",
CmdEstimateNetworkHashesPerSecondRequestMessage: "EstimateNetworkHashesPerSecondRequest",
CmdEstimateNetworkHashesPerSecondResponseMessage: "EstimateNetworkHashesPerSecondResponse",
CmdNotifyVirtualDaaScoreChangedRequestMessage: "NotifyVirtualDaaScoreChangedRequest",
CmdNotifyVirtualDaaScoreChangedResponseMessage: "NotifyVirtualDaaScoreChangedResponse",
CmdVirtualDaaScoreChangedNotificationMessage: "VirtualDaaScoreChangedNotification",
}
// Message is an interface that describes a kaspa message. A type that

View File

@@ -0,0 +1,43 @@
package appmessage
// EstimateNetworkHashesPerSecondRequestMessage is an appmessage corresponding to
// its respective RPC message
type EstimateNetworkHashesPerSecondRequestMessage struct {
baseMessage
StartHash string
WindowSize uint32
}
// Command returns the protocol command string for the message
func (msg *EstimateNetworkHashesPerSecondRequestMessage) Command() MessageCommand {
return CmdEstimateNetworkHashesPerSecondRequestMessage
}
// NewEstimateNetworkHashesPerSecondRequestMessage returns a instance of the message
func NewEstimateNetworkHashesPerSecondRequestMessage(startHash string, windowSize uint32) *EstimateNetworkHashesPerSecondRequestMessage {
return &EstimateNetworkHashesPerSecondRequestMessage{
StartHash: startHash,
WindowSize: windowSize,
}
}
// EstimateNetworkHashesPerSecondResponseMessage is an appmessage corresponding to
// its respective RPC message
type EstimateNetworkHashesPerSecondResponseMessage struct {
baseMessage
NetworkHashesPerSecond uint64
Error *RPCError
}
// Command returns the protocol command string for the message
func (msg *EstimateNetworkHashesPerSecondResponseMessage) Command() MessageCommand {
return CmdEstimateNetworkHashesPerSecondResponseMessage
}
// NewEstimateNetworkHashesPerSecondResponseMessage returns a instance of the message
func NewEstimateNetworkHashesPerSecondResponseMessage(networkHashesPerSecond uint64) *EstimateNetworkHashesPerSecondResponseMessage {
return &EstimateNetworkHashesPerSecondResponseMessage{
NetworkHashesPerSecond: networkHashesPerSecond,
}
}

View File

@@ -4,8 +4,8 @@ package appmessage
// its respective RPC message
type GetBlockRequestMessage struct {
baseMessage
Hash string
IncludeTransactionVerboseData bool
Hash string
IncludeTransactions bool
}
// Command returns the protocol command string for the message
@@ -14,10 +14,10 @@ func (msg *GetBlockRequestMessage) Command() MessageCommand {
}
// NewGetBlockRequestMessage returns a instance of the message
func NewGetBlockRequestMessage(hash string, includeTransactionVerboseData bool) *GetBlockRequestMessage {
func NewGetBlockRequestMessage(hash string, includeTransactions bool) *GetBlockRequestMessage {
return &GetBlockRequestMessage{
Hash: hash,
IncludeTransactionVerboseData: includeTransactionVerboseData,
Hash: hash,
IncludeTransactions: includeTransactions,
}
}

View File

@@ -28,6 +28,7 @@ type GetBlockDAGInfoResponseMessage struct {
Difficulty float64
PastMedianTime int64
PruningPointHash string
VirtualDAAScore uint64
Error *RPCError
}

View File

@@ -4,9 +4,9 @@ package appmessage
// its respective RPC message
type GetBlocksRequestMessage struct {
baseMessage
LowHash string
IncludeBlocks bool
IncludeTransactionVerboseData bool
LowHash string
IncludeBlocks bool
IncludeTransactions bool
}
// Command returns the protocol command string for the message
@@ -16,11 +16,11 @@ func (msg *GetBlocksRequestMessage) Command() MessageCommand {
// NewGetBlocksRequestMessage returns a instance of the message
func NewGetBlocksRequestMessage(lowHash string, includeBlocks bool,
includeTransactionVerboseData bool) *GetBlocksRequestMessage {
includeTransactions bool) *GetBlocksRequestMessage {
return &GetBlocksRequestMessage{
LowHash: lowHash,
IncludeBlocks: includeBlocks,
IncludeTransactionVerboseData: includeTransactionVerboseData,
LowHash: lowHash,
IncludeBlocks: includeBlocks,
IncludeTransactions: includeTransactions,
}
}

View File

@@ -0,0 +1,55 @@
package appmessage
// NotifyVirtualDaaScoreChangedRequestMessage is an appmessage corresponding to
// its respective RPC message
type NotifyVirtualDaaScoreChangedRequestMessage struct {
baseMessage
}
// Command returns the protocol command string for the message
func (msg *NotifyVirtualDaaScoreChangedRequestMessage) Command() MessageCommand {
return CmdNotifyVirtualDaaScoreChangedRequestMessage
}
// NewNotifyVirtualDaaScoreChangedRequestMessage returns a instance of the message
func NewNotifyVirtualDaaScoreChangedRequestMessage() *NotifyVirtualDaaScoreChangedRequestMessage {
return &NotifyVirtualDaaScoreChangedRequestMessage{}
}
// NotifyVirtualDaaScoreChangedResponseMessage is an appmessage corresponding to
// its respective RPC message
type NotifyVirtualDaaScoreChangedResponseMessage struct {
baseMessage
Error *RPCError
}
// Command returns the protocol command string for the message
func (msg *NotifyVirtualDaaScoreChangedResponseMessage) Command() MessageCommand {
return CmdNotifyVirtualDaaScoreChangedResponseMessage
}
// NewNotifyVirtualDaaScoreChangedResponseMessage returns a instance of the message
func NewNotifyVirtualDaaScoreChangedResponseMessage() *NotifyVirtualDaaScoreChangedResponseMessage {
return &NotifyVirtualDaaScoreChangedResponseMessage{}
}
// VirtualDaaScoreChangedNotificationMessage is an appmessage corresponding to
// its respective RPC message
type VirtualDaaScoreChangedNotificationMessage struct {
baseMessage
VirtualDaaScore uint64
}
// Command returns the protocol command string for the message
func (msg *VirtualDaaScoreChangedNotificationMessage) Command() MessageCommand {
return CmdVirtualDaaScoreChangedNotificationMessage
}
// NewVirtualDaaScoreChangedNotificationMessage returns a instance of the message
func NewVirtualDaaScoreChangedNotificationMessage(
virtualDaaScore uint64) *VirtualDaaScoreChangedNotificationMessage {
return &VirtualDaaScoreChangedNotificationMessage{
VirtualDaaScore: virtualDaaScore,
}
}

View File

@@ -4,7 +4,6 @@ import (
"fmt"
"sync/atomic"
"github.com/kaspanet/kaspad/app/appmessage"
"github.com/kaspanet/kaspad/app/protocol"
"github.com/kaspanet/kaspad/app/rpc"
"github.com/kaspanet/kaspad/domain"
@@ -14,7 +13,6 @@ import (
infrastructuredatabase "github.com/kaspanet/kaspad/infrastructure/db/database"
"github.com/kaspanet/kaspad/infrastructure/network/addressmanager"
"github.com/kaspanet/kaspad/infrastructure/network/connmanager"
"github.com/kaspanet/kaspad/infrastructure/network/dnsseed"
"github.com/kaspanet/kaspad/infrastructure/network/netadapter"
"github.com/kaspanet/kaspad/infrastructure/network/netadapter/id"
"github.com/kaspanet/kaspad/util/panics"
@@ -46,8 +44,6 @@ func (a *ComponentManager) Start() {
panics.Exit(log, fmt.Sprintf("Error starting the net adapter: %+v", err))
}
a.maybeSeedFromDNS()
a.connectionManager.Start()
}
@@ -157,23 +153,6 @@ func setupRPC(
return rpcManager
}
func (a *ComponentManager) maybeSeedFromDNS() {
if !a.cfg.DisableDNSSeed {
dnsseed.SeedFromDNS(a.cfg.NetParams(), a.cfg.DNSSeed, false, nil,
a.cfg.Lookup, func(addresses []*appmessage.NetAddress) {
// Kaspad uses a lookup of the dns seeder here. Since seeder returns
// IPs of nodes and not its own IP, we can not know real IP of
// source. So we'll take first returned address as source.
a.addressManager.AddAddresses(addresses...)
})
dnsseed.SeedFromGRPC(a.cfg.NetParams(), a.cfg.GRPCSeed, false, nil,
func(addresses []*appmessage.NetAddress) {
a.addressManager.AddAddresses(addresses...)
})
}
}
// P2PNodeID returns the network ID associated with this ComponentManager
func (a *ComponentManager) P2PNodeID() *id.ID {
return a.netAdapter.ID()

View File

@@ -130,6 +130,14 @@ type fakeRelayInvsContext struct {
rwLock sync.RWMutex
}
func (f *fakeRelayInvsContext) EstimateNetworkHashesPerSecond(startHash *externalapi.DomainHash, windowSize int) (uint64, error) {
panic(errors.Errorf("called unimplemented function from test '%s'", f.testName))
}
func (f *fakeRelayInvsContext) GetBlockEvenIfHeaderOnly(blockHash *externalapi.DomainHash) (*externalapi.DomainBlock, error) {
panic("implement me")
}
func (f *fakeRelayInvsContext) GetBlockRelations(blockHash *externalapi.DomainHash) ([]*externalapi.DomainHash, *externalapi.DomainHash, []*externalapi.DomainHash, error) {
panic(errors.Errorf("called unimplemented function from test '%s'", f.testName))
}

View File

@@ -7,7 +7,7 @@ import (
"github.com/kaspanet/kaspad/domain"
"github.com/kaspanet/kaspad/domain/consensus/model/externalapi"
"github.com/kaspanet/kaspad/domain/consensus/utils/consensushashing"
"github.com/kaspanet/kaspad/domain/miningmanager/mempool"
"github.com/kaspanet/kaspad/domain/miningmanager/mempool_old"
"github.com/kaspanet/kaspad/infrastructure/network/netadapter"
"github.com/kaspanet/kaspad/infrastructure/network/netadapter/router"
"github.com/pkg/errors"
@@ -175,15 +175,15 @@ func (flow *handleRelayedTransactionsFlow) receiveTransactions(requestedTransact
err = flow.Domain().MiningManager().ValidateAndInsertTransaction(tx, true)
if err != nil {
ruleErr := &mempool.RuleError{}
ruleErr := &mempool_old.RuleError{}
if !errors.As(err, ruleErr) {
return errors.Wrapf(err, "failed to process transaction %s", txID)
}
shouldBan := true
if txRuleErr := (&mempool.TxRuleError{}); errors.As(ruleErr.Err, txRuleErr) {
if txRuleErr.RejectCode != mempool.RejectInvalid {
shouldBan = false
shouldBan := false
if txRuleErr := (&mempool_old.TxRuleError{}); errors.As(ruleErr.Err, txRuleErr) {
if txRuleErr.RejectCode == mempool_old.RejectInvalid {
shouldBan = true
}
}

View File

@@ -0,0 +1,189 @@
package transactionrelay_test
import (
"errors"
"github.com/kaspanet/kaspad/app/protocol/flows/transactionrelay"
"github.com/kaspanet/kaspad/app/protocol/protocolerrors"
"github.com/kaspanet/kaspad/domain"
"github.com/kaspanet/kaspad/domain/consensus"
"github.com/kaspanet/kaspad/domain/consensus/model/externalapi"
"github.com/kaspanet/kaspad/domain/consensus/utils/testutils"
"github.com/kaspanet/kaspad/infrastructure/logger"
"github.com/kaspanet/kaspad/util/panics"
"strings"
"testing"
"github.com/kaspanet/kaspad/app/appmessage"
"github.com/kaspanet/kaspad/infrastructure/config"
"github.com/kaspanet/kaspad/infrastructure/network/netadapter"
"github.com/kaspanet/kaspad/infrastructure/network/netadapter/router"
)
type mocTransactionsRelayContext struct {
netAdapter *netadapter.NetAdapter
domain domain.Domain
sharedRequestedTransactions *transactionrelay.SharedRequestedTransactions
}
func (m *mocTransactionsRelayContext) NetAdapter() *netadapter.NetAdapter {
return m.netAdapter
}
func (m *mocTransactionsRelayContext) Domain() domain.Domain {
return m.domain
}
func (m *mocTransactionsRelayContext) SharedRequestedTransactions() *transactionrelay.SharedRequestedTransactions {
return m.sharedRequestedTransactions
}
func (m *mocTransactionsRelayContext) Broadcast(appmessage.Message) error {
return nil
}
func (m *mocTransactionsRelayContext) OnTransactionAddedToMempool() {
}
// TestHandleRelayedTransactionsNotFound tests the flow of HandleRelayedTransactions when the peer doesn't
// have the requested transactions in the mempool.
func TestHandleRelayedTransactionsNotFound(t *testing.T) {
testutils.ForAllNets(t, true, func(t *testing.T, consensusConfig *consensus.Config) {
var log = logger.RegisterSubSystem("PROT")
var spawn = panics.GoroutineWrapperFunc(log)
factory := consensus.NewFactory()
tc, teardown, err := factory.NewTestConsensus(consensusConfig, "TestHandleRelayedTransactionsNotFound")
if err != nil {
t.Fatalf("Error setting up test consensus: %+v", err)
}
defer teardown(false)
sharedRequestedTransactions := transactionrelay.NewSharedRequestedTransactions()
adapter, err := netadapter.NewNetAdapter(config.DefaultConfig())
if err != nil {
t.Fatalf("Failed to create a NetAdapter: %v", err)
}
domainInstance, err := domain.New(consensusConfig, tc.Database())
if err != nil {
t.Fatalf("Failed to set up a domain instance: %v", err)
}
context := &mocTransactionsRelayContext{
netAdapter: adapter,
domain: domainInstance,
sharedRequestedTransactions: sharedRequestedTransactions,
}
incomingRoute := router.NewRoute()
defer incomingRoute.Close()
peerIncomingRoute := router.NewRoute()
defer peerIncomingRoute.Close()
txID1 := externalapi.NewDomainTransactionIDFromByteArray(&[externalapi.DomainHashSize]byte{
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01})
txID2 := externalapi.NewDomainTransactionIDFromByteArray(&[externalapi.DomainHashSize]byte{
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02})
txIDs := []*externalapi.DomainTransactionID{txID1, txID2}
invMessage := appmessage.NewMsgInvTransaction(txIDs)
err = incomingRoute.Enqueue(invMessage)
if err != nil {
t.Fatalf("Unexpected error from incomingRoute.Enqueue: %v", err)
}
// The goroutine is representing the peer's actions.
spawn("peerResponseToTheTransactionsRequest", func() {
msg, err := peerIncomingRoute.Dequeue()
if err != nil {
t.Fatalf("Dequeue: %v", err)
}
inv := msg.(*appmessage.MsgRequestTransactions)
if len(txIDs) != len(inv.IDs) {
t.Fatalf("TestHandleRelayedTransactions: expected %d transactions ID, but got %d", len(txIDs), len(inv.IDs))
}
for i, id := range inv.IDs {
if txIDs[i].String() != id.String() {
t.Fatalf("TestHandleRelayedTransactions: expected equal txID: expected %s, but got %s", txIDs[i].String(), id.String())
}
err = incomingRoute.Enqueue(appmessage.NewMsgTransactionNotFound(txIDs[i]))
if err != nil {
t.Fatalf("Unexpected error from incomingRoute.Enqueue: %v", err)
}
}
// Insert an unexpected message type to stop the infinity loop.
err = incomingRoute.Enqueue(&appmessage.MsgAddresses{})
if err != nil {
t.Fatalf("Unexpected error from incomingRoute.Enqueue: %v", err)
}
})
err = transactionrelay.HandleRelayedTransactions(context, incomingRoute, peerIncomingRoute)
// Since we inserted an unexpected message type to stop the infinity loop,
// we expect the error will be infected from this specific message and also the
// error will count as a protocol message.
if protocolErr := (protocolerrors.ProtocolError{}); err == nil || !errors.As(err, &protocolErr) {
t.Fatalf("Expected to protocol error")
} else {
if !protocolErr.ShouldBan {
t.Fatalf("Exepcted shouldBan true, but got false.")
}
if !strings.Contains(err.Error(), "unexpected Addresses [code 3] message in the block relay flow while expecting an inv message") {
t.Fatalf("Unexpected error: expected: an error due to existence of an Addresses message "+
"in the block relay flow, but got: %v", protocolErr.Cause)
}
}
})
}
// TestOnClosedIncomingRoute verifies that an appropriate error message will be returned when
// trying to dequeue a message from a closed route.
func TestOnClosedIncomingRoute(t *testing.T) {
testutils.ForAllNets(t, true, func(t *testing.T, consensusConfig *consensus.Config) {
factory := consensus.NewFactory()
tc, teardown, err := factory.NewTestConsensus(consensusConfig, "TestOnClosedIncomingRoute")
if err != nil {
t.Fatalf("Error setting up test consensus: %+v", err)
}
defer teardown(false)
sharedRequestedTransactions := transactionrelay.NewSharedRequestedTransactions()
adapter, err := netadapter.NewNetAdapter(config.DefaultConfig())
if err != nil {
t.Fatalf("Failed to creat a NetAdapter : %v", err)
}
domainInstance, err := domain.New(consensusConfig, tc.Database())
if err != nil {
t.Fatalf("Failed to set up a domain instance: %v", err)
}
context := &mocTransactionsRelayContext{
netAdapter: adapter,
domain: domainInstance,
sharedRequestedTransactions: sharedRequestedTransactions,
}
incomingRoute := router.NewRoute()
outgoingRoute := router.NewRoute()
defer outgoingRoute.Close()
txID := externalapi.NewDomainTransactionIDFromByteArray(&[externalapi.DomainHashSize]byte{
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01})
txIDs := []*externalapi.DomainTransactionID{txID}
err = incomingRoute.Enqueue(&appmessage.MsgInvTransaction{TxIDs: txIDs})
if err != nil {
t.Fatalf("Unexpected error from incomingRoute.Enqueue: %v", err)
}
incomingRoute.Close()
err = transactionrelay.HandleRelayedTransactions(context, incomingRoute, outgoingRoute)
if err == nil || !errors.Is(err, router.ErrRouteClosed) {
t.Fatalf("Unexpected error: expected: %v, got : %v", router.ErrRouteClosed, err)
}
})
}

View File

@@ -0,0 +1,88 @@
package transactionrelay_test
import (
"github.com/kaspanet/kaspad/app/appmessage"
"github.com/kaspanet/kaspad/app/protocol/flows/transactionrelay"
"github.com/kaspanet/kaspad/domain"
"github.com/kaspanet/kaspad/domain/consensus"
"github.com/kaspanet/kaspad/domain/consensus/model/externalapi"
"github.com/kaspanet/kaspad/domain/consensus/utils/testutils"
"github.com/kaspanet/kaspad/infrastructure/config"
"github.com/kaspanet/kaspad/infrastructure/logger"
"github.com/kaspanet/kaspad/infrastructure/network/netadapter"
"github.com/kaspanet/kaspad/infrastructure/network/netadapter/router"
"github.com/kaspanet/kaspad/util/panics"
"github.com/pkg/errors"
"testing"
)
// TestHandleRequestedTransactionsNotFound tests the flow of HandleRequestedTransactions
// when the requested transactions don't found in the mempool.
func TestHandleRequestedTransactionsNotFound(t *testing.T) {
testutils.ForAllNets(t, true, func(t *testing.T, consensusConfig *consensus.Config) {
var log = logger.RegisterSubSystem("PROT")
var spawn = panics.GoroutineWrapperFunc(log)
factory := consensus.NewFactory()
tc, teardown, err := factory.NewTestConsensus(consensusConfig, "TestHandleRequestedTransactionsNotFound")
if err != nil {
t.Fatalf("Error setting up test Consensus: %+v", err)
}
defer teardown(false)
sharedRequestedTransactions := transactionrelay.NewSharedRequestedTransactions()
adapter, err := netadapter.NewNetAdapter(config.DefaultConfig())
if err != nil {
t.Fatalf("Failed to create a NetAdapter: %v", err)
}
domainInstance, err := domain.New(consensusConfig, tc.Database())
if err != nil {
t.Fatalf("Failed to set up a domain Instance: %v", err)
}
context := &mocTransactionsRelayContext{
netAdapter: adapter,
domain: domainInstance,
sharedRequestedTransactions: sharedRequestedTransactions,
}
incomingRoute := router.NewRoute()
outgoingRoute := router.NewRoute()
defer outgoingRoute.Close()
txID1 := externalapi.NewDomainTransactionIDFromByteArray(&[externalapi.DomainHashSize]byte{
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01})
txID2 := externalapi.NewDomainTransactionIDFromByteArray(&[externalapi.DomainHashSize]byte{
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02})
txIDs := []*externalapi.DomainTransactionID{txID1, txID2}
msg := appmessage.NewMsgRequestTransactions(txIDs)
err = incomingRoute.Enqueue(msg)
if err != nil {
t.Fatalf("Unexpected error from incomingRoute.Enqueue: %v", err)
}
// The goroutine is representing the peer's actions.
spawn("peerResponseToTheTransactionsMessages", func() {
for i, id := range txIDs {
msg, err := outgoingRoute.Dequeue()
if err != nil {
t.Fatalf("Dequeue: %s", err)
}
outMsg := msg.(*appmessage.MsgTransactionNotFound)
if txIDs[i].String() != outMsg.ID.String() {
t.Fatalf("TestHandleRelayedTransactions: expected equal txID: expected %s, but got %s", txIDs[i].String(), id.String())
}
}
// Closed the incomingRoute for stop the infinity loop.
incomingRoute.Close()
})
err = transactionrelay.HandleRequestedTransactions(context, incomingRoute, outgoingRoute)
// Make sure the error is due to the closed route.
if err == nil || !errors.Is(err, router.ErrRouteClosed) {
t.Fatalf("Unexpected error: expected: %v, got : %v", router.ErrRouteClosed, err)
}
})
}

View File

@@ -1,8 +1,6 @@
package protocol
import (
"github.com/kaspanet/kaspad/app/protocol/flows/rejects"
"github.com/kaspanet/kaspad/infrastructure/network/connmanager"
"sync"
"sync/atomic"
@@ -11,10 +9,12 @@ import (
"github.com/kaspanet/kaspad/app/protocol/flows/blockrelay"
"github.com/kaspanet/kaspad/app/protocol/flows/handshake"
"github.com/kaspanet/kaspad/app/protocol/flows/ping"
"github.com/kaspanet/kaspad/app/protocol/flows/rejects"
"github.com/kaspanet/kaspad/app/protocol/flows/transactionrelay"
peerpkg "github.com/kaspanet/kaspad/app/protocol/peer"
"github.com/kaspanet/kaspad/app/protocol/protocolerrors"
"github.com/kaspanet/kaspad/infrastructure/network/addressmanager"
"github.com/kaspanet/kaspad/infrastructure/network/connmanager"
"github.com/kaspanet/kaspad/infrastructure/network/netadapter"
routerpkg "github.com/kaspanet/kaspad/infrastructure/network/netadapter/router"
"github.com/pkg/errors"
@@ -106,7 +106,7 @@ func (m *Manager) handleError(err error, netConnection *netadapter.NetConnection
log.Warnf("Banning %s (reason: %s)", netConnection, protocolErr.Cause)
err := m.context.ConnectionManager().Ban(netConnection)
if !errors.Is(err, connmanager.ErrCannotBanPermanent) {
if err != nil && !errors.Is(err, connmanager.ErrCannotBanPermanent) {
panic(err)
}

View File

@@ -64,6 +64,11 @@ func (m *Manager) NotifyBlockAddedToDAG(block *externalapi.DomainBlock, blockIns
return err
}
err = m.notifyVirtualDaaScoreChanged()
if err != nil {
return err
}
err = m.notifyVirtualSelectedParentChainChanged(blockInsertionResult)
if err != nil {
return err
@@ -153,6 +158,19 @@ func (m *Manager) notifyVirtualSelectedParentBlueScoreChanged() error {
return m.context.NotificationManager.NotifyVirtualSelectedParentBlueScoreChanged(notification)
}
func (m *Manager) notifyVirtualDaaScoreChanged() error {
onEnd := logger.LogAndMeasureExecutionTime(log, "RPCManager.NotifyVirtualDaaScoreChanged")
defer onEnd()
virtualInfo, err := m.context.Domain.Consensus().GetVirtualInfo()
if err != nil {
return err
}
notification := appmessage.NewVirtualDaaScoreChangedNotificationMessage(virtualInfo.DAAScore)
return m.context.NotificationManager.NotifyVirtualDaaScoreChanged(notification)
}
func (m *Manager) notifyVirtualSelectedParentChainChanged(blockInsertionResult *externalapi.BlockInsertionResult) error {
onEnd := logger.LogAndMeasureExecutionTime(log, "RPCManager.NotifyVirtualSelectedParentChainChanged")
defer onEnd()

View File

@@ -44,6 +44,8 @@ var handlers = map[appmessage.MessageCommand]handler{
appmessage.CmdGetInfoRequestMessage: rpchandlers.HandleGetInfo,
appmessage.CmdNotifyPruningPointUTXOSetOverrideRequestMessage: rpchandlers.HandleNotifyPruningPointUTXOSetOverrideRequest,
appmessage.CmdStopNotifyingPruningPointUTXOSetOverrideRequestMessage: rpchandlers.HandleStopNotifyingPruningPointUTXOSetOverrideRequest,
appmessage.CmdEstimateNetworkHashesPerSecondRequestMessage: rpchandlers.HandleEstimateNetworkHashesPerSecond,
appmessage.CmdNotifyVirtualDaaScoreChangedRequestMessage: rpchandlers.HandleNotifyVirtualDaaScoreChanged,
}
func (m *Manager) routerInitializer(router *router.Router, netConnection *netadapter.NetConnection) {

View File

@@ -30,6 +30,7 @@ type NotificationListener struct {
propagateFinalityConflictResolvedNotifications bool
propagateUTXOsChangedNotifications bool
propagateVirtualSelectedParentBlueScoreChangedNotifications bool
propagateVirtualDaaScoreChangedNotifications bool
propagatePruningPointUTXOSetOverrideNotifications bool
propagateUTXOsChangedNotificationAddresses map[utxoindex.ScriptPublicKeyString]*UTXOsChangedNotificationAddress
@@ -181,6 +182,25 @@ func (nm *NotificationManager) NotifyVirtualSelectedParentBlueScoreChanged(
return nil
}
// NotifyVirtualDaaScoreChanged notifies the notification manager that the DAG's
// virtual DAA score has changed
func (nm *NotificationManager) NotifyVirtualDaaScoreChanged(
notification *appmessage.VirtualDaaScoreChangedNotificationMessage) error {
nm.RLock()
defer nm.RUnlock()
for router, listener := range nm.listeners {
if listener.propagateVirtualDaaScoreChangedNotifications {
err := router.OutgoingRoute().Enqueue(notification)
if err != nil {
return err
}
}
}
return nil
}
// NotifyPruningPointUTXOSetOverride notifies the notification manager that the UTXO index
// reset due to pruning point change via IBD.
func (nm *NotificationManager) NotifyPruningPointUTXOSetOverride() error {
@@ -308,6 +328,12 @@ func (nl *NotificationListener) PropagateVirtualSelectedParentBlueScoreChangedNo
nl.propagateVirtualSelectedParentBlueScoreChangedNotifications = true
}
// PropagateVirtualDaaScoreChangedNotifications instructs the listener to send
// virtual DAA score notifications to the remote listener
func (nl *NotificationListener) PropagateVirtualDaaScoreChangedNotifications() {
nl.propagateVirtualDaaScoreChangedNotifications = true
}
// PropagatePruningPointUTXOSetOverrideNotifications instructs the listener to send pruning point UTXO set override notifications
// to the remote listener.
func (nl *NotificationListener) PropagatePruningPointUTXOSetOverrideNotifications() {

View File

@@ -80,7 +80,7 @@ func (ctx *Context) PopulateBlockWithVerboseData(block *appmessage.RPCBlock, dom
// Get the block if we didn't receive it previously
if domainBlock == nil {
domainBlock, err = ctx.Domain.Consensus().GetBlock(blockHash)
domainBlock, err = ctx.Domain.Consensus().GetBlockEvenIfHeaderOnly(blockHash)
if err != nil {
return err
}

View File

@@ -0,0 +1,39 @@
package rpchandlers
import (
"github.com/kaspanet/kaspad/app/appmessage"
"github.com/kaspanet/kaspad/app/rpc/rpccontext"
"github.com/kaspanet/kaspad/domain/consensus/model"
"github.com/kaspanet/kaspad/domain/consensus/model/externalapi"
"github.com/kaspanet/kaspad/infrastructure/network/netadapter/router"
)
// HandleEstimateNetworkHashesPerSecond handles the respectively named RPC command
func HandleEstimateNetworkHashesPerSecond(
context *rpccontext.Context, _ *router.Router, request appmessage.Message) (appmessage.Message, error) {
estimateNetworkHashesPerSecondRequest := request.(*appmessage.EstimateNetworkHashesPerSecondRequestMessage)
windowSize := int(estimateNetworkHashesPerSecondRequest.WindowSize)
startHash := model.VirtualBlockHash
if estimateNetworkHashesPerSecondRequest.StartHash != "" {
var err error
startHash, err = externalapi.NewDomainHashFromString(estimateNetworkHashesPerSecondRequest.StartHash)
if err != nil {
response := &appmessage.EstimateNetworkHashesPerSecondResponseMessage{}
response.Error = appmessage.RPCErrorf("StartHash '%s' is not a valid block hash",
estimateNetworkHashesPerSecondRequest.StartHash)
return response, nil
}
}
networkHashesPerSecond, err := context.Domain.Consensus().EstimateNetworkHashesPerSecond(startHash, windowSize)
if err != nil {
response := &appmessage.EstimateNetworkHashesPerSecondResponseMessage{}
response.Error = appmessage.RPCErrorf("could not resolve network hashes per "+
"second for startHash %s and window size %d: %s", startHash, windowSize, err)
return response, nil
}
return appmessage.NewEstimateNetworkHashesPerSecondResponseMessage(networkHashesPerSecond), nil
}

View File

@@ -20,18 +20,22 @@ func HandleGetBlock(context *rpccontext.Context, _ *router.Router, request appme
return errorMessage, nil
}
header, err := context.Domain.Consensus().GetBlockHeader(hash)
block, err := context.Domain.Consensus().GetBlockEvenIfHeaderOnly(hash)
if err != nil {
errorMessage := &appmessage.GetBlockResponseMessage{}
errorMessage.Error = appmessage.RPCErrorf("Block %s not found", hash)
return errorMessage, nil
}
block := &externalapi.DomainBlock{Header: header}
response := appmessage.NewGetBlockResponseMessage()
response.Block = appmessage.DomainBlockToRPCBlock(block)
err = context.PopulateBlockWithVerboseData(response.Block, header, nil, getBlockRequest.IncludeTransactionVerboseData)
if getBlockRequest.IncludeTransactions {
response.Block = appmessage.DomainBlockToRPCBlock(block)
} else {
response.Block = appmessage.DomainBlockToRPCBlock(&externalapi.DomainBlock{Header: block.Header})
}
err = context.PopulateBlockWithVerboseData(response.Block, block.Header, block, getBlockRequest.IncludeTransactions)
if err != nil {
if errors.Is(err, rpccontext.ErrBuildBlockVerboseDataInvalidBlock) {
errorMessage := &appmessage.GetBlockResponseMessage{}

View File

@@ -35,6 +35,7 @@ func HandleGetBlockDAGInfo(context *rpccontext.Context, _ *router.Router, _ appm
response.VirtualParentHashes = hashes.ToStrings(virtualInfo.ParentHashes)
response.Difficulty = context.GetDifficultyRatio(virtualInfo.Bits, context.Config.ActiveNetParams)
response.PastMedianTime = virtualInfo.PastMedianTime
response.VirtualDAAScore = virtualInfo.DAAScore
pruningPoint, err := context.Domain.Consensus().PruningPoint()
if err != nil {

View File

@@ -18,11 +18,11 @@ const (
func HandleGetBlocks(context *rpccontext.Context, _ *router.Router, request appmessage.Message) (appmessage.Message, error) {
getBlocksRequest := request.(*appmessage.GetBlocksRequestMessage)
// Validate that user didn't set IncludeTransactionVerboseData without setting IncludeBlocks
if !getBlocksRequest.IncludeBlocks && getBlocksRequest.IncludeTransactionVerboseData {
// Validate that user didn't set IncludeTransactions without setting IncludeBlocks
if !getBlocksRequest.IncludeBlocks && getBlocksRequest.IncludeTransactions {
return &appmessage.GetBlocksResponseMessage{
Error: appmessage.RPCErrorf(
"If includeTransactionVerboseData is set, then includeBlockVerboseData must be set as well"),
"If includeTransactions is set, then includeBlockVerboseData must be set as well"),
}, nil
}
@@ -84,19 +84,24 @@ func HandleGetBlocks(context *rpccontext.Context, _ *router.Router, request appm
response := appmessage.NewGetBlocksResponseMessage()
response.BlockHashes = hashes.ToStrings(blockHashes)
if getBlocksRequest.IncludeBlocks {
blocks := make([]*appmessage.RPCBlock, len(blockHashes))
rpcBlocks := make([]*appmessage.RPCBlock, len(blockHashes))
for i, blockHash := range blockHashes {
blockHeader, err := context.Domain.Consensus().GetBlockHeader(blockHash)
block, err := context.Domain.Consensus().GetBlockEvenIfHeaderOnly(blockHash)
if err != nil {
return nil, err
}
block := &externalapi.DomainBlock{Header: blockHeader}
blocks[i] = appmessage.DomainBlockToRPCBlock(block)
err = context.PopulateBlockWithVerboseData(blocks[i], blockHeader, nil, getBlocksRequest.IncludeTransactionVerboseData)
if getBlocksRequest.IncludeTransactions {
rpcBlocks[i] = appmessage.DomainBlockToRPCBlock(block)
} else {
rpcBlocks[i] = appmessage.DomainBlockToRPCBlock(&externalapi.DomainBlock{Header: block.Header})
}
err = context.PopulateBlockWithVerboseData(rpcBlocks[i], block.Header, nil, getBlocksRequest.IncludeTransactions)
if err != nil {
return nil, err
}
}
response.Blocks = rpcBlocks
}
return response, nil

View File

@@ -0,0 +1,19 @@
package rpchandlers
import (
"github.com/kaspanet/kaspad/app/appmessage"
"github.com/kaspanet/kaspad/app/rpc/rpccontext"
"github.com/kaspanet/kaspad/infrastructure/network/netadapter/router"
)
// HandleNotifyVirtualDaaScoreChanged handles the respectively named RPC command
func HandleNotifyVirtualDaaScoreChanged(context *rpccontext.Context, router *router.Router, _ appmessage.Message) (appmessage.Message, error) {
listener, err := context.NotificationManager.Listener(router)
if err != nil {
return nil, err
}
listener.PropagateVirtualDaaScoreChangedNotifications()
response := appmessage.NewNotifyVirtualDaaScoreChangedResponseMessage()
return response, nil
}

View File

@@ -4,7 +4,7 @@ import (
"github.com/kaspanet/kaspad/app/appmessage"
"github.com/kaspanet/kaspad/app/rpc/rpccontext"
"github.com/kaspanet/kaspad/domain/consensus/utils/consensushashing"
"github.com/kaspanet/kaspad/domain/miningmanager/mempool"
"github.com/kaspanet/kaspad/domain/miningmanager/mempool_old"
"github.com/kaspanet/kaspad/infrastructure/network/netadapter/router"
"github.com/pkg/errors"
)
@@ -23,7 +23,7 @@ func HandleSubmitTransaction(context *rpccontext.Context, _ *router.Router, requ
transactionID := consensushashing.TransactionID(domainTransaction)
err = context.ProtocolManager.AddTransaction(domainTransaction)
if err != nil {
if !errors.As(err, &mempool.RuleError{}) {
if !errors.As(err, &mempool_old.RuleError{}) {
return nil, err
}

View File

@@ -1,4 +1,14 @@
Kaspad v0.10.1 0 2021-05-11
Kaspad v0.10.2 - 2021-05-18
===========================
Non-breaking changes:
* Fix getBlock and getBlocks RPC commands to return blocks and transactions properly (#1716)
* serializeAddress should always serialize as IPv6, since it assumes the IP size is 16 bytes (#1720)
* Add VirtualDaaScore to GetBlockDagInfo (#1719)
* Fix calcTxSequenceLockFromReferencedUTXOEntries for loop break condition (#1723)
* Fix overflow when checking coinbase maturity and don't ban peers that send transactions with immature spend (#1722)
Kaspad v0.10.1 - 2021-05-11
===========================
* Calculate virtual's acceptance data and multiset after importing a new pruning point (#1700)
Kaspad v0.10.0 - 2021-04-26

View File

@@ -48,6 +48,10 @@ func setField(commandValue reflect.Value, parameterValue reflect.Value, paramete
}
func stringToValue(parameterDesc *parameterDescription, valueStr string) (reflect.Value, error) {
if valueStr == "-" {
return reflect.Zero(parameterDesc.typeof), nil
}
var value interface{}
var err error
switch parameterDesc.typeof.Kind() {

View File

@@ -24,6 +24,7 @@ var commandTypes = []reflect.Type{
reflect.TypeOf(protowire.KaspadMessage_GetVirtualSelectedParentBlueScoreRequest{}),
reflect.TypeOf(protowire.KaspadMessage_GetVirtualSelectedParentChainFromBlockRequest{}),
reflect.TypeOf(protowire.KaspadMessage_ResolveFinalityConflictRequest{}),
reflect.TypeOf(protowire.KaspadMessage_EstimateNetworkHashesPerSecondRequest{}),
reflect.TypeOf(protowire.KaspadMessage_GetBlockTemplateRequest{}),
reflect.TypeOf(protowire.KaspadMessage_SubmitBlockRequest{}),

View File

@@ -27,7 +27,8 @@ func parseConfig() (*configFlags, error) {
}
parser := flags.NewParser(cfg, flags.HelpFlag)
parser.Usage = "kaspactl [OPTIONS] [COMMAND] [COMMAND PARAMETERS].\n\nCommand can be supplied only if --json is not used." +
"\n\nUse `kaspactl --list-commands` to get a list of all commands and their parameters"
"\n\nUse `kaspactl --list-commands` to get a list of all commands and their parameters." +
"\nFor optional parameters- use '-' without quotes to not pass the parameter.\n"
remainingArgs, err := parser.Parse()
if err != nil {
return nil, err

View File

@@ -1,51 +1,30 @@
package main
import (
"context"
"fmt"
"github.com/kaspanet/kaspad/cmd/kaspawallet/keys"
"github.com/kaspanet/kaspad/cmd/kaspawallet/libkaspawallet"
"github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/client"
"github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/pb"
"github.com/kaspanet/kaspad/util"
)
func balance(conf *balanceConfig) error {
client, err := connectToRPC(conf.NetParams(), conf.RPCServer)
daemonClient, tearDown, err := client.Connect(conf.DaemonAddress)
if err != nil {
return err
}
defer tearDown()
ctx, cancel := context.WithTimeout(context.Background(), daemonTimeout)
defer cancel()
response, err := daemonClient.GetBalance(ctx, &pb.GetBalanceRequest{})
if err != nil {
return err
}
keysFile, err := keys.ReadKeysFile(conf.NetParams(), conf.KeysFile)
if err != nil {
return err
}
addr, err := libkaspawallet.Address(conf.NetParams(), keysFile.PublicKeys, keysFile.MinimumSignatures, keysFile.ECDSA)
if err != nil {
return err
}
getUTXOsByAddressesResponse, err := client.GetUTXOsByAddresses([]string{addr.String()})
if err != nil {
return err
}
virtualSelectedParentBlueScoreResponse, err := client.GetVirtualSelectedParentBlueScore()
if err != nil {
return err
}
virtualSelectedParentBlueScore := virtualSelectedParentBlueScoreResponse.BlueScore
var availableBalance, pendingBalance uint64
for _, entry := range getUTXOsByAddressesResponse.Entries {
if isUTXOSpendable(entry, virtualSelectedParentBlueScore, conf.ActiveNetParams.BlockCoinbaseMaturity) {
availableBalance += entry.UTXOEntry.Amount
} else {
pendingBalance += entry.UTXOEntry.Amount
}
}
fmt.Printf("Balance:\t\tKAS %f\n", float64(availableBalance)/util.SompiPerKaspa)
if pendingBalance > 0 {
fmt.Printf("Pending balance:\tKAS %f\n", float64(pendingBalance)/util.SompiPerKaspa)
fmt.Printf("Balance:\t\tKAS %f\n", float64(response.Available)/util.SompiPerKaspa)
if response.Pending > 0 {
fmt.Printf("Pending balance:\tKAS %f\n", float64(response.Pending)/util.SompiPerKaspa)
}
return nil

View File

@@ -1,34 +1,35 @@
package main
import (
"context"
"encoding/hex"
"fmt"
"github.com/kaspanet/kaspad/cmd/kaspawallet/libkaspawallet"
"github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/client"
"github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/pb"
)
func broadcast(conf *broadcastConfig) error {
client, err := connectToRPC(conf.NetParams(), conf.RPCServer)
daemonClient, tearDown, err := client.Connect(conf.DaemonAddress)
if err != nil {
return err
}
defer tearDown()
ctx, cancel := context.WithTimeout(context.Background(), daemonTimeout)
defer cancel()
transaction, err := hex.DecodeString(conf.Transaction)
if err != nil {
return err
}
psTxBytes, err := hex.DecodeString(conf.Transaction)
if err != nil {
return err
}
tx, err := libkaspawallet.ExtractTransaction(psTxBytes)
if err != nil {
return err
}
transactionID, err := sendTransaction(client, tx)
response, err := daemonClient.Broadcast(ctx, &pb.BroadcastRequest{Transaction: transaction})
if err != nil {
return err
}
fmt.Println("Transaction was sent successfully")
fmt.Printf("Transaction ID: \t%s\n", transactionID)
fmt.Printf("Transaction ID: \t%s\n", response.TxID)
return nil
}

View File

@@ -2,31 +2,13 @@ package main
import (
"fmt"
"github.com/kaspanet/kaspad/app/appmessage"
"github.com/kaspanet/kaspad/domain/dagconfig"
"github.com/kaspanet/kaspad/infrastructure/network/rpcclient"
"os"
"time"
)
func isUTXOSpendable(entry *appmessage.UTXOsByAddressesEntry, virtualSelectedParentBlueScore uint64, coinbaseMaturity uint64) bool {
if !entry.UTXOEntry.IsCoinbase {
return true
}
blockBlueScore := entry.UTXOEntry.BlockDAAScore
// TODO: Check for a better alternative than virtualSelectedParentBlueScore
return blockBlueScore+coinbaseMaturity < virtualSelectedParentBlueScore
}
const daemonTimeout = 2 * time.Minute
func printErrorAndExit(err error) {
fmt.Fprintf(os.Stderr, "%s\n", err)
os.Exit(1)
}
func connectToRPC(params *dagconfig.Params, rpcServer string) (*rpcclient.RPCClient, error) {
rpcAddress, err := params.NormalizeRPCServerAddress(rpcServer)
if err != nil {
return nil, err
}
return rpcclient.NewRPCClient(rpcAddress)
}

View File

@@ -17,6 +17,12 @@ const (
broadcastSubCmd = "broadcast"
showAddressSubCmd = "show-address"
dumpUnencryptedDataSubCmd = "dump-unencrypted-data"
startDaemonSubCmd = "start-daemon"
)
const (
defaultListen = "localhost:8082"
defaultRPCServer = "localhost"
)
type configFlags struct {
@@ -25,6 +31,8 @@ type configFlags struct {
type createConfig struct {
KeysFile string `long:"keys-file" short:"f" description:"Keys file location (default: ~/.kaspawallet/keys.json (*nix), %USERPROFILE%\\AppData\\Local\\Kaspawallet\\key.json (Windows))"`
Password string `long:"password" short:"p" description:"Wallet password"`
Yes bool `long:"yes" short:"y" description:"Assume \"yes\" to all questions"`
MinimumSignatures uint32 `long:"min-signatures" short:"m" description:"Minimum required signatures" default:"1"`
NumPrivateKeys uint32 `long:"num-private-keys" short:"k" description:"Number of private keys" default:"1"`
NumPublicKeys uint32 `long:"num-public-keys" short:"n" description:"Total number of keys" default:"1"`
@@ -34,46 +42,56 @@ type createConfig struct {
}
type balanceConfig struct {
KeysFile string `long:"keys-file" short:"f" description:"Keys file location (default: ~/.kaspawallet/keys.json (*nix), %USERPROFILE%\\AppData\\Local\\Kaspawallet\\key.json (Windows))"`
RPCServer string `long:"rpcserver" short:"s" description:"RPC server to connect to"`
DaemonAddress string `long:"daemonaddress" short:"d" description:"Wallet daemon server to connect to (default: localhost:8082)"`
config.NetworkFlags
}
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))"`
RPCServer string `long:"rpcserver" short:"s" description:"RPC server to connect to"`
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"`
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 (default: localhost:8082)"`
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"`
config.NetworkFlags
}
type createUnsignedTransactionConfig struct {
KeysFile string `long:"keys-file" short:"f" description:"Keys file location (default: ~/.kaspawallet/keys.json (*nix), %USERPROFILE%\\AppData\\Local\\Kaspawallet\\key.json (Windows))"`
RPCServer string `long:"rpcserver" short:"s" description:"RPC server to connect to"`
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"`
DaemonAddress string `long:"daemonaddress" short:"d" description:"Wallet daemon server to connect to (default: localhost:8082)"`
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"`
config.NetworkFlags
}
type signConfig struct {
KeysFile string `long:"keys-file" short:"f" description:"Keys file location (default: ~/.kaspawallet/keys.json (*nix), %USERPROFILE%\\AppData\\Local\\Kaspawallet\\key.json (Windows))"`
Password string `long:"password" short:"p" description:"Wallet password"`
Transaction string `long:"transaction" short:"t" description:"The unsigned transaction to sign on (encoded in hex)" required:"true"`
config.NetworkFlags
}
type broadcastConfig struct {
RPCServer string `long:"rpcserver" short:"s" description:"RPC server to connect to"`
Transaction string `long:"transaction" short:"t" description:"The signed transaction to broadcast (encoded in hex)" required:"true"`
DaemonAddress string `long:"daemonaddress" short:"d" description:"Wallet daemon server to connect to (default: localhost:8082)"`
Transaction string `long:"transaction" short:"t" description:"The signed transaction to broadcast (encoded in hex)" required:"true"`
config.NetworkFlags
}
type showAddressConfig struct {
KeysFile string `long:"keys-file" short:"f" description:"Keys file location (default: ~/.kaspawallet/keys.json (*nix), %USERPROFILE%\\AppData\\Local\\Kaspawallet\\key.json (Windows))"`
DaemonAddress string `long:"daemonaddress" short:"d" description:"Wallet daemon server to connect to (default: localhost:8082)"`
config.NetworkFlags
}
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))"`
Password string `long:"password" short:"p" description:"Wallet password"`
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)"`
config.NetworkFlags
}
type dumpUnencryptedDataConfig struct {
KeysFile string `long:"keys-file" short:"f" description:"Keys file location (default: ~/.kaspawallet/keys.json (*nix), %USERPROFILE%\\AppData\\Local\\Kaspawallet\\key.json (Windows))"`
Password string `long:"password" short:"p" description:"Wallet password"`
Yes bool `long:"yes" short:"y" description:"Assume \"yes\" to all questions"`
config.NetworkFlags
}
@@ -85,15 +103,15 @@ func parseCommandLine() (subCommand string, config interface{}) {
parser.AddCommand(createSubCmd, "Creates a new wallet",
"Creates a private key and 3 public addresses, one for each of MainNet, TestNet and DevNet", createConf)
balanceConf := &balanceConfig{}
balanceConf := &balanceConfig{DaemonAddress: defaultListen}
parser.AddCommand(balanceSubCmd, "Shows the balance of a public address",
"Shows the balance for a public address in Kaspa", balanceConf)
sendConf := &sendConfig{}
sendConf := &sendConfig{DaemonAddress: defaultListen}
parser.AddCommand(sendSubCmd, "Sends a Kaspa transaction to a public address",
"Sends a Kaspa transaction to a public address", sendConf)
createUnsignedTransactionConf := &createUnsignedTransactionConfig{}
createUnsignedTransactionConf := &createUnsignedTransactionConfig{DaemonAddress: defaultListen}
parser.AddCommand(createUnsignedTransactionSubCmd, "Create an unsigned Kaspa transaction",
"Create an unsigned Kaspa transaction", createUnsignedTransactionConf)
@@ -101,11 +119,11 @@ func parseCommandLine() (subCommand string, config interface{}) {
parser.AddCommand(signSubCmd, "Sign the given partially signed transaction",
"Sign the given partially signed transaction", signConf)
broadcastConf := &broadcastConfig{}
broadcastConf := &broadcastConfig{DaemonAddress: defaultListen}
parser.AddCommand(broadcastSubCmd, "Broadcast the given transaction",
"Broadcast the given transaction", broadcastConf)
showAddressConf := &showAddressConfig{}
showAddressConf := &showAddressConfig{DaemonAddress: defaultListen}
parser.AddCommand(showAddressSubCmd, "Shows the public address of the current wallet",
"Shows the public address of the current wallet", showAddressConf)
@@ -114,6 +132,12 @@ func parseCommandLine() (subCommand string, config interface{}) {
"Prints the unencrypted wallet data including its private keys. Anyone that sees it can access "+
"the funds. Use only on safe environment.", dumpUnencryptedDataConf)
startDaemonConf := &startDaemonConfig{
RPCServer: defaultRPCServer,
Listen: defaultListen,
}
parser.AddCommand(startDaemonSubCmd, "Start the wallet daemon", "Start the wallet daemon", startDaemonConf)
_, err := parser.Parse()
if err != nil {
@@ -183,6 +207,13 @@ func parseCommandLine() (subCommand string, config interface{}) {
printErrorAndExit(err)
}
config = dumpUnencryptedDataConf
case startDaemonSubCmd:
combineNetworkFlags(&startDaemonConf.NetworkFlags, &cfg.NetworkFlags)
err := startDaemonConf.ResolveNetwork(parser)
if err != nil {
printErrorAndExit(err)
}
config = startDaemonConf
}
return parser.Command.Active.Name, config

View File

@@ -2,70 +2,77 @@ package main
import (
"bufio"
"encoding/hex"
"fmt"
"github.com/kaspanet/kaspad/cmd/kaspawallet/libkaspawallet"
"github.com/kaspanet/kaspad/cmd/kaspawallet/libkaspawallet/bip32"
"github.com/kaspanet/kaspad/cmd/kaspawallet/utils"
"github.com/pkg/errors"
"os"
"github.com/kaspanet/kaspad/cmd/kaspawallet/keys"
"github.com/kaspanet/kaspad/cmd/kaspawallet/libkaspawallet"
"github.com/pkg/errors"
)
func create(conf *createConfig) error {
var encryptedPrivateKeys []*keys.EncryptedPrivateKey
var publicKeys [][]byte
var encryptedMnemonics []*keys.EncryptedMnemonic
var signerExtendedPublicKeys []string
var err error
isMultisig := conf.NumPublicKeys > 1
if !conf.Import {
encryptedPrivateKeys, publicKeys, err = keys.CreateKeyPairs(conf.NumPrivateKeys, conf.ECDSA)
encryptedMnemonics, signerExtendedPublicKeys, err = keys.CreateMnemonics(conf.NetParams(), conf.NumPrivateKeys, conf.Password, isMultisig)
} else {
encryptedPrivateKeys, publicKeys, err = keys.ImportKeyPairs(conf.NumPrivateKeys)
encryptedMnemonics, signerExtendedPublicKeys, err = keys.ImportMnemonics(conf.NetParams(), conf.NumPrivateKeys, conf.Password, isMultisig)
}
if err != nil {
return err
}
for i, publicKey := range publicKeys {
fmt.Printf("Public key of private key #%d:\n%x\n\n", i+1, publicKey)
for i, extendedPublicKey := range signerExtendedPublicKeys {
fmt.Printf("Extended public key of mnemonic #%d:\n%s\n\n", i+1, extendedPublicKey)
}
extendedPublicKeys := make([]string, conf.NumPrivateKeys, conf.NumPublicKeys)
copy(extendedPublicKeys, signerExtendedPublicKeys)
reader := bufio.NewReader(os.Stdin)
for i := conf.NumPrivateKeys; i < conf.NumPublicKeys; i++ {
fmt.Printf("Enter public key #%d here:\n", i+1)
line, isPrefix, err := reader.ReadLine()
extendedPublicKey, err := utils.ReadLine(reader)
if err != nil {
return err
}
_, err = bip32.DeserializeExtendedKey(string(extendedPublicKey))
if err != nil {
return errors.Wrapf(err, "%s is invalid extended public key", string(extendedPublicKey))
}
fmt.Println()
if isPrefix {
return errors.Errorf("Public key is too long")
}
publicKey, err := hex.DecodeString(string(line))
if err != nil {
return err
}
publicKeys = append(publicKeys, publicKey)
extendedPublicKeys = append(extendedPublicKeys, string(extendedPublicKey))
}
err = keys.WriteKeysFile(
conf.NetParams(), conf.KeysFile, encryptedPrivateKeys, publicKeys, conf.MinimumSignatures, conf.ECDSA)
cosignerIndex, err := libkaspawallet.MinimumCosignerIndex(signerExtendedPublicKeys, extendedPublicKeys)
if err != nil {
return err
}
keysFile, err := keys.ReadKeysFile(conf.NetParams(), conf.KeysFile)
file := keys.File{
EncryptedMnemonics: encryptedMnemonics,
ExtendedPublicKeys: extendedPublicKeys,
MinimumSignatures: conf.MinimumSignatures,
CosignerIndex: cosignerIndex,
ECDSA: conf.ECDSA,
}
err = file.SetPath(conf.NetParams(), conf.KeysFile, conf.Yes)
if err != nil {
return err
}
addr, err := libkaspawallet.Address(conf.NetParams(), keysFile.PublicKeys, keysFile.MinimumSignatures, keysFile.ECDSA)
err = file.Save()
if err != nil {
return err
}
fmt.Printf("The wallet address is:\n%s\n", addr)
fmt.Printf("Wrote the keys into %s\n", file.Path())
return nil
}

View File

@@ -1,62 +1,34 @@
package main
import (
"context"
"encoding/hex"
"fmt"
"github.com/kaspanet/kaspad/cmd/kaspawallet/keys"
"github.com/kaspanet/kaspad/cmd/kaspawallet/libkaspawallet"
"github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/client"
"github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/pb"
"github.com/kaspanet/kaspad/util"
)
func createUnsignedTransaction(conf *createUnsignedTransactionConfig) error {
keysFile, err := keys.ReadKeysFile(conf.NetParams(), conf.KeysFile)
daemonClient, tearDown, err := client.Connect(conf.DaemonAddress)
if err != nil {
return err
}
defer tearDown()
toAddress, err := util.DecodeAddress(conf.ToAddress, conf.NetParams().Prefix)
if err != nil {
return err
}
fromAddress, err := libkaspawallet.Address(conf.NetParams(), keysFile.PublicKeys, keysFile.MinimumSignatures, keysFile.ECDSA)
if err != nil {
return err
}
client, err := connectToRPC(conf.NetParams(), conf.RPCServer)
if err != nil {
return err
}
utxos, err := fetchSpendableUTXOs(conf.NetParams(), client, fromAddress.String())
if err != nil {
return err
}
ctx, cancel := context.WithTimeout(context.Background(), daemonTimeout)
defer cancel()
sendAmountSompi := uint64(conf.SendAmount * util.SompiPerKaspa)
const feePerInput = 1000
selectedUTXOs, changeSompi, err := selectUTXOs(utxos, sendAmountSompi, feePerInput)
if err != nil {
return err
}
psTx, err := libkaspawallet.CreateUnsignedTransaction(keysFile.PublicKeys,
keysFile.MinimumSignatures,
keysFile.ECDSA,
[]*libkaspawallet.Payment{{
Address: toAddress,
Amount: sendAmountSompi,
}, {
Address: fromAddress,
Amount: changeSompi,
}}, selectedUTXOs)
response, err := daemonClient.CreateUnsignedTransaction(ctx, &pb.CreateUnsignedTransactionRequest{
Address: conf.ToAddress,
Amount: sendAmountSompi,
})
if err != nil {
return err
}
fmt.Println("Created unsigned transaction")
fmt.Println(hex.EncodeToString(psTx))
fmt.Println(hex.EncodeToString(response.UnsignedTransaction))
return nil
}

View File

@@ -0,0 +1,18 @@
package client
import (
"github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/pb"
"google.golang.org/grpc"
)
// Connect connects to the kaspawalletd server, and returns the client instance
func Connect(address string) (pb.KaspawalletdClient, func(), error) {
conn, err := grpc.Dial(address, grpc.WithInsecure(), grpc.WithBlock())
if err != nil {
return nil, nil, err
}
return pb.NewKaspawalletdClient(conn), func() {
conn.Close()
}, nil
}

View File

@@ -0,0 +1,3 @@
//go:generate protoc --go_out=. --go-grpc_out=. --go_opt=paths=source_relative --go-grpc_opt=paths=source_relative kaspawalletd.proto
package pb

View File

@@ -0,0 +1,730 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.25.0
// protoc v3.12.3
// source: kaspawalletd.proto
package pb
import (
proto "github.com/golang/protobuf/proto"
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
// This is a compile-time assertion that a sufficiently up-to-date version
// of the legacy proto package is being used.
const _ = proto.ProtoPackageIsVersion4
type GetBalanceRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
}
func (x *GetBalanceRequest) Reset() {
*x = GetBalanceRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_kaspawalletd_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *GetBalanceRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*GetBalanceRequest) ProtoMessage() {}
func (x *GetBalanceRequest) ProtoReflect() protoreflect.Message {
mi := &file_kaspawalletd_proto_msgTypes[0]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use GetBalanceRequest.ProtoReflect.Descriptor instead.
func (*GetBalanceRequest) Descriptor() ([]byte, []int) {
return file_kaspawalletd_proto_rawDescGZIP(), []int{0}
}
type GetBalanceResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Available uint64 `protobuf:"varint,1,opt,name=available,proto3" json:"available,omitempty"`
Pending uint64 `protobuf:"varint,2,opt,name=pending,proto3" json:"pending,omitempty"`
}
func (x *GetBalanceResponse) Reset() {
*x = GetBalanceResponse{}
if protoimpl.UnsafeEnabled {
mi := &file_kaspawalletd_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *GetBalanceResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*GetBalanceResponse) ProtoMessage() {}
func (x *GetBalanceResponse) ProtoReflect() protoreflect.Message {
mi := &file_kaspawalletd_proto_msgTypes[1]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use GetBalanceResponse.ProtoReflect.Descriptor instead.
func (*GetBalanceResponse) Descriptor() ([]byte, []int) {
return file_kaspawalletd_proto_rawDescGZIP(), []int{1}
}
func (x *GetBalanceResponse) GetAvailable() uint64 {
if x != nil {
return x.Available
}
return 0
}
func (x *GetBalanceResponse) GetPending() uint64 {
if x != nil {
return x.Pending
}
return 0
}
type CreateUnsignedTransactionRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Address string `protobuf:"bytes,1,opt,name=address,proto3" json:"address,omitempty"`
Amount uint64 `protobuf:"varint,2,opt,name=amount,proto3" json:"amount,omitempty"`
}
func (x *CreateUnsignedTransactionRequest) Reset() {
*x = CreateUnsignedTransactionRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_kaspawalletd_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *CreateUnsignedTransactionRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*CreateUnsignedTransactionRequest) ProtoMessage() {}
func (x *CreateUnsignedTransactionRequest) ProtoReflect() protoreflect.Message {
mi := &file_kaspawalletd_proto_msgTypes[2]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use CreateUnsignedTransactionRequest.ProtoReflect.Descriptor instead.
func (*CreateUnsignedTransactionRequest) Descriptor() ([]byte, []int) {
return file_kaspawalletd_proto_rawDescGZIP(), []int{2}
}
func (x *CreateUnsignedTransactionRequest) GetAddress() string {
if x != nil {
return x.Address
}
return ""
}
func (x *CreateUnsignedTransactionRequest) GetAmount() uint64 {
if x != nil {
return x.Amount
}
return 0
}
type CreateUnsignedTransactionResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
UnsignedTransaction []byte `protobuf:"bytes,1,opt,name=unsignedTransaction,proto3" json:"unsignedTransaction,omitempty"`
}
func (x *CreateUnsignedTransactionResponse) Reset() {
*x = CreateUnsignedTransactionResponse{}
if protoimpl.UnsafeEnabled {
mi := &file_kaspawalletd_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *CreateUnsignedTransactionResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*CreateUnsignedTransactionResponse) ProtoMessage() {}
func (x *CreateUnsignedTransactionResponse) ProtoReflect() protoreflect.Message {
mi := &file_kaspawalletd_proto_msgTypes[3]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use CreateUnsignedTransactionResponse.ProtoReflect.Descriptor instead.
func (*CreateUnsignedTransactionResponse) Descriptor() ([]byte, []int) {
return file_kaspawalletd_proto_rawDescGZIP(), []int{3}
}
func (x *CreateUnsignedTransactionResponse) GetUnsignedTransaction() []byte {
if x != nil {
return x.UnsignedTransaction
}
return nil
}
type GetReceiveAddressRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
}
func (x *GetReceiveAddressRequest) Reset() {
*x = GetReceiveAddressRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_kaspawalletd_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *GetReceiveAddressRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*GetReceiveAddressRequest) ProtoMessage() {}
func (x *GetReceiveAddressRequest) ProtoReflect() protoreflect.Message {
mi := &file_kaspawalletd_proto_msgTypes[4]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use GetReceiveAddressRequest.ProtoReflect.Descriptor instead.
func (*GetReceiveAddressRequest) Descriptor() ([]byte, []int) {
return file_kaspawalletd_proto_rawDescGZIP(), []int{4}
}
type GetReceiveAddressResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Address string `protobuf:"bytes,1,opt,name=address,proto3" json:"address,omitempty"`
}
func (x *GetReceiveAddressResponse) Reset() {
*x = GetReceiveAddressResponse{}
if protoimpl.UnsafeEnabled {
mi := &file_kaspawalletd_proto_msgTypes[5]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *GetReceiveAddressResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*GetReceiveAddressResponse) ProtoMessage() {}
func (x *GetReceiveAddressResponse) ProtoReflect() protoreflect.Message {
mi := &file_kaspawalletd_proto_msgTypes[5]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use GetReceiveAddressResponse.ProtoReflect.Descriptor instead.
func (*GetReceiveAddressResponse) Descriptor() ([]byte, []int) {
return file_kaspawalletd_proto_rawDescGZIP(), []int{5}
}
func (x *GetReceiveAddressResponse) GetAddress() string {
if x != nil {
return x.Address
}
return ""
}
type BroadcastRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Transaction []byte `protobuf:"bytes,1,opt,name=transaction,proto3" json:"transaction,omitempty"`
}
func (x *BroadcastRequest) Reset() {
*x = BroadcastRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_kaspawalletd_proto_msgTypes[6]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *BroadcastRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*BroadcastRequest) ProtoMessage() {}
func (x *BroadcastRequest) ProtoReflect() protoreflect.Message {
mi := &file_kaspawalletd_proto_msgTypes[6]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use BroadcastRequest.ProtoReflect.Descriptor instead.
func (*BroadcastRequest) Descriptor() ([]byte, []int) {
return file_kaspawalletd_proto_rawDescGZIP(), []int{6}
}
func (x *BroadcastRequest) GetTransaction() []byte {
if x != nil {
return x.Transaction
}
return nil
}
type BroadcastResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
TxID string `protobuf:"bytes,1,opt,name=txID,proto3" json:"txID,omitempty"`
}
func (x *BroadcastResponse) Reset() {
*x = BroadcastResponse{}
if protoimpl.UnsafeEnabled {
mi := &file_kaspawalletd_proto_msgTypes[7]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *BroadcastResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*BroadcastResponse) ProtoMessage() {}
func (x *BroadcastResponse) ProtoReflect() protoreflect.Message {
mi := &file_kaspawalletd_proto_msgTypes[7]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use BroadcastResponse.ProtoReflect.Descriptor instead.
func (*BroadcastResponse) Descriptor() ([]byte, []int) {
return file_kaspawalletd_proto_rawDescGZIP(), []int{7}
}
func (x *BroadcastResponse) GetTxID() string {
if x != nil {
return x.TxID
}
return ""
}
type ShutdownRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
}
func (x *ShutdownRequest) Reset() {
*x = ShutdownRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_kaspawalletd_proto_msgTypes[8]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *ShutdownRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ShutdownRequest) ProtoMessage() {}
func (x *ShutdownRequest) ProtoReflect() protoreflect.Message {
mi := &file_kaspawalletd_proto_msgTypes[8]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ShutdownRequest.ProtoReflect.Descriptor instead.
func (*ShutdownRequest) Descriptor() ([]byte, []int) {
return file_kaspawalletd_proto_rawDescGZIP(), []int{8}
}
type ShutdownResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
}
func (x *ShutdownResponse) Reset() {
*x = ShutdownResponse{}
if protoimpl.UnsafeEnabled {
mi := &file_kaspawalletd_proto_msgTypes[9]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *ShutdownResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ShutdownResponse) ProtoMessage() {}
func (x *ShutdownResponse) ProtoReflect() protoreflect.Message {
mi := &file_kaspawalletd_proto_msgTypes[9]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ShutdownResponse.ProtoReflect.Descriptor instead.
func (*ShutdownResponse) Descriptor() ([]byte, []int) {
return file_kaspawalletd_proto_rawDescGZIP(), []int{9}
}
var File_kaspawalletd_proto protoreflect.FileDescriptor
var file_kaspawalletd_proto_rawDesc = []byte{
0x0a, 0x12, 0x6b, 0x61, 0x73, 0x70, 0x61, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x64, 0x2e, 0x70,
0x72, 0x6f, 0x74, 0x6f, 0x22, 0x13, 0x0a, 0x11, 0x47, 0x65, 0x74, 0x42, 0x61, 0x6c, 0x61, 0x6e,
0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x4c, 0x0a, 0x12, 0x47, 0x65, 0x74,
0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12,
0x1c, 0x0a, 0x09, 0x61, 0x76, 0x61, 0x69, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x18, 0x01, 0x20, 0x01,
0x28, 0x04, 0x52, 0x09, 0x61, 0x76, 0x61, 0x69, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x12, 0x18, 0x0a,
0x07, 0x70, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x07,
0x70, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x22, 0x54, 0x0a, 0x20, 0x43, 0x72, 0x65, 0x61, 0x74,
0x65, 0x55, 0x6e, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63,
0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x61,
0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, 0x64,
0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x18,
0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x22, 0x55, 0x0a,
0x21, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x55, 0x6e, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x54,
0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
0x73, 0x65, 0x12, 0x30, 0x0a, 0x13, 0x75, 0x6e, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x54, 0x72,
0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52,
0x13, 0x75, 0x6e, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63,
0x74, 0x69, 0x6f, 0x6e, 0x22, 0x1a, 0x0a, 0x18, 0x47, 0x65, 0x74, 0x52, 0x65, 0x63, 0x65, 0x69,
0x76, 0x65, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
0x22, 0x35, 0x0a, 0x19, 0x47, 0x65, 0x74, 0x52, 0x65, 0x63, 0x65, 0x69, 0x76, 0x65, 0x41, 0x64,
0x64, 0x72, 0x65, 0x73, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x18, 0x0a,
0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07,
0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x22, 0x34, 0x0a, 0x10, 0x42, 0x72, 0x6f, 0x61, 0x64,
0x63, 0x61, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x20, 0x0a, 0x0b, 0x74,
0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c,
0x52, 0x0b, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x27, 0x0a,
0x11, 0x42, 0x72, 0x6f, 0x61, 0x64, 0x63, 0x61, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
0x73, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x78, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
0x52, 0x04, 0x74, 0x78, 0x49, 0x44, 0x22, 0x11, 0x0a, 0x0f, 0x53, 0x68, 0x75, 0x74, 0x64, 0x6f,
0x77, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x12, 0x0a, 0x10, 0x53, 0x68, 0x75,
0x74, 0x64, 0x6f, 0x77, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32, 0xe4, 0x02,
0x0a, 0x0c, 0x6b, 0x61, 0x73, 0x70, 0x61, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x64, 0x12, 0x37,
0x0a, 0x0a, 0x47, 0x65, 0x74, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x12, 0x12, 0x2e, 0x47,
0x65, 0x74, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
0x1a, 0x13, 0x2e, 0x47, 0x65, 0x74, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x52, 0x65, 0x73,
0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x64, 0x0a, 0x19, 0x43, 0x72, 0x65, 0x61, 0x74,
0x65, 0x55, 0x6e, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63,
0x74, 0x69, 0x6f, 0x6e, 0x12, 0x21, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x55, 0x6e, 0x73,
0x69, 0x67, 0x6e, 0x65, 0x64, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e,
0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65,
0x55, 0x6e, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74,
0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x4c, 0x0a,
0x11, 0x47, 0x65, 0x74, 0x52, 0x65, 0x63, 0x65, 0x69, 0x76, 0x65, 0x41, 0x64, 0x64, 0x72, 0x65,
0x73, 0x73, 0x12, 0x19, 0x2e, 0x47, 0x65, 0x74, 0x52, 0x65, 0x63, 0x65, 0x69, 0x76, 0x65, 0x41,
0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e,
0x47, 0x65, 0x74, 0x52, 0x65, 0x63, 0x65, 0x69, 0x76, 0x65, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73,
0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x31, 0x0a, 0x08, 0x53,
0x68, 0x75, 0x74, 0x64, 0x6f, 0x77, 0x6e, 0x12, 0x10, 0x2e, 0x53, 0x68, 0x75, 0x74, 0x64, 0x6f,
0x77, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x11, 0x2e, 0x53, 0x68, 0x75, 0x74,
0x64, 0x6f, 0x77, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x34,
0x0a, 0x09, 0x42, 0x72, 0x6f, 0x61, 0x64, 0x63, 0x61, 0x73, 0x74, 0x12, 0x11, 0x2e, 0x42, 0x72,
0x6f, 0x61, 0x64, 0x63, 0x61, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x12,
0x2e, 0x42, 0x72, 0x6f, 0x61, 0x64, 0x63, 0x61, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
0x73, 0x65, 0x22, 0x00, 0x42, 0x36, 0x5a, 0x34, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63,
0x6f, 0x6d, 0x2f, 0x6b, 0x61, 0x73, 0x70, 0x61, 0x6e, 0x65, 0x74, 0x2f, 0x6b, 0x61, 0x73, 0x70,
0x61, 0x64, 0x2f, 0x63, 0x6d, 0x64, 0x2f, 0x6b, 0x61, 0x73, 0x70, 0x61, 0x77, 0x61, 0x6c, 0x6c,
0x65, 0x74, 0x2f, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2f, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72,
0x6f, 0x74, 0x6f, 0x33,
}
var (
file_kaspawalletd_proto_rawDescOnce sync.Once
file_kaspawalletd_proto_rawDescData = file_kaspawalletd_proto_rawDesc
)
func file_kaspawalletd_proto_rawDescGZIP() []byte {
file_kaspawalletd_proto_rawDescOnce.Do(func() {
file_kaspawalletd_proto_rawDescData = protoimpl.X.CompressGZIP(file_kaspawalletd_proto_rawDescData)
})
return file_kaspawalletd_proto_rawDescData
}
var file_kaspawalletd_proto_msgTypes = make([]protoimpl.MessageInfo, 10)
var file_kaspawalletd_proto_goTypes = []interface{}{
(*GetBalanceRequest)(nil), // 0: GetBalanceRequest
(*GetBalanceResponse)(nil), // 1: GetBalanceResponse
(*CreateUnsignedTransactionRequest)(nil), // 2: CreateUnsignedTransactionRequest
(*CreateUnsignedTransactionResponse)(nil), // 3: CreateUnsignedTransactionResponse
(*GetReceiveAddressRequest)(nil), // 4: GetReceiveAddressRequest
(*GetReceiveAddressResponse)(nil), // 5: GetReceiveAddressResponse
(*BroadcastRequest)(nil), // 6: BroadcastRequest
(*BroadcastResponse)(nil), // 7: BroadcastResponse
(*ShutdownRequest)(nil), // 8: ShutdownRequest
(*ShutdownResponse)(nil), // 9: ShutdownResponse
}
var file_kaspawalletd_proto_depIdxs = []int32{
0, // 0: kaspawalletd.GetBalance:input_type -> GetBalanceRequest
2, // 1: kaspawalletd.CreateUnsignedTransaction:input_type -> CreateUnsignedTransactionRequest
4, // 2: kaspawalletd.GetReceiveAddress:input_type -> GetReceiveAddressRequest
8, // 3: kaspawalletd.Shutdown:input_type -> ShutdownRequest
6, // 4: kaspawalletd.Broadcast:input_type -> BroadcastRequest
1, // 5: kaspawalletd.GetBalance:output_type -> GetBalanceResponse
3, // 6: kaspawalletd.CreateUnsignedTransaction:output_type -> CreateUnsignedTransactionResponse
5, // 7: kaspawalletd.GetReceiveAddress:output_type -> GetReceiveAddressResponse
9, // 8: kaspawalletd.Shutdown:output_type -> ShutdownResponse
7, // 9: kaspawalletd.Broadcast:output_type -> BroadcastResponse
5, // [5:10] is the sub-list for method output_type
0, // [0:5] is the sub-list for method input_type
0, // [0:0] is the sub-list for extension type_name
0, // [0:0] is the sub-list for extension extendee
0, // [0:0] is the sub-list for field type_name
}
func init() { file_kaspawalletd_proto_init() }
func file_kaspawalletd_proto_init() {
if File_kaspawalletd_proto != nil {
return
}
if !protoimpl.UnsafeEnabled {
file_kaspawalletd_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*GetBalanceRequest); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_kaspawalletd_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*GetBalanceResponse); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_kaspawalletd_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*CreateUnsignedTransactionRequest); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_kaspawalletd_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*CreateUnsignedTransactionResponse); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_kaspawalletd_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*GetReceiveAddressRequest); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_kaspawalletd_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*GetReceiveAddressResponse); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_kaspawalletd_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*BroadcastRequest); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_kaspawalletd_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*BroadcastResponse); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_kaspawalletd_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*ShutdownRequest); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_kaspawalletd_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*ShutdownResponse); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_kaspawalletd_proto_rawDesc,
NumEnums: 0,
NumMessages: 10,
NumExtensions: 0,
NumServices: 1,
},
GoTypes: file_kaspawalletd_proto_goTypes,
DependencyIndexes: file_kaspawalletd_proto_depIdxs,
MessageInfos: file_kaspawalletd_proto_msgTypes,
}.Build()
File_kaspawalletd_proto = out.File
file_kaspawalletd_proto_rawDesc = nil
file_kaspawalletd_proto_goTypes = nil
file_kaspawalletd_proto_depIdxs = nil
}

View File

@@ -0,0 +1,49 @@
syntax = "proto3";
option go_package = "github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/pb";
service kaspawalletd {
rpc GetBalance (GetBalanceRequest) returns (GetBalanceResponse) {}
rpc CreateUnsignedTransaction (CreateUnsignedTransactionRequest) returns (CreateUnsignedTransactionResponse) {}
rpc GetReceiveAddress (GetReceiveAddressRequest) returns (GetReceiveAddressResponse) {}
rpc Shutdown (ShutdownRequest) returns (ShutdownResponse) {}
rpc Broadcast (BroadcastRequest) returns (BroadcastResponse) {}
}
message GetBalanceRequest {
}
message GetBalanceResponse {
uint64 available = 1;
uint64 pending = 2;
}
message CreateUnsignedTransactionRequest {
string address = 1;
uint64 amount = 2;
}
message CreateUnsignedTransactionResponse {
bytes unsignedTransaction = 1;
}
message GetReceiveAddressRequest {
}
message GetReceiveAddressResponse {
string address = 1;
}
message BroadcastRequest {
bytes transaction = 1;
}
message BroadcastResponse {
string txID = 1;
}
message ShutdownRequest {
}
message ShutdownResponse {
}

View File

@@ -0,0 +1,234 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
package pb
import (
context "context"
grpc "google.golang.org/grpc"
codes "google.golang.org/grpc/codes"
status "google.golang.org/grpc/status"
)
// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
const _ = grpc.SupportPackageIsVersion6
// KaspawalletdClient is the client API for Kaspawalletd service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
type KaspawalletdClient interface {
GetBalance(ctx context.Context, in *GetBalanceRequest, opts ...grpc.CallOption) (*GetBalanceResponse, error)
CreateUnsignedTransaction(ctx context.Context, in *CreateUnsignedTransactionRequest, opts ...grpc.CallOption) (*CreateUnsignedTransactionResponse, error)
GetReceiveAddress(ctx context.Context, in *GetReceiveAddressRequest, opts ...grpc.CallOption) (*GetReceiveAddressResponse, error)
Shutdown(ctx context.Context, in *ShutdownRequest, opts ...grpc.CallOption) (*ShutdownResponse, error)
Broadcast(ctx context.Context, in *BroadcastRequest, opts ...grpc.CallOption) (*BroadcastResponse, error)
}
type kaspawalletdClient struct {
cc grpc.ClientConnInterface
}
func NewKaspawalletdClient(cc grpc.ClientConnInterface) KaspawalletdClient {
return &kaspawalletdClient{cc}
}
func (c *kaspawalletdClient) GetBalance(ctx context.Context, in *GetBalanceRequest, opts ...grpc.CallOption) (*GetBalanceResponse, error) {
out := new(GetBalanceResponse)
err := c.cc.Invoke(ctx, "/kaspawalletd/GetBalance", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *kaspawalletdClient) CreateUnsignedTransaction(ctx context.Context, in *CreateUnsignedTransactionRequest, opts ...grpc.CallOption) (*CreateUnsignedTransactionResponse, error) {
out := new(CreateUnsignedTransactionResponse)
err := c.cc.Invoke(ctx, "/kaspawalletd/CreateUnsignedTransaction", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *kaspawalletdClient) GetReceiveAddress(ctx context.Context, in *GetReceiveAddressRequest, opts ...grpc.CallOption) (*GetReceiveAddressResponse, error) {
out := new(GetReceiveAddressResponse)
err := c.cc.Invoke(ctx, "/kaspawalletd/GetReceiveAddress", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *kaspawalletdClient) Shutdown(ctx context.Context, in *ShutdownRequest, opts ...grpc.CallOption) (*ShutdownResponse, error) {
out := new(ShutdownResponse)
err := c.cc.Invoke(ctx, "/kaspawalletd/Shutdown", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *kaspawalletdClient) Broadcast(ctx context.Context, in *BroadcastRequest, opts ...grpc.CallOption) (*BroadcastResponse, error) {
out := new(BroadcastResponse)
err := c.cc.Invoke(ctx, "/kaspawalletd/Broadcast", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
// KaspawalletdServer is the server API for Kaspawalletd service.
// All implementations must embed UnimplementedKaspawalletdServer
// for forward compatibility
type KaspawalletdServer interface {
GetBalance(context.Context, *GetBalanceRequest) (*GetBalanceResponse, error)
CreateUnsignedTransaction(context.Context, *CreateUnsignedTransactionRequest) (*CreateUnsignedTransactionResponse, error)
GetReceiveAddress(context.Context, *GetReceiveAddressRequest) (*GetReceiveAddressResponse, error)
Shutdown(context.Context, *ShutdownRequest) (*ShutdownResponse, error)
Broadcast(context.Context, *BroadcastRequest) (*BroadcastResponse, error)
mustEmbedUnimplementedKaspawalletdServer()
}
// UnimplementedKaspawalletdServer must be embedded to have forward compatible implementations.
type UnimplementedKaspawalletdServer struct {
}
func (*UnimplementedKaspawalletdServer) GetBalance(context.Context, *GetBalanceRequest) (*GetBalanceResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetBalance not implemented")
}
func (*UnimplementedKaspawalletdServer) CreateUnsignedTransaction(context.Context, *CreateUnsignedTransactionRequest) (*CreateUnsignedTransactionResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method CreateUnsignedTransaction not implemented")
}
func (*UnimplementedKaspawalletdServer) GetReceiveAddress(context.Context, *GetReceiveAddressRequest) (*GetReceiveAddressResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetReceiveAddress not implemented")
}
func (*UnimplementedKaspawalletdServer) Shutdown(context.Context, *ShutdownRequest) (*ShutdownResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method Shutdown not implemented")
}
func (*UnimplementedKaspawalletdServer) Broadcast(context.Context, *BroadcastRequest) (*BroadcastResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method Broadcast not implemented")
}
func (*UnimplementedKaspawalletdServer) mustEmbedUnimplementedKaspawalletdServer() {}
func RegisterKaspawalletdServer(s *grpc.Server, srv KaspawalletdServer) {
s.RegisterService(&_Kaspawalletd_serviceDesc, srv)
}
func _Kaspawalletd_GetBalance_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(GetBalanceRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(KaspawalletdServer).GetBalance(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/kaspawalletd/GetBalance",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(KaspawalletdServer).GetBalance(ctx, req.(*GetBalanceRequest))
}
return interceptor(ctx, in, info, handler)
}
func _Kaspawalletd_CreateUnsignedTransaction_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(CreateUnsignedTransactionRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(KaspawalletdServer).CreateUnsignedTransaction(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/kaspawalletd/CreateUnsignedTransaction",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(KaspawalletdServer).CreateUnsignedTransaction(ctx, req.(*CreateUnsignedTransactionRequest))
}
return interceptor(ctx, in, info, handler)
}
func _Kaspawalletd_GetReceiveAddress_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(GetReceiveAddressRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(KaspawalletdServer).GetReceiveAddress(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/kaspawalletd/GetReceiveAddress",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(KaspawalletdServer).GetReceiveAddress(ctx, req.(*GetReceiveAddressRequest))
}
return interceptor(ctx, in, info, handler)
}
func _Kaspawalletd_Shutdown_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ShutdownRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(KaspawalletdServer).Shutdown(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/kaspawalletd/Shutdown",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(KaspawalletdServer).Shutdown(ctx, req.(*ShutdownRequest))
}
return interceptor(ctx, in, info, handler)
}
func _Kaspawalletd_Broadcast_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).Broadcast(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/kaspawalletd/Broadcast",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(KaspawalletdServer).Broadcast(ctx, req.(*BroadcastRequest))
}
return interceptor(ctx, in, info, handler)
}
var _Kaspawalletd_serviceDesc = grpc.ServiceDesc{
ServiceName: "kaspawalletd",
HandlerType: (*KaspawalletdServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "GetBalance",
Handler: _Kaspawalletd_GetBalance_Handler,
},
{
MethodName: "CreateUnsignedTransaction",
Handler: _Kaspawalletd_CreateUnsignedTransaction_Handler,
},
{
MethodName: "GetReceiveAddress",
Handler: _Kaspawalletd_GetReceiveAddress_Handler,
},
{
MethodName: "Shutdown",
Handler: _Kaspawalletd_Shutdown_Handler,
},
{
MethodName: "Broadcast",
Handler: _Kaspawalletd_Broadcast_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "kaspawalletd.proto",
}

View File

@@ -0,0 +1,83 @@
package server
import (
"context"
"fmt"
"github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/pb"
"github.com/kaspanet/kaspad/cmd/kaspawallet/libkaspawallet"
"github.com/kaspanet/kaspad/util"
"github.com/pkg/errors"
)
func (s *server) changeAddress() (util.Address, error) {
err := s.keysFile.SetLastUsedInternalIndex(s.keysFile.LastUsedInternalIndex() + 1)
if err != nil {
return nil, err
}
err = s.keysFile.Save()
if err != nil {
return nil, err
}
walletAddr := &walletAddress{
index: s.keysFile.LastUsedInternalIndex(),
cosignerIndex: s.keysFile.CosignerIndex,
keyChain: internalKeychain,
}
path := s.walletAddressPath(walletAddr)
return libkaspawallet.Address(s.params, s.keysFile.ExtendedPublicKeys, s.keysFile.MinimumSignatures, path, s.keysFile.ECDSA)
}
func (s *server) GetReceiveAddress(_ context.Context, request *pb.GetReceiveAddressRequest) (*pb.GetReceiveAddressResponse, error) {
s.lock.Lock()
defer s.lock.Unlock()
if !s.isSynced() {
return nil, errors.New("server is not synced")
}
err := s.keysFile.SetLastUsedExternalIndex(s.keysFile.LastUsedExternalIndex() + 1)
if err != nil {
return nil, err
}
err = s.keysFile.Save()
if err != nil {
return nil, err
}
walletAddr := &walletAddress{
index: s.keysFile.LastUsedExternalIndex(),
cosignerIndex: s.keysFile.CosignerIndex,
keyChain: externalKeychain,
}
path := s.walletAddressPath(walletAddr)
address, err := libkaspawallet.Address(s.params, s.keysFile.ExtendedPublicKeys, s.keysFile.MinimumSignatures, path, s.keysFile.ECDSA)
if err != nil {
return nil, err
}
return &pb.GetReceiveAddressResponse{Address: address.String()}, nil
}
func (s *server) walletAddressString(wAddr *walletAddress) (string, error) {
path := s.walletAddressPath(wAddr)
addr, err := libkaspawallet.Address(s.params, s.keysFile.ExtendedPublicKeys, s.keysFile.MinimumSignatures, path, s.keysFile.ECDSA)
if err != nil {
return "", err
}
return addr.String(), nil
}
func (s *server) walletAddressPath(wAddr *walletAddress) string {
if s.isMultisig() {
return fmt.Sprintf("m/%d/%d/%d", wAddr.cosignerIndex, wAddr.keyChain, wAddr.index)
}
return fmt.Sprintf("m/%d/%d", wAddr.keyChain, wAddr.index)
}
func (s *server) isMultisig() bool {
return len(s.keysFile.ExtendedPublicKeys) > 1
}

View File

@@ -0,0 +1,37 @@
package server
import (
"context"
"github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/pb"
)
func (s *server) GetBalance(_ context.Context, _ *pb.GetBalanceRequest) (*pb.GetBalanceResponse, error) {
s.lock.RLock()
defer s.lock.RUnlock()
dagInfo, err := s.rpcClient.GetBlockDAGInfo()
if err != nil {
return nil, err
}
var availableBalance, pendingBalance uint64
for _, entry := range s.utxos {
if isUTXOSpendable(entry, dagInfo.VirtualDAAScore, s.params.BlockCoinbaseMaturity) {
availableBalance += entry.UTXOEntry.Amount()
} else {
pendingBalance += entry.UTXOEntry.Amount()
}
}
return &pb.GetBalanceResponse{
Available: availableBalance,
Pending: pendingBalance,
}, nil
}
func isUTXOSpendable(entry *walletUTXO, virtualDAAScore uint64, coinbaseMaturity uint64) bool {
if !entry.UTXOEntry.IsCoinbase() {
return true
}
return entry.UTXOEntry.BlockDAAScore()+coinbaseMaturity < virtualDAAScore
}

View File

@@ -0,0 +1,33 @@
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/infrastructure/network/rpcclient"
"github.com/pkg/errors"
)
func (s *server) Broadcast(_ context.Context, request *pb.BroadcastRequest) (*pb.BroadcastResponse, error) {
tx, err := libkaspawallet.ExtractTransaction(request.Transaction, s.keysFile.ECDSA)
if err != nil {
return nil, err
}
txID, err := sendTransaction(s.rpcClient, tx)
if err != nil {
return nil, err
}
return &pb.BroadcastResponse{TxID: txID}, nil
}
func sendTransaction(client *rpcclient.RPCClient, tx *externalapi.DomainTransaction) (string, error) {
submitTransactionResponse, err := client.SubmitTransaction(appmessage.DomainTransactionToRPCTransaction(tx))
if err != nil {
return "", errors.Wrapf(err, "error submitting transaction")
}
return submitTransactionResponse.TransactionID, nil
}

View File

@@ -0,0 +1,15 @@
package server
import "github.com/kaspanet/kaspad/domain/consensus/model/externalapi"
type walletUTXO struct {
Outpoint *externalapi.DomainOutpoint
UTXOEntry externalapi.UTXOEntry
address *walletAddress
}
type walletAddress struct {
index uint32
cosignerIndex uint32
keyChain uint8
}

View File

@@ -0,0 +1,95 @@
package server
import (
"context"
"github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/pb"
"github.com/kaspanet/kaspad/cmd/kaspawallet/libkaspawallet"
"github.com/kaspanet/kaspad/util"
"github.com/pkg/errors"
)
func (s *server) CreateUnsignedTransaction(_ context.Context, request *pb.CreateUnsignedTransactionRequest) (*pb.CreateUnsignedTransactionResponse, error) {
s.lock.Lock()
defer s.lock.Unlock()
if !s.isSynced() {
return nil, errors.New("server is not synced")
}
err := s.refreshExistingUTXOs()
if err != nil {
return nil, err
}
toAddress, err := util.DecodeAddress(request.Address, s.params.Prefix)
if err != nil {
return nil, err
}
// TODO: Implement a better fee estimation mechanism
const feePerInput = 1000
selectedUTXOs, changeSompi, err := s.selectUTXOs(request.Amount, feePerInput)
if err != nil {
return nil, err
}
changeAddress, 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
}
return &pb.CreateUnsignedTransactionResponse{UnsignedTransaction: unsignedTransaction}, nil
}
func (s *server) selectUTXOs(spendAmount uint64, feePerInput uint64) (
selectedUTXOs []*libkaspawallet.UTXO, changeSompi uint64, err error) {
selectedUTXOs = []*libkaspawallet.UTXO{}
totalValue := uint64(0)
dagInfo, err := s.rpcClient.GetBlockDAGInfo()
if err != nil {
return nil, 0, err
}
for _, utxo := range s.utxos {
if !isUTXOSpendable(utxo, dagInfo.VirtualDAAScore, s.params.BlockCoinbaseMaturity) {
continue
}
selectedUTXOs = append(selectedUTXOs, &libkaspawallet.UTXO{
Outpoint: utxo.Outpoint,
UTXOEntry: utxo.UTXOEntry,
DerivationPath: s.walletAddressPath(utxo.address),
})
totalValue += utxo.UTXOEntry.Amount()
fee := feePerInput * uint64(len(selectedUTXOs))
totalSpend := spendAmount + fee
if totalValue >= totalSpend {
break
}
}
fee := feePerInput * uint64(len(selectedUTXOs))
totalSpend := spendAmount + fee
if totalValue < totalSpend {
return nil, 0, errors.Errorf("Insufficient funds for send: %f required, while only %f available",
float64(totalSpend)/util.SompiPerKaspa, float64(totalValue)/util.SompiPerKaspa)
}
return selectedUTXOs, totalValue - totalSpend, nil
}

View File

@@ -0,0 +1,47 @@
package server
import (
"fmt"
"os"
"path/filepath"
"github.com/kaspanet/kaspad/util"
"github.com/kaspanet/kaspad/infrastructure/logger"
"github.com/kaspanet/kaspad/util/panics"
)
var (
backendLog = logger.NewBackend()
log = backendLog.Logger("KSWD")
spawn = panics.GoroutineWrapperFunc(log)
defaultAppDir = util.AppDir("kaspawallet", false)
defaultLogFile = filepath.Join(defaultAppDir, "daemon.log")
defaultErrLogFile = filepath.Join(defaultAppDir, "daemon_err.log")
)
func initLog(logFile, errLogFile string) {
log.SetLevel(logger.LevelDebug)
err := backendLog.AddLogFile(logFile, logger.LevelTrace)
if err != nil {
fmt.Fprintf(os.Stderr, "Error adding log file %s as log rotator for level %s: %s", logFile, logger.LevelTrace, err)
os.Exit(1)
}
err = backendLog.AddLogFile(errLogFile, logger.LevelWarn)
if err != nil {
fmt.Fprintf(os.Stderr, "Error adding log file %s as log rotator for level %s: %s", errLogFile, logger.LevelWarn, err)
os.Exit(1)
}
err = backendLog.AddLogWriter(os.Stdout, logger.LevelInfo)
if err != nil {
fmt.Fprintf(os.Stderr, "Error adding stdout to the loggerfor level %s: %s", logger.LevelWarn, err)
os.Exit(1)
}
err = backendLog.Run()
if err != nil {
fmt.Fprintf(os.Stderr, "Error starting the logger: %s ", err)
os.Exit(1)
}
}

View File

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

View File

@@ -0,0 +1,109 @@
package server
import (
"fmt"
"net"
"os"
"sync"
"time"
"github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/pb"
"github.com/kaspanet/kaspad/cmd/kaspawallet/keys"
"github.com/kaspanet/kaspad/domain/consensus/model/externalapi"
"github.com/kaspanet/kaspad/domain/dagconfig"
"github.com/kaspanet/kaspad/infrastructure/network/rpcclient"
"github.com/kaspanet/kaspad/infrastructure/os/signal"
"github.com/kaspanet/kaspad/util/panics"
"github.com/pkg/errors"
"google.golang.org/grpc"
)
type server struct {
pb.UnimplementedKaspawalletdServer
rpcClient *rpcclient.RPCClient
params *dagconfig.Params
lock sync.RWMutex
utxos map[externalapi.DomainOutpoint]*walletUTXO
nextSyncStartIndex uint32
keysFile *keys.File
shutdown chan struct{}
}
// Start starts the kaspawalletd server
func Start(params *dagconfig.Params, listen, rpcServer string, keysFilePath string) error {
initLog(defaultLogFile, defaultErrLogFile)
defer panics.HandlePanic(log, "MAIN", nil)
interrupt := signal.InterruptListener()
listener, err := net.Listen("tcp", listen)
if err != nil {
return (errors.Wrapf(err, "Error listening to tcp at %s", listen))
}
log.Infof("Listening on %s", listen)
rpcClient, err := connectToRPC(params, rpcServer)
if err != nil {
return (errors.Wrapf(err, "Error connecting to RPC server %s", rpcServer))
}
keysFile, err := keys.ReadKeysFile(params, keysFilePath)
if err != nil {
return (errors.Wrapf(err, "Error connecting to RPC server %s", rpcServer))
}
serverInstance := &server{
rpcClient: rpcClient,
params: params,
utxos: make(map[externalapi.DomainOutpoint]*walletUTXO),
nextSyncStartIndex: 0,
keysFile: keysFile,
shutdown: make(chan struct{}),
}
spawn("serverInstance.sync", func() {
err := serverInstance.sync()
if err != nil {
printErrorAndExit(errors.Wrap(err, "error syncing the wallet"))
}
})
grpcServer := grpc.NewServer()
pb.RegisterKaspawalletdServer(grpcServer, serverInstance)
spawn("grpcServer.Serve", func() {
err := grpcServer.Serve(listener)
if err != nil {
printErrorAndExit(errors.Wrap(err, "Error serving gRPC"))
}
})
select {
case <-serverInstance.shutdown:
case <-interrupt:
const stopTimeout = 2 * time.Second
stopChan := make(chan interface{})
spawn("gRPCServer.Stop", func() {
grpcServer.GracefulStop()
close(stopChan)
})
select {
case <-stopChan:
case <-time.After(stopTimeout):
log.Warnf("Could not gracefully stop: timed out after %s", stopTimeout)
grpcServer.Stop()
}
}
return nil
}
func printErrorAndExit(err error) {
fmt.Fprintf(os.Stderr, "%+v\n", err)
os.Exit(1)
}

View File

@@ -0,0 +1,13 @@
package server
import (
"context"
"github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/pb"
)
func (s *server) Shutdown(ctx context.Context, request *pb.ShutdownRequest) (*pb.ShutdownResponse, error) {
s.lock.Lock()
defer s.lock.Unlock()
close(s.shutdown)
return &pb.ShutdownResponse{}, nil
}

View File

@@ -0,0 +1,263 @@
package server
import (
"github.com/kaspanet/kaspad/app/appmessage"
"github.com/kaspanet/kaspad/domain/consensus/model/externalapi"
"github.com/pkg/errors"
"time"
)
const (
// externalKeychain is the key chain that is used to create receive addresses
externalKeychain = 0
// internalKeychain is used to create change addresses
internalKeychain = 1
)
var keyChains = []uint8{externalKeychain, internalKeychain}
type walletAddressSet map[string]*walletAddress
func (was walletAddressSet) strings() []string {
addresses := make([]string, 0, len(was))
for addr := range was {
addresses = append(addresses, addr)
}
return addresses
}
func (s *server) sync() error {
ticker := time.NewTicker(time.Second)
defer ticker.Stop()
for range ticker.C {
err := s.collectUTXOsFromRecentAddresses()
if err != nil {
return err
}
err = s.collectUTXOsFromFarAddresses()
if err != nil {
return err
}
err = s.refreshExistingUTXOsWithLock()
if err != nil {
return err
}
}
return nil
}
const numIndexesToQuery = 100
// addressesToQuery scans the addresses in the given range. Because
// each cosigner in a multisig has its own unique path for generating
// addresses it goes over all the cosigners and add their addresses
// for each key chain.
func (s *server) addressesToQuery(start, end uint32) (walletAddressSet, error) {
addresses := make(walletAddressSet)
for index := start; index < end; index++ {
for cosignerIndex := uint32(0); cosignerIndex < uint32(len(s.keysFile.ExtendedPublicKeys)); cosignerIndex++ {
for _, keychain := range keyChains {
address := &walletAddress{
index: index,
cosignerIndex: cosignerIndex,
keyChain: keychain,
}
addressString, err := s.walletAddressString(address)
if err != nil {
return nil, err
}
addresses[addressString] = address
}
}
}
return addresses, nil
}
// collectUTXOsFromFarAddresses collects numIndexesToQuery UTXOs
// from the last point it stopped in the previous call.
func (s *server) collectUTXOsFromFarAddresses() error {
s.lock.Lock()
defer s.lock.Unlock()
err := s.collectUTXOs(s.nextSyncStartIndex, s.nextSyncStartIndex+numIndexesToQuery)
if err != nil {
return err
}
s.nextSyncStartIndex += numIndexesToQuery
return nil
}
func (s *server) maxUsedIndex() uint32 {
s.lock.RLock()
defer s.lock.RUnlock()
maxUsedIndex := s.keysFile.LastUsedExternalIndex()
if s.keysFile.LastUsedInternalIndex() > maxUsedIndex {
maxUsedIndex = s.keysFile.LastUsedInternalIndex()
}
return maxUsedIndex
}
// collectUTXOsFromRecentAddresses collects UTXOs from used addresses until
// the address with the index of the last used address + 1000.
// collectUTXOsFromRecentAddresses scans addresses in batches of numIndexesToQuery,
// and releases the lock between scans.
func (s *server) collectUTXOsFromRecentAddresses() error {
maxUsedIndex := s.maxUsedIndex()
for i := uint32(0); i < maxUsedIndex+1000; i += numIndexesToQuery {
err := s.collectUTXOsWithLock(i, i+numIndexesToQuery)
if err != nil {
return err
}
}
return nil
}
func (s *server) collectUTXOsWithLock(start, end uint32) error {
s.lock.Lock()
defer s.lock.Unlock()
return s.collectUTXOs(start, end)
}
func (s *server) collectUTXOs(start, end uint32) error {
addressSet, err := s.addressesToQuery(start, end)
if err != nil {
return err
}
getUTXOsByAddressesResponse, err := s.rpcClient.GetUTXOsByAddresses(addressSet.strings())
if err != nil {
return err
}
err = s.updateLastUsedIndexes(addressSet, getUTXOsByAddressesResponse)
if err != nil {
return err
}
err = s.updateUTXOs(addressSet, getUTXOsByAddressesResponse)
if err != nil {
return err
}
return nil
}
func (s *server) updateUTXOs(addressSet walletAddressSet,
getUTXOsByAddressesResponse *appmessage.GetUTXOsByAddressesResponseMessage) error {
for _, entry := range getUTXOsByAddressesResponse.Entries {
err := s.addEntryToUTXOSet(entry, addressSet)
if err != nil {
return err
}
}
return nil
}
func (s *server) updateLastUsedIndexes(addressSet walletAddressSet,
getUTXOsByAddressesResponse *appmessage.GetUTXOsByAddressesResponseMessage) error {
lastUsedExternalIndex := s.keysFile.LastUsedExternalIndex()
lastUsedInternalIndex := s.keysFile.LastUsedInternalIndex()
for _, entry := range getUTXOsByAddressesResponse.Entries {
walletAddress, ok := addressSet[entry.Address]
if !ok {
return errors.Errorf("Got result from address %s even though it wasn't requested", entry.Address)
}
if walletAddress.cosignerIndex != s.keysFile.CosignerIndex {
continue
}
if walletAddress.keyChain == externalKeychain {
if walletAddress.index > lastUsedExternalIndex {
lastUsedExternalIndex = walletAddress.index
}
continue
}
if walletAddress.index > lastUsedInternalIndex {
lastUsedInternalIndex = walletAddress.index
}
}
err := s.keysFile.SetLastUsedExternalIndex(lastUsedExternalIndex)
if err != nil {
return err
}
return s.keysFile.SetLastUsedInternalIndex(lastUsedInternalIndex)
}
func (s *server) refreshExistingUTXOsWithLock() error {
s.lock.Lock()
defer s.lock.Unlock()
return s.refreshExistingUTXOs()
}
func (s *server) addEntryToUTXOSet(entry *appmessage.UTXOsByAddressesEntry, addressSet walletAddressSet) error {
outpoint, err := appmessage.RPCOutpointToDomainOutpoint(entry.Outpoint)
if err != nil {
return err
}
utxoEntry, err := appmessage.RPCUTXOEntryToUTXOEntry(entry.UTXOEntry)
if err != nil {
return err
}
address, ok := addressSet[entry.Address]
if !ok {
return errors.Errorf("Got result from address %s even though it wasn't requested", entry.Address)
}
s.utxos[*outpoint] = &walletUTXO{
Outpoint: outpoint,
UTXOEntry: utxoEntry,
address: address,
}
return nil
}
func (s *server) refreshExistingUTXOs() error {
addressSet := make(walletAddressSet, len(s.utxos))
for _, utxo := range s.utxos {
addressString, err := s.walletAddressString(utxo.address)
if err != nil {
return err
}
addressSet[addressString] = utxo.address
}
getUTXOsByAddressesResponse, err := s.rpcClient.GetUTXOsByAddresses(addressSet.strings())
if err != nil {
return err
}
s.utxos = make(map[externalapi.DomainOutpoint]*walletUTXO, len(getUTXOsByAddressesResponse.Entries))
for _, entry := range getUTXOsByAddressesResponse.Entries {
err := s.addEntryToUTXOSet(entry, addressSet)
if err != nil {
return err
}
}
return nil
}
func (s *server) isSynced() bool {
return s.nextSyncStartIndex > s.keysFile.LastUsedInternalIndex() && s.nextSyncStartIndex > s.keysFile.LastUsedExternalIndex()
}

View File

@@ -3,17 +3,20 @@ package main
import (
"bufio"
"fmt"
"os"
"github.com/kaspanet/kaspad/cmd/kaspawallet/keys"
"github.com/kaspanet/kaspad/cmd/kaspawallet/libkaspawallet"
"github.com/kaspanet/kaspad/cmd/kaspawallet/utils"
"os"
"github.com/pkg/errors"
)
func dumpUnencryptedData(conf *dumpUnencryptedDataConfig) error {
err := confirmDump()
if err != nil {
return err
if !conf.Yes {
err := confirmDump()
if err != nil {
return err
}
}
keysFile, err := keys.ReadKeysFile(conf.NetParams(), conf.KeysFile)
@@ -21,29 +24,29 @@ func dumpUnencryptedData(conf *dumpUnencryptedDataConfig) error {
return err
}
privateKeys, err := keysFile.DecryptPrivateKeys()
mnemonics, err := keysFile.DecryptMnemonics(conf.Password)
if err != nil {
return err
}
privateKeysPublicKeys := make(map[string]struct{})
for i, privateKey := range privateKeys {
fmt.Printf("Private key #%d:\n%x\n\n", i+1, privateKey)
publicKey, err := libkaspawallet.PublicKeyFromPrivateKey(privateKey)
mnemonicPublicKeys := make(map[string]struct{})
for i, mnemonic := range mnemonics {
fmt.Printf("Mnemonic #%d:\n%s\n\n", i+1, mnemonic)
publicKey, err := libkaspawallet.MasterPublicKeyFromMnemonic(conf.NetParams(), mnemonic, len(keysFile.ExtendedPublicKeys) > 1)
if err != nil {
return err
}
privateKeysPublicKeys[string(publicKey)] = struct{}{}
mnemonicPublicKeys[publicKey] = struct{}{}
}
i := 1
for _, publicKey := range keysFile.PublicKeys {
if _, exists := privateKeysPublicKeys[string(publicKey)]; exists {
for _, extendedPublicKey := range keysFile.ExtendedPublicKeys {
if _, exists := mnemonicPublicKeys[extendedPublicKey]; exists {
continue
}
fmt.Printf("Public key #%d:\n%x\n\n", i, publicKey)
fmt.Printf("Extended Public key #%d:\n%s\n\n", i, extendedPublicKey)
i++
}
@@ -55,14 +58,14 @@ func confirmDump() error {
reader := bufio.NewReader(os.Stdin)
fmt.Printf("This operation will print your unencrypted keys on the screen. Anyone that sees this information " +
"will be able to steal your funds. Are you sure you want to proceed (y/N)? ")
line, isPrefix, err := reader.ReadLine()
line, err := utils.ReadLine(reader)
if err != nil {
return err
}
fmt.Println()
if isPrefix || string(line) != "y" {
if string(line) != "y" {
return errors.Errorf("Dump aborted by user")
}

View File

@@ -4,73 +4,79 @@ import (
"bufio"
"crypto/rand"
"crypto/subtle"
"encoding/hex"
"fmt"
"github.com/kaspanet/kaspad/cmd/kaspawallet/libkaspawallet"
"github.com/kaspanet/kaspad/cmd/kaspawallet/utils"
"github.com/kaspanet/kaspad/domain/dagconfig"
"github.com/pkg/errors"
"github.com/tyler-smith/go-bip39"
"os"
)
// CreateKeyPairs generates `numKeys` number of key pairs.
func CreateKeyPairs(numKeys uint32, ecdsa bool) (encryptedPrivateKeys []*EncryptedPrivateKey, publicKeys [][]byte, err error) {
return createKeyPairsFromFunction(numKeys, func(_ uint32) ([]byte, []byte, error) {
return libkaspawallet.CreateKeyPair(ecdsa)
})
}
// ImportKeyPairs imports a `numKeys` of private keys and generates key pairs out of them.
func ImportKeyPairs(numKeys uint32) (encryptedPrivateKeys []*EncryptedPrivateKey, publicKeys [][]byte, err error) {
return createKeyPairsFromFunction(numKeys, func(keyIndex uint32) ([]byte, []byte, error) {
fmt.Printf("Enter private key #%d here:\n", keyIndex+1)
reader := bufio.NewReader(os.Stdin)
line, isPrefix, err := reader.ReadLine()
// CreateMnemonics generates `numKeys` number of mnemonics.
func CreateMnemonics(params *dagconfig.Params, numKeys uint32, cmdLinePassword string, isMultisig bool) (encryptedPrivateKeys []*EncryptedMnemonic, extendedPublicKeys []string, err error) {
mnemonics := make([]string, numKeys)
for i := uint32(0); i < numKeys; i++ {
var err error
mnemonics[i], err = libkaspawallet.CreateMnemonic()
if err != nil {
return nil, nil, err
}
if isPrefix {
return nil, nil, errors.Errorf("Private key is too long")
}
privateKey, err := hex.DecodeString(string(line))
if err != nil {
return nil, nil, err
}
publicKey, err := libkaspawallet.PublicKeyFromPrivateKey(privateKey)
if err != nil {
return nil, nil, err
}
return privateKey, publicKey, nil
})
}
func createKeyPairsFromFunction(numKeys uint32, keyPairFunction func(keyIndex uint32) ([]byte, []byte, error)) (
encryptedPrivateKeys []*EncryptedPrivateKey, publicKeys [][]byte, err error) {
password := getPassword("Enter password for the key file:")
confirmPassword := getPassword("Confirm password:")
if subtle.ConstantTimeCompare(password, confirmPassword) != 1 {
return nil, nil, errors.New("Passwords are not identical")
}
encryptedPrivateKeys = make([]*EncryptedPrivateKey, 0, numKeys)
return encryptedMnemonicExtendedPublicKeyPairs(params, mnemonics, cmdLinePassword, isMultisig)
}
// ImportMnemonics imports a `numKeys` of mnemonics.
func ImportMnemonics(params *dagconfig.Params, numKeys uint32, cmdLinePassword string, isMultisig bool) (encryptedPrivateKeys []*EncryptedMnemonic, extendedPublicKeys []string, err error) {
mnemonics := make([]string, numKeys)
for i := uint32(0); i < numKeys; i++ {
privateKey, publicKey, err := keyPairFunction(i)
fmt.Printf("Enter mnemonic #%d here:\n", i+1)
reader := bufio.NewReader(os.Stdin)
mnemonic, err := utils.ReadLine(reader)
if err != nil {
return nil, nil, err
}
publicKeys = append(publicKeys, publicKey)
if !bip39.IsMnemonicValid(string(mnemonic)) {
return nil, nil, errors.Errorf("mnemonic is invalid")
}
encryptedPrivateKey, err := encryptPrivateKey(privateKey, password)
mnemonics[i] = string(mnemonic)
}
return encryptedMnemonicExtendedPublicKeyPairs(params, mnemonics, cmdLinePassword, isMultisig)
}
func encryptedMnemonicExtendedPublicKeyPairs(params *dagconfig.Params, mnemonics []string, cmdLinePassword string, isMultisig bool) (
encryptedPrivateKeys []*EncryptedMnemonic, extendedPublicKeys []string, err error) {
password := []byte(cmdLinePassword)
if len(password) == 0 {
password = getPassword("Enter password for the key file:")
confirmPassword := getPassword("Confirm password:")
if subtle.ConstantTimeCompare(password, confirmPassword) != 1 {
return nil, nil, errors.New("Passwords are not identical")
}
}
encryptedPrivateKeys = make([]*EncryptedMnemonic, 0, len(mnemonics))
for _, mnemonic := range mnemonics {
extendedPublicKey, err := libkaspawallet.MasterPublicKeyFromMnemonic(params, mnemonic, isMultisig)
if err != nil {
return nil, nil, err
}
extendedPublicKeys = append(extendedPublicKeys, extendedPublicKey)
encryptedPrivateKey, err := encryptMnemonic(mnemonic, password)
if err != nil {
return nil, nil, err
}
encryptedPrivateKeys = append(encryptedPrivateKeys, encryptedPrivateKey)
}
return encryptedPrivateKeys, publicKeys, nil
return encryptedPrivateKeys, extendedPublicKeys, nil
}
func generateSalt() ([]byte, error) {
@@ -83,7 +89,9 @@ func generateSalt() ([]byte, error) {
return salt, nil
}
func encryptPrivateKey(privateKey []byte, password []byte) (*EncryptedPrivateKey, error) {
func encryptMnemonic(mnemonic string, password []byte) (*EncryptedMnemonic, error) {
mnemonicBytes := []byte(mnemonic)
salt, err := generateSalt()
if err != nil {
return nil, err
@@ -95,15 +103,15 @@ func encryptPrivateKey(privateKey []byte, password []byte) (*EncryptedPrivateKey
}
// Select a random nonce, and leave capacity for the ciphertext.
nonce := make([]byte, aead.NonceSize(), aead.NonceSize()+len(privateKey)+aead.Overhead())
nonce := make([]byte, aead.NonceSize(), aead.NonceSize()+len(mnemonicBytes)+aead.Overhead())
if _, err := rand.Read(nonce); err != nil {
return nil, err
}
// Encrypt the message and append the ciphertext to the nonce.
cipher := aead.Seal(nonce, nonce, privateKey, nil)
cipher := aead.Seal(nonce, nonce, []byte(mnemonicBytes), nil)
return &EncryptedPrivateKey{
return &EncryptedMnemonic{
cipher: cipher,
salt: salt,
}, nil

View File

@@ -6,6 +6,7 @@ import (
"encoding/hex"
"encoding/json"
"fmt"
"github.com/kaspanet/kaspad/cmd/kaspawallet/utils"
"os"
"path/filepath"
"runtime"
@@ -31,53 +32,62 @@ type encryptedPrivateKeyJSON struct {
}
type keysFileJSON struct {
EncryptedPrivateKeys []*encryptedPrivateKeyJSON `json:"encryptedPrivateKeys"`
PublicKeys []string `json:"publicKeys"`
MinimumSignatures uint32 `json:"minimumSignatures"`
ECDSA bool `json:"ecdsa"`
EncryptedPrivateKeys []*encryptedPrivateKeyJSON `json:"encryptedMnemonics"`
ExtendedPublicKeys []string `json:"publicKeys"`
MinimumSignatures uint32 `json:"minimumSignatures"`
CosignerIndex uint32 `json:"cosignerIndex"`
LastUsedExternalIndex uint32 `json:"lastUsedExternalIndex"`
LastUsedInternalIndex uint32 `json:"lastUsedInternalIndex"`
ECDSA bool `json:"ecdsa"`
}
// EncryptedPrivateKey represents an encrypted private key
type EncryptedPrivateKey struct {
// EncryptedMnemonic represents an encrypted mnemonic
type EncryptedMnemonic struct {
cipher []byte
salt []byte
}
// Data holds all the data related to the wallet keys
type Data struct {
encryptedPrivateKeys []*EncryptedPrivateKey
PublicKeys [][]byte
MinimumSignatures uint32
ECDSA bool
// File holds all the data related to the wallet keys
type File struct {
EncryptedMnemonics []*EncryptedMnemonic
ExtendedPublicKeys []string
MinimumSignatures uint32
CosignerIndex uint32
lastUsedExternalIndex uint32
lastUsedInternalIndex uint32
ECDSA bool
path string
}
func (d *Data) toJSON() *keysFileJSON {
encryptedPrivateKeysJSON := make([]*encryptedPrivateKeyJSON, len(d.encryptedPrivateKeys))
for i, encryptedPrivateKey := range d.encryptedPrivateKeys {
func (d *File) toJSON() *keysFileJSON {
encryptedPrivateKeysJSON := make([]*encryptedPrivateKeyJSON, len(d.EncryptedMnemonics))
for i, encryptedPrivateKey := range d.EncryptedMnemonics {
encryptedPrivateKeysJSON[i] = &encryptedPrivateKeyJSON{
Cipher: hex.EncodeToString(encryptedPrivateKey.cipher),
Salt: hex.EncodeToString(encryptedPrivateKey.salt),
}
}
publicKeysHex := make([]string, len(d.PublicKeys))
for i, publicKey := range d.PublicKeys {
publicKeysHex[i] = hex.EncodeToString(publicKey)
}
return &keysFileJSON{
EncryptedPrivateKeys: encryptedPrivateKeysJSON,
PublicKeys: publicKeysHex,
MinimumSignatures: d.MinimumSignatures,
ECDSA: d.ECDSA,
EncryptedPrivateKeys: encryptedPrivateKeysJSON,
ExtendedPublicKeys: d.ExtendedPublicKeys,
MinimumSignatures: d.MinimumSignatures,
ECDSA: d.ECDSA,
CosignerIndex: d.CosignerIndex,
LastUsedExternalIndex: d.lastUsedExternalIndex,
LastUsedInternalIndex: d.lastUsedInternalIndex,
}
}
func (d *Data) fromJSON(fileJSON *keysFileJSON) error {
func (d *File) fromJSON(fileJSON *keysFileJSON) error {
d.MinimumSignatures = fileJSON.MinimumSignatures
d.ECDSA = fileJSON.ECDSA
d.ExtendedPublicKeys = fileJSON.ExtendedPublicKeys
d.CosignerIndex = fileJSON.CosignerIndex
d.lastUsedExternalIndex = fileJSON.LastUsedExternalIndex
d.lastUsedInternalIndex = fileJSON.LastUsedInternalIndex
d.encryptedPrivateKeys = make([]*EncryptedPrivateKey, len(fileJSON.EncryptedPrivateKeys))
d.EncryptedMnemonics = make([]*EncryptedMnemonic, len(fileJSON.EncryptedPrivateKeys))
for i, encryptedPrivateKeyJSON := range fileJSON.EncryptedPrivateKeys {
cipher, err := hex.DecodeString(encryptedPrivateKeyJSON.Cipher)
if err != nil {
@@ -89,32 +99,92 @@ func (d *Data) fromJSON(fileJSON *keysFileJSON) error {
return err
}
d.encryptedPrivateKeys[i] = &EncryptedPrivateKey{
d.EncryptedMnemonics[i] = &EncryptedMnemonic{
cipher: cipher,
salt: salt,
}
}
d.PublicKeys = make([][]byte, len(fileJSON.PublicKeys))
for i, publicKey := range fileJSON.PublicKeys {
var err error
d.PublicKeys[i], err = hex.DecodeString(publicKey)
if err != nil {
return err
}
}
return nil
}
// DecryptPrivateKeys asks the user to enter the password for the private keys and
// SetPath sets the path where the file is saved to.
func (d *File) SetPath(params *dagconfig.Params, path string, forceOverride bool) error {
if path == "" {
path = defaultKeysFile(params)
}
if !forceOverride {
exists, err := pathExists(path)
if err != nil {
return err
}
if exists {
reader := bufio.NewReader(os.Stdin)
fmt.Printf("The file %s already exists. Are you sure you want to override it (type 'y' to approve)? ", d.path)
line, err := utils.ReadLine(reader)
if err != nil {
return err
}
if string(line) != "y" {
return errors.Errorf("aborted setting the file path to %s", path)
}
}
}
d.path = path
return nil
}
// Path returns the file path.
func (d *File) Path() string {
return d.path
}
// SetLastUsedExternalIndex sets the last used index in the external key
// chain, and saves the file with the updated data.
func (d *File) SetLastUsedExternalIndex(index uint32) error {
if d.lastUsedExternalIndex == index {
return nil
}
d.lastUsedExternalIndex = index
return d.Save()
}
// LastUsedExternalIndex returns the last used index in the external key
// chain and saves the file with the updated data.
func (d *File) LastUsedExternalIndex() uint32 {
return d.lastUsedExternalIndex
}
// SetLastUsedInternalIndex sets the last used index in the internal key chain, and saves the file.
func (d *File) SetLastUsedInternalIndex(index uint32) error {
if d.lastUsedInternalIndex == index {
return nil
}
d.lastUsedInternalIndex = index
return d.Save()
}
// LastUsedInternalIndex returns the last used index in the internal key chain
func (d *File) LastUsedInternalIndex() uint32 {
return d.lastUsedInternalIndex
}
// DecryptMnemonics asks the user to enter the password for the private keys and
// returns the decrypted private keys.
func (d *Data) DecryptPrivateKeys() ([][]byte, error) {
password := getPassword("Password:")
privateKeys := make([][]byte, len(d.encryptedPrivateKeys))
for i, encryptedPrivateKey := range d.encryptedPrivateKeys {
func (d *File) DecryptMnemonics(cmdLinePassword string) ([]string, error) {
password := []byte(cmdLinePassword)
if len(password) == 0 {
password = getPassword("Password:")
}
privateKeys := make([]string, len(d.EncryptedMnemonics))
for i, encryptedPrivateKey := range d.EncryptedMnemonics {
var err error
privateKeys[i], err = decryptPrivateKey(encryptedPrivateKey, password)
privateKeys[i], err = decryptMnemonic(encryptedPrivateKey, password)
if err != nil {
return nil, err
}
@@ -124,7 +194,7 @@ func (d *Data) DecryptPrivateKeys() ([][]byte, error) {
}
// ReadKeysFile returns the data related to the keys file
func ReadKeysFile(netParams *dagconfig.Params, path string) (*Data, error) {
func ReadKeysFile(netParams *dagconfig.Params, path string) (*File, error) {
if path == "" {
path = defaultKeysFile(netParams)
}
@@ -142,7 +212,9 @@ func ReadKeysFile(netParams *dagconfig.Params, path string) (*Data, error) {
return nil, err
}
keysFile := &Data{}
keysFile := &File{
path: path,
}
err = keysFile.fromJSON(decodedFile)
if err != nil {
return nil, err
@@ -180,57 +252,29 @@ func pathExists(path string) (bool, error) {
return false, err
}
// WriteKeysFile writes a keys file with the given data
func WriteKeysFile(netParams *dagconfig.Params, path string, encryptedPrivateKeys []*EncryptedPrivateKey,
publicKeys [][]byte, minimumSignatures uint32, ecdsa bool) error {
if path == "" {
path = defaultKeysFile(netParams)
// Save writes the file contents to the disk.
func (d *File) Save() error {
if d.path == "" {
return errors.New("cannot save a file with uninitialized path")
}
exists, err := pathExists(path)
err := createFileDirectoryIfDoesntExist(d.path)
if err != nil {
return err
}
if exists {
reader := bufio.NewReader(os.Stdin)
fmt.Printf("The file %s already exists. Are you sure you want to override it (type 'y' to approve)? ", path)
line, _, err := reader.ReadLine()
if err != nil {
return err
}
if string(line) != "y" {
return errors.Errorf("aborted keys file creation")
}
}
err = createFileDirectoryIfDoesntExist(path)
if err != nil {
return err
}
file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE, 0600)
file, err := os.OpenFile(d.path, os.O_WRONLY|os.O_CREATE, 0600)
if err != nil {
return err
}
defer file.Close()
keysFile := &Data{
encryptedPrivateKeys: encryptedPrivateKeys,
PublicKeys: publicKeys,
MinimumSignatures: minimumSignatures,
ECDSA: ecdsa,
}
encoder := json.NewEncoder(file)
err = encoder.Encode(keysFile.toJSON())
err = encoder.Encode(d.toJSON())
if err != nil {
return err
}
fmt.Printf("Wrote the keys into %s\n", path)
return nil
}
@@ -239,19 +283,24 @@ func getAEAD(password, salt []byte) (cipher.AEAD, error) {
return chacha20poly1305.NewX(key)
}
func decryptPrivateKey(encryptedPrivateKey *EncryptedPrivateKey, password []byte) ([]byte, error) {
func decryptMnemonic(encryptedPrivateKey *EncryptedMnemonic, password []byte) (string, error) {
aead, err := getAEAD(password, encryptedPrivateKey.salt)
if err != nil {
return nil, err
return "", err
}
if len(encryptedPrivateKey.cipher) < aead.NonceSize() {
return nil, errors.New("ciphertext too short")
return "", errors.New("ciphertext too short")
}
// Split nonce and ciphertext.
nonce, ciphertext := encryptedPrivateKey.cipher[:aead.NonceSize()], encryptedPrivateKey.cipher[aead.NonceSize():]
// Decrypt the message and check it wasn't tampered with.
return aead.Open(nil, nonce, ciphertext, nil)
decrypted, err := aead.Open(nil, nonce, ciphertext, nil)
if err != nil {
return "", err
}
return string(decrypted), nil
}

View File

@@ -0,0 +1,49 @@
// Copyright (c) 2015 The btcsuite developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
// AUTOGENERATED by genalphabet.go; do not edit.
package base58
const (
// alphabet is the modified base58 alphabet used by Bitcoin.
alphabet = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
alphabetIdx0 = '1'
)
var b58 = [256]byte{
255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255,
255, 0, 1, 2, 3, 4, 5, 6,
7, 8, 255, 255, 255, 255, 255, 255,
255, 9, 10, 11, 12, 13, 14, 15,
16, 255, 17, 18, 19, 20, 21, 255,
22, 23, 24, 25, 26, 27, 28, 29,
30, 31, 32, 255, 255, 255, 255, 255,
255, 33, 34, 35, 36, 37, 38, 39,
40, 41, 42, 43, 255, 44, 45, 46,
47, 48, 49, 50, 51, 52, 53, 54,
55, 56, 57, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255,
}

View File

@@ -0,0 +1,75 @@
// Copyright (c) 2013-2015 The btcsuite developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
package base58
import (
"math/big"
)
//go:generate go run genalphabet.go
var bigRadix = big.NewInt(58)
var bigZero = big.NewInt(0)
// Decode decodes a modified base58 string to a byte slice.
func Decode(b string) []byte {
answer := big.NewInt(0)
j := big.NewInt(1)
scratch := new(big.Int)
for i := len(b) - 1; i >= 0; i-- {
tmp := b58[b[i]]
if tmp == 255 {
return []byte("")
}
scratch.SetInt64(int64(tmp))
scratch.Mul(j, scratch)
answer.Add(answer, scratch)
j.Mul(j, bigRadix)
}
tmpval := answer.Bytes()
var numZeros int
for numZeros = 0; numZeros < len(b); numZeros++ {
if b[numZeros] != alphabetIdx0 {
break
}
}
flen := numZeros + len(tmpval)
val := make([]byte, flen)
copy(val[numZeros:], tmpval)
return val
}
// Encode encodes a byte slice to a modified base58 string.
func Encode(b []byte) string {
x := new(big.Int)
x.SetBytes(b)
answer := make([]byte, 0, len(b)*136/100)
for x.Cmp(bigZero) > 0 {
mod := new(big.Int)
x.DivMod(x, bigRadix, mod)
answer = append(answer, alphabet[mod.Int64()])
}
// leading zero bytes
for _, i := range b {
if i != 0 {
break
}
answer = append(answer, alphabetIdx0)
}
// reverse
alen := len(answer)
for i := 0; i < alen/2; i++ {
answer[i], answer[alen-1-i] = answer[alen-1-i], answer[i]
}
return string(answer)
}

View File

@@ -0,0 +1,98 @@
// Copyright (c) 2013-2017 The btcsuite developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
package base58_test
import (
"bytes"
"encoding/hex"
"testing"
"github.com/btcsuite/btcutil/base58"
)
var stringTests = []struct {
in string
out string
}{
{"", ""},
{" ", "Z"},
{"-", "n"},
{"0", "q"},
{"1", "r"},
{"-1", "4SU"},
{"11", "4k8"},
{"abc", "ZiCa"},
{"1234598760", "3mJr7AoUXx2Wqd"},
{"abcdefghijklmnopqrstuvwxyz", "3yxU3u1igY8WkgtjK92fbJQCd4BZiiT1v25f"},
{"00000000000000000000000000000000000000000000000000000000000000", "3sN2THZeE9Eh9eYrwkvZqNstbHGvrxSAM7gXUXvyFQP8XvQLUqNCS27icwUeDT7ckHm4FUHM2mTVh1vbLmk7y"},
}
var invalidStringTests = []struct {
in string
out string
}{
{"0", ""},
{"O", ""},
{"I", ""},
{"l", ""},
{"3mJr0", ""},
{"O3yxU", ""},
{"3sNI", ""},
{"4kl8", ""},
{"0OIl", ""},
{"!@#$%^&*()-_=+~`", ""},
}
var hexTests = []struct {
in string
out string
}{
{"61", "2g"},
{"626262", "a3gV"},
{"636363", "aPEr"},
{"73696d706c792061206c6f6e6720737472696e67", "2cFupjhnEsSn59qHXstmK2ffpLv2"},
{"00eb15231dfceb60925886b67d065299925915aeb172c06647", "1NS17iag9jJgTHD1VXjvLCEnZuQ3rJDE9L"},
{"516b6fcd0f", "ABnLTmg"},
{"bf4f89001e670274dd", "3SEo3LWLoPntC"},
{"572e4794", "3EFU7m"},
{"ecac89cad93923c02321", "EJDM8drfXA6uyA"},
{"10c8511e", "Rt5zm"},
{"00000000000000000000", "1111111111"},
}
func TestBase58(t *testing.T) {
// Encode tests
for x, test := range stringTests {
tmp := []byte(test.in)
if res := base58.Encode(tmp); res != test.out {
t.Errorf("Encode test #%d failed: got: %s want: %s",
x, res, test.out)
continue
}
}
// Decode tests
for x, test := range hexTests {
b, err := hex.DecodeString(test.in)
if err != nil {
t.Errorf("hex.DecodeString failed failed #%d: got: %s", x, test.in)
continue
}
if res := base58.Decode(test.out); !bytes.Equal(res, b) {
t.Errorf("Decode test #%d failed: got: %q want: %q",
x, res, test.in)
continue
}
}
// Decode with invalid input
for x, test := range invalidStringTests {
if res := base58.Decode(test.in); string(res) != test.out {
t.Errorf("Decode invalidString test #%d failed: got: %q want: %q",
x, res, test.out)
continue
}
}
}

View File

@@ -0,0 +1,35 @@
// Copyright (c) 2013-2014 The btcsuite developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
package base58_test
import (
"bytes"
"testing"
"github.com/btcsuite/btcutil/base58"
)
func BenchmarkBase58Encode(b *testing.B) {
b.StopTimer()
data := bytes.Repeat([]byte{0xff}, 5000)
b.SetBytes(int64(len(data)))
b.StartTimer()
for i := 0; i < b.N; i++ {
base58.Encode(data)
}
}
func BenchmarkBase58Decode(b *testing.B) {
b.StopTimer()
data := bytes.Repeat([]byte{0xff}, 5000)
encoded := base58.Encode(data)
b.SetBytes(int64(len(encoded)))
b.StartTimer()
for i := 0; i < b.N; i++ {
base58.Decode(encoded)
}
}

View File

@@ -0,0 +1,52 @@
// Copyright (c) 2013-2014 The btcsuite developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
package base58
import (
"crypto/sha256"
"errors"
)
// ErrChecksum indicates that the checksum of a check-encoded string does not verify against
// the checksum.
var ErrChecksum = errors.New("checksum error")
// ErrInvalidFormat indicates that the check-encoded string has an invalid format.
var ErrInvalidFormat = errors.New("invalid format: version and/or checksum bytes missing")
// checksum: first four bytes of sha256^2
func checksum(input []byte) (cksum [4]byte) {
h := sha256.Sum256(input)
h2 := sha256.Sum256(h[:])
copy(cksum[:], h2[:4])
return
}
// CheckEncode prepends a version byte and appends a four byte checksum.
func CheckEncode(input []byte, version byte) string {
b := make([]byte, 0, 1+len(input)+4)
b = append(b, version)
b = append(b, input[:]...)
cksum := checksum(b)
b = append(b, cksum[:]...)
return Encode(b)
}
// CheckDecode decodes a string that was encoded with CheckEncode and verifies the checksum.
func CheckDecode(input string) (result []byte, version byte, err error) {
decoded := Decode(input)
if len(decoded) < 5 {
return nil, 0, ErrInvalidFormat
}
version = decoded[0]
var cksum [4]byte
copy(cksum[:], decoded[len(decoded)-4:])
if checksum(decoded[:len(decoded)-4]) != cksum {
return nil, 0, ErrChecksum
}
payload := decoded[1 : len(decoded)-4]
result = append(result, payload...)
return
}

View File

@@ -0,0 +1,66 @@
// Copyright (c) 2013-2014 The btcsuite developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
package base58_test
import (
"testing"
"github.com/btcsuite/btcutil/base58"
)
var checkEncodingStringTests = []struct {
version byte
in string
out string
}{
{20, "", "3MNQE1X"},
{20, " ", "B2Kr6dBE"},
{20, "-", "B3jv1Aft"},
{20, "0", "B482yuaX"},
{20, "1", "B4CmeGAC"},
{20, "-1", "mM7eUf6kB"},
{20, "11", "mP7BMTDVH"},
{20, "abc", "4QiVtDjUdeq"},
{20, "1234598760", "ZmNb8uQn5zvnUohNCEPP"},
{20, "abcdefghijklmnopqrstuvwxyz", "K2RYDcKfupxwXdWhSAxQPCeiULntKm63UXyx5MvEH2"},
{20, "00000000000000000000000000000000000000000000000000000000000000", "bi1EWXwJay2udZVxLJozuTb8Meg4W9c6xnmJaRDjg6pri5MBAxb9XwrpQXbtnqEoRV5U2pixnFfwyXC8tRAVC8XxnjK"},
}
func TestBase58Check(t *testing.T) {
for x, test := range checkEncodingStringTests {
// test encoding
if res := base58.CheckEncode([]byte(test.in), test.version); res != test.out {
t.Errorf("CheckEncode test #%d failed: got %s, want: %s", x, res, test.out)
}
// test decoding
res, version, err := base58.CheckDecode(test.out)
if err != nil {
t.Errorf("CheckDecode test #%d failed with err: %v", x, err)
} else if version != test.version {
t.Errorf("CheckDecode test #%d failed: got version: %d want: %d", x, version, test.version)
} else if string(res) != test.in {
t.Errorf("CheckDecode test #%d failed: got: %s want: %s", x, res, test.in)
}
}
// test the two decoding failure cases
// case 1: checksum error
_, _, err := base58.CheckDecode("3MNQE1Y")
if err != base58.ErrChecksum {
t.Error("Checkdecode test failed, expected ErrChecksum")
}
// case 2: invalid formats (string lengths below 5 mean the version byte and/or the checksum
// bytes are missing).
testString := ""
for len := 0; len < 4; len++ {
// make a string of length `len`
_, _, err = base58.CheckDecode(testString)
if err != base58.ErrInvalidFormat {
t.Error("Checkdecode test failed, expected ErrInvalidFormat")
}
}
}

View File

@@ -0,0 +1,17 @@
#!/bin/sh
# This script uses gocov to generate a test coverage report.
# The gocov tool my be obtained with the following command:
# go get github.com/axw/gocov/gocov
#
# It will be installed to $GOPATH/bin, so ensure that location is in your $PATH.
# Check for gocov.
type gocov >/dev/null 2>&1
if [ $? -ne 0 ]; then
echo >&2 "This script requires the gocov tool."
echo >&2 "You may obtain it with the following command:"
echo >&2 "go get github.com/axw/gocov/gocov"
exit 1
fi
gocov test | gocov report

View File

@@ -0,0 +1,29 @@
// Copyright (c) 2014 The btcsuite developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
/*
Package base58 provides an API for working with modified base58 and Base58Check
encodings.
Modified Base58 Encoding
Standard base58 encoding is similar to standard base64 encoding except, as the
name implies, it uses a 58 character alphabet which results in an alphanumeric
string and allows some characters which are problematic for humans to be
excluded. Due to this, there can be various base58 alphabets.
The modified base58 alphabet used by Bitcoin, and hence this package, omits the
0, O, I, and l characters that look the same in many fonts and are therefore
hard to humans to distinguish.
Base58Check Encoding Scheme
The Base58Check encoding scheme is primarily used for Bitcoin addresses at the
time of this writing, however it can be used to generically encode arbitrary
byte arrays into human-readable strings along with a version byte that can be
used to differentiate the same payload. For Bitcoin addresses, the extra
version is used to differentiate the network of otherwise identical public keys
which helps prevent using an address intended for one network on another.
*/
package base58

View File

@@ -0,0 +1,71 @@
// Copyright (c) 2014 The btcsuite developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
package base58_test
import (
"fmt"
"github.com/btcsuite/btcutil/base58"
)
// This example demonstrates how to decode modified base58 encoded data.
func ExampleDecode() {
// Decode example modified base58 encoded data.
encoded := "25JnwSn7XKfNQ"
decoded := base58.Decode(encoded)
// Show the decoded data.
fmt.Println("Decoded Data:", string(decoded))
// Output:
// Decoded Data: Test data
}
// This example demonstrates how to encode data using the modified base58
// encoding scheme.
func ExampleEncode() {
// Encode example data with the modified base58 encoding scheme.
data := []byte("Test data")
encoded := base58.Encode(data)
// Show the encoded data.
fmt.Println("Encoded Data:", encoded)
// Output:
// Encoded Data: 25JnwSn7XKfNQ
}
// This example demonstrates how to decode Base58Check encoded data.
func ExampleCheckDecode() {
// Decode an example Base58Check encoded data.
encoded := "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa"
decoded, version, err := base58.CheckDecode(encoded)
if err != nil {
fmt.Println(err)
return
}
// Show the decoded data.
fmt.Printf("Decoded data: %x\n", decoded)
fmt.Println("Version Byte:", version)
// Output:
// Decoded data: 62e907b15cbf27d5425399ebf6f0fb50ebb88f18
// Version Byte: 0
}
// This example demonstrates how to encode data using the Base58Check encoding
// scheme.
func ExampleCheckEncode() {
// Encode example data with the Base58Check encoding scheme.
data := []byte("Test data")
encoded := base58.CheckEncode(data, 0)
// Show the encoded data.
fmt.Println("Encoded Data:", encoded)
// Output:
// Encoded Data: 182iP79GRURMp7oMHDU
}

View File

@@ -0,0 +1,46 @@
package bip32
import "crypto/rand"
// GenerateSeed generates seed that can be used to initialize a master key.
func GenerateSeed() ([]byte, error) {
randBytes := make([]byte, 32)
_, err := rand.Read(randBytes)
if err != nil {
return nil, err
}
return randBytes, nil
}
// NewMasterWithPath returns a new master key based on the given seed and version, with a derivation
// to the given path.
func NewMasterWithPath(seed []byte, version [4]byte, pathString string) (*ExtendedKey, error) {
masterKey, err := NewMaster(seed, version)
if err != nil {
return nil, err
}
return masterKey.DeriveFromPath(pathString)
}
// NewPublicMasterWithPath returns a new public master key based on the given seed and version, with a derivation
// to the given path.
func NewPublicMasterWithPath(seed []byte, version [4]byte, pathString string) (*ExtendedKey, error) {
masterKey, err := NewMaster(seed, version)
if err != nil {
return nil, err
}
path, err := parsePath(pathString)
if err != nil {
return nil, err
}
descendantKey, err := masterKey.path(path)
if err != nil {
return nil, err
}
return descendantKey.Public()
}

View File

@@ -0,0 +1,420 @@
package bip32
import (
"encoding/hex"
"math/rand"
"strconv"
"strings"
"testing"
)
func TestBIP32SpecVectors(t *testing.T) {
type testPath struct {
path string
extendedPublicKey string
extendedPrivateKey string
}
type testVector struct {
seed string
version [4]byte
paths []testPath
}
// test vectors are copied from https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki#Test_Vectors
testVectors := []testVector{
{
seed: "000102030405060708090a0b0c0d0e0f",
version: BitcoinMainnetPrivate,
paths: []testPath{
{
path: "m",
extendedPublicKey: "xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8",
extendedPrivateKey: "xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi",
},
{
path: "m/0'",
extendedPublicKey: "xpub68Gmy5EdvgibQVfPdqkBBCHxA5htiqg55crXYuXoQRKfDBFA1WEjWgP6LHhwBZeNK1VTsfTFUHCdrfp1bgwQ9xv5ski8PX9rL2dZXvgGDnw",
extendedPrivateKey: "xprv9uHRZZhk6KAJC1avXpDAp4MDc3sQKNxDiPvvkX8Br5ngLNv1TxvUxt4cV1rGL5hj6KCesnDYUhd7oWgT11eZG7XnxHrnYeSvkzY7d2bhkJ7",
},
{
path: "m/0'/1",
extendedPublicKey: "xpub6ASuArnXKPbfEwhqN6e3mwBcDTgzisQN1wXN9BJcM47sSikHjJf3UFHKkNAWbWMiGj7Wf5uMash7SyYq527Hqck2AxYysAA7xmALppuCkwQ",
extendedPrivateKey: "xprv9wTYmMFdV23N2TdNG573QoEsfRrWKQgWeibmLntzniatZvR9BmLnvSxqu53Kw1UmYPxLgboyZQaXwTCg8MSY3H2EU4pWcQDnRnrVA1xe8fs",
},
{
path: "m/0'/1/2'",
extendedPublicKey: "xpub6D4BDPcP2GT577Vvch3R8wDkScZWzQzMMUm3PWbmWvVJrZwQY4VUNgqFJPMM3No2dFDFGTsxxpG5uJh7n7epu4trkrX7x7DogT5Uv6fcLW5",
extendedPrivateKey: "xprv9z4pot5VBttmtdRTWfWQmoH1taj2axGVzFqSb8C9xaxKymcFzXBDptWmT7FwuEzG3ryjH4ktypQSAewRiNMjANTtpgP4mLTj34bhnZX7UiM",
},
{
path: "m/0'/1/2'/2",
extendedPublicKey: "xpub6FHa3pjLCk84BayeJxFW2SP4XRrFd1JYnxeLeU8EqN3vDfZmbqBqaGJAyiLjTAwm6ZLRQUMv1ZACTj37sR62cfN7fe5JnJ7dh8zL4fiyLHV",
extendedPrivateKey: "xprvA2JDeKCSNNZky6uBCviVfJSKyQ1mDYahRjijr5idH2WwLsEd4Hsb2Tyh8RfQMuPh7f7RtyzTtdrbdqqsunu5Mm3wDvUAKRHSC34sJ7in334",
},
{
path: "m/0'/1/2'/2/1000000000",
extendedPublicKey: "xpub6H1LXWLaKsWFhvm6RVpEL9P4KfRZSW7abD2ttkWP3SSQvnyA8FSVqNTEcYFgJS2UaFcxupHiYkro49S8yGasTvXEYBVPamhGW6cFJodrTHy",
extendedPrivateKey: "xprvA41z7zogVVwxVSgdKUHDy1SKmdb533PjDz7J6N6mV6uS3ze1ai8FHa8kmHScGpWmj4WggLyQjgPie1rFSruoUihUZREPSL39UNdE3BBDu76",
},
},
},
{
seed: "000102030405060708090a0b0c0d0e0f",
version: KaspaMainnetPrivate,
paths: []testPath{
{
path: "m",
extendedPublicKey: "kpub2C2CKMtB3F5r4LEGRnS3o73omeQB3KJ5QfAzC5R3t9bpChBEZNitvn92JYeCTMtnR7oE1im7DhsxGqV72JErXFG9G3YnTHRnZPkGZLFE6PZ",
extendedPrivateKey: "kprv5y2qurMHCsXYqr9oKku3Ry75DcZgdraE3SFPPh1SKp4qKtr61qQeNypYTGztwUUiVauHWmjxaQXeUKHxj4QCuDG4ULpZHkvBoH9XX19ynXm",
},
{
path: "m/0'",
extendedPublicKey: "kpub2EHcK5Be8WCqCwMydYJgg99v6TxXRPn66GbtAAoArLo6ZyUQycFz3vVS5pCuCfoKRL5nsxJXxLx3FETEyKyEb8isTgM3NbL15KsprxXRXYP",
extendedPrivateKey: "kprv61JFuZekJ8eXzTHWXWmgK1DBYS831w4Ej3gHMnPZJ1G7hB9GS4wjW8AxEYMEMBrgCdnyt54pxmNXC5KgNegPhHLaYDVhXid5WHnNxE7Nir6",
},
{
path: "m/0'/1",
extendedPublicKey: "kpub2GTjWrjXXD5u3PQRMoCZGt3a9qwdRRWP2bGikSZynybJoWyYhQgJ1VPfVtfUccWfP3hqfNke4wSWqYC4Sf98GnYoktBtrELGi4Qc9xmGTUP",
extendedPrivateKey: "kprv63UP7MCdgqXbpuKxFmfYuk6qbp791xnXfNM7x4ANEe4KvieQ9sN3Th5BebYHx7dieiYfgtfG3UKwL1quVzUNUSq23zTRbUPwB66kV2rWPC8",
},
{
path: "m/0'/1/2'",
extendedPublicKey: "kpub2K51ZPZPE5wJuZCWcPbvdt5iNzp9gy6NN8WPzms8xqxkDNAfWAWiuvwb3urK4UwyjZoaGkjFSt1VHsLM9kgfLEheLnA2wBPxRkKkFDqc9zP",
extendedPrivateKey: "kprv665f9t2VPiP1h583WN4vGk8ypxyfHWNWzuaoCPTXQWRmLZqWxdCUN8d7CdkuvM9DABa4HMcBTt9qZDaf61PZbYGgQc1ykQdsnMqy7fTCNrm",
},
{
path: "m/0'/1/2'/2",
extendedPublicKey: "kpub2MJQPpgLQZcHz2gEJep1XPF2Tp6tKZQZocPhFjPcHHXMaTo2ZwD67WQWjEqhUH6iCsvkQmDCVcubrHgMF47s3qAuFZiDmNHnSSEbPpuRWiZ",
extendedPrivateKey: "kprv68K3zK9SaC3zmYbmCdH1AFJHunGPv6giSPU6TLyziwzNhfTt2PtqZi62sxANP1YeDyhkuGqkNhc12QV7HRvunvrior75JVTawLK8d8zN34Z",
},
{
path: "m/0'/1/2'/2/1000000000",
extendedPublicKey: "kpub2P2AsWHaXgzVWNTgRCNjq6F2G3gC94DbbrnFW1mkVMurHbCR6MTkNcZaN4keKYBRgaDHv7912pcCSi5NLuchu6L2878JZqsRFPrWduDKq9i",
extendedPrivateKey: "kprv6A2pTzkghKSCHtPDKAqjTxJHi1qhjbVkEdrehdN8w2NsQnsGYp9VppF6WowaHvfiqP71gdphDk982aVUpVwdutWG9LsJRQDJDfsVNMbtSap",
},
},
},
{
seed: "fffcf9f6f3f0edeae7e4e1dedbd8d5d2cfccc9c6c3c0bdbab7b4b1aeaba8a5a29f9c999693908d8a8784817e7b7875726f6c696663605d5a5754514e4b484542",
version: BitcoinMainnetPrivate,
paths: []testPath{
{
path: "m",
extendedPublicKey: "xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB",
extendedPrivateKey: "xprv9s21ZrQH143K31xYSDQpPDxsXRTUcvj2iNHm5NUtrGiGG5e2DtALGdso3pGz6ssrdK4PFmM8NSpSBHNqPqm55Qn3LqFtT2emdEXVYsCzC2U",
},
{
path: "m/0",
extendedPublicKey: "xpub69H7F5d8KSRgmmdJg2KhpAK8SR3DjMwAdkxj3ZuxV27CprR9LgpeyGmXUbC6wb7ERfvrnKZjXoUmmDznezpbZb7ap6r1D3tgFxHmwMkQTPH",
extendedPrivateKey: "xprv9vHkqa6EV4sPZHYqZznhT2NPtPCjKuDKGY38FBWLvgaDx45zo9WQRUT3dKYnjwih2yJD9mkrocEZXo1ex8G81dwSM1fwqWpWkeS3v86pgKt",
},
{
path: "m/0/2147483647'",
extendedPublicKey: "xpub6ASAVgeehLbnwdqV6UKMHVzgqAG8Gr6riv3Fxxpj8ksbH9ebxaEyBLZ85ySDhKiLDBrQSARLq1uNRts8RuJiHjaDMBU4Zn9h8LZNnBC5y4a",
extendedPrivateKey: "xprv9wSp6B7kry3Vj9m1zSnLvN3xH8RdsPP1Mh7fAaR7aRLcQMKTR2vidYEeEg2mUCTAwCd6vnxVrcjfy2kRgVsFawNzmjuHc2YmYRmagcEPdU9",
},
{
path: "m/0/2147483647'/1",
extendedPublicKey: "xpub6DF8uhdarytz3FWdA8TvFSvvAh8dP3283MY7p2V4SeE2wyWmG5mg5EwVvmdMVCQcoNJxGoWaU9DCWh89LojfZ537wTfunKau47EL2dhHKon",
extendedPrivateKey: "xprv9zFnWC6h2cLgpmSA46vutJzBcfJ8yaJGg8cX1e5StJh45BBciYTRXSd25UEPVuesF9yog62tGAQtHjXajPPdbRCHuWS6T8XA2ECKADdw4Ef",
},
{
path: "m/0/2147483647'/1/2147483646'",
extendedPublicKey: "xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL",
extendedPrivateKey: "xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc",
},
{
path: "m/0/2147483647'/1/2147483646'/2",
extendedPublicKey: "xpub6FnCn6nSzZAw5Tw7cgR9bi15UV96gLZhjDstkXXxvCLsUXBGXPdSnLFbdpq8p9HmGsApME5hQTZ3emM2rnY5agb9rXpVGyy3bdW6EEgAtqt",
extendedPrivateKey: "xprvA2nrNbFZABcdryreWet9Ea4LvTJcGsqrMzxHx98MMrotbir7yrKCEXw7nadnHM8Dq38EGfSh6dqA9QWTyefMLEcBYJUuekgW4BYPJcr9E7j",
},
},
},
{
seed: "fffcf9f6f3f0edeae7e4e1dedbd8d5d2cfccc9c6c3c0bdbab7b4b1aeaba8a5a29f9c999693908d8a8784817e7b7875726f6c696663605d5a5754514e4b484542",
version: KaspaMainnetPrivate,
paths: []testPath{
{
path: "m",
extendedPublicKey: "kpub2C2CKMtB3F5r3wjbXwWLFJma1qYbiwYu6ExiV29srXigVgCRjXVqMgJceejBAcFkKg31vPRGcnPCzdDL9VA1fAG67ykFHmvSsmRNqKZg1po",
extendedPrivateKey: "kprv5y2qurMHCsXYqTf8RuyKtApqToi7KUq3j237gdkGJCBhcssHBzBaosz8oLmx7z2ojdeiG4CQrWZqZr24mUnuWaapvktoS6pvNXmkszbHsFE",
},
{
path: "m/0",
extendedPublicKey: "kpub2FHwb5a8XFuvaDKtfitDK7B6NoHrRv3BeQi5eqBKvwaeBeeQJnquWWssE7h4xhGBXzXBncR21sEB9ne22drRzkvNQ2UvC84q1FY3GVzjZr1",
extendedPrivateKey: "kprv62JbBa3EgtMdMjFRZhMCwyEMpmTN2TKLHBnUrSmiNc3fJrKFmFXexiZPNr3km3se9HtYA4c9HfyxvMetKmHxSokDvwJrpazfVwgKFEAdr1L",
},
{
path: "m/0/2147483647'",
extendedPublicKey: "kpub2GSzqgbeuA62k5Y56AsrnSremYWkyQCsjZncaE66agM2dwsrvgGDiafTqVwBiRsHKWSjSTGdK5empTWMoYLYiuNzw76yYrKqsdoe7KSjW9n",
extendedPrivateKey: "kprv63TeSB4m4nXjXbTbz9LrRJuvDWgGZwV2NLs1mqgV2Lp3m9YiP8wyAnLyzCXjVJc83XDRw5onLgV5MbPf48u627BnMfYCb6ivHj1r1gJwAAq",
},
{
path: "m/0/2147483647'/1",
extendedPublicKey: "kpub2KFyFhab4oPDqhDD9q2RkPnt75PG5b8941HURHkRtZhUJmk2EBnvcV3qgJ8KWJZZuguHH6MrxCxbuFmNiSmVzEquXPJpmPm3oQUbMkjZU7h",
extendedPrivateKey: "kprv66GcrC3hERpvdD8k3oVRPFr9Z3Ymg8QHgnMscuLpLEAVRyQsgeUg4gjMpzjMX1opMUa8gNtAkEAHgJAp72RU2b15VS51SChJmXSaVHSHVgJ",
},
{
path: "m/0/2147483647'/1/2147483646'",
extendedPublicKey: "kpub2LS1AfWwgCLw8eSotJqy7uV51ord8Zke5i1Mx1SqjKxim84xKriw91QwJxFphg61s8Yv5bRZzpHTYtvmQKt1hbYMoHdKKgrTfdZAtem6FS7",
extendedPrivateKey: "kprv67Sem9z3qpndvANLnHJxkmYLTn28j72niV5m9d3EAzRjtKjonKQgbD6TThTk9SC6u3rpzCfA8bjsVRGBcyKxiRgFKNcKaQiw77T6Z6V751r",
},
{
path: "m/0/2147483647'/1/2147483646'/2",
extendedPublicKey: "kpub2Mo386jTCNfAsudhcNyf6es3QsPjNtfijsdFMnoLN7pJqKQXVVehKaMwPML6qFSiPBm9MWvytXJT3KzGERZv1rPwSTTQG49CLvkMZGaHgA1",
extendedPrivateKey: "kprv68ogibCZN16sfRZEWMSejWvJrqZEyRwsNeheZQPionHKxX5NwxLSmn3TY78kJTHAwMiZGxHyahaZXy9hMHhBmQQy8E7pdpreoUnedk17vmK",
},
},
},
{
seed: "4b381541583be4423346c643850da4b320e46a87ae3d2a4e6da11eba819cd4acba45d239319ac14f863b8d5ab5a0d0c64d2e8a1e7d1457df2e5a3c51c73235be",
version: BitcoinMainnetPrivate,
paths: []testPath{
{
path: "m",
extendedPublicKey: "xpub661MyMwAqRbcEZVB4dScxMAdx6d4nFc9nvyvH3v4gJL378CSRZiYmhRoP7mBy6gSPSCYk6SzXPTf3ND1cZAceL7SfJ1Z3GC8vBgp2epUt13",
extendedPrivateKey: "xprv9s21ZrQH143K25QhxbucbDDuQ4naNntJRi4KUfWT7xo4EKsHt2QJDu7KXp1A3u7Bi1j8ph3EGsZ9Xvz9dGuVrtHHs7pXeTzjuxBrCmmhgC6",
},
{
path: "m/0'",
extendedPublicKey: "xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y",
extendedPrivateKey: "xprv9uPDJpEQgRQfDcW7BkF7eTya6RPxXeJCqCJGHuCJ4GiRVLzkTXBAJMu2qaMWPrS7AANYqdq6vcBcBUdJCVVFceUvJFjaPdGZ2y9WACViL4L",
},
},
},
{
seed: "4b381541583be4423346c643850da4b320e46a87ae3d2a4e6da11eba819cd4acba45d239319ac14f863b8d5ab5a0d0c64d2e8a1e7d1457df2e5a3c51c73235be",
version: KaspaMainnetPrivate,
paths: []testPath{
{
path: "m",
extendedPublicKey: "kpub2C2CKMtB3F5r31Bm4L18TJ2btUshUoiAoajGtKBS8DoUTvRhPfjoJwY98eG9zCqPVknskPJH1TD4RvrEzCCT5VvEFDeU2LNHfUw5MkWwVFF",
extendedPrivateKey: "kprv5y2qurMHCsXYpX7HxJU86A5sLT3D5LzKSMog5vmpZtGVb86Yr8RYm9DfHLW851G8pLKTpytWkwJYvVdNzuwLJ465T3TSdYAtfFS7Xx2owSo",
},
{
path: "m/0'",
extendedPublicKey: "kpub2EPQ4KiJicTCEYHAHULdWYnGaqV5df85D4yDhYsH4XiqiwZ9yAWfPQKrSN6fiZS8h8HiXM41rQQZ4PnavS8dekCAvKbMaBs69fHz2AFgp7S",
extendedPrivateKey: "kprv61Q3epBQtEtu24ChBSod9QqY2oebECQDqr3cuATfWCBrr9E1RdCQqc1Nb6rUQxb4GUxsqvgPQfw1a3GXa8X63pHhtBNVNhShnGPmVHW2UAU",
},
},
},
{
seed: "4b381541583be4423346c643850da4b320e46a87ae3d2a4e6da11eba819cd4acba45d239319ac14f863b8d5ab5a0d0c64d2e8a1e7d1457df2e5a3c51c73235be",
version: KaspaTestnetPrivate,
paths: []testPath{
{
path: "m",
extendedPublicKey: "ktub1vi816jr1DomD4Qct6C1n3qZ98kvP4TCgq21QUVkJ1xTADT74WfStAQAv5NA5ACsLYRLiAEs2YMz91LoWXh6YY6bFd6BidfhFE7dxhSVK7H",
extendedPrivateKey: "ktrv5himbbCxArFTzaL9n4f1Qutpb6vRybjMKc6Qc668jgRUHR7xWyMCLN5h4mc89xdcf7wvnkq6n2TUda7wXFRym6GSTSuAKqUJEzcg8smnbs9",
},
{
path: "m/0'",
extendedPublicKey: "ktub1y5Kk4ZygbB7QbW27EXWqJbDqVNJXus76KFxDiBbEKspREaZe1SJxdBtDoCfoWocXuvBV7zbsVZUmUH9SmdH7nNXvj35GVAVjQUYd41EFiN",
extendedPrivateKey: "ktrv5k5yLZ35rDcpC7RZ1CzWUAeVHTXp8T9Fj6LMRKmyfzLqYSFR6U84QpsQNXxUVuxY7GbLohcyRm5wH7m66U1jWrU4tapD4zk7N1aL6C3rnp7",
},
},
},
{
seed: "4b381541583be4423346c643850da4b320e46a87ae3d2a4e6da11eba819cd4acba45d239319ac14f863b8d5ab5a0d0c64d2e8a1e7d1457df2e5a3c51c73235be",
version: KaspaDevnetPrivate,
paths: []testPath{
{
path: "m",
extendedPublicKey: "kdub4zZ57oeuxodRE6ZjEtX6KnUpww7hSaHbRg745QUF8GSe3Y6EKZa6tkjuJvCw8cTYni4dWNgh3tr5HoER7gTzpEyQcM4VNJmxyu9H2k8C3SL",
extendedPrivateKey: "kdrv8mZiiJ828S581cVG8rz5xeY6PuHD37Zk4TBTH24dZvufAjm5n2FrLxRRTcSuDQtJ7HbDayGvoNwZnN1Z8QCt2o9FpAsTyWaZyfeKCvnUDsn",
},
{
path: "m/0'",
extendedPublicKey: "kdub52vGrmV3eAzmRdf8U2rbP3EVeHj5bRhVqALzteA64aN1JZDgu4LxyDXcce3Sry4Hz5ZUHLSRtr3ZvGAm3vQBPVFMHT1NvAGmU5WBhDEnN5e",
extendedPrivateKey: "kdrv8ovvTFx9ooSUD9afN1Kb1uHm6FtbBxyeTwRQ6FkUWEq2RktYMX2iRRD8mNoFZNDDZSEdbv4oT7a2RuehhcndnZLtFJnWifrP6gbyAN52eWX",
},
},
},
{
seed: "4b381541583be4423346c643850da4b320e46a87ae3d2a4e6da11eba819cd4acba45d239319ac14f863b8d5ab5a0d0c64d2e8a1e7d1457df2e5a3c51c73235be",
version: KaspaSimnetPrivate,
paths: []testPath{
{
path: "m",
extendedPublicKey: "ksub8Dp9tLuSVjDpAnKQ1sEyyZfrLiYhUQ7jd5TD92tgeX7mTQdyCwZYDNuhiN2WC23BwhGKgKvoJaWSPERp5gSuV4h2QEt1JwHyrSCjoEf5VHR",
extendedPrivateKey: "ksrv3h87jvsCMMY9HKHc8DaPaA1jmh2mK4vq3mrRrWrPEFKNcvPCaYjx2tHqYCMLkgu1qonn9ScvgKMgeUG5pkgVdEgB579ytyAQBLrnezdttFb",
},
{
path: "m/0'",
extendedPublicKey: "ksub8GBMdJjaB6bANKQoF1aV2pRX35A5dFXe2Zh9xGaXaq38iRmRnSLQHqhR25s1vNdw94mATHgY9Xhw1hNA1vP64Jxy5LptrnnnLcZeTbX9Sng",
extendedPrivateKey: "ksrv3jVKUthL2iuVUrP1MMutdQmQU3e9TvLjTG6NfkYEAZEjswWfA3Wp7M5Yqxhh6eDwHxSCAPQoL3z9J1uEPyGFNzsoWF52e8SDJMpScP8xuRw",
},
},
},
}
for i, vector := range testVectors {
seed, err := hex.DecodeString(vector.seed)
if err != nil {
t.Fatalf("DecodeString: %+v", err)
}
masterKey, err := NewMaster(seed, vector.version)
if err != nil {
t.Fatalf("NewMaster: %+v", err)
}
for j, path := range vector.paths {
extendedPrivateKey, err := masterKey.DeriveFromPath(path.path)
if err != nil {
t.Fatalf("Path: %+v", err)
}
if extendedPrivateKey.String() != path.extendedPrivateKey {
t.Fatalf("Test (%d, %d): expected extended private key %s but got %s", i, j, path.extendedPrivateKey, extendedPrivateKey.String())
}
decodedExtendedPrivateKey, err := DeserializeExtendedKey(extendedPrivateKey.String())
if err != nil {
t.Fatalf("DeserializeExtendedKey: %+v", err)
}
if extendedPrivateKey.String() != decodedExtendedPrivateKey.String() {
t.Fatalf("Test (%d, %d): deserializing and serializing the extended private key didn't preserve the data", i, j)
}
extendedPublicKey, err := extendedPrivateKey.Public()
if err != nil {
t.Fatalf("Public: %+v", err)
}
if extendedPublicKey.String() != path.extendedPublicKey {
t.Fatalf("Test (%d, %d): expected extended public key %s but got %s", i, j, path.extendedPublicKey, extendedPublicKey.String())
}
decodedExtendedPublicKey, err := DeserializeExtendedKey(extendedPublicKey.String())
if err != nil {
t.Fatalf("DeserializeExtendedPublicKey: %+v", err)
}
if extendedPublicKey.String() != decodedExtendedPublicKey.String() {
t.Fatalf("Test (%d, %d): deserializing and serializing the ext pub didn't preserve the data", i, j)
}
}
}
}
// TestExtendedKey_DeriveFromPath checks that path that derive from extended private key and extended
// public key lead to the same public keys.
func TestExtendedKey_DeriveFromPath(t *testing.T) {
r := rand.New(rand.NewSource(0))
seed, err := GenerateSeed()
if err != nil {
t.Fatalf("GenerateSeed: %+v", err)
}
master, err := NewMaster(seed, KaspaMainnetPrivate)
if err != nil {
t.Fatalf("GenerateSeed: %+v", err)
}
masterPublic, err := master.Public()
if err != nil {
t.Fatalf("Public: %+v", err)
}
for i := 0; i < 10; i++ {
numIndexes := 1 + r.Intn(100)
indexes := make([]string, numIndexes)
for i := 0; i < numIndexes; i++ {
index := r.Intn(hardenedIndexStart)
indexes[i] = strconv.Itoa(int(index))
}
indexesStr := strings.Join(indexes, "/")
pathPrivate := "m/" + indexesStr
pathPublic := "M/" + indexesStr
extendedPrivateKey, err := master.DeriveFromPath(pathPrivate)
if err != nil {
t.Fatalf("Path: %+v", err)
}
extendedPublicKeyFromPrivateKey, err := extendedPrivateKey.Public()
if err != nil {
t.Fatalf("Public: %+v", err)
}
extendedPublicKey, err := masterPublic.DeriveFromPath(pathPublic)
if err != nil {
t.Fatalf("Path: %+v", err)
}
if extendedPublicKeyFromPrivateKey.String() != extendedPublicKey.String() {
t.Fatalf("Path gives different result from private and public master keys")
}
}
}
// TestPublicParentPublicChildDerivation was copied and modified from https://github.com/tyler-smith/go-bip32/blob/master/bip32_test.go
func TestPublicParentPublicChildDerivation(t *testing.T) {
// Generated using https://iancoleman.github.io/bip39/
// Root key:
// xprv9s21ZrQH143K2Cfj4mDZBcEecBmJmawReGwwoAou2zZzG45bM6cFPJSvobVTCB55L6Ld2y8RzC61CpvadeAnhws3CHsMFhNjozBKGNgucYm
// Derivation Path m/44'/60'/0'/0:
// xprv9zy5o7z1GMmYdaeQdmabWFhUf52Ytbpe3G5hduA4SghboqWe7aDGWseN8BJy1GU72wPjkCbBE1hvbXYqpCecAYdaivxjNnBoSNxwYD4wHpW
// xpub6DxSCdWu6jKqr4isjo7bsPeDD6s3J4YVQV1JSHZg12Eagdqnf7XX4fxqyW2sLhUoFWutL7tAELU2LiGZrEXtjVbvYptvTX5Eoa4Mamdjm9u
extendedMasterPublic, err := DeserializeExtendedKey("xpub6DxSCdWu6jKqr4isjo7bsPeDD6s3J4YVQV1JSHZg12Eagdqnf7XX4fxqyW2sLhUoFWutL7tAELU2LiGZrEXtjVbvYptvTX5Eoa4Mamdjm9u")
if err != nil {
t.Fatalf("DeserializeExtendedPublicKey: %+v", err)
}
type testChildKey struct {
pathFragment uint32
privKey string
pubKey string
hexPubKey string
}
expectedChildren := []testChildKey{
{pathFragment: 0, hexPubKey: "0243187e1a2ba9ba824f5f81090650c8f4faa82b7baf93060d10b81f4b705afd46"},
{pathFragment: 1, hexPubKey: "023790d11eb715c4320d8e31fba3a09b700051dc2cdbcce03f44b11c274d1e220b"},
{pathFragment: 2, hexPubKey: "0302c5749c3c75cea234878ae3f4d8f65b75d584bcd7ed0943b016d6f6b59a2bad"},
{pathFragment: 3, hexPubKey: "03f0440c94e5b14ea5b15875934597afff541bec287c6e65dc1102cafc07f69699"},
{pathFragment: 4, hexPubKey: "026419d0d8996707605508ac44c5871edc7fe206a79ef615b74f2eea09c5852e2b"},
{pathFragment: 5, hexPubKey: "02f63c6f195eea98bdb163c4a094260dea71d264b21234bed4df3899236e6c2298"},
{pathFragment: 6, hexPubKey: "02d74709cd522081064858f393d009ead5a0ecd43ede3a1f57befcc942025cb5f9"},
{pathFragment: 7, hexPubKey: "03e54bb92630c943d38bbd8a4a2e65fca7605e672d30a0e545a7198cbb60729ceb"},
{pathFragment: 8, hexPubKey: "027e9d5acd14d39c4938697fba388cd2e8f31fc1c5dc02fafb93a10a280de85199"},
{pathFragment: 9, hexPubKey: "02a167a9f0d57468fb6abf2f3f7967e2cadf574314753a06a9ef29bc76c54638d2"},
{pathFragment: 100, hexPubKey: "020db9ba00ddf68428e3f5bfe54252bbcd75b21e42f51bf3bfc4172bf0e5fa7905"},
{pathFragment: 101, hexPubKey: "0299e3790956570737d6164e6fcda5a3daa304065ca95ba46bc73d436b84f34d46"},
{pathFragment: 102, hexPubKey: "0202e0732c4c5d2b1036af173640e01957998cfd4f9cdaefab6ffe76eb869e2c59"},
{pathFragment: 103, hexPubKey: "03d050adbd996c0c5d737ff638402dfbb8c08e451fef10e6d62fb57887c1ac6cb2"},
{pathFragment: 104, hexPubKey: "038d466399e2d68b4b16043ad4d88893b3b2f84fc443368729a973df1e66f4f530"},
{pathFragment: 105, hexPubKey: "034811e2f0c8c50440c08c2c9799b99c911c036e877e8325386ff61723ae3ffdce"},
{pathFragment: 106, hexPubKey: "026339fd5842921888e711a6ba9104a5f0c94cc0569855273cf5faefdfbcd3cc29"},
{pathFragment: 107, hexPubKey: "02833705c1069fab2aa92c6b0dac27807290d72e9f52378d493ac44849ca003b22"},
{pathFragment: 108, hexPubKey: "032d2639bde1eb7bdf8444bd4f6cc26a9d1bdecd8ea15fac3b992c3da68d9d1df5"},
{pathFragment: 109, hexPubKey: "02479c6d4a64b93a2f4343aa862c938fbc658c99219dd7bebb4830307cbd76c9e9"},
}
for i, child := range expectedChildren {
extendedPublicKey, err := extendedMasterPublic.Child(child.pathFragment)
if err != nil {
t.Fatalf("Child: %+v", err)
}
publicKey, err := extendedPublicKey.PublicKey()
if err != nil {
t.Fatalf("PublicKey: %+v", err)
}
pubKeyBytes, err := publicKey.Serialize()
if err != nil {
t.Fatalf("Serialize: %+v", err)
}
pubKeyHex := hex.EncodeToString(pubKeyBytes[:])
if child.hexPubKey != pubKeyHex {
t.Fatalf("Test #%d: expected public key %s but got %s", i, child.hexPubKey, pubKeyHex)
}
}
}

View File

@@ -0,0 +1,152 @@
package bip32
import (
"encoding/binary"
"github.com/kaspanet/go-secp256k1"
"github.com/pkg/errors"
)
const hardenedIndexStart = 0x80000000
// NewMaster returns a new extended private key based on the given seed and version
func NewMaster(seed []byte, version [4]byte) (*ExtendedKey, error) {
mac := newHMACWriter([]byte("Bitcoin seed"))
mac.InfallibleWrite(seed)
I := mac.Sum(nil)
var iL, iR [32]byte
copy(iL[:], I[:32])
copy(iR[:], I[32:])
privateKey, err := secp256k1.DeserializeECDSAPrivateKeyFromSlice(iL[:])
if err != nil {
return nil, err
}
return &ExtendedKey{
privateKey: privateKey,
Version: version,
Depth: 0,
ParentFingerprint: [4]byte{},
ChildNumber: 0,
ChainCode: iR,
}, nil
}
func isHardened(i uint32) bool {
return i >= hardenedIndexStart
}
// Child return the i'th derived child of extKey.
func (extKey *ExtendedKey) Child(i uint32) (*ExtendedKey, error) {
I, err := extKey.calcI(i)
if err != nil {
return nil, err
}
var iL, iR [32]byte
copy(iL[:], I[:32])
copy(iR[:], I[32:])
fingerPrint, err := extKey.calcFingerprint()
if err != nil {
return nil, err
}
childExt := &ExtendedKey{
Version: extKey.Version,
Depth: extKey.Depth + 1,
ParentFingerprint: fingerPrint,
ChildNumber: i,
ChainCode: iR,
}
if extKey.privateKey != nil {
childExt.privateKey, err = privateKeyAdd(extKey.privateKey, iL)
if err != nil {
return nil, err
}
} else {
publicKey, err := extKey.PublicKey()
if err != nil {
return nil, err
}
childExt.publicKey, err = pointAdd(publicKey, iL)
if err != nil {
return nil, err
}
}
return childExt, nil
}
func (extKey *ExtendedKey) calcFingerprint() ([4]byte, error) {
publicKey, err := extKey.PublicKey()
if err != nil {
return [4]byte{}, err
}
serializedPoint, err := publicKey.Serialize()
if err != nil {
return [4]byte{}, err
}
hash := hash160(serializedPoint[:])
var fingerprint [4]byte
copy(fingerprint[:], hash[:4])
return fingerprint, nil
}
func privateKeyAdd(k *secp256k1.ECDSAPrivateKey, tweak [32]byte) (*secp256k1.ECDSAPrivateKey, error) {
kCopy := *k
err := kCopy.Add(tweak)
if err != nil {
return nil, err
}
return &kCopy, nil
}
func (extKey *ExtendedKey) calcI(i uint32) ([]byte, error) {
if isHardened(i) && !extKey.IsPrivate() {
return nil, errors.Errorf("Cannot calculate hardened child for public key")
}
mac := newHMACWriter(extKey.ChainCode[:])
if isHardened(i) {
mac.InfallibleWrite([]byte{0x00})
mac.InfallibleWrite(extKey.privateKey.Serialize()[:])
} else {
publicKey, err := extKey.PublicKey()
if err != nil {
return nil, err
}
serializedPublicKey, err := publicKey.Serialize()
if err != nil {
return nil, errors.Wrap(err, "error serializing public key")
}
mac.InfallibleWrite(serializedPublicKey[:])
}
mac.InfallibleWrite(serializeUint32(i))
return mac.Sum(nil), nil
}
func serializeUint32(v uint32) []byte {
serialized := make([]byte, 4)
binary.BigEndian.PutUint32(serialized, v)
return serialized
}
func pointAdd(point *secp256k1.ECDSAPublicKey, tweak [32]byte) (*secp256k1.ECDSAPublicKey, error) {
pointCopy := *point
err := pointCopy.Add(tweak)
if err != nil {
return nil, err
}
return &pointCopy, nil
}

View File

@@ -0,0 +1,104 @@
package bip32
import (
"github.com/btcsuite/btcutil/base58"
"github.com/kaspanet/go-secp256k1"
"github.com/pkg/errors"
)
// ExtendedKey is a bip32 extended key
type ExtendedKey struct {
privateKey *secp256k1.ECDSAPrivateKey
publicKey *secp256k1.ECDSAPublicKey
Version [4]byte
Depth uint8
ParentFingerprint [4]byte
ChildNumber uint32
ChainCode [32]byte
}
// PrivateKey returns the ECDSA private key associated with the extended key
func (extKey *ExtendedKey) PrivateKey() *secp256k1.ECDSAPrivateKey {
return extKey.privateKey
}
// PublicKey returns the ECDSA public key associated with the extended key
func (extKey *ExtendedKey) PublicKey() (*secp256k1.ECDSAPublicKey, error) {
if extKey.publicKey != nil {
return extKey.publicKey, nil
}
publicKey, err := extKey.privateKey.ECDSAPublicKey()
if err != nil {
return nil, err
}
extKey.publicKey = publicKey
return publicKey, nil
}
// IsPrivate returns whether the extended key is private
func (extKey *ExtendedKey) IsPrivate() bool {
return extKey.privateKey != nil
}
// Public returns public version of the extended key
func (extKey *ExtendedKey) Public() (*ExtendedKey, error) {
if !extKey.IsPrivate() {
return extKey, nil
}
publicKey, err := extKey.PublicKey()
if err != nil {
return nil, errors.Wrap(err, "error calculating publicKey")
}
version, err := toPublicVersion(extKey.Version)
if err != nil {
return nil, err
}
return &ExtendedKey{
publicKey: publicKey,
Version: version,
Depth: extKey.Depth,
ParentFingerprint: extKey.ParentFingerprint,
ChildNumber: extKey.ChildNumber,
ChainCode: extKey.ChainCode,
}, nil
}
// DeriveFromPath returns the extended key derived from the given path
func (extKey *ExtendedKey) DeriveFromPath(pathString string) (*ExtendedKey, error) {
path, err := parsePath(pathString)
if err != nil {
return nil, err
}
return extKey.path(path)
}
func (extKey *ExtendedKey) path(path *path) (*ExtendedKey, error) {
descendantExtKey := extKey
for _, index := range path.indexes {
var err error
descendantExtKey, err = descendantExtKey.Child(index)
if err != nil {
return nil, err
}
}
if path.isPublic {
return descendantExtKey.Public()
}
return descendantExtKey, nil
}
func (extKey *ExtendedKey) String() string {
serialized, err := extKey.serialize()
if err != nil {
panic(errors.Wrap(err, "error serializing key"))
}
return base58.Encode(serialized)
}

View File

@@ -0,0 +1,58 @@
package bip32
import (
"bytes"
"crypto/hmac"
"crypto/sha256"
"crypto/sha512"
"github.com/pkg/errors"
"golang.org/x/crypto/ripemd160"
"hash"
)
func newHMACWriter(key []byte) hmacWriter {
return hmacWriter{
Hash: hmac.New(sha512.New, key),
}
}
type hmacWriter struct {
hash.Hash
}
func (hw hmacWriter) InfallibleWrite(p []byte) {
_, err := hw.Write(p)
if err != nil {
panic(errors.Wrap(err, "writing to hmac should never fail"))
}
}
func calcChecksum(data []byte) []byte {
return doubleSha256(data)[:checkSumLen]
}
func doubleSha256(data []byte) []byte {
inner := sha256.Sum256(data)
outer := sha256.Sum256(inner[:])
return outer[:]
}
// validateChecksum validates that the last checkSumLen bytes of the
// given data are its valid checksum.
func validateChecksum(data []byte) error {
checksum := data[len(data)-checkSumLen:]
expectedChecksum := calcChecksum(data[:len(data)-checkSumLen])
if !bytes.Equal(expectedChecksum, checksum) {
return errors.Errorf("expected checksum %x but got %x", expectedChecksum, checksum)
}
return nil
}
func hash160(data []byte) []byte {
sha := sha256.New()
ripe := ripemd160.New()
sha.Write(data)
ripe.Write(sha.Sum(nil))
return ripe.Sum(nil)
}

View File

@@ -0,0 +1,60 @@
package bip32
import (
"github.com/pkg/errors"
"strconv"
"strings"
)
type path struct {
isPublic bool
indexes []uint32
}
func parsePath(pathString string) (*path, error) {
parts := strings.Split(pathString, "/")
isPublic := false
switch parts[0] {
case "m":
isPublic = false
case "M":
isPublic = true
default:
return nil, errors.Errorf("%s is an invalid extended key type", parts[0])
}
indexParts := parts[1:]
indexes := make([]uint32, len(indexParts))
for i, part := range indexParts {
var err error
indexes[i], err = parseIndex(part)
if err != nil {
return nil, err
}
}
return &path{
isPublic: isPublic,
indexes: indexes,
}, nil
}
func parseIndex(indexString string) (uint32, error) {
const isHardenedSuffix = "'"
isHardened := strings.HasSuffix(indexString, isHardenedSuffix)
trimmedIndexString := strings.TrimSuffix(indexString, isHardenedSuffix)
index, err := strconv.Atoi(trimmedIndexString)
if err != nil {
return 0, err
}
if index >= hardenedIndexStart {
return 0, errors.Errorf("max index value is %d but got %d", hardenedIndexStart, index)
}
if isHardened {
return uint32(index) + hardenedIndexStart, nil
}
return uint32(index), nil
}

View File

@@ -0,0 +1,148 @@
package bip32
import (
"encoding/binary"
"github.com/kaspanet/go-secp256k1"
"github.com/kaspanet/kaspad/cmd/kaspawallet/libkaspawallet/bip32/base58"
"github.com/pkg/errors"
)
const (
versionSerializationLen = 4
depthSerializationLen = 1
fingerprintSerializationLen = 4
childNumberSerializationLen = 4
chainCodeSerializationLen = 32
keySerializationLen = 33
checkSumLen = 4
)
const extendedKeySerializationLen = versionSerializationLen +
depthSerializationLen +
fingerprintSerializationLen +
childNumberSerializationLen +
chainCodeSerializationLen +
keySerializationLen +
checkSumLen
// DeserializeExtendedKey deserialized the given base58 string and returns an extended key
func DeserializeExtendedKey(extKeyString string) (*ExtendedKey, error) {
serializedBytes := base58.Decode(extKeyString)
return deserializeExtendedPrivateKey(serializedBytes)
}
func deserializeExtendedPrivateKey(serialized []byte) (*ExtendedKey, error) {
if len(serialized) != extendedKeySerializationLen {
return nil, errors.Errorf("key length must be %d bytes but got %d", extendedKeySerializationLen, len(serialized))
}
err := validateChecksum(serialized)
if err != nil {
return nil, err
}
extKey := &ExtendedKey{}
copy(extKey.Version[:], serialized[:versionSerializationLen])
extKey.Depth = serialized[versionSerializationLen]
copy(extKey.ParentFingerprint[:], serialized[versionSerializationLen+depthSerializationLen:])
extKey.ChildNumber = binary.BigEndian.Uint32(
serialized[versionSerializationLen+depthSerializationLen+fingerprintSerializationLen:],
)
copy(
extKey.ChainCode[:],
serialized[versionSerializationLen+depthSerializationLen+fingerprintSerializationLen+childNumberSerializationLen:],
)
isPrivate := isPrivateVersion(extKey.Version)
if isPrivate {
privateKeyPadding := serialized[versionSerializationLen+
depthSerializationLen+
fingerprintSerializationLen+
childNumberSerializationLen+
chainCodeSerializationLen]
if privateKeyPadding != 0 {
return nil, errors.Errorf("expected 0 padding for private key but got %d", privateKeyPadding)
}
extKey.privateKey, err = secp256k1.DeserializeECDSAPrivateKeyFromSlice(serialized[versionSerializationLen+
depthSerializationLen+
fingerprintSerializationLen+
childNumberSerializationLen+
chainCodeSerializationLen+1 : versionSerializationLen+
depthSerializationLen+
fingerprintSerializationLen+
childNumberSerializationLen+
chainCodeSerializationLen+
keySerializationLen])
if err != nil {
return nil, err
}
} else {
extKey.publicKey, err = secp256k1.DeserializeECDSAPubKey(serialized[versionSerializationLen+
depthSerializationLen+
fingerprintSerializationLen+
childNumberSerializationLen+
chainCodeSerializationLen : versionSerializationLen+
depthSerializationLen+
fingerprintSerializationLen+
childNumberSerializationLen+
chainCodeSerializationLen+
keySerializationLen])
if err != nil {
return nil, err
}
}
return extKey, nil
}
func (extKey *ExtendedKey) serialize() ([]byte, error) {
var serialized [extendedKeySerializationLen]byte
copy(serialized[:], extKey.Version[:])
serialized[versionSerializationLen] = extKey.Depth
copy(serialized[versionSerializationLen+depthSerializationLen:], extKey.ParentFingerprint[:])
binary.BigEndian.PutUint32(
serialized[versionSerializationLen+depthSerializationLen+fingerprintSerializationLen:],
extKey.ChildNumber,
)
copy(
serialized[versionSerializationLen+depthSerializationLen+fingerprintSerializationLen+childNumberSerializationLen:],
extKey.ChainCode[:],
)
if extKey.IsPrivate() {
serialized[versionSerializationLen+depthSerializationLen+fingerprintSerializationLen+childNumberSerializationLen+chainCodeSerializationLen] = 0
copy(
serialized[versionSerializationLen+
depthSerializationLen+
fingerprintSerializationLen+
childNumberSerializationLen+
chainCodeSerializationLen+
1:],
extKey.privateKey.Serialize()[:],
)
} else {
publicKey, err := extKey.PublicKey()
if err != nil {
return nil, err
}
serializedPublicKey, err := publicKey.Serialize()
if err != nil {
return nil, err
}
copy(
serialized[versionSerializationLen+depthSerializationLen+fingerprintSerializationLen+childNumberSerializationLen+chainCodeSerializationLen:],
serializedPublicKey[:],
)
}
checkSum := doubleSha256(serialized[:len(serialized)-checkSumLen])
copy(
serialized[versionSerializationLen+depthSerializationLen+fingerprintSerializationLen+childNumberSerializationLen+chainCodeSerializationLen+keySerializationLen:],
checkSum,
)
return serialized[:], nil
}

View File

@@ -0,0 +1,137 @@
package bip32
import "github.com/pkg/errors"
// BitcoinMainnetPrivate is the version that is used for
// bitcoin mainnet bip32 private extended keys.
// Ecnodes to xprv in base58.
var BitcoinMainnetPrivate = [4]byte{
0x04,
0x88,
0xad,
0xe4,
}
// BitcoinMainnetPublic is the version that is used for
// bitcoin mainnet bip32 public extended keys.
// Ecnodes to xpub in base58.
var BitcoinMainnetPublic = [4]byte{
0x04,
0x88,
0xb2,
0x1e,
}
// KaspaMainnetPrivate is the version that is used for
// kaspa mainnet bip32 private extended keys.
// Ecnodes to xprv in base58.
var KaspaMainnetPrivate = [4]byte{
0x03,
0x8f,
0x2e,
0xf4,
}
// KaspaMainnetPublic is the version that is used for
// kaspa mainnet bip32 public extended keys.
// Ecnodes to kpub in base58.
var KaspaMainnetPublic = [4]byte{
0x03,
0x8f,
0x33,
0x2e,
}
// KaspaTestnetPrivate is the version that is used for
// kaspa testnet bip32 public extended keys.
// Ecnodes to ktrv in base58.
var KaspaTestnetPrivate = [4]byte{
0x03,
0x90,
0x9e,
0x07,
}
// KaspaTestnetPublic is the version that is used for
// kaspa testnet bip32 public extended keys.
// Ecnodes to ktub in base58.
var KaspaTestnetPublic = [4]byte{
0x03,
0x90,
0xa2,
0x41,
}
// KaspaDevnetPrivate is the version that is used for
// kaspa devnet bip32 public extended keys.
// Ecnodes to kdrv in base58.
var KaspaDevnetPrivate = [4]byte{
0x03,
0x8b,
0x3d,
0x80,
}
// KaspaDevnetPublic is the version that is used for
// kaspa devnet bip32 public extended keys.
// Ecnodes to xdub in base58.
var KaspaDevnetPublic = [4]byte{
0x03,
0x8b,
0x41,
0xba,
}
// KaspaSimnetPrivate is the version that is used for
// kaspa simnet bip32 public extended keys.
// Ecnodes to ksrv in base58.
var KaspaSimnetPrivate = [4]byte{
0x03,
0x90,
0x42,
0x42,
}
// KaspaSimnetPublic is the version that is used for
// kaspa simnet bip32 public extended keys.
// Ecnodes to xsub in base58.
var KaspaSimnetPublic = [4]byte{
0x03,
0x90,
0x46,
0x7d,
}
func toPublicVersion(version [4]byte) ([4]byte, error) {
switch version {
case BitcoinMainnetPrivate:
return BitcoinMainnetPublic, nil
case KaspaMainnetPrivate:
return KaspaMainnetPublic, nil
case KaspaTestnetPrivate:
return KaspaTestnetPublic, nil
case KaspaDevnetPrivate:
return KaspaDevnetPublic, nil
case KaspaSimnetPrivate:
return KaspaSimnetPublic, nil
}
return [4]byte{}, errors.Errorf("unknown version %x", version)
}
func isPrivateVersion(version [4]byte) bool {
switch version {
case BitcoinMainnetPrivate:
return true
case KaspaMainnetPrivate:
return true
case KaspaTestnetPrivate:
return true
case KaspaDevnetPrivate:
return true
case KaspaSimnetPrivate:
return true
}
return false
}

View File

@@ -0,0 +1,82 @@
package libkaspawallet
import (
"fmt"
"github.com/kaspanet/kaspad/cmd/kaspawallet/libkaspawallet/bip32"
"github.com/kaspanet/kaspad/domain/dagconfig"
"github.com/pkg/errors"
"github.com/tyler-smith/go-bip39"
)
// CreateMnemonic creates a new bip-39 compatible mnemonic
func CreateMnemonic() (string, error) {
const bip39BitSize = 256
entropy, _ := bip39.NewEntropy(bip39BitSize)
return bip39.NewMnemonic(entropy)
}
// Purpose and CoinType constants
const (
SingleSignerPurpose = 44
// Note: this is not entirely compatible to BIP 45 since
// BIP 45 doesn't have a coin type in its derivation path.
MultiSigPurpose = 45
// TODO: Register the coin type in https://github.com/satoshilabs/slips/blob/master/slip-0044.md
CoinType = 111111
)
func defaultPath(isMultisig bool) string {
purpose := SingleSignerPurpose
if isMultisig {
purpose = MultiSigPurpose
}
return fmt.Sprintf("m/%d'/%d'/0'", purpose, CoinType)
}
// MasterPublicKeyFromMnemonic returns the master public key with the correct derivation for the given mnemonic.
func MasterPublicKeyFromMnemonic(params *dagconfig.Params, mnemonic string, isMultisig bool) (string, error) {
path := defaultPath(isMultisig)
extendedKey, err := extendedKeyFromMnemonicAndPath(mnemonic, path, params)
if err != nil {
return "", err
}
extendedPublicKey, err := extendedKey.Public()
if err != nil {
return "", err
}
return extendedPublicKey.String(), nil
}
func extendedKeyFromMnemonicAndPath(mnemonic string, path string, params *dagconfig.Params) (*bip32.ExtendedKey, error) {
seed := bip39.NewSeed(mnemonic, "")
version, err := versionFromParams(params)
if err != nil {
return nil, err
}
master, err := bip32.NewMasterWithPath(seed, version, path)
if err != nil {
return nil, err
}
return master, nil
}
func versionFromParams(params *dagconfig.Params) ([4]byte, error) {
switch params.Name {
case dagconfig.MainnetParams.Name:
return bip32.KaspaMainnetPrivate, nil
case dagconfig.TestnetParams.Name:
return bip32.KaspaTestnetPrivate, nil
case dagconfig.DevnetParams.Name:
return bip32.KaspaDevnetPrivate, nil
case dagconfig.SimnetParams.Name:
return bip32.KaspaSimnetPrivate, nil
}
return [4]byte{}, errors.Errorf("unknown network %s", params.Name)
}

View File

@@ -1,7 +1,12 @@
package libkaspawallet
import (
"math"
"sort"
"strings"
"github.com/kaspanet/go-secp256k1"
"github.com/kaspanet/kaspad/cmd/kaspawallet/libkaspawallet/bip32"
"github.com/kaspanet/kaspad/domain/dagconfig"
"github.com/kaspanet/kaspad/util"
"github.com/pkg/errors"
@@ -71,23 +76,94 @@ func PublicKeyFromPrivateKey(privateKeyBytes []byte) ([]byte, error) {
}
// Address returns the address associated with the given public keys and minimum signatures parameters.
func Address(params *dagconfig.Params, pubKeys [][]byte, minimumSignatures uint32, ecdsa bool) (util.Address, error) {
sortPublicKeys(pubKeys)
if uint32(len(pubKeys)) < minimumSignatures {
func Address(params *dagconfig.Params, extendedPublicKeys []string, minimumSignatures uint32, path string, ecdsa bool) (util.Address, error) {
sortPublicKeys(extendedPublicKeys)
if uint32(len(extendedPublicKeys)) < minimumSignatures {
return nil, errors.Errorf("The minimum amount of signatures (%d) is greater than the amount of "+
"provided public keys (%d)", minimumSignatures, len(pubKeys))
}
if len(pubKeys) == 1 {
if ecdsa {
return util.NewAddressPublicKeyECDSA(pubKeys[0][:], params.Prefix)
}
return util.NewAddressPublicKey(pubKeys[0][:], params.Prefix)
"provided public keys (%d)", minimumSignatures, len(extendedPublicKeys))
}
redeemScript, err := multiSigRedeemScript(pubKeys, minimumSignatures, ecdsa)
if len(extendedPublicKeys) == 1 {
return p2pkAddress(params, extendedPublicKeys[0], path, ecdsa)
}
redeemScript, err := multiSigRedeemScript(extendedPublicKeys, minimumSignatures, path, ecdsa)
if err != nil {
return nil, err
}
return util.NewAddressScriptHash(redeemScript, params.Prefix)
}
func p2pkAddress(params *dagconfig.Params, extendedPublicKey string, path string, ecdsa bool) (util.Address, error) {
extendedKey, err := bip32.DeserializeExtendedKey(extendedPublicKey)
if err != nil {
return nil, err
}
derivedKey, err := extendedKey.DeriveFromPath(path)
if err != nil {
return nil, err
}
publicKey, err := derivedKey.PublicKey()
if err != nil {
return nil, err
}
if ecdsa {
serializedECDSAPublicKey, err := publicKey.Serialize()
if err != nil {
return nil, err
}
return util.NewAddressPublicKeyECDSA(serializedECDSAPublicKey[:], params.Prefix)
}
schnorrPublicKey, err := publicKey.ToSchnorr()
if err != nil {
return nil, err
}
serializedSchnorrPublicKey, err := schnorrPublicKey.Serialize()
if err != nil {
return nil, err
}
return util.NewAddressPublicKey(serializedSchnorrPublicKey[:], params.Prefix)
}
func sortPublicKeys(extendedPublicKeys []string) {
sort.Slice(extendedPublicKeys, func(i, j int) bool {
return strings.Compare(extendedPublicKeys[i], extendedPublicKeys[j]) < 0
})
}
func cosignerIndex(extendedPublicKey string, sortedExtendedPublicKeys []string) (uint32, error) {
cosignerIndex := sort.SearchStrings(sortedExtendedPublicKeys, extendedPublicKey)
if cosignerIndex == len(sortedExtendedPublicKeys) {
return 0, errors.Errorf("couldn't find extended public key %s", extendedPublicKey)
}
return uint32(cosignerIndex), nil
}
// MinimumCosignerIndex returns the minimum index for the cosigner from the set of all extended public keys.
func MinimumCosignerIndex(cosignerExtendedPublicKeys, allExtendedPublicKeys []string) (uint32, error) {
allExtendedPublicKeysCopy := make([]string, len(allExtendedPublicKeys))
copy(allExtendedPublicKeysCopy, allExtendedPublicKeys)
sortPublicKeys(allExtendedPublicKeysCopy)
min := uint32(math.MaxUint32)
for _, extendedPublicKey := range cosignerExtendedPublicKeys {
cosignerIndex, err := cosignerIndex(extendedPublicKey, allExtendedPublicKeysCopy)
if err != nil {
return 0, err
}
if cosignerIndex < min {
min = cosignerIndex
}
}
return min, nil
}

View File

@@ -89,6 +89,7 @@ type PartiallySignedInput struct {
PrevOutput *TransactionOutput `protobuf:"bytes,2,opt,name=prevOutput,proto3" json:"prevOutput,omitempty"`
MinimumSignatures uint32 `protobuf:"varint,3,opt,name=minimumSignatures,proto3" json:"minimumSignatures,omitempty"`
PubKeySignaturePairs []*PubKeySignaturePair `protobuf:"bytes,4,rep,name=pubKeySignaturePairs,proto3" json:"pubKeySignaturePairs,omitempty"`
DerivationPath string `protobuf:"bytes,5,opt,name=DerivationPath,proto3" json:"DerivationPath,omitempty"`
}
func (x *PartiallySignedInput) Reset() {
@@ -151,13 +152,20 @@ func (x *PartiallySignedInput) GetPubKeySignaturePairs() []*PubKeySignaturePair
return nil
}
func (x *PartiallySignedInput) GetDerivationPath() string {
if x != nil {
return x.DerivationPath
}
return ""
}
type PubKeySignaturePair struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
PubKey []byte `protobuf:"bytes,1,opt,name=pubKey,proto3" json:"pubKey,omitempty"`
Signature []byte `protobuf:"bytes,2,opt,name=signature,proto3" json:"signature,omitempty"`
ExtendedPubKey string `protobuf:"bytes,1,opt,name=extendedPubKey,proto3" json:"extendedPubKey,omitempty"`
Signature []byte `protobuf:"bytes,2,opt,name=signature,proto3" json:"signature,omitempty"`
}
func (x *PubKeySignaturePair) Reset() {
@@ -192,11 +200,11 @@ func (*PubKeySignaturePair) Descriptor() ([]byte, []int) {
return file_wallet_proto_rawDescGZIP(), []int{2}
}
func (x *PubKeySignaturePair) GetPubKey() []byte {
func (x *PubKeySignaturePair) GetExtendedPubKey() string {
if x != nil {
return x.PubKey
return x.ExtendedPubKey
}
return nil
return ""
}
func (x *PubKeySignaturePair) GetSignature() []byte {
@@ -639,7 +647,7 @@ var file_wallet_proto_rawDesc = []byte{
0x73, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x50, 0x61,
0x72, 0x74, 0x69, 0x61, 0x6c, 0x6c, 0x79, 0x53, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x49, 0x6e, 0x70,
0x75, 0x74, 0x52, 0x15, 0x70, 0x61, 0x72, 0x74, 0x69, 0x61, 0x6c, 0x6c, 0x79, 0x53, 0x69, 0x67,
0x6e, 0x65, 0x64, 0x49, 0x6e, 0x70, 0x75, 0x74, 0x73, 0x22, 0x8c, 0x02, 0x0a, 0x14, 0x50, 0x61,
0x6e, 0x65, 0x64, 0x49, 0x6e, 0x70, 0x75, 0x74, 0x73, 0x22, 0xb4, 0x02, 0x0a, 0x14, 0x50, 0x61,
0x72, 0x74, 0x69, 0x61, 0x6c, 0x6c, 0x79, 0x53, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x49, 0x6e, 0x70,
0x75, 0x74, 0x12, 0x22, 0x0a, 0x0c, 0x72, 0x65, 0x64, 0x65, 0x65, 0x6d, 0x53, 0x63, 0x72, 0x69,
0x70, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0c, 0x72, 0x65, 0x64, 0x65, 0x65, 0x6d,
@@ -656,72 +664,75 @@ var file_wallet_proto_rawDesc = []byte{
0x6f, 0x73, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x50,
0x75, 0x62, 0x4b, 0x65, 0x79, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x50, 0x61,
0x69, 0x72, 0x52, 0x14, 0x70, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x74,
0x75, 0x72, 0x65, 0x50, 0x61, 0x69, 0x72, 0x73, 0x22, 0x4b, 0x0a, 0x13, 0x50, 0x75, 0x62, 0x4b,
0x65, 0x79, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x50, 0x61, 0x69, 0x72, 0x12,
0x16, 0x0a, 0x06, 0x70, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52,
0x06, 0x70, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x69, 0x67, 0x6e, 0x61,
0x74, 0x75, 0x72, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x73, 0x69, 0x67, 0x6e,
0x61, 0x74, 0x75, 0x72, 0x65, 0x22, 0x24, 0x0a, 0x0c, 0x53, 0x75, 0x62, 0x6e, 0x65, 0x74, 0x77,
0x6f, 0x72, 0x6b, 0x49, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x62, 0x79, 0x74, 0x65, 0x73, 0x18, 0x01,
0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x62, 0x79, 0x74, 0x65, 0x73, 0x22, 0xbb, 0x02, 0x0a, 0x12,
0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x4d, 0x65, 0x73, 0x73, 0x61,
0x67, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20,
0x01, 0x28, 0x0d, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x3c, 0x0a, 0x06,
0x69, 0x6e, 0x70, 0x75, 0x74, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x24, 0x2e, 0x70,
0x72, 0x6f, 0x74, 0x6f, 0x73, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f,
0x6e, 0x2e, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x6e, 0x70,
0x75, 0x74, 0x52, 0x06, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x73, 0x12, 0x3f, 0x0a, 0x07, 0x6f, 0x75,
0x74, 0x70, 0x75, 0x74, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x70, 0x72,
0x6f, 0x74, 0x6f, 0x73, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e,
0x2e, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x4f, 0x75, 0x74, 0x70,
0x75, 0x74, 0x52, 0x07, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x73, 0x12, 0x1a, 0x0a, 0x08, 0x6c,
0x6f, 0x63, 0x6b, 0x54, 0x69, 0x6d, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x04, 0x52, 0x08, 0x6c,
0x6f, 0x63, 0x6b, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x44, 0x0a, 0x0c, 0x73, 0x75, 0x62, 0x6e, 0x65,
0x74, 0x77, 0x6f, 0x72, 0x6b, 0x49, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x20, 0x2e,
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x73, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x69, 0x7a, 0x61, 0x74, 0x69,
0x6f, 0x6e, 0x2e, 0x53, 0x75, 0x62, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x49, 0x64, 0x52,
0x0c, 0x73, 0x75, 0x62, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x49, 0x64, 0x12, 0x10, 0x0a,
0x03, 0x67, 0x61, 0x73, 0x18, 0x06, 0x20, 0x01, 0x28, 0x04, 0x52, 0x03, 0x67, 0x61, 0x73, 0x12,
0x18, 0x0a, 0x07, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0c,
0x52, 0x07, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x22, 0xa2, 0x01, 0x0a, 0x10, 0x54, 0x72,
0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x6e, 0x70, 0x75, 0x74, 0x12, 0x48,
0x0a, 0x10, 0x70, 0x72, 0x65, 0x76, 0x69, 0x6f, 0x75, 0x73, 0x4f, 0x75, 0x74, 0x70, 0x6f, 0x69,
0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f,
0x73, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x4f, 0x75,
0x74, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x52, 0x10, 0x70, 0x72, 0x65, 0x76, 0x69, 0x6f, 0x75, 0x73,
0x4f, 0x75, 0x74, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x28, 0x0a, 0x0f, 0x73, 0x69, 0x67, 0x6e,
0x61, 0x74, 0x75, 0x72, 0x65, 0x53, 0x63, 0x72, 0x69, 0x70, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28,
0x0c, 0x52, 0x0f, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x53, 0x63, 0x72, 0x69,
0x70, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x73, 0x65, 0x71, 0x75, 0x65, 0x6e, 0x63, 0x65, 0x18, 0x03,
0x20, 0x01, 0x28, 0x04, 0x52, 0x08, 0x73, 0x65, 0x71, 0x75, 0x65, 0x6e, 0x63, 0x65, 0x22, 0x69,
0x0a, 0x08, 0x4f, 0x75, 0x74, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x47, 0x0a, 0x0d, 0x74, 0x72,
0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28,
0x0b, 0x32, 0x21, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x73, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x69,
0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69,
0x6f, 0x6e, 0x49, 0x64, 0x52, 0x0d, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f,
0x6e, 0x49, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x18, 0x02, 0x20, 0x01,
0x28, 0x0d, 0x52, 0x05, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x22, 0x25, 0x0a, 0x0d, 0x54, 0x72, 0x61,
0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x62, 0x79,
0x74, 0x65, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x62, 0x79, 0x74, 0x65, 0x73,
0x22, 0x43, 0x0a, 0x0f, 0x53, 0x63, 0x72, 0x69, 0x70, 0x74, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63,
0x4b, 0x65, 0x79, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x18, 0x01, 0x20,
0x01, 0x28, 0x0c, 0x52, 0x06, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x76,
0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x76, 0x65,
0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x78, 0x0a, 0x11, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63,
0x74, 0x69, 0x6f, 0x6e, 0x4f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61,
0x6c, 0x75, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65,
0x12, 0x4d, 0x0a, 0x0f, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63,
0x4b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x23, 0x2e, 0x70, 0x72, 0x6f, 0x74,
0x6f, 0x73, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x53,
0x63, 0x72, 0x69, 0x70, 0x74, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x52, 0x0f,
0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x42,
0x5c, 0x5a, 0x5a, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6b, 0x61,
0x73, 0x70, 0x61, 0x6e, 0x65, 0x74, 0x2f, 0x6b, 0x61, 0x73, 0x70, 0x61, 0x64, 0x2f, 0x63, 0x6d,
0x64, 0x2f, 0x6b, 0x61, 0x73, 0x70, 0x61, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x2f, 0x6c, 0x69,
0x62, 0x6b, 0x61, 0x73, 0x70, 0x61, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x2f, 0x73, 0x65, 0x72,
0x69, 0x61, 0x6c, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f,
0x73, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x62, 0x06, 0x70,
0x72, 0x6f, 0x74, 0x6f, 0x33,
0x75, 0x72, 0x65, 0x50, 0x61, 0x69, 0x72, 0x73, 0x12, 0x26, 0x0a, 0x0e, 0x44, 0x65, 0x72, 0x69,
0x76, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x61, 0x74, 0x68, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09,
0x52, 0x0e, 0x44, 0x65, 0x72, 0x69, 0x76, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x61, 0x74, 0x68,
0x22, 0x5b, 0x0a, 0x13, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x74,
0x75, 0x72, 0x65, 0x50, 0x61, 0x69, 0x72, 0x12, 0x26, 0x0a, 0x0e, 0x65, 0x78, 0x74, 0x65, 0x6e,
0x64, 0x65, 0x64, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52,
0x0e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x64, 0x65, 0x64, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x12,
0x1c, 0x0a, 0x09, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x18, 0x02, 0x20, 0x01,
0x28, 0x0c, 0x52, 0x09, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x22, 0x24, 0x0a,
0x0c, 0x53, 0x75, 0x62, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x49, 0x64, 0x12, 0x14, 0x0a,
0x05, 0x62, 0x79, 0x74, 0x65, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x62, 0x79,
0x74, 0x65, 0x73, 0x22, 0xbb, 0x02, 0x0a, 0x12, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74,
0x69, 0x6f, 0x6e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65,
0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x76, 0x65, 0x72,
0x73, 0x69, 0x6f, 0x6e, 0x12, 0x3c, 0x0a, 0x06, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x73, 0x18, 0x02,
0x20, 0x03, 0x28, 0x0b, 0x32, 0x24, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x73, 0x65, 0x72, 0x69,
0x61, 0x6c, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61,
0x63, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x6e, 0x70, 0x75, 0x74, 0x52, 0x06, 0x69, 0x6e, 0x70, 0x75,
0x74, 0x73, 0x12, 0x3f, 0x0a, 0x07, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x73, 0x18, 0x03, 0x20,
0x03, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x73, 0x65, 0x72, 0x69, 0x61,
0x6c, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63,
0x74, 0x69, 0x6f, 0x6e, 0x4f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x52, 0x07, 0x6f, 0x75, 0x74, 0x70,
0x75, 0x74, 0x73, 0x12, 0x1a, 0x0a, 0x08, 0x6c, 0x6f, 0x63, 0x6b, 0x54, 0x69, 0x6d, 0x65, 0x18,
0x04, 0x20, 0x01, 0x28, 0x04, 0x52, 0x08, 0x6c, 0x6f, 0x63, 0x6b, 0x54, 0x69, 0x6d, 0x65, 0x12,
0x44, 0x0a, 0x0c, 0x73, 0x75, 0x62, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x49, 0x64, 0x18,
0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x73, 0x65, 0x72,
0x69, 0x61, 0x6c, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x53, 0x75, 0x62, 0x6e, 0x65,
0x74, 0x77, 0x6f, 0x72, 0x6b, 0x49, 0x64, 0x52, 0x0c, 0x73, 0x75, 0x62, 0x6e, 0x65, 0x74, 0x77,
0x6f, 0x72, 0x6b, 0x49, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x67, 0x61, 0x73, 0x18, 0x06, 0x20, 0x01,
0x28, 0x04, 0x52, 0x03, 0x67, 0x61, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x70, 0x61, 0x79, 0x6c, 0x6f,
0x61, 0x64, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61,
0x64, 0x22, 0xa2, 0x01, 0x0a, 0x10, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f,
0x6e, 0x49, 0x6e, 0x70, 0x75, 0x74, 0x12, 0x48, 0x0a, 0x10, 0x70, 0x72, 0x65, 0x76, 0x69, 0x6f,
0x75, 0x73, 0x4f, 0x75, 0x74, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b,
0x32, 0x1c, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x73, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x69, 0x7a,
0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x4f, 0x75, 0x74, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x52, 0x10,
0x70, 0x72, 0x65, 0x76, 0x69, 0x6f, 0x75, 0x73, 0x4f, 0x75, 0x74, 0x70, 0x6f, 0x69, 0x6e, 0x74,
0x12, 0x28, 0x0a, 0x0f, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x53, 0x63, 0x72,
0x69, 0x70, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0f, 0x73, 0x69, 0x67, 0x6e, 0x61,
0x74, 0x75, 0x72, 0x65, 0x53, 0x63, 0x72, 0x69, 0x70, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x73, 0x65,
0x71, 0x75, 0x65, 0x6e, 0x63, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x08, 0x73, 0x65,
0x71, 0x75, 0x65, 0x6e, 0x63, 0x65, 0x22, 0x69, 0x0a, 0x08, 0x4f, 0x75, 0x74, 0x70, 0x6f, 0x69,
0x6e, 0x74, 0x12, 0x47, 0x0a, 0x0d, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f,
0x6e, 0x49, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x70, 0x72, 0x6f, 0x74,
0x6f, 0x73, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x54,
0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x52, 0x0d, 0x74, 0x72,
0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x69,
0x6e, 0x64, 0x65, 0x78, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x05, 0x69, 0x6e, 0x64, 0x65,
0x78, 0x22, 0x25, 0x0a, 0x0d, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e,
0x49, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x62, 0x79, 0x74, 0x65, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28,
0x0c, 0x52, 0x05, 0x62, 0x79, 0x74, 0x65, 0x73, 0x22, 0x43, 0x0a, 0x0f, 0x53, 0x63, 0x72, 0x69,
0x70, 0x74, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x12, 0x16, 0x0a, 0x06, 0x73,
0x63, 0x72, 0x69, 0x70, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x73, 0x63, 0x72,
0x69, 0x70, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02,
0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x78, 0x0a,
0x11, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x4f, 0x75, 0x74, 0x70,
0x75, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28,
0x04, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x4d, 0x0a, 0x0f, 0x73, 0x63, 0x72, 0x69,
0x70, 0x74, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28,
0x0b, 0x32, 0x23, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x73, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x69,
0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x53, 0x63, 0x72, 0x69, 0x70, 0x74, 0x50, 0x75, 0x62,
0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x52, 0x0f, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x50, 0x75,
0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x42, 0x5c, 0x5a, 0x5a, 0x67, 0x69, 0x74, 0x68, 0x75,
0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6b, 0x61, 0x73, 0x70, 0x61, 0x6e, 0x65, 0x74, 0x2f, 0x6b,
0x61, 0x73, 0x70, 0x61, 0x64, 0x2f, 0x63, 0x6d, 0x64, 0x2f, 0x6b, 0x61, 0x73, 0x70, 0x61, 0x77,
0x61, 0x6c, 0x6c, 0x65, 0x74, 0x2f, 0x6c, 0x69, 0x62, 0x6b, 0x61, 0x73, 0x70, 0x61, 0x77, 0x61,
0x6c, 0x6c, 0x65, 0x74, 0x2f, 0x73, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x69, 0x7a, 0x61, 0x74, 0x69,
0x6f, 0x6e, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x73, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x69, 0x7a,
0x61, 0x74, 0x69, 0x6f, 0x6e, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (

View File

@@ -13,10 +13,11 @@ message PartiallySignedInput{
TransactionOutput prevOutput = 2;
uint32 minimumSignatures = 3;
repeated PubKeySignaturePair pubKeySignaturePairs = 4;
string DerivationPath = 5;
}
message PubKeySignaturePair{
bytes pubKey = 1;
string extendedPubKey = 1;
bytes signature = 2;
}

View File

@@ -21,16 +21,16 @@ type PartiallySignedTransaction struct {
// PartiallySignedInput represents an input signed
// only by some of the relevant parties.
type PartiallySignedInput struct {
RedeeemScript []byte
PrevOutput *externalapi.DomainTransactionOutput
MinimumSignatures uint32
PubKeySignaturePairs []*PubKeySignaturePair
DerivationPath string
}
// PubKeySignaturePair is a pair of public key and (potentially) its associated signature
type PubKeySignaturePair struct {
PubKey []byte
Signature []byte
ExtendedPublicKey string
Signature []byte
}
// DeserializePartiallySignedTransaction deserializes a byte slice into PartiallySignedTransaction.
@@ -93,10 +93,10 @@ func partiallySignedInputFromProto(protoPartiallySignedInput *protoserialization
}
return &PartiallySignedInput{
RedeeemScript: protoPartiallySignedInput.RedeemScript,
PrevOutput: output,
MinimumSignatures: protoPartiallySignedInput.MinimumSignatures,
PubKeySignaturePairs: pubKeySignaturePairs,
DerivationPath: protoPartiallySignedInput.DerivationPath,
}, nil
}
@@ -107,24 +107,24 @@ func partiallySignedInputToProto(partiallySignedInput *PartiallySignedInput) *pr
}
return &protoserialization.PartiallySignedInput{
RedeemScript: partiallySignedInput.RedeeemScript,
PrevOutput: transactionOutputToProto(partiallySignedInput.PrevOutput),
MinimumSignatures: partiallySignedInput.MinimumSignatures,
PubKeySignaturePairs: protoPairs,
DerivationPath: partiallySignedInput.DerivationPath,
}
}
func pubKeySignaturePairFromProto(protoPubKeySignaturePair *protoserialization.PubKeySignaturePair) *PubKeySignaturePair {
return &PubKeySignaturePair{
PubKey: protoPubKeySignaturePair.PubKey,
Signature: protoPubKeySignaturePair.Signature,
ExtendedPublicKey: protoPubKeySignaturePair.ExtendedPubKey,
Signature: protoPubKeySignaturePair.Signature,
}
}
func pubKeySignaturePairToProto(pubKeySignaturePair *PubKeySignaturePair) *protoserialization.PubKeySignaturePair {
return &protoserialization.PubKeySignaturePair{
PubKey: pubKeySignaturePair.PubKey,
Signature: pubKeySignaturePair.Signature,
ExtendedPubKey: pubKeySignaturePair.ExtendedPublicKey,
Signature: pubKeySignaturePair.Signature,
}
}

View File

@@ -1,100 +1,41 @@
package libkaspawallet
import (
"bytes"
"github.com/kaspanet/go-secp256k1"
"github.com/kaspanet/kaspad/cmd/kaspawallet/libkaspawallet/bip32"
"github.com/kaspanet/kaspad/cmd/kaspawallet/libkaspawallet/serialization"
"github.com/kaspanet/kaspad/domain/consensus/model/externalapi"
"github.com/kaspanet/kaspad/domain/consensus/utils/consensushashing"
"github.com/kaspanet/kaspad/domain/consensus/utils/txscript"
"github.com/kaspanet/kaspad/domain/consensus/utils/utxo"
"github.com/kaspanet/kaspad/domain/dagconfig"
"github.com/pkg/errors"
)
type signer interface {
rawTxInSignature(tx *externalapi.DomainTransaction, idx int, hashType consensushashing.SigHashType,
sighashReusedValues *consensushashing.SighashReusedValues) ([]byte, error)
serializedPublicKey() ([]byte, error)
}
func rawTxInSignature(extendedKey *bip32.ExtendedKey, tx *externalapi.DomainTransaction, idx int, hashType consensushashing.SigHashType,
sighashReusedValues *consensushashing.SighashReusedValues, ecdsa bool) ([]byte, error) {
type schnorrSigner secp256k1.SchnorrKeyPair
func (s *schnorrSigner) rawTxInSignature(tx *externalapi.DomainTransaction, idx int, hashType consensushashing.SigHashType,
sighashReusedValues *consensushashing.SighashReusedValues) ([]byte, error) {
return txscript.RawTxInSignature(tx, idx, hashType, (*secp256k1.SchnorrKeyPair)(s), sighashReusedValues)
}
func (s *schnorrSigner) serializedPublicKey() ([]byte, error) {
publicKey, err := (*secp256k1.SchnorrKeyPair)(s).SchnorrPublicKey()
if err != nil {
return nil, err
}
serializedPublicKey, err := publicKey.Serialize()
if err != nil {
return nil, err
}
return serializedPublicKey[:], nil
}
type ecdsaSigner secp256k1.ECDSAPrivateKey
func (e *ecdsaSigner) rawTxInSignature(tx *externalapi.DomainTransaction, idx int, hashType consensushashing.SigHashType,
sighashReusedValues *consensushashing.SighashReusedValues) ([]byte, error) {
return txscript.RawTxInSignatureECDSA(tx, idx, hashType, (*secp256k1.ECDSAPrivateKey)(e), sighashReusedValues)
}
func (e *ecdsaSigner) serializedPublicKey() ([]byte, error) {
publicKey, err := (*secp256k1.ECDSAPrivateKey)(e).ECDSAPublicKey()
if err != nil {
return nil, err
}
serializedPublicKey, err := publicKey.Serialize()
if err != nil {
return nil, err
}
return serializedPublicKey[:], nil
}
func deserializeECDSAPrivateKey(privateKey []byte, ecdsa bool) (signer, error) {
privateKey := extendedKey.PrivateKey()
if ecdsa {
keyPair, err := secp256k1.DeserializeECDSAPrivateKeyFromSlice(privateKey)
if err != nil {
return nil, errors.Wrap(err, "Error deserializing private key")
}
return (*ecdsaSigner)(keyPair), nil
return txscript.RawTxInSignatureECDSA(tx, idx, hashType, privateKey, sighashReusedValues)
}
keyPair, err := secp256k1.DeserializeSchnorrPrivateKeyFromSlice(privateKey)
schnorrKeyPair, err := privateKey.ToSchnorr()
if err != nil {
return nil, errors.Wrap(err, "Error deserializing private key")
return nil, err
}
return (*schnorrSigner)(keyPair), nil
return txscript.RawTxInSignature(tx, idx, hashType, schnorrKeyPair, sighashReusedValues)
}
// Sign signs the transaction with the given private keys
func Sign(privateKeys [][]byte, serializedPSTx []byte, ecdsa bool) ([]byte, error) {
keyPairs := make([]signer, len(privateKeys))
for i, privateKey := range privateKeys {
var err error
keyPairs[i], err = deserializeECDSAPrivateKey(privateKey, ecdsa)
if err != nil {
return nil, errors.Wrap(err, "Error deserializing private key")
}
}
func Sign(params *dagconfig.Params, mnemonics []string, serializedPSTx []byte, ecdsa bool) ([]byte, error) {
partiallySignedTransaction, err := serialization.DeserializePartiallySignedTransaction(serializedPSTx)
if err != nil {
return nil, err
}
for _, keyPair := range keyPairs {
err = sign(keyPair, partiallySignedTransaction)
for _, mnemonic := range mnemonics {
err = sign(params, mnemonic, partiallySignedTransaction, ecdsa)
if err != nil {
return nil, err
}
@@ -103,20 +44,15 @@ func Sign(privateKeys [][]byte, serializedPSTx []byte, ecdsa bool) ([]byte, erro
return serialization.SerializePartiallySignedTransaction(partiallySignedTransaction)
}
func sign(keyPair signer, psTx *serialization.PartiallySignedTransaction) error {
if isTransactionFullySigned(psTx) {
func sign(params *dagconfig.Params, mnemonic string, partiallySignedTransaction *serialization.PartiallySignedTransaction, ecdsa bool) error {
if isTransactionFullySigned(partiallySignedTransaction) {
return nil
}
serializedPublicKey, err := keyPair.serializedPublicKey()
if err != nil {
return err
}
sighashReusedValues := &consensushashing.SighashReusedValues{}
for i, partiallySignedInput := range psTx.PartiallySignedInputs {
for i, partiallySignedInput := range partiallySignedTransaction.PartiallySignedInputs {
prevOut := partiallySignedInput.PrevOutput
psTx.Tx.Inputs[i].UTXOEntry = utxo.NewUTXOEntry(
partiallySignedTransaction.Tx.Inputs[i].UTXOEntry = utxo.NewUTXOEntry(
prevOut.Value,
prevOut.ScriptPublicKey,
false, // This is a fake value, because it's irrelevant for the signature
@@ -125,10 +61,27 @@ func sign(keyPair signer, psTx *serialization.PartiallySignedTransaction) error
}
signed := false
for i, partiallySignedInput := range psTx.PartiallySignedInputs {
for i, partiallySignedInput := range partiallySignedTransaction.PartiallySignedInputs {
isMultisig := len(partiallySignedInput.PubKeySignaturePairs) > 1
path := defaultPath(isMultisig)
extendedKey, err := extendedKeyFromMnemonicAndPath(mnemonic, path, params)
if err != nil {
return err
}
derivedKey, err := extendedKey.DeriveFromPath(partiallySignedInput.DerivationPath)
if err != nil {
return err
}
derivedPublicKey, err := derivedKey.Public()
if err != nil {
return err
}
for _, pair := range partiallySignedInput.PubKeySignaturePairs {
if bytes.Equal(pair.PubKey, serializedPublicKey[:]) {
pair.Signature, err = keyPair.rawTxInSignature(psTx.Tx, i, consensushashing.SigHashAll, sighashReusedValues)
if pair.ExtendedPublicKey == derivedPublicKey.String() {
pair.Signature, err = rawTxInSignature(derivedKey, partiallySignedTransaction.Tx, i, consensushashing.SigHashAll, sighashReusedValues, ecdsa)
if err != nil {
return err
}

View File

@@ -1,7 +1,7 @@
package libkaspawallet
import (
"bytes"
"github.com/kaspanet/kaspad/cmd/kaspawallet/libkaspawallet/bip32"
"github.com/kaspanet/kaspad/cmd/kaspawallet/libkaspawallet/serialization"
"github.com/kaspanet/kaspad/domain/consensus/model/externalapi"
"github.com/kaspanet/kaspad/domain/consensus/utils/constants"
@@ -9,7 +9,6 @@ import (
"github.com/kaspanet/kaspad/domain/consensus/utils/txscript"
"github.com/kaspanet/kaspad/util"
"github.com/pkg/errors"
"sort"
)
// Payment contains a recipient payment details
@@ -18,22 +17,24 @@ type Payment struct {
Amount uint64
}
func sortPublicKeys(publicKeys [][]byte) {
sort.Slice(publicKeys, func(i, j int) bool {
return bytes.Compare(publicKeys[i], publicKeys[j]) < 0
})
// UTXO is a type that stores a UTXO and meta data
// that is needed in order to sign it and create
// transactions with it.
type UTXO struct {
Outpoint *externalapi.DomainOutpoint
UTXOEntry externalapi.UTXOEntry
DerivationPath string
}
// CreateUnsignedTransaction creates an unsigned transaction
func CreateUnsignedTransaction(
pubKeys [][]byte,
extendedPublicKeys []string,
minimumSignatures uint32,
ecdsa bool,
payments []*Payment,
selectedUTXOs []*externalapi.OutpointAndUTXOEntryPair) ([]byte, error) {
selectedUTXOs []*UTXO) ([]byte, error) {
sortPublicKeys(pubKeys)
unsignedTransaction, err := createUnsignedTransaction(pubKeys, minimumSignatures, ecdsa, payments, selectedUTXOs)
sortPublicKeys(extendedPublicKeys)
unsignedTransaction, err := createUnsignedTransaction(extendedPublicKeys, minimumSignatures, payments, selectedUTXOs)
if err != nil {
return nil, err
}
@@ -41,13 +42,48 @@ func CreateUnsignedTransaction(
return serialization.SerializePartiallySignedTransaction(unsignedTransaction)
}
func multiSigRedeemScript(pubKeys [][]byte, minimumSignatures uint32, ecdsa bool) ([]byte, error) {
func multiSigRedeemScript(extendedPublicKeys []string, minimumSignatures uint32, path string, ecdsa bool) ([]byte, error) {
scriptBuilder := txscript.NewScriptBuilder()
scriptBuilder.AddInt64(int64(minimumSignatures))
for _, key := range pubKeys {
scriptBuilder.AddData(key)
for _, key := range extendedPublicKeys {
extendedKey, err := bip32.DeserializeExtendedKey(key)
if err != nil {
return nil, err
}
derivedKey, err := extendedKey.DeriveFromPath(path)
if err != nil {
return nil, err
}
publicKey, err := derivedKey.PublicKey()
if err != nil {
return nil, err
}
var serializedPublicKey []byte
if ecdsa {
serializedECDSAPublicKey, err := publicKey.Serialize()
if err != nil {
return nil, err
}
serializedPublicKey = serializedECDSAPublicKey[:]
} else {
schnorrPublicKey, err := publicKey.ToSchnorr()
if err != nil {
return nil, err
}
serializedSchnorrPublicKey, err := schnorrPublicKey.Serialize()
if err != nil {
return nil, err
}
serializedPublicKey = serializedSchnorrPublicKey[:]
}
scriptBuilder.AddData(serializedPublicKey)
}
scriptBuilder.AddInt64(int64(len(pubKeys)))
scriptBuilder.AddInt64(int64(len(extendedPublicKeys)))
if ecdsa {
scriptBuilder.AddOp(txscript.OpCheckMultiSigECDSA)
@@ -59,40 +95,40 @@ func multiSigRedeemScript(pubKeys [][]byte, minimumSignatures uint32, ecdsa bool
}
func createUnsignedTransaction(
pubKeys [][]byte,
extendedPublicKeys []string,
minimumSignatures uint32,
ecdsa bool,
payments []*Payment,
selectedUTXOs []*externalapi.OutpointAndUTXOEntryPair) (*serialization.PartiallySignedTransaction, error) {
var redeemScript []byte
if len(pubKeys) > 1 {
var err error
redeemScript, err = multiSigRedeemScript(pubKeys, minimumSignatures, ecdsa)
if err != nil {
return nil, err
}
}
selectedUTXOs []*UTXO) (*serialization.PartiallySignedTransaction, error) {
inputs := make([]*externalapi.DomainTransactionInput, len(selectedUTXOs))
partiallySignedInputs := make([]*serialization.PartiallySignedInput, len(selectedUTXOs))
for i, utxo := range selectedUTXOs {
emptyPubKeySignaturePairs := make([]*serialization.PubKeySignaturePair, len(pubKeys))
for i, pubKey := range pubKeys {
emptyPubKeySignaturePairs := make([]*serialization.PubKeySignaturePair, len(extendedPublicKeys))
for i, extendedPublicKey := range extendedPublicKeys {
extendedKey, err := bip32.DeserializeExtendedKey(extendedPublicKey)
if err != nil {
return nil, err
}
derivedKey, err := extendedKey.DeriveFromPath(utxo.DerivationPath)
if err != nil {
return nil, err
}
emptyPubKeySignaturePairs[i] = &serialization.PubKeySignaturePair{
PubKey: pubKey,
ExtendedPublicKey: derivedKey.String(),
}
}
inputs[i] = &externalapi.DomainTransactionInput{PreviousOutpoint: *utxo.Outpoint}
partiallySignedInputs[i] = &serialization.PartiallySignedInput{
RedeeemScript: redeemScript,
PrevOutput: &externalapi.DomainTransactionOutput{
Value: utxo.UTXOEntry.Amount(),
ScriptPublicKey: utxo.UTXOEntry.ScriptPublicKey(),
},
MinimumSignatures: minimumSignatures,
PubKeySignaturePairs: emptyPubKeySignaturePairs,
DerivationPath: utxo.DerivationPath,
}
}
@@ -126,8 +162,8 @@ func createUnsignedTransaction(
}
// IsTransactionFullySigned returns whether the transaction is fully signed and ready to broadcast.
func IsTransactionFullySigned(psTxBytes []byte) (bool, error) {
partiallySignedTransaction, err := serialization.DeserializePartiallySignedTransaction(psTxBytes)
func IsTransactionFullySigned(partiallySignedTransactionBytes []byte) (bool, error) {
partiallySignedTransaction, err := serialization.DeserializePartiallySignedTransaction(partiallySignedTransactionBytes)
if err != nil {
return false, err
}
@@ -135,8 +171,8 @@ func IsTransactionFullySigned(psTxBytes []byte) (bool, error) {
return isTransactionFullySigned(partiallySignedTransaction), nil
}
func isTransactionFullySigned(psTx *serialization.PartiallySignedTransaction) bool {
for _, input := range psTx.PartiallySignedInputs {
func isTransactionFullySigned(partiallySignedTransaction *serialization.PartiallySignedTransaction) bool {
for _, input := range partiallySignedTransaction.PartiallySignedInputs {
numSignatures := 0
for _, pair := range input.PubKeySignaturePairs {
if pair.Signature != nil {
@@ -152,18 +188,18 @@ func isTransactionFullySigned(psTx *serialization.PartiallySignedTransaction) bo
// ExtractTransaction extracts a domain transaction from partially signed transaction after all of the
// relevant parties have signed it.
func ExtractTransaction(psTxBytes []byte) (*externalapi.DomainTransaction, error) {
partiallySignedTransaction, err := serialization.DeserializePartiallySignedTransaction(psTxBytes)
func ExtractTransaction(partiallySignedTransactionBytes []byte, ecdsa bool) (*externalapi.DomainTransaction, error) {
partiallySignedTransaction, err := serialization.DeserializePartiallySignedTransaction(partiallySignedTransactionBytes)
if err != nil {
return nil, err
}
return extractTransaction(partiallySignedTransaction)
return extractTransaction(partiallySignedTransaction, ecdsa)
}
func extractTransaction(psTx *serialization.PartiallySignedTransaction) (*externalapi.DomainTransaction, error) {
for i, input := range psTx.PartiallySignedInputs {
isMultisig := input.RedeeemScript != nil
func extractTransaction(partiallySignedTransaction *serialization.PartiallySignedTransaction, ecdsa bool) (*externalapi.DomainTransaction, error) {
for i, input := range partiallySignedTransaction.PartiallySignedInputs {
isMultisig := len(input.PubKeySignaturePairs) > 1
scriptBuilder := txscript.NewScriptBuilder()
if isMultisig {
signatureCount := 0
@@ -177,13 +213,18 @@ func extractTransaction(psTx *serialization.PartiallySignedTransaction) (*extern
return nil, errors.Errorf("missing %d signatures", input.MinimumSignatures-uint32(signatureCount))
}
scriptBuilder.AddData(input.RedeeemScript)
redeemScript, err := partiallySignedInputMultisigRedeemScript(input, ecdsa)
if err != nil {
return nil, err
}
scriptBuilder.AddData(redeemScript)
sigScript, err := scriptBuilder.Script()
if err != nil {
return nil, err
}
psTx.Tx.Inputs[i].SignatureScript = sigScript
partiallySignedTransaction.Tx.Inputs[i].SignatureScript = sigScript
} else {
if len(input.PubKeySignaturePairs) > 1 {
return nil, errors.Errorf("Cannot sign on P2PK when len(input.PubKeySignaturePairs) > 1")
@@ -199,8 +240,17 @@ func extractTransaction(psTx *serialization.PartiallySignedTransaction) (*extern
if err != nil {
return nil, err
}
psTx.Tx.Inputs[i].SignatureScript = sigScript
partiallySignedTransaction.Tx.Inputs[i].SignatureScript = sigScript
}
}
return psTx.Tx, nil
return partiallySignedTransaction.Tx, nil
}
func partiallySignedInputMultisigRedeemScript(input *serialization.PartiallySignedInput, ecdsa bool) ([]byte, error) {
extendedPublicKeys := make([]string, len(input.PubKeySignaturePairs))
for i, pair := range input.PubKeySignaturePairs {
extendedPublicKeys[i] = pair.ExtendedPublicKey
}
return multiSigRedeemScript(extendedPublicKeys, input.MinimumSignatures, "m", ecdsa)
}

View File

@@ -26,6 +26,7 @@ func forSchnorrAndECDSA(t *testing.T, testFunc func(t *testing.T, ecdsa bool)) {
func TestMultisig(t *testing.T) {
testutils.ForAllNets(t, true, func(t *testing.T, consensusConfig *consensus.Config) {
params := &consensusConfig.Params
forSchnorrAndECDSA(t, func(t *testing.T, ecdsa bool) {
consensusConfig.BlockCoinbaseMaturity = 0
tc, teardown, err := consensus.NewFactory().NewTestConsensus(consensusConfig, "TestMultisig")
@@ -35,17 +36,24 @@ func TestMultisig(t *testing.T) {
defer teardown(false)
const numKeys = 3
privateKeys := make([][]byte, numKeys)
publicKeys := make([][]byte, numKeys)
mnemonics := make([]string, numKeys)
publicKeys := make([]string, numKeys)
for i := 0; i < numKeys; i++ {
privateKeys[i], publicKeys[i], err = libkaspawallet.CreateKeyPair(ecdsa)
var err error
mnemonics[i], err = libkaspawallet.CreateMnemonic()
if err != nil {
t.Fatalf("CreateKeyPair: %+v", err)
t.Fatalf("CreateMnemonic: %+v", err)
}
publicKeys[i], err = libkaspawallet.MasterPublicKeyFromMnemonic(&consensusConfig.Params, mnemonics[i], true)
if err != nil {
t.Fatalf("MasterPublicKeyFromMnemonic: %+v", err)
}
}
const minimumSignatures = 2
address, err := libkaspawallet.Address(&consensusConfig.Params, publicKeys, minimumSignatures, ecdsa)
path := "m/1/2/3"
address, err := libkaspawallet.Address(params, publicKeys, minimumSignatures, path, ecdsa)
if err != nil {
t.Fatalf("Address: %+v", err)
}
@@ -81,15 +89,18 @@ func TestMultisig(t *testing.T) {
block1Tx := block1.Transactions[0]
block1TxOut := block1Tx.Outputs[0]
selectedUTXOs := []*externalapi.OutpointAndUTXOEntryPair{{
Outpoint: &externalapi.DomainOutpoint{
TransactionID: *consensushashing.TransactionID(block1.Transactions[0]),
Index: 0,
selectedUTXOs := []*libkaspawallet.UTXO{
{
Outpoint: &externalapi.DomainOutpoint{
TransactionID: *consensushashing.TransactionID(block1.Transactions[0]),
Index: 0,
},
UTXOEntry: utxo.NewUTXOEntry(block1TxOut.Value, block1TxOut.ScriptPublicKey, true, 0),
DerivationPath: path,
},
UTXOEntry: utxo.NewUTXOEntry(block1TxOut.Value, block1TxOut.ScriptPublicKey, true, 0),
}}
}
unsignedTransaction, err := libkaspawallet.CreateUnsignedTransaction(publicKeys, minimumSignatures, ecdsa,
unsignedTransaction, err := libkaspawallet.CreateUnsignedTransaction(publicKeys, minimumSignatures,
[]*libkaspawallet.Payment{{
Address: address,
Amount: 10,
@@ -107,14 +118,14 @@ func TestMultisig(t *testing.T) {
t.Fatalf("Transaction is not expected to be signed")
}
_, err = libkaspawallet.ExtractTransaction(unsignedTransaction)
_, err = libkaspawallet.ExtractTransaction(unsignedTransaction, ecdsa)
if err == nil || !strings.Contains(err.Error(), fmt.Sprintf("missing %d signatures", minimumSignatures)) {
t.Fatal("Unexpectedly succeed to extract a valid transaction out of unsigned transaction")
}
signedTxStep1, err := libkaspawallet.Sign(privateKeys[:1], unsignedTransaction, ecdsa)
signedTxStep1, err := libkaspawallet.Sign(params, mnemonics[:1], unsignedTransaction, ecdsa)
if err != nil {
t.Fatalf("IsTransactionFullySigned: %+v", err)
t.Fatalf("Sign: %+v", err)
}
isFullySigned, err = libkaspawallet.IsTransactionFullySigned(signedTxStep1)
@@ -126,22 +137,22 @@ func TestMultisig(t *testing.T) {
t.Fatalf("Transaction is not expected to be fully signed")
}
signedTxStep2, err := libkaspawallet.Sign(privateKeys[1:2], signedTxStep1, ecdsa)
signedTxStep2, err := libkaspawallet.Sign(params, mnemonics[1:2], signedTxStep1, ecdsa)
if err != nil {
t.Fatalf("Sign: %+v", err)
}
extractedSignedTxStep2, err := libkaspawallet.ExtractTransaction(signedTxStep2)
extractedSignedTxStep2, err := libkaspawallet.ExtractTransaction(signedTxStep2, ecdsa)
if err != nil {
t.Fatalf("ExtractTransaction: %+v", err)
}
signedTxOneStep, err := libkaspawallet.Sign(privateKeys[:2], unsignedTransaction, ecdsa)
signedTxOneStep, err := libkaspawallet.Sign(params, mnemonics[:2], unsignedTransaction, ecdsa)
if err != nil {
t.Fatalf("Sign: %+v", err)
}
extractedSignedTxOneStep, err := libkaspawallet.ExtractTransaction(signedTxOneStep)
extractedSignedTxOneStep, err := libkaspawallet.ExtractTransaction(signedTxOneStep, ecdsa)
if err != nil {
t.Fatalf("ExtractTransaction: %+v", err)
}
@@ -170,6 +181,7 @@ func TestMultisig(t *testing.T) {
func TestP2PK(t *testing.T) {
testutils.ForAllNets(t, true, func(t *testing.T, consensusConfig *consensus.Config) {
params := &consensusConfig.Params
forSchnorrAndECDSA(t, func(t *testing.T, ecdsa bool) {
consensusConfig.BlockCoinbaseMaturity = 0
tc, teardown, err := consensus.NewFactory().NewTestConsensus(consensusConfig, "TestMultisig")
@@ -179,17 +191,24 @@ func TestP2PK(t *testing.T) {
defer teardown(false)
const numKeys = 1
privateKeys := make([][]byte, numKeys)
publicKeys := make([][]byte, numKeys)
mnemonics := make([]string, numKeys)
publicKeys := make([]string, numKeys)
for i := 0; i < numKeys; i++ {
privateKeys[i], publicKeys[i], err = libkaspawallet.CreateKeyPair(ecdsa)
var err error
mnemonics[i], err = libkaspawallet.CreateMnemonic()
if err != nil {
t.Fatalf("CreateKeyPair: %+v", err)
t.Fatalf("CreateMnemonic: %+v", err)
}
publicKeys[i], err = libkaspawallet.MasterPublicKeyFromMnemonic(&consensusConfig.Params, mnemonics[i], false)
if err != nil {
t.Fatalf("MasterPublicKeyFromMnemonic: %+v", err)
}
}
const minimumSignatures = 1
address, err := libkaspawallet.Address(&consensusConfig.Params, publicKeys, minimumSignatures, ecdsa)
path := "m/1/2/3"
address, err := libkaspawallet.Address(params, publicKeys, minimumSignatures, path, ecdsa)
if err != nil {
t.Fatalf("Address: %+v", err)
}
@@ -231,16 +250,18 @@ func TestP2PK(t *testing.T) {
block1Tx := block1.Transactions[0]
block1TxOut := block1Tx.Outputs[0]
selectedUTXOs := []*externalapi.OutpointAndUTXOEntryPair{{
Outpoint: &externalapi.DomainOutpoint{
TransactionID: *consensushashing.TransactionID(block1.Transactions[0]),
Index: 0,
selectedUTXOs := []*libkaspawallet.UTXO{
{
Outpoint: &externalapi.DomainOutpoint{
TransactionID: *consensushashing.TransactionID(block1.Transactions[0]),
Index: 0,
},
UTXOEntry: utxo.NewUTXOEntry(block1TxOut.Value, block1TxOut.ScriptPublicKey, true, 0),
DerivationPath: path,
},
UTXOEntry: utxo.NewUTXOEntry(block1TxOut.Value, block1TxOut.ScriptPublicKey, true, 0),
}}
}
unsignedTransaction, err := libkaspawallet.CreateUnsignedTransaction(publicKeys, minimumSignatures,
ecdsa,
[]*libkaspawallet.Payment{{
Address: address,
Amount: 10,
@@ -258,17 +279,17 @@ func TestP2PK(t *testing.T) {
t.Fatalf("Transaction is not expected to be signed")
}
_, err = libkaspawallet.ExtractTransaction(unsignedTransaction)
_, err = libkaspawallet.ExtractTransaction(unsignedTransaction, ecdsa)
if err == nil || !strings.Contains(err.Error(), "missing signature") {
t.Fatal("Unexpectedly succeed to extract a valid transaction out of unsigned transaction")
}
signedTx, err := libkaspawallet.Sign(privateKeys, unsignedTransaction, ecdsa)
signedTx, err := libkaspawallet.Sign(params, mnemonics, unsignedTransaction, ecdsa)
if err != nil {
t.Fatalf("IsTransactionFullySigned: %+v", err)
t.Fatalf("Sign: %+v", err)
}
tx, err := libkaspawallet.ExtractTransaction(signedTx)
tx, err := libkaspawallet.ExtractTransaction(signedTx, ecdsa)
if err != nil {
t.Fatalf("ExtractTransaction: %+v", err)
}

View File

@@ -23,6 +23,8 @@ func main() {
err = showAddress(config.(*showAddressConfig))
case dumpUnencryptedDataSubCmd:
err = dumpUnencryptedData(config.(*dumpUnencryptedDataConfig))
case startDaemonSubCmd:
err = startDaemon(config.(*startDaemonConfig))
default:
err = errors.Errorf("Unknown sub-command '%s'\n", subCmd)
}

View File

@@ -1,18 +1,12 @@
package main
import (
"encoding/hex"
"context"
"fmt"
"github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/client"
"github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/pb"
"github.com/kaspanet/kaspad/cmd/kaspawallet/keys"
"github.com/kaspanet/kaspad/cmd/kaspawallet/libkaspawallet"
utxopkg "github.com/kaspanet/kaspad/domain/consensus/utils/utxo"
"github.com/kaspanet/kaspad/domain/dagconfig"
"github.com/kaspanet/kaspad/app/appmessage"
"github.com/kaspanet/kaspad/domain/consensus/model/externalapi"
"github.com/kaspanet/kaspad/domain/consensus/utils/transactionid"
"github.com/kaspanet/kaspad/infrastructure/network/rpcclient"
"github.com/kaspanet/kaspad/util"
"github.com/pkg/errors"
)
@@ -23,149 +17,49 @@ func send(conf *sendConfig) error {
return err
}
toAddress, err := util.DecodeAddress(conf.ToAddress, conf.ActiveNetParams.Prefix)
if err != nil {
return err
if len(keysFile.ExtendedPublicKeys) > len(keysFile.EncryptedMnemonics) {
return errors.Errorf("Cannot use 'send' command for multisig wallet without all of the keys")
}
fromAddress, err := libkaspawallet.Address(conf.NetParams(), keysFile.PublicKeys, keysFile.MinimumSignatures, keysFile.ECDSA)
daemonClient, tearDown, err := client.Connect(conf.DaemonAddress)
if err != nil {
return err
}
defer tearDown()
client, err := connectToRPC(conf.NetParams(), conf.RPCServer)
if err != nil {
return err
}
utxos, err := fetchSpendableUTXOs(conf.NetParams(), client, fromAddress.String())
if err != nil {
return err
}
ctx, cancel := context.WithTimeout(context.Background(), daemonTimeout)
defer cancel()
sendAmountSompi := uint64(conf.SendAmount * util.SompiPerKaspa)
const feePerInput = 1000
selectedUTXOs, changeSompi, err := selectUTXOs(utxos, sendAmountSompi, feePerInput)
createUnsignedTransactionResponse, err := daemonClient.CreateUnsignedTransaction(ctx, &pb.CreateUnsignedTransactionRequest{
Address: conf.ToAddress,
Amount: sendAmountSompi,
})
if err != nil {
return err
}
psTx, err := libkaspawallet.CreateUnsignedTransaction(keysFile.PublicKeys,
keysFile.MinimumSignatures,
keysFile.ECDSA,
[]*libkaspawallet.Payment{{
Address: toAddress,
Amount: sendAmountSompi,
}, {
Address: fromAddress,
Amount: changeSompi,
}},
selectedUTXOs)
mnemonics, err := keysFile.DecryptMnemonics(conf.Password)
if err != nil {
return err
}
privateKeys, err := keysFile.DecryptPrivateKeys()
signedTransaction, err := libkaspawallet.Sign(conf.NetParams(), mnemonics, createUnsignedTransactionResponse.UnsignedTransaction, keysFile.ECDSA)
if err != nil {
return err
}
updatedPSTx, err := libkaspawallet.Sign(privateKeys, psTx, keysFile.ECDSA)
if err != nil {
return err
}
tx, err := libkaspawallet.ExtractTransaction(updatedPSTx)
if err != nil {
return err
}
transactionID, err := sendTransaction(client, tx)
ctx2, cancel2 := context.WithTimeout(context.Background(), daemonTimeout)
defer cancel2()
broadcastResponse, err := daemonClient.Broadcast(ctx2, &pb.BroadcastRequest{
Transaction: signedTransaction,
})
if err != nil {
return err
}
fmt.Println("Transaction was sent successfully")
fmt.Printf("Transaction ID: \t%s\n", transactionID)
fmt.Printf("Transaction ID: \t%s\n", broadcastResponse.TxID)
return nil
}
func fetchSpendableUTXOs(params *dagconfig.Params, client *rpcclient.RPCClient, address string) ([]*appmessage.UTXOsByAddressesEntry, error) {
getUTXOsByAddressesResponse, err := client.GetUTXOsByAddresses([]string{address})
if err != nil {
return nil, err
}
virtualSelectedParentBlueScoreResponse, err := client.GetVirtualSelectedParentBlueScore()
if err != nil {
return nil, err
}
virtualSelectedParentBlueScore := virtualSelectedParentBlueScoreResponse.BlueScore
spendableUTXOs := make([]*appmessage.UTXOsByAddressesEntry, 0)
for _, entry := range getUTXOsByAddressesResponse.Entries {
if !isUTXOSpendable(entry, virtualSelectedParentBlueScore, params.BlockCoinbaseMaturity) {
continue
}
spendableUTXOs = append(spendableUTXOs, entry)
}
return spendableUTXOs, nil
}
func selectUTXOs(utxos []*appmessage.UTXOsByAddressesEntry, spendAmount uint64, feePerInput uint64) (
selectedUTXOs []*externalapi.OutpointAndUTXOEntryPair, changeSompi uint64, err error) {
selectedUTXOs = []*externalapi.OutpointAndUTXOEntryPair{}
totalValue := uint64(0)
for _, utxo := range utxos {
txID, err := transactionid.FromString(utxo.Outpoint.TransactionID)
if err != nil {
return nil, 0, err
}
rpcUTXOEntry := utxo.UTXOEntry
scriptPublicKeyScript, err := hex.DecodeString(rpcUTXOEntry.ScriptPublicKey.Script)
if err != nil {
return nil, 0, err
}
scriptPublicKey := &externalapi.ScriptPublicKey{
Script: scriptPublicKeyScript,
Version: rpcUTXOEntry.ScriptPublicKey.Version,
}
utxoEntry := utxopkg.NewUTXOEntry(rpcUTXOEntry.Amount, scriptPublicKey, rpcUTXOEntry.IsCoinbase, rpcUTXOEntry.BlockDAAScore)
selectedUTXOs = append(selectedUTXOs, &externalapi.OutpointAndUTXOEntryPair{
Outpoint: &externalapi.DomainOutpoint{
TransactionID: *txID,
Index: utxo.Outpoint.Index,
},
UTXOEntry: utxoEntry,
})
totalValue += utxo.UTXOEntry.Amount
fee := feePerInput * uint64(len(selectedUTXOs))
totalSpend := spendAmount + fee
if totalValue >= totalSpend {
break
}
}
fee := feePerInput * uint64(len(selectedUTXOs))
totalSpend := spendAmount + fee
if totalValue < totalSpend {
return nil, 0, errors.Errorf("Insufficient funds for send: %f required, while only %f available",
float64(totalSpend)/util.SompiPerKaspa, float64(totalValue)/util.SompiPerKaspa)
}
return selectedUTXOs, totalValue - totalSpend, nil
}
func sendTransaction(client *rpcclient.RPCClient, tx *externalapi.DomainTransaction) (string, error) {
submitTransactionResponse, err := client.SubmitTransaction(appmessage.DomainTransactionToRPCTransaction(tx))
if err != nil {
return "", errors.Wrapf(err, "error submitting transaction")
}
return submitTransactionResponse.TransactionID, nil
}

View File

@@ -1,23 +1,27 @@
package main
import (
"context"
"fmt"
"github.com/kaspanet/kaspad/cmd/kaspawallet/keys"
"github.com/kaspanet/kaspad/cmd/kaspawallet/libkaspawallet"
"github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/client"
"github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/pb"
)
func showAddress(conf *showAddressConfig) error {
keysFile, err := keys.ReadKeysFile(conf.NetParams(), conf.KeysFile)
daemonClient, tearDown, err := client.Connect(conf.DaemonAddress)
if err != nil {
return err
}
defer tearDown()
ctx, cancel := context.WithTimeout(context.Background(), daemonTimeout)
defer cancel()
response, err := daemonClient.GetReceiveAddress(ctx, &pb.GetReceiveAddressRequest{})
if err != nil {
return err
}
address, err := libkaspawallet.Address(conf.NetParams(), keysFile.PublicKeys, keysFile.MinimumSignatures, keysFile.ECDSA)
if err != nil {
return err
}
fmt.Printf("The wallet address is:\n%s\n", address)
fmt.Printf("Address:\n%s\n", response.Address)
return nil
}

View File

@@ -3,7 +3,6 @@ package main
import (
"encoding/hex"
"fmt"
"github.com/kaspanet/kaspad/cmd/kaspawallet/keys"
"github.com/kaspanet/kaspad/cmd/kaspawallet/libkaspawallet"
)
@@ -14,22 +13,22 @@ func sign(conf *signConfig) error {
return err
}
psTxBytes, err := hex.DecodeString(conf.Transaction)
partiallySignedTransaction, err := hex.DecodeString(conf.Transaction)
if err != nil {
return err
}
privateKeys, err := keysFile.DecryptPrivateKeys()
privateKeys, err := keysFile.DecryptMnemonics(conf.Password)
if err != nil {
return err
}
updatedPSTxBytes, err := libkaspawallet.Sign(privateKeys, psTxBytes, keysFile.ECDSA)
updatedPartiallySignedTransaction, err := libkaspawallet.Sign(conf.NetParams(), privateKeys, partiallySignedTransaction, keysFile.ECDSA)
if err != nil {
return err
}
isFullySigned, err := libkaspawallet.IsTransactionFullySigned(updatedPSTxBytes)
isFullySigned, err := libkaspawallet.IsTransactionFullySigned(updatedPartiallySignedTransaction)
if err != nil {
return err
}
@@ -40,6 +39,6 @@ func sign(conf *signConfig) error {
fmt.Println("Successfully signed transaction")
}
fmt.Printf("Transaction: %x\n", updatedPSTxBytes)
fmt.Printf("Transaction: %x\n", updatedPartiallySignedTransaction)
return nil
}

View File

@@ -0,0 +1,7 @@
package main
import "github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/server"
func startDaemon(conf *startDaemonConfig) error {
return server.Start(conf.NetParams(), conf.Listen, conf.RPCServer, conf.KeysFile)
}

View File

@@ -0,0 +1,18 @@
package utils
import (
"bufio"
"strings"
"github.com/pkg/errors"
)
// ReadLine reads one line from the given reader with trimmed white space.
func ReadLine(reader *bufio.Reader) (string, error) {
line, err := reader.ReadBytes('\n')
if err != nil {
return "", errors.WithStack(err)
}
return strings.TrimSpace(string(line)), nil
}

View File

@@ -112,6 +112,30 @@ func (s *consensus) GetBlock(blockHash *externalapi.DomainHash) (*externalapi.Do
return block, nil
}
func (s *consensus) GetBlockEvenIfHeaderOnly(blockHash *externalapi.DomainHash) (*externalapi.DomainBlock, error) {
s.lock.Lock()
defer s.lock.Unlock()
stagingArea := model.NewStagingArea()
block, err := s.blockStore.Block(s.databaseContext, stagingArea, blockHash)
if err == nil {
return block, nil
}
if !errors.Is(err, database.ErrNotFound) {
return nil, err
}
header, err := s.blockHeaderStore.BlockHeader(s.databaseContext, stagingArea, blockHash)
if err != nil {
if errors.Is(err, database.ErrNotFound) {
return nil, errors.Wrapf(err, "block %s does not exist", blockHash)
}
return nil, err
}
return &externalapi.DomainBlock{Header: header}, nil
}
func (s *consensus) GetBlockHeader(blockHash *externalapi.DomainHash) (externalapi.BlockHeader, error) {
s.lock.Lock()
defer s.lock.Unlock()
@@ -363,11 +387,17 @@ func (s *consensus) GetVirtualInfo() (*externalapi.VirtualInfo, error) {
return nil, err
}
daaScore, err := s.daaBlocksStore.DAAScore(s.databaseContext, stagingArea, model.VirtualBlockHash)
if err != nil {
return nil, err
}
return &externalapi.VirtualInfo{
ParentHashes: blockRelations.Parents,
Bits: bits,
PastMedianTime: pastMedianTime,
BlueScore: virtualGHOSTDAGData.BlueScore(),
DAAScore: daaScore,
}, nil
}
@@ -505,3 +535,10 @@ func (s *consensus) Anticone(blockHash *externalapi.DomainHash) ([]*externalapi.
return s.dagTraversalManager.Anticone(stagingArea, blockHash)
}
func (s *consensus) EstimateNetworkHashesPerSecond(startHash *externalapi.DomainHash, windowSize int) (uint64, error) {
s.lock.Lock()
defer s.lock.Unlock()
return s.difficultyManager.EstimateNetworkHashesPerSecond(startHash, windowSize)
}

View File

@@ -7,6 +7,7 @@ type Consensus interface {
ValidateTransactionAndPopulateWithConsensusData(transaction *DomainTransaction) error
GetBlock(blockHash *DomainHash) (*DomainBlock, error)
GetBlockEvenIfHeaderOnly(blockHash *DomainHash) (*DomainBlock, error)
GetBlockHeader(blockHash *DomainHash) (BlockHeader, error)
GetBlockInfo(blockHash *DomainHash) (*BlockInfo, error)
GetBlockRelations(blockHash *DomainHash) (parents []*DomainHash, selectedParent *DomainHash, children []*DomainHash, err error)
@@ -32,4 +33,5 @@ type Consensus interface {
IsInSelectedParentChainOf(blockHashA *DomainHash, blockHashB *DomainHash) (bool, error)
GetHeadersSelectedTip() (*DomainHash, error)
Anticone(blockHash *DomainHash) ([]*DomainHash, error)
EstimateNetworkHashesPerSecond(startHash *DomainHash, windowSize int) (uint64, error)
}

View File

@@ -6,4 +6,5 @@ type VirtualInfo struct {
Bits uint32
PastMedianTime int64
BlueScore uint64
DAAScore uint64
}

View File

@@ -67,13 +67,3 @@ func (bgd *BlockGHOSTDAGData) MergeSetReds() []*externalapi.DomainHash {
func (bgd *BlockGHOSTDAGData) BluesAnticoneSizes() map[externalapi.DomainHash]KType {
return bgd.bluesAnticoneSizes
}
// MergeSet returns the whole MergeSet of the block (equivalent to MergeSetBlues+MergeSetReds)
func (bgd *BlockGHOSTDAGData) MergeSet() []*externalapi.DomainHash {
mergeSet := make([]*externalapi.DomainHash, len(bgd.mergeSetBlues)+len(bgd.mergeSetReds))
copy(mergeSet, bgd.mergeSetBlues)
if len(bgd.mergeSetReds) > 0 {
copy(mergeSet[len(bgd.mergeSetBlues):], bgd.mergeSetReds)
}
return mergeSet
}

View File

@@ -1,10 +1,13 @@
package model
import "github.com/kaspanet/kaspad/domain/consensus/model/externalapi"
import (
"github.com/kaspanet/kaspad/domain/consensus/model/externalapi"
)
// DifficultyManager provides a method to resolve the
// difficulty value of a block
type DifficultyManager interface {
StageDAADataAndReturnRequiredDifficulty(stagingArea *StagingArea, blockHash *externalapi.DomainHash) (uint32, error)
RequiredDifficulty(stagingArea *StagingArea, blockHash *externalapi.DomainHash) (uint32, error)
EstimateNetworkHashesPerSecond(startHash *externalapi.DomainHash, windowSize int) (uint64, error)
}

View File

@@ -8,4 +8,5 @@ type GHOSTDAGManager interface {
ChooseSelectedParent(stagingArea *StagingArea, blockHashes ...*externalapi.DomainHash) (*externalapi.DomainHash, error)
Less(blockHashA *externalapi.DomainHash, ghostdagDataA *BlockGHOSTDAGData,
blockHashB *externalapi.DomainHash, ghostdagDataB *BlockGHOSTDAGData) bool
GetSortedMergeSet(stagingArea *StagingArea, current *externalapi.DomainHash) ([]*externalapi.DomainHash, error)
}

View File

@@ -147,7 +147,7 @@ func (v *blockValidator) checkMergeSizeLimit(stagingArea *model.StagingArea, has
return err
}
mergeSetSize := len(ghostdagData.MergeSet())
mergeSetSize := len(ghostdagData.MergeSetBlues()) + len(ghostdagData.MergeSetReds())
if uint64(mergeSetSize) > v.mergeSetSizeLimit {
return errors.Wrapf(ruleerrors.ErrViolatingMergeLimit,

View File

@@ -312,3 +312,7 @@ func (dm *mocDifficultyManager) StageDAADataAndReturnRequiredDifficulty(stagingA
return dm.testDifficulty, nil
}
func (dm *mocDifficultyManager) EstimateNetworkHashesPerSecond(startHash *externalapi.DomainHash, windowSize int) (uint64, error) {
return 0, nil
}

View File

@@ -39,8 +39,9 @@ func (c *coinbaseManager) ExpectedCoinbaseTransaction(stagingArea *model.Staging
}
txOuts := make([]*externalapi.DomainTransactionOutput, 0, len(ghostdagData.MergeSetBlues()))
for i, blue := range ghostdagData.MergeSetBlues() {
txOut, hasReward, err := c.coinbaseOutputForBlueBlock(stagingArea, blue, acceptanceData[i], daaAddedBlocksSet)
acceptanceDataMap := acceptanceDataFromArrayToMap(acceptanceData)
for _, blue := range ghostdagData.MergeSetBlues() {
txOut, hasReward, err := c.coinbaseOutputForBlueBlock(stagingArea, blue, acceptanceDataMap[*blue], daaAddedBlocksSet)
if err != nil {
return nil, err
}
@@ -120,10 +121,10 @@ func (c *coinbaseManager) coinbaseOutputForRewardFromRedBlocks(stagingArea *mode
ghostdagData *model.BlockGHOSTDAGData, acceptanceData externalapi.AcceptanceData, daaAddedBlocksSet hashset.HashSet,
coinbaseData *externalapi.DomainCoinbaseData) (*externalapi.DomainTransactionOutput, bool, error) {
acceptanceDataMap := acceptanceDataFromArrayToMap(acceptanceData)
totalReward := uint64(0)
mergeSetBluesCount := len(ghostdagData.MergeSetBlues())
for i, red := range ghostdagData.MergeSetReds() {
reward, err := c.calcMergedBlockReward(stagingArea, red, acceptanceData[mergeSetBluesCount+i], daaAddedBlocksSet)
for _, red := range ghostdagData.MergeSetReds() {
reward, err := c.calcMergedBlockReward(stagingArea, red, acceptanceDataMap[*red], daaAddedBlocksSet)
if err != nil {
return nil, false, err
}
@@ -141,6 +142,14 @@ func (c *coinbaseManager) coinbaseOutputForRewardFromRedBlocks(stagingArea *mode
}, true, nil
}
func acceptanceDataFromArrayToMap(acceptanceData externalapi.AcceptanceData) map[externalapi.DomainHash]*externalapi.BlockAcceptanceData {
acceptanceDataMap := make(map[externalapi.DomainHash]*externalapi.BlockAcceptanceData, len(acceptanceData))
for _, blockAcceptanceData := range acceptanceData {
acceptanceDataMap[*blockAcceptanceData.BlockHash] = blockAcceptanceData
}
return acceptanceDataMap
}
// calcBlockSubsidy returns the subsidy amount a block at the provided blue score
// should have. This is mainly used for determining how much the coinbase for
// newly generated blocks awards as well as validating the coinbase for blocks

View File

@@ -129,7 +129,8 @@ func (csm *consensusStateManager) calculateNewTips(
if err != nil {
return nil, err
}
log.Debugf("The current tips are: %s", currentTips)
log.Debugf("The number of tips is: %d", len(currentTips))
log.Tracef("The current tips are: %s", currentTips)
newTipParents, err := csm.dagTopologyManager.Parents(stagingArea, newTipHash)
if err != nil {
@@ -151,7 +152,8 @@ func (csm *consensusStateManager) calculateNewTips(
newTips = append(newTips, currentTip)
}
}
log.Debugf("The calculated new tips are: %s", newTips)
log.Debugf("The new number of tips is: %d", len(newTips))
log.Tracef("The new tips are: %s", newTips)
return newTips, nil
}

View File

@@ -61,8 +61,7 @@ func (csm *consensusStateManager) calculatePastUTXOAndAcceptanceDataWithSelected
}
log.Debugf("Applying blue blocks to the selected parent past UTXO of block %s", blockHash)
acceptanceData, utxoDiff, err := csm.applyMergeSetBlocks(
stagingArea, blockHash, selectedParentPastUTXO, blockGHOSTDAGData, daaScore)
acceptanceData, utxoDiff, err := csm.applyMergeSetBlocks(stagingArea, blockHash, selectedParentPastUTXO, daaScore)
if err != nil {
return nil, nil, nil, err
}
@@ -136,13 +135,16 @@ func (csm *consensusStateManager) restorePastUTXO(
}
func (csm *consensusStateManager) applyMergeSetBlocks(stagingArea *model.StagingArea, blockHash *externalapi.DomainHash,
selectedParentPastUTXODiff externalapi.UTXODiff, ghostdagData *model.BlockGHOSTDAGData, daaScore uint64) (
selectedParentPastUTXODiff externalapi.UTXODiff, daaScore uint64) (
externalapi.AcceptanceData, externalapi.MutableUTXODiff, error) {
log.Debugf("applyMergeSetBlocks start for block %s", blockHash)
defer log.Debugf("applyMergeSetBlocks end for block %s", blockHash)
mergeSetHashes := ghostdagData.MergeSet()
mergeSetHashes, err := csm.ghostdagManager.GetSortedMergeSet(stagingArea, blockHash)
if err != nil {
return nil, nil, err
}
log.Debugf("Merge set for block %s is %v", blockHash, mergeSetHashes)
mergeSetBlocks, err := csm.blockStore.Blocks(csm.databaseContext, stagingArea, mergeSetHashes)
if err != nil {
@@ -266,8 +268,7 @@ func (csm *consensusStateManager) maybeAcceptTransaction(stagingArea *model.Stag
return true, accumulatedMassAfter, nil
}
func (csm *consensusStateManager) checkTransactionMass(
transaction *externalapi.DomainTransaction, accumulatedMassBefore uint64) (
func (csm *consensusStateManager) checkTransactionMass(transaction *externalapi.DomainTransaction, accumulatedMassBefore uint64) (
isAccepted bool, accumulatedMassAfter uint64) {
transactionID := consensushashing.TransactionID(transaction)

View File

@@ -2,6 +2,7 @@ package consensusstatemanager
import (
"github.com/kaspanet/kaspad/infrastructure/logger"
"github.com/kaspanet/kaspad/util/math"
"github.com/pkg/errors"
"github.com/kaspanet/kaspad/domain/consensus/model"
@@ -34,7 +35,16 @@ func (csm *consensusStateManager) pickVirtualParents(stagingArea *model.StagingA
}
log.Debugf("The selected parent of the virtual is: %s", virtualSelectedParent)
candidates := candidatesHeap.ToSlice()
// Limit to maxBlockParents*3 candidates, that way we don't go over thousands of tips when the network isn't healthy.
// There's no specific reason for a factor of 3, and its not a consensus rule, just an estimation saying we probably
// don't want to consider and calculate 3 times the amount of candidates for the set of parents.
maxCandidates := int(csm.maxBlockParents) * 3
candidateAllocationSize := math.MinInt(maxCandidates, candidatesHeap.Len())
candidates := make([]*externalapi.DomainHash, 0, candidateAllocationSize)
for len(candidates) < maxCandidates && candidatesHeap.Len() > 0 {
candidates = append(candidates, candidatesHeap.Pop())
}
// prioritize half the blocks with highest blueWork and half with lowest, so the network will merge splits faster.
if len(candidates) >= int(csm.maxBlockParents) {
// We already have the selectedParent, so we're left with csm.maxBlockParents-1.
@@ -45,12 +55,6 @@ func (csm *consensusStateManager) pickVirtualParents(stagingArea *model.StagingA
end--
}
}
// Limit to maxBlockParents*3 candidates, that way we don't go over thousands of tips when the network isn't healthy.
// There's no specific reason for a factor of 3, and its not a consensus rule, just an estimation saying we probably
// don't want to consider and calculate 3 times the amount of candidates for the set of parents.
if len(candidates) > int(csm.maxBlockParents)*3 {
candidates = candidates[:int(csm.maxBlockParents)*3]
}
selectedVirtualParents := []*externalapi.DomainHash{virtualSelectedParent}
mergeSetSize := uint64(1) // starts counting from 1 because selectedParent is already in the mergeSet

View File

@@ -2,11 +2,12 @@ package consensusstatemanager_test
import (
"errors"
"github.com/kaspanet/kaspad/domain/consensus/utils/constants"
"github.com/kaspanet/kaspad/domain/consensus/utils/subnetworks"
"github.com/kaspanet/kaspad/domain/consensus/utils/utxo"
"testing"
"github.com/kaspanet/kaspad/domain/consensus/model"
"github.com/kaspanet/kaspad/domain/consensus/utils/constants"
"github.com/kaspanet/kaspad/domain/consensus/utils/subnetworks"
"github.com/kaspanet/kaspad/domain/consensus/ruleerrors"
@@ -157,14 +158,15 @@ func TestDoubleSpends(t *testing.T) {
})
}
// TestTransactionAcceptance checks that blue blocks transactions are favoured above
// red blocks transactions, and that the block reward is paid only for blue blocks.
// TestTransactionAcceptance checks that block transactions are accepted correctly when the merge set is sorted topologically.
// DAG diagram:
// genesis <- blockA <- blockB <- blockC <- ..(chain of k-blocks).. lastBlockInChain <- blockD <- blockE <- blockF
// ^ ^ |
// | redBlock <------------------------ blueChildOfRedBlock <--------------------
func TestTransactionAcceptance(t *testing.T) {
testutils.ForAllNets(t, true, func(t *testing.T, consensusConfig *consensus.Config) {
stagingArea := model.NewStagingArea()
consensusConfig.BlockCoinbaseMaturity = 0
factory := consensus.NewFactory()
testConsensus, teardown, err := factory.NewTestConsensus(consensusConfig, "TestTransactionAcceptance")
if err != nil {
@@ -172,221 +174,199 @@ func TestTransactionAcceptance(t *testing.T) {
}
defer teardown(false)
fundingBlock1Hash, _, err := testConsensus.AddBlock([]*externalapi.DomainHash{consensusConfig.GenesisHash}, nil, nil)
blockHashA, _, err := testConsensus.AddBlock([]*externalapi.DomainHash{consensusConfig.GenesisHash}, nil, nil)
if err != nil {
t.Fatalf("Error creating fundingBlock1: %+v", err)
t.Fatalf("Error creating blockA: %+v", err)
}
fundingBlock2Hash, _, err := testConsensus.AddBlock([]*externalapi.DomainHash{fundingBlock1Hash}, nil, nil)
blockHashB, _, err := testConsensus.AddBlock([]*externalapi.DomainHash{blockHashA}, nil, nil)
if err != nil {
t.Fatalf("Error creating fundingBlock2: %+v", err)
t.Fatalf("Error creating blockB: %+v", err)
}
// Generate fundingBlock3 to pay for fundingBlock2
fundingBlock3Hash, _, err := testConsensus.AddBlock([]*externalapi.DomainHash{fundingBlock2Hash}, nil, nil)
blockHashC, _, err := testConsensus.AddBlock([]*externalapi.DomainHash{blockHashB}, nil, nil)
if err != nil {
t.Fatalf("Error creating fundingBlock3: %+v", err)
t.Fatalf("Error creating blockC: %+v", err)
}
// Add a chain of K blocks above fundingBlock3 so we'll
// Add a chain of K blocks above blockC so we'll
// be able to mine a red block on top of it.
tipHash := fundingBlock3Hash
chainTipHash := blockHashC
for i := model.KType(0); i < consensusConfig.K; i++ {
var err error
tipHash, _, err = testConsensus.AddBlock([]*externalapi.DomainHash{tipHash}, nil, nil)
chainTipHash, _, err = testConsensus.AddBlock([]*externalapi.DomainHash{chainTipHash}, nil, nil)
if err != nil {
t.Fatalf("Error creating fundingBlock1: %+v", err)
t.Fatalf("Error creating a block: %+v", err)
}
}
fundingBlock2, err := testConsensus.GetBlock(fundingBlock2Hash)
lastBlockInChain := chainTipHash
blockC, err := testConsensus.GetBlock(blockHashC)
if err != nil {
t.Fatalf("Error getting fundingBlock: %+v", err)
t.Fatalf("Error getting blockC: %+v", err)
}
fundingTransaction1 := fundingBlock2.Transactions[transactionhelper.CoinbaseTransactionIndex]
fundingBlock3, err := testConsensus.GetBlock(fundingBlock3Hash)
fees := uint64(1)
transactionFromBlockC := blockC.Transactions[transactionhelper.CoinbaseTransactionIndex]
// transactionFromRedBlock is spending TransactionFromBlockC.
transactionFromRedBlock, err := testutils.CreateTransaction(transactionFromBlockC, fees)
if err != nil {
t.Fatalf("Error getting fundingBlock: %+v", err)
t.Fatalf("Error creating a transactionFromRedBlock: %+v", err)
}
fundingTransaction2 := fundingBlock3.Transactions[transactionhelper.CoinbaseTransactionIndex]
spendingTransaction1, err := testutils.CreateTransaction(fundingTransaction1, 1)
transactionFromRedBlockInput0UTXOEntry, err := testConsensus.ConsensusStateStore().
UTXOByOutpoint(testConsensus.DatabaseContext(), stagingArea, &transactionFromRedBlock.Inputs[0].PreviousOutpoint)
if err != nil {
t.Fatalf("Error creating spendingTransaction1: %+v", err)
t.Fatalf("Error getting UTXOEntry for transactionFromRedBlockInput: %s", err)
}
spendingTransaction1UTXOEntry, err := testConsensus.ConsensusStateStore().
UTXOByOutpoint(testConsensus.DatabaseContext(), stagingArea, &spendingTransaction1.Inputs[0].PreviousOutpoint)
if err != nil {
t.Fatalf("Error getting UTXOEntry for spendingTransaction1: %s", err)
}
spendingTransaction2, err := testutils.CreateTransaction(fundingTransaction2, 1)
if err != nil {
t.Fatalf("Error creating spendingTransaction1: %+v", err)
}
spendingTransaction2UTXOEntry, err := testConsensus.ConsensusStateStore().
UTXOByOutpoint(testConsensus.DatabaseContext(), stagingArea, &spendingTransaction2.Inputs[0].PreviousOutpoint)
if err != nil {
t.Fatalf("Error getting UTXOEntry for spendingTransaction2: %s", err)
}
redHash, _, err := testConsensus.AddBlock([]*externalapi.DomainHash{fundingBlock3Hash}, nil,
[]*externalapi.DomainTransaction{spendingTransaction1, spendingTransaction2})
redHash, _, err := testConsensus.AddBlock([]*externalapi.DomainHash{blockHashC}, nil,
[]*externalapi.DomainTransaction{transactionFromRedBlock})
if err != nil {
t.Fatalf("Error creating redBlock: %+v", err)
}
blueScriptPublicKey := &externalapi.ScriptPublicKey{Script: []byte{1}, Version: 0}
blueHash, _, err := testConsensus.AddBlock([]*externalapi.DomainHash{tipHash}, &externalapi.DomainCoinbaseData{
ScriptPublicKey: blueScriptPublicKey,
ExtraData: nil,
},
[]*externalapi.DomainTransaction{spendingTransaction1})
transactionFromBlueChildOfRedBlock, err := testutils.CreateTransaction(transactionFromRedBlock, fees)
if err != nil {
t.Fatalf("Error creating blue: %+v", err)
t.Fatalf("Error creating transactionFromBlueChildOfRedBlock: %+v", err)
}
// Mining two blocks so tipHash will definitely be the selected tip.
tipHash, _, err = testConsensus.AddBlock([]*externalapi.DomainHash{tipHash}, nil, nil)
transactionFromBlueChildOfRedBlockInput0UTXOEntry, err := testConsensus.ConsensusStateStore().
UTXOByOutpoint(testConsensus.DatabaseContext(), stagingArea, &transactionFromBlueChildOfRedBlock.Inputs[0].PreviousOutpoint)
if err != nil {
t.Fatalf("Error creating tip: %+v", err)
t.Fatalf("Error getting UTXOEntry for transactionFromBlueChildOfRedBlockInput: %s", err)
}
finalTipSelectedParentScriptPublicKey := &externalapi.ScriptPublicKey{Script: []byte{3}, Version: 0}
finalTipSelectedParentHash, _, err := testConsensus.AddBlock([]*externalapi.DomainHash{tipHash},
blueChildOfRedBlockScriptPublicKey := &externalapi.ScriptPublicKey{Script: []byte{3}, Version: 0}
// The blueChildOfRedBlock contains a transaction that spent an output from the red block.
hashBlueChildOfRedBlock, _, err := testConsensus.AddBlock([]*externalapi.DomainHash{lastBlockInChain, redHash},
&externalapi.DomainCoinbaseData{
ScriptPublicKey: finalTipSelectedParentScriptPublicKey,
ScriptPublicKey: blueChildOfRedBlockScriptPublicKey,
ExtraData: nil,
}, []*externalapi.DomainTransaction{transactionFromBlueChildOfRedBlock})
if err != nil {
t.Fatalf("Error creating blueChildOfRedBlock: %+v", err)
}
// K blocks minded between blockC and blockD.
blockHashD, _, err := testConsensus.AddBlock([]*externalapi.DomainHash{lastBlockInChain}, nil, nil)
if err != nil {
t.Fatalf("Error creating blockD : %+v", err)
}
blockEScriptPublicKey := &externalapi.ScriptPublicKey{Script: []byte{4}, Version: 0}
blockHashE, _, err := testConsensus.AddBlock([]*externalapi.DomainHash{blockHashD},
&externalapi.DomainCoinbaseData{
ScriptPublicKey: blockEScriptPublicKey,
ExtraData: nil,
}, nil)
if err != nil {
t.Fatalf("Error creating tip: %+v", err)
t.Fatalf("Error creating blockE: %+v", err)
}
finalTipScriptPublicKey := &externalapi.ScriptPublicKey{Script: []byte{4}, Version: 0}
finalTipHash, _, err := testConsensus.AddBlock([]*externalapi.DomainHash{finalTipSelectedParentHash, redHash, blueHash},
blockFScriptPublicKey := &externalapi.ScriptPublicKey{Script: []byte{5}, Version: 0}
blockHashF, _, err := testConsensus.AddBlock([]*externalapi.DomainHash{blockHashE, hashBlueChildOfRedBlock},
&externalapi.DomainCoinbaseData{
ScriptPublicKey: finalTipScriptPublicKey,
ScriptPublicKey: blockFScriptPublicKey,
ExtraData: nil,
},
nil)
}, nil)
if err != nil {
t.Fatalf("Error creating finalTip: %+v", err)
t.Fatalf("Error creating blockF: %+v", err)
}
acceptanceData, err := testConsensus.AcceptanceDataStore().Get(testConsensus.DatabaseContext(), stagingArea, finalTipHash)
acceptanceData, err := testConsensus.AcceptanceDataStore().Get(testConsensus.DatabaseContext(), stagingArea, blockHashF)
if err != nil {
t.Fatalf("Error getting acceptance data: %+v", err)
}
finalTipSelectedParent, err := testConsensus.GetBlock(finalTipSelectedParentHash)
blueChildOfRedBlock, err := testConsensus.GetBlock(hashBlueChildOfRedBlock)
if err != nil {
t.Fatalf("Error getting finalTipSelectedParent: %+v", err)
t.Fatalf("Error getting blueChildOfRedBlock: %+v", err)
}
blue, err := testConsensus.GetBlock(blueHash)
blockE, err := testConsensus.GetBlock(blockHashE)
if err != nil {
t.Fatalf("Error getting blue: %+v", err)
t.Fatalf("Error getting blockE: %+v", err)
}
red, err := testConsensus.GetBlock(redHash)
redBlock, err := testConsensus.GetBlock(redHash)
if err != nil {
t.Fatalf("Error getting red: %+v", err)
t.Fatalf("Error getting redBlock: %+v", err)
}
// We expect spendingTransaction1 to be accepted by the blue block and not by the red one, because
// blue blocks in the merge set should always be ordered before red blocks in the merge set.
// We also expect spendingTransaction2 to be accepted by the red because nothing conflicts it.
blockF, err := testConsensus.GetBlock(blockHashF)
if err != nil {
t.Fatalf("Error getting blockF: %+v", err)
}
updatedDAAScoreVirtualBlock := 25
//We expect the second transaction in the "blue block" (blueChildOfRedBlock) to be accepted because the merge set is ordered topologically
//and the red block is ordered topologically before the "blue block" so the input is known in the UTXOSet.
expectedAcceptanceData := externalapi.AcceptanceData{
{
BlockHash: finalTipSelectedParentHash,
BlockHash: blockHashE,
TransactionAcceptanceData: []*externalapi.TransactionAcceptanceData{
{
Transaction: finalTipSelectedParent.Transactions[0],
Transaction: blockE.Transactions[0],
Fee: 0,
IsAccepted: true,
TransactionInputUTXOEntries: []externalapi.UTXOEntry{},
},
},
},
{
BlockHash: blueHash,
TransactionAcceptanceData: []*externalapi.TransactionAcceptanceData{
{
Transaction: blue.Transactions[0],
Fee: 0,
IsAccepted: false,
TransactionInputUTXOEntries: []externalapi.UTXOEntry{},
},
{
Transaction: spendingTransaction1,
Fee: 1,
IsAccepted: true,
TransactionInputUTXOEntries: []externalapi.UTXOEntry{spendingTransaction1UTXOEntry},
},
},
},
{
BlockHash: redHash,
TransactionAcceptanceData: []*externalapi.TransactionAcceptanceData{
{
Transaction: red.Transactions[0],
{ //Coinbase transaction outputs are added to the UTXO-set only if they are in the selected parent chain,
// and this block isn't.
Transaction: redBlock.Transactions[0],
Fee: 0,
IsAccepted: false,
TransactionInputUTXOEntries: []externalapi.UTXOEntry{},
},
{
Transaction: spendingTransaction1,
Fee: 0,
IsAccepted: false,
TransactionInputUTXOEntries: []externalapi.UTXOEntry{},
},
{
Transaction: spendingTransaction2,
Fee: 1,
Transaction: redBlock.Transactions[1],
Fee: fees,
IsAccepted: true,
TransactionInputUTXOEntries: []externalapi.UTXOEntry{spendingTransaction2UTXOEntry},
TransactionInputUTXOEntries: []externalapi.UTXOEntry{transactionFromRedBlockInput0UTXOEntry},
},
},
},
{
BlockHash: hashBlueChildOfRedBlock,
TransactionAcceptanceData: []*externalapi.TransactionAcceptanceData{
{ //Coinbase transaction outputs are added to the UTXO-set only if they are in the selected parent chain,
// and this block isn't.
Transaction: blueChildOfRedBlock.Transactions[0],
Fee: 0,
IsAccepted: false,
TransactionInputUTXOEntries: []externalapi.UTXOEntry{},
},
{ // The DAAScore was calculated by the virtual block pov. The DAAScore has changed since more blocks were added to the DAG.
// So we will change the DAAScore in the UTXOEntryInput to the updated virtual DAAScore.
Transaction: blueChildOfRedBlock.Transactions[1],
Fee: fees,
IsAccepted: true,
TransactionInputUTXOEntries: []externalapi.UTXOEntry{
utxo.NewUTXOEntry(transactionFromBlueChildOfRedBlockInput0UTXOEntry.Amount(),
transactionFromBlueChildOfRedBlockInput0UTXOEntry.ScriptPublicKey(),
transactionFromBlueChildOfRedBlockInput0UTXOEntry.IsCoinbase(), uint64(updatedDAAScoreVirtualBlock))},
},
},
},
}
if !acceptanceData.Equal(expectedAcceptanceData) {
t.Fatalf("The acceptance data is not the expected acceptance data")
}
finalTip, err := testConsensus.GetBlock(finalTipHash)
if err != nil {
t.Fatalf("Error getting finalTip: %+v", err)
}
// We expect the coinbase transaction to pay reward for the selected parent, the
// blue block, and bestow the red block reward to the merging block.
// We expect the coinbase transaction to pay reward for the selected parent(block E), the
// blueChildOfRedBlock, and bestow the red block reward to the merging block.
expectedCoinbase := &externalapi.DomainTransaction{
Version: constants.MaxTransactionVersion,
Inputs: nil,
Outputs: []*externalapi.DomainTransactionOutput{
{
Value: 50 * constants.SompiPerKaspa,
ScriptPublicKey: finalTipSelectedParentScriptPublicKey,
ScriptPublicKey: blockEScriptPublicKey,
},
{
Value: 50*constants.SompiPerKaspa + 1, // testutils.CreateTransaction pays a fee of 1 sompi
ScriptPublicKey: blueScriptPublicKey,
Value: 50*constants.SompiPerKaspa + fees, // testutils.CreateTransaction pays fees
ScriptPublicKey: blueChildOfRedBlockScriptPublicKey,
},
{
Value: 50*constants.SompiPerKaspa + 1,
ScriptPublicKey: finalTipScriptPublicKey,
Value: 50*constants.SompiPerKaspa + fees, // testutils.CreateTransaction pays fees
ScriptPublicKey: blockFScriptPublicKey,
},
},
LockTime: 0,
SubnetworkID: subnetworks.SubnetworkIDCoinbase,
Gas: 0,
Payload: finalTip.Transactions[0].Payload,
Payload: blockF.Transactions[0].Payload,
}
if !finalTip.Transactions[transactionhelper.CoinbaseTransactionIndex].Equal(expectedCoinbase) {
if !blockF.Transactions[transactionhelper.CoinbaseTransactionIndex].Equal(expectedCoinbase) {
t.Fatalf("Unexpected coinbase transaction")
}
})

View File

@@ -31,7 +31,7 @@ func (dm *difficultyManager) getDifficultyBlock(
}
// blockWindow returns a blockWindow of the given size that contains the
// blocks in the past of startindNode, the sorting is unspecified.
// blocks in the past of startingNode, the sorting is unspecified.
// If the number of blocks in the past of startingNode is less then windowSize,
// the window will be padded by genesis blocks to achieve a size of windowSize.
func (dm *difficultyManager) blockWindow(stagingArea *model.StagingArea, startingNode *externalapi.DomainHash, windowSize int) (blockWindow,

View File

@@ -78,6 +78,9 @@ func (dm *difficultyManager) genesisBits(stagingArea *model.StagingArea) (uint32
func (dm *difficultyManager) StageDAADataAndReturnRequiredDifficulty(
stagingArea *model.StagingArea, blockHash *externalapi.DomainHash) (uint32, error) {
onEnd := logger.LogAndMeasureExecutionTime(log, "StageDAADataAndReturnRequiredDifficulty")
defer onEnd()
// Fetch window of dag.difficultyAdjustmentWindowSize + 1 so we can have dag.difficultyAdjustmentWindowSize block intervals
targetsWindow, windowHashes, err := dm.blockWindow(stagingArea, blockHash, dm.difficultyAdjustmentWindowSize+1)
if err != nil {
@@ -164,9 +167,13 @@ func (dm *difficultyManager) calculateDaaScoreAndAddedBlocks(stagingArea *model.
if err != nil {
return 0, nil, err
}
mergeSetLength := len(ghostdagData.MergeSetBlues()) + len(ghostdagData.MergeSetReds())
mergeSet := make(map[externalapi.DomainHash]struct{}, mergeSetLength)
for _, hash := range ghostdagData.MergeSetBlues() {
mergeSet[*hash] = struct{}{}
}
mergeSet := make(map[externalapi.DomainHash]struct{}, len(ghostdagData.MergeSet()))
for _, hash := range ghostdagData.MergeSet() {
for _, hash := range ghostdagData.MergeSetReds() {
mergeSet[*hash] = struct{}{}
}

View File

@@ -0,0 +1,74 @@
package difficultymanager
import (
"math/big"
"github.com/kaspanet/kaspad/domain/consensus/model"
"github.com/kaspanet/kaspad/domain/consensus/model/externalapi"
"github.com/kaspanet/kaspad/infrastructure/logger"
"github.com/pkg/errors"
)
func (dm *difficultyManager) EstimateNetworkHashesPerSecond(startHash *externalapi.DomainHash, windowSize int) (uint64, error) {
onEnd := logger.LogAndMeasureExecutionTime(log, "EstimateNetworkHashesPerSecond")
defer onEnd()
stagingArea := model.NewStagingArea()
return dm.estimateNetworkHashesPerSecond(stagingArea, startHash, windowSize)
}
func (dm *difficultyManager) estimateNetworkHashesPerSecond(stagingArea *model.StagingArea,
startHash *externalapi.DomainHash, windowSize int) (uint64, error) {
const minWindowSize = 1000
if windowSize < minWindowSize {
return 0, errors.Errorf("windowSize must be equal to or greater than %d", minWindowSize)
}
blockWindow, windowHashes, err := dm.blockWindow(stagingArea, startHash, windowSize)
if err != nil {
return 0, err
}
// return 0 if no blocks had been mined yet
if len(windowHashes) == 0 {
return 0, nil
}
minWindowTimestamp, maxWindowTimestamp, _, _ := blockWindow.minMaxTimestamps()
if minWindowTimestamp == maxWindowTimestamp {
return 0, errors.Errorf("min window timestamp is equal to the max window timestamp")
}
firstHash := windowHashes[0]
firstBlockGHOSTDAGData, err := dm.ghostdagStore.Get(dm.databaseContext, stagingArea, firstHash)
if err != nil {
return 0, err
}
firstBlockBlueWork := firstBlockGHOSTDAGData.BlueWork()
minWindowBlueWork := firstBlockBlueWork
maxWindowBlueWork := firstBlockBlueWork
for _, hash := range windowHashes[1:] {
blockGHOSTDAGData, err := dm.ghostdagStore.Get(dm.databaseContext, stagingArea, hash)
if err != nil {
return 0, err
}
blockBlueWork := blockGHOSTDAGData.BlueWork()
if blockBlueWork.Cmp(minWindowBlueWork) < 0 {
minWindowBlueWork = blockBlueWork
}
if blockBlueWork.Cmp(maxWindowBlueWork) > 0 {
maxWindowBlueWork = blockBlueWork
}
}
windowsDiff := (maxWindowTimestamp - minWindowTimestamp) / 1000 // Divided by 1000 to convert milliseconds to seconds
if windowsDiff == 0 {
return 0, nil
}
nominator := new(big.Int).Sub(maxWindowBlueWork, minWindowBlueWork)
denominator := big.NewInt(windowsDiff)
networkHashesPerSecondBigInt := new(big.Int).Div(nominator, denominator)
return networkHashesPerSecondBigInt.Uint64(), nil
}

View File

@@ -409,3 +409,7 @@ func (gh *ghostdagHelper) ChooseSelectedParent(stagingArea *model.StagingArea, b
func (gh *ghostdagHelper) Less(blockHashA *externalapi.DomainHash, ghostdagDataA *model.BlockGHOSTDAGData, blockHashB *externalapi.DomainHash, ghostdagDataB *model.BlockGHOSTDAGData) bool {
panic("implement me")
}
func (gh *ghostdagHelper) GetSortedMergeSet(*model.StagingArea, *externalapi.DomainHash) ([]*externalapi.DomainHash, error) {
panic("implement me")
}

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