Compare commits

...

15 Commits

Author SHA1 Message Date
tal
16a83f9bac Initialize the selcted parent on GHOSTDAG. 2021-04-08 14:27:22 +03:00
tal
7d20ee6b58 Adds a comment to exported function (GHOSTDAG). 2021-04-08 14:10:03 +03:00
tal
1ab05c3fbc Redesign the code of GHOSTDAG function. 2021-04-08 14:04:28 +03:00
Svarog
347dd8fc4b Support resolveBlockStatus without separate stagingAreas for usage of testConsensus (#1666) 2021-04-08 11:26:17 +03:00
Ori Newman
d2cccd2829 Add ECDSA support to the wallet (#1664)
* Add ECDSA support to the wallet

* Fix genkeypair

* Fix typo and rename var
2021-04-06 17:25:09 +03:00
Ori Newman
7186f83095 Add OpCheckMultiSigECDSA (#1663)
Co-authored-by: stasatdaglabs <39559713+stasatdaglabs@users.noreply.github.com>
2021-04-06 16:29:16 +03:00
Ori Newman
5c394c2951 Add PubKeyECDSATy (#1662) 2021-04-06 15:56:31 +03:00
Ori Newman
a786cdc15e Add ECDSA support (#1657)
* Add ECDSA support

* Add domain separation to ECDSA sighash

* Use InfallibleWrite instead of Write

* Rename funcs

* Fix wrong use if vm.sigCache

* Add TestCalculateSignatureHashECDSA

* Add consts

* Fix comment and test name

* Move consts to the top

* Fix comment
2021-04-06 14:27:18 +03:00
Ori Newman
6dd3d4a9e7 Add dump unencrypted data sub command to the wallet (#1661) 2021-04-06 12:29:13 +03:00
stasatdaglabs
73b36f12f0 Implement importing private keys into the wallet (#1655)
* Implement importing private keys into the wallet.

* Fix bad --import default.

* Fix typo in --import annotation.

* Make go lint happy.

* Make go lint happier.

Co-authored-by: Ori Newman <orinewman1@gmail.com>
2021-04-05 18:10:33 +03:00
stasatdaglabs
a795a9e619 Add a size limit to the address manager (#1652)
* Remove a random address from the address manager if it's full.

* Implement TestOverfillAddressManager.

* Add connectionFailedCount to addresses.

* Mark connection failures.

* Mark connection successes.

* Implement removing by most connection failures.

* Expand TestOverfillAddressManager.

* Add comments.

* Use a better method for finding the address with the greatest connectionFailedCount.

* Fix a comment.

* Compare addresses by IP in TestOverfillAddressManager.

* Add a comment for updateNotBanned.

Co-authored-by: Ori Newman <orinewman1@gmail.com>
2021-04-05 17:56:13 +03:00
Ori Newman
0be1bba408 Fix TestAddresses (#1656) 2021-04-05 16:24:22 +03:00
Ori Newman
6afc06ce58 Replace p2pkh with p2pk (#1650)
* Replace p2pkh with p2pk

* Fix tests

* Fix comments and variable names

* Add README.md for genkeypair

* Rename pubkey->publicKey

* Rename p2pkh to p2pk

* Use util.PublicKeySize where needed

* Remove redundant pointer

* Fix comment

* Rename pubKey->publicKey
2021-04-05 14:35:34 +03:00
stasatdaglabs
d01a213f3d Add a show-address subcommand to kaspawallet (#1653)
* Add a show-address subcommand to kaspawallet.

* Update the description of the key-file command line parameter.
2021-04-05 14:22:03 +03:00
stasatdaglabs
7ad8ce521c Implement reconnection logic within the RPC client (#1643)
* Add a reconnect mechanism to RPCClient.

* Fix Reconnect().

* Connect the internal reconnection logic to the miner reconnection logic.

* Rename shouldReconnect to isClosed.

* Move safe reconnection logic from the miner to rpcclient.

* Remove sleep from HandleSubmitBlock.

* Properly handle client errors and only disconnect if we're already connected.

* Make go lint happy.

Co-authored-by: Ori Newman <orinewman1@gmail.com>
2021-04-05 13:57:28 +03:00
69 changed files with 2427 additions and 1006 deletions

9
cmd/genkeypair/README.md Normal file
View File

@@ -0,0 +1,9 @@
genkeypair
========
A tool for generating private-key-address pairs.
Note: This tool prints unencrypted private keys and is not recommended for day
to day use, and is intended mainly for tests.
In order to manage your funds it's recommended to use [kaspawallet](../kaspawallet)

26
cmd/genkeypair/config.go Normal file
View File

@@ -0,0 +1,26 @@
package main
import (
"github.com/jessevdk/go-flags"
"github.com/kaspanet/kaspad/infrastructure/config"
)
type configFlags struct {
config.NetworkFlags
}
func parseConfig() (*configFlags, error) {
cfg := &configFlags{}
parser := flags.NewParser(cfg, flags.PrintErrors|flags.HelpFlag)
_, err := parser.Parse()
if err != nil {
return nil, err
}
err = cfg.ResolveNetwork(parser)
if err != nil {
return nil, err
}
return cfg, nil
}

27
cmd/genkeypair/main.go Normal file
View File

@@ -0,0 +1,27 @@
package main
import (
"fmt"
"github.com/kaspanet/kaspad/cmd/kaspawallet/libkaspawallet"
"github.com/kaspanet/kaspad/util"
)
func main() {
cfg, err := parseConfig()
if err != nil {
panic(err)
}
privateKey, publicKey, err := libkaspawallet.CreateKeyPair(false)
if err != nil {
panic(err)
}
addr, err := util.NewAddressPublicKey(publicKey, cfg.NetParams().Prefix)
if err != nil {
panic(err)
}
fmt.Printf("Private key: %x\n", privateKey)
fmt.Printf("Address: %s\n", addr)
}

View File

@@ -5,72 +5,32 @@ import (
"github.com/kaspanet/kaspad/infrastructure/logger"
"github.com/kaspanet/kaspad/infrastructure/network/rpcclient"
"github.com/pkg/errors"
"sync"
"sync/atomic"
"time"
)
const minerTimeout = 10 * time.Second
type minerClient struct {
isReconnecting uint32
clientLock sync.RWMutex
rpcClient *rpcclient.RPCClient
*rpcclient.RPCClient
cfg *configFlags
blockAddedNotificationChan chan struct{}
}
func (mc *minerClient) safeRPCClient() *rpcclient.RPCClient {
mc.clientLock.RLock()
defer mc.clientLock.RUnlock()
return mc.rpcClient
}
func (mc *minerClient) reconnect() {
swapped := atomic.CompareAndSwapUint32(&mc.isReconnecting, 0, 1)
if !swapped {
return
}
defer atomic.StoreUint32(&mc.isReconnecting, 0)
mc.clientLock.Lock()
defer mc.clientLock.Unlock()
retryDuration := time.Second
const maxRetryDuration = time.Minute
log.Infof("Reconnecting RPC connection")
for {
err := mc.connect()
if err == nil {
return
}
if retryDuration < time.Minute {
retryDuration *= 2
} else {
retryDuration = maxRetryDuration
}
log.Errorf("Got error '%s' while reconnecting. Trying again in %s", err, retryDuration)
time.Sleep(retryDuration)
}
}
func (mc *minerClient) connect() error {
rpcAddress, err := mc.cfg.NetParams().NormalizeRPCServerAddress(mc.cfg.RPCServer)
if err != nil {
return err
}
mc.rpcClient, err = rpcclient.NewRPCClient(rpcAddress)
rpcClient, err := rpcclient.NewRPCClient(rpcAddress)
if err != nil {
return err
}
mc.rpcClient.SetTimeout(minerTimeout)
mc.rpcClient.SetLogger(backendLog, logger.LevelTrace)
mc.RPCClient = rpcClient
mc.SetTimeout(minerTimeout)
mc.SetLogger(backendLog, logger.LevelTrace)
err = mc.rpcClient.RegisterForBlockAddedNotifications(func(_ *appmessage.BlockAddedNotificationMessage) {
err = mc.RegisterForBlockAddedNotifications(func(_ *appmessage.BlockAddedNotificationMessage) {
select {
case mc.blockAddedNotificationChan <- struct{}{}:
default:

View File

@@ -40,7 +40,7 @@ func main() {
if err != nil {
panic(errors.Wrap(err, "error connecting to the RPC server"))
}
defer client.safeRPCClient().Disconnect()
defer client.Disconnect()
miningAddr, err := util.DecodeAddress(cfg.MiningAddr, cfg.ActiveNetParams.Prefix)
if err != nil {

View File

@@ -114,13 +114,17 @@ func logHashRate() {
func handleFoundBlock(client *minerClient, block *externalapi.DomainBlock) error {
blockHash := consensushashing.BlockHash(block)
log.Infof("Submitting block %s to %s", blockHash, client.safeRPCClient().Address())
log.Infof("Submitting block %s to %s", blockHash, client.Address())
rejectReason, err := client.safeRPCClient().SubmitBlock(block)
rejectReason, err := client.SubmitBlock(block)
if err != nil {
if nativeerrors.Is(err, router.ErrTimeout) {
log.Warnf("Got timeout while submitting block %s to %s: %s", blockHash, client.safeRPCClient().Address(), err)
client.reconnect()
log.Warnf("Got timeout while submitting block %s to %s: %s", blockHash, client.Address(), err)
return client.Reconnect()
}
if nativeerrors.Is(err, router.ErrRouteClosed) {
log.Debugf("Got route is closed while requesting block template from %s. "+
"The client is most likely reconnecting", client.Address())
return nil
}
if rejectReason == appmessage.RejectReasonIsInIBD {
@@ -129,7 +133,7 @@ func handleFoundBlock(client *minerClient, block *externalapi.DomainBlock) error
time.Sleep(waitTime)
return nil
}
return errors.Wrapf(err, "Error submitting block %s to %s", blockHash, client.safeRPCClient().Address())
return errors.Wrapf(err, "Error submitting block %s to %s", blockHash, client.Address())
}
return nil
}
@@ -188,19 +192,27 @@ func getBlockForMining(mineWhenNotSynced bool) *externalapi.DomainBlock {
func templatesLoop(client *minerClient, miningAddr util.Address, errChan chan error) {
getBlockTemplate := func() {
template, err := client.safeRPCClient().GetBlockTemplate(miningAddr.String())
template, err := client.GetBlockTemplate(miningAddr.String())
if nativeerrors.Is(err, router.ErrTimeout) {
log.Warnf("Got timeout while requesting block template from %s: %s", client.safeRPCClient().Address(), err)
client.reconnect()
log.Warnf("Got timeout while requesting block template from %s: %s", client.Address(), err)
reconnectErr := client.Reconnect()
if reconnectErr != nil {
errChan <- reconnectErr
}
return
}
if nativeerrors.Is(err, router.ErrRouteClosed) {
log.Debugf("Got route is closed while requesting block template from %s. "+
"The client is most likely reconnecting", client.Address())
return
}
if err != nil {
errChan <- errors.Wrapf(err, "Error getting block template from %s", client.safeRPCClient().Address())
errChan <- errors.Wrapf(err, "Error getting block template from %s", client.Address())
return
}
err = templatemanager.Set(template)
if err != nil {
errChan <- errors.Wrapf(err, "Error setting block template from %s", client.safeRPCClient().Address())
errChan <- errors.Wrapf(err, "Error setting block template from %s", client.Address())
return
}
}

View File

@@ -18,7 +18,7 @@ func balance(conf *balanceConfig) error {
return err
}
addr, err := libkaspawallet.Address(conf.NetParams(), keysFile.PublicKeys, keysFile.MinimumSignatures)
addr, err := libkaspawallet.Address(conf.NetParams(), keysFile.PublicKeys, keysFile.MinimumSignatures, keysFile.ECDSA)
if err != nil {
return err
}

View File

@@ -12,9 +12,11 @@ const (
createSubCmd = "create"
balanceSubCmd = "balance"
sendSubCmd = "send"
createUnsignedTransactionSubCmd = "createUnsignedTransaction"
createUnsignedTransactionSubCmd = "create-unsigned-transaction"
signSubCmd = "sign"
broadcastSubCmd = "broadcast"
showAddressSubCmd = "show-address"
dumpUnencryptedDataSubCmd = "dump-unencrypted-data"
)
type configFlags struct {
@@ -22,21 +24,23 @@ type configFlags struct {
}
type createConfig struct {
KeysFile string `long:"keys-file" short:"f" description:"Keys file location (only if different from default)"`
KeysFile string `long:"keys-file" short:"f" description:"Keys file location (default: ~/.kaspawallet/keys.json (*nix), %USERPROFILE%\\AppData\\Local\\Kaspawallet\\key.json (Windows))"`
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 to generate" 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"`
ECDSA bool `long:"ecdsa" description:"Create an ECDSA wallet"`
Import bool `long:"import" short:"i" description:"Import private keys (as opposed to generating them)"`
config.NetworkFlags
}
type balanceConfig struct {
KeysFile string `long:"keys-file" short:"f" description:"Keys file location (only if different from default)"`
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"`
config.NetworkFlags
}
type sendConfig struct {
KeysFile string `long:"keys-file" short:"f" description:"Keys file location (only if different from default)"`
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"`
@@ -44,7 +48,7 @@ type sendConfig struct {
}
type createUnsignedTransactionConfig struct {
KeysFile string `long:"keys-file" short:"f" description:"Keys file location (only if different from default)"`
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"`
@@ -52,7 +56,7 @@ type createUnsignedTransactionConfig struct {
}
type signConfig struct {
KeysFile string `long:"keys-file" short:"f" description:"Keys file location (only if different from default)"`
KeysFile string `long:"keys-file" short:"f" description:"Keys file location (default: ~/.kaspawallet/keys.json (*nix), %USERPROFILE%\\AppData\\Local\\Kaspawallet\\key.json (Windows))"`
Transaction string `long:"transaction" short:"t" description:"The unsigned transaction to sign on (encoded in hex)" required:"true"`
config.NetworkFlags
}
@@ -63,6 +67,16 @@ type broadcastConfig struct {
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))"`
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))"`
config.NetworkFlags
}
func parseCommandLine() (subCommand string, config interface{}) {
cfg := &configFlags{}
parser := flags.NewParser(cfg, flags.PrintErrors|flags.HelpFlag)
@@ -91,6 +105,15 @@ func parseCommandLine() (subCommand string, config interface{}) {
parser.AddCommand(broadcastSubCmd, "Broadcast the given transaction",
"Broadcast the given transaction", broadcastConf)
showAddressConf := &showAddressConfig{}
parser.AddCommand(showAddressSubCmd, "Shows the public address of the current wallet",
"Shows the public address of the current wallet", showAddressConf)
dumpUnencryptedDataConf := &dumpUnencryptedDataConfig{}
parser.AddCommand(dumpUnencryptedDataSubCmd, "Prints the unencrypted wallet data",
"Prints the unencrypted wallet data including its private keys. Anyone that sees it can access "+
"the funds. Use only on safe environment.", dumpUnencryptedDataConf)
_, err := parser.Parse()
if err != nil {
@@ -146,6 +169,20 @@ func parseCommandLine() (subCommand string, config interface{}) {
printErrorAndExit(err)
}
config = broadcastConf
case showAddressSubCmd:
combineNetworkFlags(&showAddressConf.NetworkFlags, &cfg.NetworkFlags)
err := showAddressConf.ResolveNetwork(parser)
if err != nil {
printErrorAndExit(err)
}
config = showAddressConf
case dumpUnencryptedDataSubCmd:
combineNetworkFlags(&dumpUnencryptedDataConf.NetworkFlags, &cfg.NetworkFlags)
err := dumpUnencryptedDataConf.ResolveNetwork(parser)
if err != nil {
printErrorAndExit(err)
}
config = dumpUnencryptedDataConf
}
return parser.Command.Active.Name, config

View File

@@ -11,7 +11,14 @@ import (
)
func create(conf *createConfig) error {
encryptedPrivateKeys, publicKeys, err := keys.CreateKeyPairs(conf.NumPrivateKeys)
var encryptedPrivateKeys []*keys.EncryptedPrivateKey
var publicKeys [][]byte
var err error
if !conf.Import {
encryptedPrivateKeys, publicKeys, err = keys.CreateKeyPairs(conf.NumPrivateKeys, conf.ECDSA)
} else {
encryptedPrivateKeys, publicKeys, err = keys.ImportKeyPairs(conf.NumPrivateKeys)
}
if err != nil {
return err
}
@@ -42,7 +49,7 @@ func create(conf *createConfig) error {
publicKeys = append(publicKeys, publicKey)
}
err = keys.WriteKeysFile(conf.KeysFile, encryptedPrivateKeys, publicKeys, conf.MinimumSignatures)
err = keys.WriteKeysFile(conf.KeysFile, encryptedPrivateKeys, publicKeys, conf.MinimumSignatures, conf.ECDSA)
if err != nil {
return err
}
@@ -52,7 +59,7 @@ func create(conf *createConfig) error {
return err
}
addr, err := libkaspawallet.Address(conf.NetParams(), keysFile.PublicKeys, keysFile.MinimumSignatures)
addr, err := libkaspawallet.Address(conf.NetParams(), keysFile.PublicKeys, keysFile.MinimumSignatures, keysFile.ECDSA)
if err != nil {
return err
}

View File

@@ -19,7 +19,7 @@ func createUnsignedTransaction(conf *createUnsignedTransactionConfig) error {
return err
}
fromAddress, err := libkaspawallet.Address(conf.NetParams(), keysFile.PublicKeys, keysFile.MinimumSignatures)
fromAddress, err := libkaspawallet.Address(conf.NetParams(), keysFile.PublicKeys, keysFile.MinimumSignatures, keysFile.ECDSA)
if err != nil {
return err
}
@@ -41,13 +41,16 @@ func createUnsignedTransaction(conf *createUnsignedTransactionConfig) error {
return err
}
psTx, err := libkaspawallet.CreateUnsignedTransaction(keysFile.PublicKeys, keysFile.MinimumSignatures, []*libkaspawallet.Payment{{
Address: toAddress,
Amount: sendAmountSompi,
}, {
Address: fromAddress,
Amount: changeSompi,
}}, selectedUTXOs)
psTx, err := libkaspawallet.CreateUnsignedTransaction(keysFile.PublicKeys,
keysFile.MinimumSignatures,
keysFile.ECDSA,
[]*libkaspawallet.Payment{{
Address: toAddress,
Amount: sendAmountSompi,
}, {
Address: fromAddress,
Amount: changeSompi,
}}, selectedUTXOs)
if err != nil {
return err
}

View File

@@ -0,0 +1,69 @@
package main
import (
"bufio"
"fmt"
"github.com/kaspanet/kaspad/cmd/kaspawallet/keys"
"github.com/kaspanet/kaspad/cmd/kaspawallet/libkaspawallet"
"github.com/pkg/errors"
"os"
)
func dumpUnencryptedData(conf *dumpUnencryptedDataConfig) error {
err := confirmDump()
if err != nil {
return err
}
keysFile, err := keys.ReadKeysFile(conf.KeysFile)
if err != nil {
return err
}
privateKeys, err := keysFile.DecryptPrivateKeys()
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)
if err != nil {
return err
}
privateKeysPublicKeys[string(publicKey)] = struct{}{}
}
i := 1
for _, publicKey := range keysFile.PublicKeys {
if _, exists := privateKeysPublicKeys[string(publicKey)]; exists {
continue
}
fmt.Printf("Public key #%d:\n%x\n\n", i, publicKey)
i++
}
fmt.Printf("Minimum number of signatures: %d\n", keysFile.MinimumSignatures)
return nil
}
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()
if err != nil {
return err
}
fmt.Println()
if isPrefix || string(line) != "y" {
return errors.Errorf("Dump aborted by user")
}
return nil
}

View File

@@ -1,14 +1,52 @@
package keys
import (
"bufio"
"crypto/rand"
"crypto/subtle"
"encoding/hex"
"fmt"
"github.com/kaspanet/kaspad/cmd/kaspawallet/libkaspawallet"
"github.com/pkg/errors"
"os"
)
// CreateKeyPairs generates `numKeys` number of key pairs.
func CreateKeyPairs(numKeys uint32) (encryptedPrivateKeys []*EncryptedPrivateKey, publicKeys [][]byte, err error) {
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()
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:")
@@ -18,7 +56,7 @@ func CreateKeyPairs(numKeys uint32) (encryptedPrivateKeys []*EncryptedPrivateKey
encryptedPrivateKeys = make([]*EncryptedPrivateKey, 0, numKeys)
for i := uint32(0); i < numKeys; i++ {
privateKey, publicKey, err := libkaspawallet.CreateKeyPair()
privateKey, publicKey, err := keyPairFunction(i)
if err != nil {
return nil, nil, err
}

View File

@@ -29,6 +29,7 @@ type keysFileJSON struct {
EncryptedPrivateKeys []*encryptedPrivateKeyJSON `json:"encryptedPrivateKeys"`
PublicKeys []string `json:"publicKeys"`
MinimumSignatures uint32 `json:"minimumSignatures"`
ECDSA bool `json:"ecdsa"`
}
// EncryptedPrivateKey represents an encrypted private key
@@ -42,6 +43,7 @@ type Data struct {
encryptedPrivateKeys []*EncryptedPrivateKey
PublicKeys [][]byte
MinimumSignatures uint32
ECDSA bool
}
func (d *Data) toJSON() *keysFileJSON {
@@ -62,14 +64,16 @@ func (d *Data) toJSON() *keysFileJSON {
EncryptedPrivateKeys: encryptedPrivateKeysJSON,
PublicKeys: publicKeysHex,
MinimumSignatures: d.MinimumSignatures,
ECDSA: d.ECDSA,
}
}
func (d *Data) fromJSON(kfj *keysFileJSON) error {
d.MinimumSignatures = kfj.MinimumSignatures
func (d *Data) fromJSON(fileJSON *keysFileJSON) error {
d.MinimumSignatures = fileJSON.MinimumSignatures
d.ECDSA = fileJSON.ECDSA
d.encryptedPrivateKeys = make([]*EncryptedPrivateKey, len(kfj.EncryptedPrivateKeys))
for i, encryptedPrivateKeyJSON := range kfj.EncryptedPrivateKeys {
d.encryptedPrivateKeys = make([]*EncryptedPrivateKey, len(fileJSON.EncryptedPrivateKeys))
for i, encryptedPrivateKeyJSON := range fileJSON.EncryptedPrivateKeys {
cipher, err := hex.DecodeString(encryptedPrivateKeyJSON.Cipher)
if err != nil {
return err
@@ -86,8 +90,8 @@ func (d *Data) fromJSON(kfj *keysFileJSON) error {
}
}
d.PublicKeys = make([][]byte, len(kfj.PublicKeys))
for i, publicKey := range kfj.PublicKeys {
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 {
@@ -172,7 +176,11 @@ func pathExists(path string) (bool, error) {
}
// WriteKeysFile writes a keys file with the given data
func WriteKeysFile(path string, encryptedPrivateKeys []*EncryptedPrivateKey, publicKeys [][]byte, minimumSignatures uint32) error {
func WriteKeysFile(path string,
encryptedPrivateKeys []*EncryptedPrivateKey,
publicKeys [][]byte,
minimumSignatures uint32,
ecdsa bool) error {
if path == "" {
path = defaultKeysFile
}
@@ -210,6 +218,7 @@ func WriteKeysFile(path string, encryptedPrivateKeys []*EncryptedPrivateKey, pub
encryptedPrivateKeys: encryptedPrivateKeys,
PublicKeys: publicKeys,
MinimumSignatures: minimumSignatures,
ECDSA: ecdsa,
}
encoder := json.NewEncoder(file)

View File

@@ -8,12 +8,20 @@ import (
)
// CreateKeyPair generates a private-public key pair
func CreateKeyPair() ([]byte, []byte, error) {
privateKey, err := secp256k1.GenerateSchnorrKeyPair()
func CreateKeyPair(ecdsa bool) ([]byte, []byte, error) {
if ecdsa {
return createKeyPairECDSA()
}
return createKeyPair()
}
func createKeyPair() ([]byte, []byte, error) {
keyPair, err := secp256k1.GenerateSchnorrKeyPair()
if err != nil {
return nil, nil, errors.Wrap(err, "Failed to generate private key")
}
publicKey, err := privateKey.SchnorrPublicKey()
publicKey, err := keyPair.SchnorrPublicKey()
if err != nil {
return nil, nil, errors.Wrap(err, "Failed to generate public key")
}
@@ -22,30 +30,61 @@ func CreateKeyPair() ([]byte, []byte, error) {
return nil, nil, errors.Wrap(err, "Failed to serialize public key")
}
return privateKey.SerializePrivateKey()[:], publicKeySerialized[:], nil
return keyPair.SerializePrivateKey()[:], publicKeySerialized[:], nil
}
func addressFromPublicKey(params *dagconfig.Params, publicKeySerialized []byte) (util.Address, error) {
addr, err := util.NewAddressPubKeyHashFromPublicKey(publicKeySerialized[:], params.Prefix)
func createKeyPairECDSA() ([]byte, []byte, error) {
keyPair, err := secp256k1.GenerateECDSAPrivateKey()
if err != nil {
return nil, errors.Wrap(err, "Failed to generate p2pkh address")
return nil, nil, errors.Wrap(err, "Failed to generate private key")
}
publicKey, err := keyPair.ECDSAPublicKey()
if err != nil {
return nil, nil, errors.Wrap(err, "Failed to generate public key")
}
publicKeySerialized, err := publicKey.Serialize()
if err != nil {
return nil, nil, errors.Wrap(err, "Failed to serialize public key")
}
return addr, nil
return keyPair.Serialize()[:], publicKeySerialized[:], nil
}
// PublicKeyFromPrivateKey returns the public key associated with a private key
func PublicKeyFromPrivateKey(privateKeyBytes []byte) ([]byte, error) {
keyPair, err := secp256k1.DeserializeSchnorrPrivateKeyFromSlice(privateKeyBytes)
if err != nil {
return nil, errors.Wrap(err, "Failed to deserialize private key")
}
publicKey, err := keyPair.SchnorrPublicKey()
if err != nil {
return nil, errors.Wrap(err, "Failed to generate public key")
}
publicKeySerialized, err := publicKey.Serialize()
if err != nil {
return nil, errors.Wrap(err, "Failed to serialize public key")
}
return publicKeySerialized[:], nil
}
// Address returns the address associated with the given public keys and minimum signatures parameters.
func Address(params *dagconfig.Params, pubKeys [][]byte, minimumSignatures uint32) (util.Address, error) {
func Address(params *dagconfig.Params, pubKeys [][]byte, minimumSignatures uint32, ecdsa bool) (util.Address, error) {
sortPublicKeys(pubKeys)
if uint32(len(pubKeys)) < 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 {
return addressFromPublicKey(params, pubKeys[0])
if ecdsa {
return util.NewAddressPublicKeyECDSA(pubKeys[0][:], params.Prefix)
}
return util.NewAddressPublicKey(pubKeys[0][:], params.Prefix)
}
redeemScript, err := multiSigRedeemScript(pubKeys, minimumSignatures)
redeemScript, err := multiSigRedeemScript(pubKeys, minimumSignatures, ecdsa)
if err != nil {
return nil, err
}

View File

@@ -0,0 +1,146 @@
package libkaspawallet
import (
"bytes"
"github.com/kaspanet/go-secp256k1"
"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/pkg/errors"
)
type signer interface {
rawTxInSignature(tx *externalapi.DomainTransaction, idx int, hashType consensushashing.SigHashType,
sighashReusedValues *consensushashing.SighashReusedValues) ([]byte, error)
serializedPublicKey() ([]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) {
if ecdsa {
keyPair, err := secp256k1.DeserializeECDSAPrivateKeyFromSlice(privateKey)
if err != nil {
return nil, errors.Wrap(err, "Error deserializing private key")
}
return (*ecdsaSigner)(keyPair), nil
}
keyPair, err := secp256k1.DeserializeSchnorrPrivateKeyFromSlice(privateKey)
if err != nil {
return nil, errors.Wrap(err, "Error deserializing private key")
}
return (*schnorrSigner)(keyPair), nil
}
// 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")
}
}
partiallySignedTransaction, err := serialization.DeserializePartiallySignedTransaction(serializedPSTx)
if err != nil {
return nil, err
}
for _, keyPair := range keyPairs {
err = sign(keyPair, partiallySignedTransaction)
if err != nil {
return nil, err
}
}
return serialization.SerializePartiallySignedTransaction(partiallySignedTransaction)
}
func sign(keyPair signer, psTx *serialization.PartiallySignedTransaction) error {
if isTransactionFullySigned(psTx) {
return nil
}
serializedPublicKey, err := keyPair.serializedPublicKey()
if err != nil {
return err
}
sighashReusedValues := &consensushashing.SighashReusedValues{}
for i, partiallySignedInput := range psTx.PartiallySignedInputs {
prevOut := partiallySignedInput.PrevOutput
psTx.Tx.Inputs[i].UTXOEntry = utxo.NewUTXOEntry(
prevOut.Value,
prevOut.ScriptPublicKey,
false, // This is a fake value, because it's irrelevant for the signature
0, // This is a fake value, because it's irrelevant for the signature
)
}
signed := false
for i, partiallySignedInput := range psTx.PartiallySignedInputs {
for _, pair := range partiallySignedInput.PubKeySignaturePairs {
if bytes.Equal(pair.PubKey, serializedPublicKey[:]) {
pair.Signature, err = keyPair.rawTxInSignature(psTx.Tx, i, consensushashing.SigHashAll, sighashReusedValues)
if err != nil {
return err
}
signed = true
}
}
}
if !signed {
return errors.Errorf("Public key doesn't match any of the transaction public keys")
}
return nil
}

View File

@@ -2,14 +2,11 @@ package libkaspawallet
import (
"bytes"
"github.com/kaspanet/go-secp256k1"
"github.com/kaspanet/kaspad/cmd/kaspawallet/libkaspawallet/serialization"
"github.com/kaspanet/kaspad/domain/consensus/model/externalapi"
"github.com/kaspanet/kaspad/domain/consensus/utils/consensushashing"
"github.com/kaspanet/kaspad/domain/consensus/utils/constants"
"github.com/kaspanet/kaspad/domain/consensus/utils/subnetworks"
"github.com/kaspanet/kaspad/domain/consensus/utils/txscript"
"github.com/kaspanet/kaspad/domain/consensus/utils/utxo"
"github.com/kaspanet/kaspad/util"
"github.com/pkg/errors"
"sort"
@@ -31,11 +28,12 @@ func sortPublicKeys(publicKeys [][]byte) {
func CreateUnsignedTransaction(
pubKeys [][]byte,
minimumSignatures uint32,
ecdsa bool,
payments []*Payment,
selectedUTXOs []*externalapi.OutpointAndUTXOEntryPair) ([]byte, error) {
sortPublicKeys(pubKeys)
unsignedTransaction, err := createUnsignedTransaction(pubKeys, minimumSignatures, payments, selectedUTXOs)
unsignedTransaction, err := createUnsignedTransaction(pubKeys, minimumSignatures, ecdsa, payments, selectedUTXOs)
if err != nil {
return nil, err
}
@@ -43,53 +41,34 @@ func CreateUnsignedTransaction(
return serialization.SerializePartiallySignedTransaction(unsignedTransaction)
}
// Sign signs the transaction with the given private keys
func Sign(privateKeys [][]byte, serializedPSTx []byte) ([]byte, error) {
keyPairs := make([]*secp256k1.SchnorrKeyPair, len(privateKeys))
for i, privateKey := range privateKeys {
var err error
keyPairs[i], err = secp256k1.DeserializeSchnorrPrivateKeyFromSlice(privateKey)
if err != nil {
return nil, errors.Wrap(err, "Error deserializing private key")
}
}
partiallySignedTransaction, err := serialization.DeserializePartiallySignedTransaction(serializedPSTx)
if err != nil {
return nil, err
}
for _, keyPair := range keyPairs {
err = sign(keyPair, partiallySignedTransaction)
if err != nil {
return nil, err
}
}
return serialization.SerializePartiallySignedTransaction(partiallySignedTransaction)
}
func multiSigRedeemScript(pubKeys [][]byte, minimumSignatures uint32) ([]byte, error) {
func multiSigRedeemScript(pubKeys [][]byte, minimumSignatures uint32, ecdsa bool) ([]byte, error) {
scriptBuilder := txscript.NewScriptBuilder()
scriptBuilder.AddInt64(int64(minimumSignatures))
for _, key := range pubKeys {
scriptBuilder.AddData(key)
}
scriptBuilder.AddInt64(int64(len(pubKeys)))
scriptBuilder.AddOp(txscript.OpCheckMultiSig)
if ecdsa {
scriptBuilder.AddOp(txscript.OpCheckMultiSigECDSA)
} else {
scriptBuilder.AddOp(txscript.OpCheckMultiSig)
}
return scriptBuilder.Script()
}
func createUnsignedTransaction(
pubKeys [][]byte,
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)
redeemScript, err = multiSigRedeemScript(pubKeys, minimumSignatures, ecdsa)
if err != nil {
return nil, err
}
@@ -146,53 +125,6 @@ func createUnsignedTransaction(
}, nil
}
func sign(keyPair *secp256k1.SchnorrKeyPair, psTx *serialization.PartiallySignedTransaction) error {
if isTransactionFullySigned(psTx) {
return nil
}
publicKey, err := keyPair.SchnorrPublicKey()
if err != nil {
return err
}
serializedPublicKey, err := publicKey.Serialize()
if err != nil {
return err
}
sighashReusedValues := &consensushashing.SighashReusedValues{}
for i, partiallySignedInput := range psTx.PartiallySignedInputs {
prevOut := partiallySignedInput.PrevOutput
psTx.Tx.Inputs[i].UTXOEntry = utxo.NewUTXOEntry(
prevOut.Value,
prevOut.ScriptPublicKey,
false, // This is a fake value, because it's irrelevant for the signature
0, // This is a fake value, because it's irrelevant for the signature
)
}
signed := false
for i, partiallySignedInput := range psTx.PartiallySignedInputs {
for _, pair := range partiallySignedInput.PubKeySignaturePairs {
if bytes.Equal(pair.PubKey, serializedPublicKey[:]) {
pair.Signature, err = txscript.RawTxInSignature(psTx.Tx, i, consensushashing.SigHashAll, keyPair, sighashReusedValues)
if err != nil {
return err
}
signed = true
}
}
}
if !signed {
return errors.Errorf("Public key doesn't match any of the transaction public keys")
}
return nil
}
// IsTransactionFullySigned returns whether the transaction is fully signed and ready to broadcast.
func IsTransactionFullySigned(psTxBytes []byte) (bool, error) {
partiallySignedTransaction, err := serialization.DeserializePartiallySignedTransaction(psTxBytes)
@@ -254,7 +186,7 @@ func extractTransaction(psTx *serialization.PartiallySignedTransaction) (*extern
psTx.Tx.Inputs[i].SignatureScript = sigScript
} else {
if len(input.PubKeySignaturePairs) > 1 {
return nil, errors.Errorf("Cannot sign on P2PKH when len(input.PubKeySignaturePairs) > 1")
return nil, errors.Errorf("Cannot sign on P2PK when len(input.PubKeySignaturePairs) > 1")
}
if input.PubKeySignaturePairs[0].Signature == nil {
@@ -263,7 +195,6 @@ func extractTransaction(psTx *serialization.PartiallySignedTransaction) (*extern
sigScript, err := txscript.NewScriptBuilder().
AddData(input.PubKeySignaturePairs[0].Signature).
AddData(input.PubKeySignaturePairs[0].PubKey).
Script()
if err != nil {
return nil, err

View File

@@ -15,254 +15,277 @@ import (
"testing"
)
func forSchnorrAndECDSA(t *testing.T, testFunc func(t *testing.T, ecdsa bool)) {
t.Run("schnorr", func(t *testing.T) {
testFunc(t, false)
})
t.Run("ecdsa", func(t *testing.T) {
testFunc(t, true)
})
}
func TestMultisig(t *testing.T) {
testutils.ForAllNets(t, true, func(t *testing.T, params *dagconfig.Params) {
params.BlockCoinbaseMaturity = 0
tc, teardown, err := consensus.NewFactory().NewTestConsensus(params, false, "TestMultisig")
if err != nil {
t.Fatalf("Error setting up tc: %+v", err)
}
defer teardown(false)
const numKeys = 3
privateKeys := make([][]byte, numKeys)
publicKeys := make([][]byte, numKeys)
for i := 0; i < numKeys; i++ {
privateKeys[i], publicKeys[i], err = libkaspawallet.CreateKeyPair()
forSchnorrAndECDSA(t, func(t *testing.T, ecdsa bool) {
params.BlockCoinbaseMaturity = 0
tc, teardown, err := consensus.NewFactory().NewTestConsensus(params, false, "TestMultisig")
if err != nil {
t.Fatalf("CreateKeyPair: %+v", err)
t.Fatalf("Error setting up tc: %+v", err)
}
}
defer teardown(false)
const minimumSignatures = 2
address, err := libkaspawallet.Address(params, publicKeys, minimumSignatures)
if err != nil {
t.Fatalf("Address: %+v", err)
}
const numKeys = 3
privateKeys := make([][]byte, numKeys)
publicKeys := make([][]byte, numKeys)
for i := 0; i < numKeys; i++ {
privateKeys[i], publicKeys[i], err = libkaspawallet.CreateKeyPair(ecdsa)
if err != nil {
t.Fatalf("CreateKeyPair: %+v", err)
}
}
if _, ok := address.(*util.AddressScriptHash); !ok {
t.Fatalf("The address is of unexpected type")
}
const minimumSignatures = 2
address, err := libkaspawallet.Address(params, publicKeys, minimumSignatures, ecdsa)
if err != nil {
t.Fatalf("Address: %+v", err)
}
scriptPublicKey, err := txscript.PayToAddrScript(address)
if err != nil {
t.Fatalf("PayToAddrScript: %+v", err)
}
if _, ok := address.(*util.AddressScriptHash); !ok {
t.Fatalf("The address is of unexpected type")
}
coinbaseData := &externalapi.DomainCoinbaseData{
ScriptPublicKey: scriptPublicKey,
ExtraData: nil,
}
scriptPublicKey, err := txscript.PayToAddrScript(address)
if err != nil {
t.Fatalf("PayToAddrScript: %+v", err)
}
fundingBlockHash, _, err := tc.AddBlock([]*externalapi.DomainHash{params.GenesisHash}, coinbaseData, nil)
if err != nil {
t.Fatalf("AddBlock: %+v", err)
}
coinbaseData := &externalapi.DomainCoinbaseData{
ScriptPublicKey: scriptPublicKey,
ExtraData: nil,
}
block1Hash, _, err := tc.AddBlock([]*externalapi.DomainHash{fundingBlockHash}, nil, nil)
if err != nil {
t.Fatalf("AddBlock: %+v", err)
}
fundingBlockHash, _, err := tc.AddBlock([]*externalapi.DomainHash{params.GenesisHash}, coinbaseData, nil)
if err != nil {
t.Fatalf("AddBlock: %+v", err)
}
block1, err := tc.GetBlock(block1Hash)
if err != nil {
t.Fatalf("GetBlock: %+v", err)
}
block1Hash, _, err := tc.AddBlock([]*externalapi.DomainHash{fundingBlockHash}, nil, nil)
if err != nil {
t.Fatalf("AddBlock: %+v", err)
}
block1Tx := block1.Transactions[0]
block1TxOut := block1Tx.Outputs[0]
selectedUTXOs := []*externalapi.OutpointAndUTXOEntryPair{{
Outpoint: &externalapi.DomainOutpoint{
TransactionID: *consensushashing.TransactionID(block1.Transactions[0]),
block1, err := tc.GetBlock(block1Hash)
if err != nil {
t.Fatalf("GetBlock: %+v", err)
}
block1Tx := block1.Transactions[0]
block1TxOut := block1Tx.Outputs[0]
selectedUTXOs := []*externalapi.OutpointAndUTXOEntryPair{{
Outpoint: &externalapi.DomainOutpoint{
TransactionID: *consensushashing.TransactionID(block1.Transactions[0]),
Index: 0,
},
UTXOEntry: utxo.NewUTXOEntry(block1TxOut.Value, block1TxOut.ScriptPublicKey, true, 0),
}}
unsignedTransaction, err := libkaspawallet.CreateUnsignedTransaction(publicKeys, minimumSignatures, ecdsa,
[]*libkaspawallet.Payment{{
Address: address,
Amount: 10,
}}, selectedUTXOs)
if err != nil {
t.Fatalf("CreateUnsignedTransaction: %+v", err)
}
isFullySigned, err := libkaspawallet.IsTransactionFullySigned(unsignedTransaction)
if err != nil {
t.Fatalf("IsTransactionFullySigned: %+v", err)
}
if isFullySigned {
t.Fatalf("Transaction is not expected to be signed")
}
_, err = libkaspawallet.ExtractTransaction(unsignedTransaction)
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)
if err != nil {
t.Fatalf("IsTransactionFullySigned: %+v", err)
}
isFullySigned, err = libkaspawallet.IsTransactionFullySigned(signedTxStep1)
if err != nil {
t.Fatalf("IsTransactionFullySigned: %+v", err)
}
if isFullySigned {
t.Fatalf("Transaction is not expected to be fully signed")
}
signedTxStep2, err := libkaspawallet.Sign(privateKeys[1:2], signedTxStep1, ecdsa)
if err != nil {
t.Fatalf("Sign: %+v", err)
}
extractedSignedTxStep2, err := libkaspawallet.ExtractTransaction(signedTxStep2)
if err != nil {
t.Fatalf("ExtractTransaction: %+v", err)
}
signedTxOneStep, err := libkaspawallet.Sign(privateKeys[:2], unsignedTransaction, ecdsa)
if err != nil {
t.Fatalf("Sign: %+v", err)
}
extractedSignedTxOneStep, err := libkaspawallet.ExtractTransaction(signedTxOneStep)
if err != nil {
t.Fatalf("ExtractTransaction: %+v", err)
}
// We check IDs instead of comparing the actual transactions because the actual transactions have different
// signature scripts due to non deterministic signature scheme.
if !consensushashing.TransactionID(extractedSignedTxStep2).Equal(consensushashing.TransactionID(extractedSignedTxOneStep)) {
t.Fatalf("Expected extractedSignedTxOneStep and extractedSignedTxStep2 IDs to be equal")
}
_, insertionResult, err := tc.AddBlock([]*externalapi.DomainHash{block1Hash}, nil, []*externalapi.DomainTransaction{extractedSignedTxStep2})
if err != nil {
t.Fatalf("AddBlock: %+v", err)
}
addedUTXO := &externalapi.DomainOutpoint{
TransactionID: *consensushashing.TransactionID(extractedSignedTxStep2),
Index: 0,
},
UTXOEntry: utxo.NewUTXOEntry(block1TxOut.Value, block1TxOut.ScriptPublicKey, true, 0),
}}
unsignedTransaction, err := libkaspawallet.CreateUnsignedTransaction(publicKeys, minimumSignatures, []*libkaspawallet.Payment{{
Address: address,
Amount: 10,
}}, selectedUTXOs)
if err != nil {
t.Fatalf("CreateUnsignedTransaction: %+v", err)
}
isFullySigned, err := libkaspawallet.IsTransactionFullySigned(unsignedTransaction)
if err != nil {
t.Fatalf("IsTransactionFullySigned: %+v", err)
}
if isFullySigned {
t.Fatalf("Transaction is not expected to be signed")
}
_, err = libkaspawallet.ExtractTransaction(unsignedTransaction)
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)
if err != nil {
t.Fatalf("IsTransactionFullySigned: %+v", err)
}
isFullySigned, err = libkaspawallet.IsTransactionFullySigned(signedTxStep1)
if err != nil {
t.Fatalf("IsTransactionFullySigned: %+v", err)
}
if isFullySigned {
t.Fatalf("Transaction is not expected to be fully signed")
}
signedTxStep2, err := libkaspawallet.Sign(privateKeys[1:2], signedTxStep1)
if err != nil {
t.Fatalf("Sign: %+v", err)
}
extractedSignedTxStep2, err := libkaspawallet.ExtractTransaction(signedTxStep2)
if err != nil {
t.Fatalf("ExtractTransaction: %+v", err)
}
signedTxOneStep, err := libkaspawallet.Sign(privateKeys[:2], unsignedTransaction)
if err != nil {
t.Fatalf("Sign: %+v", err)
}
extractedSignedTxOneStep, err := libkaspawallet.ExtractTransaction(signedTxOneStep)
if err != nil {
t.Fatalf("ExtractTransaction: %+v", err)
}
// We check IDs instead of comparing the actual transactions because the actual transactions have different
// signature scripts due to non deterministic signature scheme.
if !consensushashing.TransactionID(extractedSignedTxStep2).Equal(consensushashing.TransactionID(extractedSignedTxOneStep)) {
t.Fatalf("Expected extractedSignedTxOneStep and extractedSignedTxStep2 IDs to be equal")
}
_, insertionResult, err := tc.AddBlock([]*externalapi.DomainHash{block1Hash}, nil, []*externalapi.DomainTransaction{extractedSignedTxStep2})
if err != nil {
t.Fatalf("AddBlock: %+v", err)
}
addedUTXO := &externalapi.DomainOutpoint{
TransactionID: *consensushashing.TransactionID(extractedSignedTxStep2),
Index: 0,
}
if !insertionResult.VirtualUTXODiff.ToAdd().Contains(addedUTXO) {
t.Fatalf("Transaction wasn't accepted in the DAG")
}
}
if !insertionResult.VirtualUTXODiff.ToAdd().Contains(addedUTXO) {
t.Fatalf("Transaction wasn't accepted in the DAG")
}
})
})
}
func TestP2PKH(t *testing.T) {
func TestP2PK(t *testing.T) {
testutils.ForAllNets(t, true, func(t *testing.T, params *dagconfig.Params) {
params.BlockCoinbaseMaturity = 0
tc, teardown, err := consensus.NewFactory().NewTestConsensus(params, false, "TestMultisig")
if err != nil {
t.Fatalf("Error setting up tc: %+v", err)
}
defer teardown(false)
const numKeys = 1
privateKeys := make([][]byte, numKeys)
publicKeys := make([][]byte, numKeys)
for i := 0; i < numKeys; i++ {
privateKeys[i], publicKeys[i], err = libkaspawallet.CreateKeyPair()
forSchnorrAndECDSA(t, func(t *testing.T, ecdsa bool) {
params.BlockCoinbaseMaturity = 0
tc, teardown, err := consensus.NewFactory().NewTestConsensus(params, false, "TestMultisig")
if err != nil {
t.Fatalf("CreateKeyPair: %+v", err)
t.Fatalf("Error setting up tc: %+v", err)
}
}
defer teardown(false)
const minimumSignatures = 1
address, err := libkaspawallet.Address(params, publicKeys, minimumSignatures)
if err != nil {
t.Fatalf("Address: %+v", err)
}
const numKeys = 1
privateKeys := make([][]byte, numKeys)
publicKeys := make([][]byte, numKeys)
for i := 0; i < numKeys; i++ {
privateKeys[i], publicKeys[i], err = libkaspawallet.CreateKeyPair(ecdsa)
if err != nil {
t.Fatalf("CreateKeyPair: %+v", err)
}
}
if _, ok := address.(*util.AddressPubKeyHash); !ok {
t.Fatalf("The address is of unexpected type")
}
const minimumSignatures = 1
address, err := libkaspawallet.Address(params, publicKeys, minimumSignatures, ecdsa)
if err != nil {
t.Fatalf("Address: %+v", err)
}
scriptPublicKey, err := txscript.PayToAddrScript(address)
if err != nil {
t.Fatalf("PayToAddrScript: %+v", err)
}
if ecdsa {
if _, ok := address.(*util.AddressPublicKeyECDSA); !ok {
t.Fatalf("The address is of unexpected type")
}
} else {
if _, ok := address.(*util.AddressPublicKey); !ok {
t.Fatalf("The address is of unexpected type")
}
}
coinbaseData := &externalapi.DomainCoinbaseData{
ScriptPublicKey: scriptPublicKey,
ExtraData: nil,
}
scriptPublicKey, err := txscript.PayToAddrScript(address)
if err != nil {
t.Fatalf("PayToAddrScript: %+v", err)
}
fundingBlockHash, _, err := tc.AddBlock([]*externalapi.DomainHash{params.GenesisHash}, coinbaseData, nil)
if err != nil {
t.Fatalf("AddBlock: %+v", err)
}
coinbaseData := &externalapi.DomainCoinbaseData{
ScriptPublicKey: scriptPublicKey,
ExtraData: nil,
}
block1Hash, _, err := tc.AddBlock([]*externalapi.DomainHash{fundingBlockHash}, nil, nil)
if err != nil {
t.Fatalf("AddBlock: %+v", err)
}
fundingBlockHash, _, err := tc.AddBlock([]*externalapi.DomainHash{params.GenesisHash}, coinbaseData, nil)
if err != nil {
t.Fatalf("AddBlock: %+v", err)
}
block1, err := tc.GetBlock(block1Hash)
if err != nil {
t.Fatalf("GetBlock: %+v", err)
}
block1Hash, _, err := tc.AddBlock([]*externalapi.DomainHash{fundingBlockHash}, nil, nil)
if err != nil {
t.Fatalf("AddBlock: %+v", err)
}
block1Tx := block1.Transactions[0]
block1TxOut := block1Tx.Outputs[0]
selectedUTXOs := []*externalapi.OutpointAndUTXOEntryPair{{
Outpoint: &externalapi.DomainOutpoint{
TransactionID: *consensushashing.TransactionID(block1.Transactions[0]),
block1, err := tc.GetBlock(block1Hash)
if err != nil {
t.Fatalf("GetBlock: %+v", err)
}
block1Tx := block1.Transactions[0]
block1TxOut := block1Tx.Outputs[0]
selectedUTXOs := []*externalapi.OutpointAndUTXOEntryPair{{
Outpoint: &externalapi.DomainOutpoint{
TransactionID: *consensushashing.TransactionID(block1.Transactions[0]),
Index: 0,
},
UTXOEntry: utxo.NewUTXOEntry(block1TxOut.Value, block1TxOut.ScriptPublicKey, true, 0),
}}
unsignedTransaction, err := libkaspawallet.CreateUnsignedTransaction(publicKeys, minimumSignatures,
ecdsa,
[]*libkaspawallet.Payment{{
Address: address,
Amount: 10,
}}, selectedUTXOs)
if err != nil {
t.Fatalf("CreateUnsignedTransaction: %+v", err)
}
isFullySigned, err := libkaspawallet.IsTransactionFullySigned(unsignedTransaction)
if err != nil {
t.Fatalf("IsTransactionFullySigned: %+v", err)
}
if isFullySigned {
t.Fatalf("Transaction is not expected to be signed")
}
_, err = libkaspawallet.ExtractTransaction(unsignedTransaction)
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)
if err != nil {
t.Fatalf("IsTransactionFullySigned: %+v", err)
}
tx, err := libkaspawallet.ExtractTransaction(signedTx)
if err != nil {
t.Fatalf("ExtractTransaction: %+v", err)
}
_, insertionResult, err := tc.AddBlock([]*externalapi.DomainHash{block1Hash}, nil, []*externalapi.DomainTransaction{tx})
if err != nil {
t.Fatalf("AddBlock: %+v", err)
}
addedUTXO := &externalapi.DomainOutpoint{
TransactionID: *consensushashing.TransactionID(tx),
Index: 0,
},
UTXOEntry: utxo.NewUTXOEntry(block1TxOut.Value, block1TxOut.ScriptPublicKey, true, 0),
}}
unsignedTransaction, err := libkaspawallet.CreateUnsignedTransaction(publicKeys, minimumSignatures, []*libkaspawallet.Payment{{
Address: address,
Amount: 10,
}}, selectedUTXOs)
if err != nil {
t.Fatalf("CreateUnsignedTransaction: %+v", err)
}
isFullySigned, err := libkaspawallet.IsTransactionFullySigned(unsignedTransaction)
if err != nil {
t.Fatalf("IsTransactionFullySigned: %+v", err)
}
if isFullySigned {
t.Fatalf("Transaction is not expected to be signed")
}
_, err = libkaspawallet.ExtractTransaction(unsignedTransaction)
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)
if err != nil {
t.Fatalf("IsTransactionFullySigned: %+v", err)
}
tx, err := libkaspawallet.ExtractTransaction(signedTx)
if err != nil {
t.Fatalf("ExtractTransaction: %+v", err)
}
_, insertionResult, err := tc.AddBlock([]*externalapi.DomainHash{block1Hash}, nil, []*externalapi.DomainTransaction{tx})
if err != nil {
t.Fatalf("AddBlock: %+v", err)
}
addedUTXO := &externalapi.DomainOutpoint{
TransactionID: *consensushashing.TransactionID(tx),
Index: 0,
}
if !insertionResult.VirtualUTXODiff.ToAdd().Contains(addedUTXO) {
t.Fatalf("Transaction wasn't accepted in the DAG")
}
}
if !insertionResult.VirtualUTXODiff.ToAdd().Contains(addedUTXO) {
t.Fatalf("Transaction wasn't accepted in the DAG")
}
})
})
}

View File

@@ -19,6 +19,10 @@ func main() {
err = sign(config.(*signConfig))
case broadcastSubCmd:
err = broadcast(config.(*broadcastConfig))
case showAddressSubCmd:
err = showAddress(config.(*showAddressConfig))
case dumpUnencryptedDataSubCmd:
err = dumpUnencryptedData(config.(*dumpUnencryptedDataConfig))
default:
err = errors.Errorf("Unknown sub-command '%s'\n", subCmd)
}

View File

@@ -27,7 +27,7 @@ func send(conf *sendConfig) error {
return err
}
fromAddress, err := libkaspawallet.Address(conf.NetParams(), keysFile.PublicKeys, keysFile.MinimumSignatures)
fromAddress, err := libkaspawallet.Address(conf.NetParams(), keysFile.PublicKeys, keysFile.MinimumSignatures, keysFile.ECDSA)
if err != nil {
return err
}
@@ -49,13 +49,17 @@ func send(conf *sendConfig) error {
return err
}
psTx, err := libkaspawallet.CreateUnsignedTransaction(keysFile.PublicKeys, keysFile.MinimumSignatures, []*libkaspawallet.Payment{{
Address: toAddress,
Amount: sendAmountSompi,
}, {
Address: fromAddress,
Amount: changeSompi,
}}, selectedUTXOs)
psTx, err := libkaspawallet.CreateUnsignedTransaction(keysFile.PublicKeys,
keysFile.MinimumSignatures,
keysFile.ECDSA,
[]*libkaspawallet.Payment{{
Address: toAddress,
Amount: sendAmountSompi,
}, {
Address: fromAddress,
Amount: changeSompi,
}},
selectedUTXOs)
if err != nil {
return err
}
@@ -65,7 +69,7 @@ func send(conf *sendConfig) error {
return err
}
updatedPSTx, err := libkaspawallet.Sign(privateKeys, psTx)
updatedPSTx, err := libkaspawallet.Sign(privateKeys, psTx, keysFile.ECDSA)
if err != nil {
return err
}

View File

@@ -0,0 +1,22 @@
package main
import (
"fmt"
"github.com/kaspanet/kaspad/cmd/kaspawallet/keys"
"github.com/kaspanet/kaspad/cmd/kaspawallet/libkaspawallet"
)
func showAddress(conf *showAddressConfig) error {
keysFile, err := keys.ReadKeysFile(conf.KeysFile)
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)
return nil
}

View File

@@ -23,7 +23,7 @@ func sign(conf *signConfig) error {
return err
}
updatedPSTxBytes, err := libkaspawallet.Sign(privateKeys, psTxBytes)
updatedPSTxBytes, err := libkaspawallet.Sign(privateKeys, psTxBytes, keysFile.ECDSA)
if err != nil {
return err
}

View File

@@ -10,5 +10,6 @@ type TestConsensusStateManager interface {
model.ConsensusStateManager
AddUTXOToMultiset(multiset model.Multiset, entry externalapi.UTXOEntry,
outpoint *externalapi.DomainOutpoint) error
ResolveBlockStatus(stagingArea *model.StagingArea, blockHash *externalapi.DomainHash) (externalapi.BlockStatus, error)
ResolveBlockStatus(stagingArea *model.StagingArea, blockHash *externalapi.DomainHash,
useSeparateStagingAreasPerBlock bool) (externalapi.BlockStatus, error)
}

View File

@@ -143,7 +143,8 @@ func (bb *testBlockBuilder) buildBlockWithParents(stagingArea *model.StagingArea
return nil, nil, err
}
selectedParentStatus, err := bb.testConsensus.ConsensusStateManager().ResolveBlockStatus(stagingArea, ghostdagData.SelectedParent())
selectedParentStatus, err := bb.testConsensus.ConsensusStateManager().ResolveBlockStatus(
stagingArea, ghostdagData.SelectedParent(), false)
if err != nil {
return nil, nil, err
}

View File

@@ -37,7 +37,7 @@ func (csm *consensusStateManager) AddBlock(stagingArea *model.StagingArea, block
if !isViolatingFinality {
log.Debugf("Block %s doesn't violate finality. Resolving its block status", blockHash)
blockStatus, err := csm.resolveBlockStatus(stagingArea, blockHash)
blockStatus, err := csm.resolveBlockStatus(stagingArea, blockHash, true)
if err != nil {
return nil, nil, err
}

View File

@@ -282,7 +282,7 @@ func (csm *consensusStateManager) RestorePastUTXOSetIterator(stagingArea *model.
onEnd := logger.LogAndMeasureExecutionTime(log, "RestorePastUTXOSetIterator")
defer onEnd()
blockStatus, err := csm.resolveBlockStatus(stagingArea, blockHash)
blockStatus, err := csm.resolveBlockStatus(stagingArea, blockHash, true)
if err != nil {
return nil, err
}

View File

@@ -12,8 +12,8 @@ import (
"github.com/pkg/errors"
)
func (csm *consensusStateManager) resolveBlockStatus(
stagingArea *model.StagingArea, blockHash *externalapi.DomainHash) (externalapi.BlockStatus, error) {
func (csm *consensusStateManager) resolveBlockStatus(stagingArea *model.StagingArea, blockHash *externalapi.DomainHash,
useSeparateStagingAreasPerBlock bool) (externalapi.BlockStatus, error) {
onEnd := logger.LogAndMeasureExecutionTime(log, fmt.Sprintf("resolveBlockStatus for %s", blockHash))
defer onEnd()
@@ -53,8 +53,8 @@ func (csm *consensusStateManager) resolveBlockStatus(
unverifiedBlockHash := unverifiedBlocks[i]
stagingAreaForCurrentBlock := stagingArea
isCurrentBlockTopBlock := i == 0
if !isCurrentBlockTopBlock {
useSeparateStagingArea := useSeparateStagingAreasPerBlock && (i != 0)
if useSeparateStagingArea {
stagingAreaForCurrentBlock = model.NewStagingArea()
}
@@ -72,7 +72,7 @@ func (csm *consensusStateManager) resolveBlockStatus(
log.Debugf("Block %s status resolved to `%s`, finished %d/%d of unverified blocks",
unverifiedBlockHash, blockStatus, len(unverifiedBlocks)-i, len(unverifiedBlocks))
if !isCurrentBlockTopBlock {
if useSeparateStagingArea {
err := staging.CommitAllChanges(csm.databaseContext, stagingAreaForCurrentBlock)
if err != nil {
return 0, err

View File

@@ -147,7 +147,8 @@ func TestDoubleSpends(t *testing.T) {
t.Fatalf("Error adding goodBlock: %+v", err)
}
//use ResolveBlockStatus, since goodBlock2 might not be the selectedTip
goodBlock2Status, err := consensus.ConsensusStateManager().ResolveBlockStatus(stagingArea, goodBlock2Hash)
goodBlock2Status, err := consensus.ConsensusStateManager().ResolveBlockStatus(
stagingArea, goodBlock2Hash, true)
if err != nil {
t.Fatalf("Error getting status of goodBlock: %+v", err)
}

View File

@@ -21,8 +21,8 @@ func (csm *testConsensusStateManager) AddUTXOToMultiset(
return addUTXOToMultiset(multiset, entry, outpoint)
}
func (csm *testConsensusStateManager) ResolveBlockStatus(stagingArea *model.StagingArea,
blockHash *externalapi.DomainHash) (externalapi.BlockStatus, error) {
func (csm *testConsensusStateManager) ResolveBlockStatus(stagingArea *model.StagingArea, blockHash *externalapi.DomainHash,
useSeparateStagingAreasPerBlock bool) (externalapi.BlockStatus, error) {
return csm.resolveBlockStatus(stagingArea, blockHash)
return csm.resolveBlockStatus(stagingArea, blockHash, useSeparateStagingAreasPerBlock)
}

View File

@@ -1,9 +1,8 @@
package ghostdag2
import (
"sort"
"github.com/kaspanet/kaspad/util/difficulty"
"sort"
"math/big"
@@ -19,7 +18,7 @@ type ghostdagHelper struct {
headerStore model.BlockHeaderStore
}
// New creates a new instance of this alternative ghostdag impl
// New creates a new instance of this alternative GHOSTDAG impl
func New(
databaseContext model.DBReader,
dagTopologyManager model.DAGTopologyManager,
@@ -36,53 +35,60 @@ func New(
}
}
/* --------------------------------------------- */
func (gh *ghostdagHelper) GHOSTDAG(stagingArea *model.StagingArea, blockCandidate *externalapi.DomainHash) error {
myWork := new(big.Int)
maxWork := new(big.Int)
var myScore uint64
var spScore uint64
/* find the selectedParent */
blockParents, err := gh.dagTopologyManager.Parents(stagingArea, blockCandidate)
var blueScore uint64 = 0
var blueWork = new(big.Int)
blueWork.SetUint64(0)
var selectedParent *externalapi.DomainHash = nil
var mergeSetBlues = make([]*externalapi.DomainHash, 0)
var mergeSetReds = make([]*externalapi.DomainHash, 0)
parents, err := gh.dagTopologyManager.Parents(stagingArea, blockCandidate)
if err != nil {
return err
}
var selectedParent = blockParents[0]
for _, parent := range blockParents {
blockData, err := gh.dataStore.Get(gh.dbAccess, stagingArea, parent)
// If genesis:
if len(parents) == 0 {
blockGHOSTDAGData := model.NewBlockGHOSTDAGData(blueScore, blueWork, selectedParent, mergeSetBlues, mergeSetReds, nil)
gh.dataStore.Stage(stagingArea, blockCandidate, blockGHOSTDAGData)
return nil
}
maxBlueWork := new(big.Int)
maxBlueWork.SetUint64(0)
maxBlueScore := uint64(0)
selectedParent = parents[0]
// Find the selected parent.
for _, parent := range parents {
parentBlockData, err := gh.dataStore.Get(gh.dbAccess, stagingArea, parent)
if err != nil {
return err
}
blockWork := blockData.BlueWork()
blockScore := blockData.BlueScore()
if blockWork.Cmp(maxWork) == 1 {
parentBlueWork := parentBlockData.BlueWork()
switch parentBlueWork.Cmp(maxBlueWork) {
case 0:
if isMoreHash(parent, selectedParent) {
selectedParent = parent
maxBlueWork = parentBlueWork
maxBlueScore = parentBlockData.BlueScore()
}
case 1:
selectedParent = parent
maxWork = blockWork
spScore = blockScore
}
if blockWork.Cmp(maxWork) == 0 && ismoreHash(parent, selectedParent) {
selectedParent = parent
maxWork = blockWork
spScore = blockScore
maxBlueWork = parentBlueWork
maxBlueScore = parentBlockData.BlueScore()
}
}
myWork.Set(maxWork)
myScore = spScore
/* Goal: iterate blockCandidate's mergeSet and divide it to : blue, blues, reds. */
var mergeSetBlues = make([]*externalapi.DomainHash, 0)
var mergeSetReds = make([]*externalapi.DomainHash, 0)
var blueSet = make([]*externalapi.DomainHash, 0)
blueWork.Set(maxBlueWork)
blueScore = maxBlueScore
blueSet := make([]*externalapi.DomainHash, 0)
blueSet = append(blueSet, selectedParent)
mergeSetBlues = append(mergeSetBlues, selectedParent)
mergeSetArr, err := gh.findMergeSet(stagingArea, blockParents, selectedParent)
mergeSet, err := gh.findMergeSet(stagingArea, parents, selectedParent)
if err != nil {
return err
}
err = gh.sortByBlueWork(stagingArea, mergeSetArr)
err = gh.sortByBlueWork(stagingArea, mergeSet)
if err != nil {
return err
}
@@ -90,41 +96,36 @@ func (gh *ghostdagHelper) GHOSTDAG(stagingArea *model.StagingArea, blockCandidat
if err != nil {
return err
}
for _, mergeSetBlock := range mergeSetArr {
// Iterate on mergeSet and divide it to mergeSetBlues and mergeSetReds.
for _, mergeSetBlock := range mergeSet {
if mergeSetBlock.Equal(selectedParent) {
if !contains(selectedParent, mergeSetBlues) {
mergeSetBlues = append(mergeSetBlues, selectedParent)
blueSet = append(blueSet, selectedParent)
}
continue
}
err := gh.divideBlueRed(stagingArea, selectedParent, mergeSetBlock, &mergeSetBlues, &mergeSetReds, &blueSet)
err := gh.divideToBlueAndRed(stagingArea, mergeSetBlock, &mergeSetBlues, &mergeSetReds, &blueSet)
if err != nil {
return err
}
}
myScore += uint64(len(mergeSetBlues))
blueScore += uint64(len(mergeSetBlues))
// We add up all the *work*(not blueWork) that all our blues and selected parent did
// Calculation of blue work
for _, blue := range mergeSetBlues {
header, err := gh.headerStore.BlockHeader(gh.dbAccess, stagingArea, blue)
if err != nil {
return err
}
myWork.Add(myWork, difficulty.CalcWork(header.Bits()))
blueWork.Add(blueWork, difficulty.CalcWork(header.Bits()))
}
e := model.NewBlockGHOSTDAGData(myScore, myWork, selectedParent, mergeSetBlues, mergeSetReds, nil)
gh.dataStore.Stage(stagingArea, blockCandidate, e)
blockGHOSTDAGData := model.NewBlockGHOSTDAGData(blueScore, blueWork, selectedParent, mergeSetBlues, mergeSetReds, nil)
gh.dataStore.Stage(stagingArea, blockCandidate, blockGHOSTDAGData)
return nil
}
/* --------isMoreHash(w, selectedParent)----------------*/
func ismoreHash(parent *externalapi.DomainHash, selectedParent *externalapi.DomainHash) bool {
func isMoreHash(parent *externalapi.DomainHash, selectedParent *externalapi.DomainHash) bool {
parentByteArray := parent.ByteArray()
selectedParentByteArray := selectedParent.ByteArray()
//Check if parentHash is more then selectedParentHash
for i := 0; i < len(parentByteArray); i++ {
switch {
case parentByteArray[i] < selectedParentByteArray[i]:
@@ -136,46 +137,39 @@ func ismoreHash(parent *externalapi.DomainHash, selectedParent *externalapi.Doma
return false
}
/* 1. blue = selectedParent.blue + blues
2. not connected to at most K blocks (from the blue group)
3. for each block at blue , check if not destroy
*/
/* ---------------divideBluesReds--------------------- */
func (gh *ghostdagHelper) divideBlueRed(stagingArea *model.StagingArea,
selectedParent *externalapi.DomainHash, desiredBlock *externalapi.DomainHash,
func (gh *ghostdagHelper) divideToBlueAndRed(stagingArea *model.StagingArea, blockToCheck *externalapi.DomainHash,
blues *[]*externalapi.DomainHash, reds *[]*externalapi.DomainHash, blueSet *[]*externalapi.DomainHash) error {
var k = int(gh.k)
counter := 0
var suspectsBlues = make([]*externalapi.DomainHash, 0)
anticoneBlocksCounter := 0
anticoneBlues := make([]*externalapi.DomainHash, 0)
isMergeBlue := true
//check that not-connected to at most k.
for _, block := range *blueSet {
isAnticone, err := gh.isAnticone(stagingArea, block, desiredBlock)
for _, blueblock := range *blueSet {
isAnticone, err := gh.isAnticone(stagingArea, blueblock, blockToCheck)
if err != nil {
return err
}
if isAnticone {
counter++
suspectsBlues = append(suspectsBlues, block)
anticoneBlocksCounter++
anticoneBlues = append(anticoneBlues, blueblock)
}
if counter > k {
if anticoneBlocksCounter > k {
isMergeBlue = false
break
}
}
if !isMergeBlue {
if !contains(desiredBlock, *reds) {
*reds = append(*reds, desiredBlock)
if !contains(blockToCheck, *reds) {
*reds = append(*reds, blockToCheck)
}
return nil
}
// check that the k-cluster of each blue is still valid.
for _, blue := range suspectsBlues {
isDestroyed, err := gh.checkIfDestroy(stagingArea, blue, blueSet)
// check that the k-cluster of each anticone blue block is still valid.
for _, blueBlock := range anticoneBlues {
isDestroyed, err := gh.checkIfDestroy(stagingArea, blueBlock, blueSet)
if err != nil {
return err
}
@@ -185,39 +179,37 @@ func (gh *ghostdagHelper) divideBlueRed(stagingArea *model.StagingArea,
}
}
if !isMergeBlue {
if !contains(desiredBlock, *reds) {
*reds = append(*reds, desiredBlock)
if !contains(blockToCheck, *reds) {
*reds = append(*reds, blockToCheck)
}
return nil
}
if !contains(desiredBlock, *blues) {
*blues = append(*blues, desiredBlock)
if !contains(blockToCheck, *blues) {
*blues = append(*blues, blockToCheck)
}
if !contains(desiredBlock, *blueSet) {
*blueSet = append(*blueSet, desiredBlock)
if !contains(blockToCheck, *blueSet) {
*blueSet = append(*blueSet, blockToCheck)
}
return nil
}
/* ---------------isAnticone-------------------------- */
func (gh *ghostdagHelper) isAnticone(stagingArea *model.StagingArea, blockA, blockB *externalapi.DomainHash) (bool, error) {
isAAncestorOfAB, err := gh.dagTopologyManager.IsAncestorOf(stagingArea, blockA, blockB)
isBlockAAncestorOfBlockB, err := gh.dagTopologyManager.IsAncestorOf(stagingArea, blockA, blockB)
if err != nil {
return false, err
}
isBAncestorOfA, err := gh.dagTopologyManager.IsAncestorOf(stagingArea, blockB, blockA)
isBlockBAncestorOfBlockA, err := gh.dagTopologyManager.IsAncestorOf(stagingArea, blockB, blockA)
if err != nil {
return false, err
}
return !isAAncestorOfAB && !isBAncestorOfA, nil
return !isBlockAAncestorOfBlockB && !isBlockBAncestorOfBlockA, nil
}
/* ----------------validateKCluster------------------- */
func (gh *ghostdagHelper) validateKCluster(stagingArea *model.StagingArea, chain *externalapi.DomainHash,
checkedBlock *externalapi.DomainHash, counter *int, blueSet *[]*externalapi.DomainHash) (bool, error) {
func (gh *ghostdagHelper) validateKCluster(stagingArea *model.StagingArea, chain *externalapi.DomainHash, checkedBlock *externalapi.DomainHash,
counter *int, blueSet *[]*externalapi.DomainHash) (bool, error) {
var k = int(gh.k)
k := int(gh.k)
isAnticone, err := gh.isAnticone(stagingArea, chain, checkedBlock)
if err != nil {
return false, err
@@ -236,6 +228,7 @@ func (gh *ghostdagHelper) validateKCluster(stagingArea *model.StagingArea, chain
*counter++
return true, nil
}
isAncestorOf, err := gh.dagTopologyManager.IsAncestorOf(stagingArea, checkedBlock, chain)
if err != nil {
return false, err
@@ -251,27 +244,24 @@ func (gh *ghostdagHelper) validateKCluster(stagingArea *model.StagingArea, chain
} else {
return true, nil
}
return false, nil
}
/*----------------contains-------------------------- */
func contains(item *externalapi.DomainHash, items []*externalapi.DomainHash) bool {
for _, r := range items {
if r.Equal(item) {
func contains(desiredItem *externalapi.DomainHash, items []*externalapi.DomainHash) bool {
for _, item := range items {
if item.Equal(desiredItem) {
return true
}
}
return false
}
/* ----------------checkIfDestroy------------------- */
/* find number of not-connected in his blue*/
// find number of not-connected in blueSet
func (gh *ghostdagHelper) checkIfDestroy(stagingArea *model.StagingArea, blockBlue *externalapi.DomainHash,
blueSet *[]*externalapi.DomainHash) (bool, error) {
// Goal: check that the K-cluster of each block in the blueSet is not destroyed when adding the block to the mergeSet.
var k = int(gh.k)
// check that the K-cluster of each block in the blueSet is not destroyed when adding the block to the mergeSet.
k := int(gh.k)
counter := 0
for _, blue := range *blueSet {
isAnticone, err := gh.isAnticone(stagingArea, blue, blockBlue)
@@ -288,51 +278,45 @@ func (gh *ghostdagHelper) checkIfDestroy(stagingArea *model.StagingArea, blockBl
return false, nil
}
/* ----------------findMergeSet------------------- */
func (gh *ghostdagHelper) findMergeSet(stagingArea *model.StagingArea, parents []*externalapi.DomainHash,
selectedParent *externalapi.DomainHash) ([]*externalapi.DomainHash, error) {
allMergeSet := make([]*externalapi.DomainHash, 0)
blockQueue := make([]*externalapi.DomainHash, 0)
mergeSet := make([]*externalapi.DomainHash, 0)
blocksQueue := make([]*externalapi.DomainHash, 0)
for _, parent := range parents {
if !contains(parent, blockQueue) {
blockQueue = append(blockQueue, parent)
if !contains(parent, blocksQueue) {
blocksQueue = append(blocksQueue, parent)
}
}
for len(blockQueue) > 0 {
block := blockQueue[0]
blockQueue = blockQueue[1:]
for len(blocksQueue) > 0 {
block := blocksQueue[0]
blocksQueue = blocksQueue[1:]
if selectedParent.Equal(block) {
if !contains(block, allMergeSet) {
allMergeSet = append(allMergeSet, block)
if !contains(block, mergeSet) {
mergeSet = append(mergeSet, block)
}
continue
}
isancestorOf, err := gh.dagTopologyManager.IsAncestorOf(stagingArea, block, selectedParent)
isAncestorOf, err := gh.dagTopologyManager.IsAncestorOf(stagingArea, block, selectedParent)
if err != nil {
return nil, err
}
if isancestorOf {
if isAncestorOf {
continue
}
if !contains(block, allMergeSet) {
allMergeSet = append(allMergeSet, block)
if !contains(block, mergeSet) {
mergeSet = append(mergeSet, block)
}
err = gh.insertParent(stagingArea, block, &blockQueue)
err = gh.insertParent(stagingArea, block, &blocksQueue)
if err != nil {
return nil, err
}
}
return allMergeSet, nil
return mergeSet, nil
}
/* ----------------insertParent------------------- */
/* Insert all parents to the queue*/
func (gh *ghostdagHelper) insertParent(stagingArea *model.StagingArea, child *externalapi.DomainHash,
queue *[]*externalapi.DomainHash) error {
// Insert all parents to the queue
func (gh *ghostdagHelper) insertParent(stagingArea *model.StagingArea, child *externalapi.DomainHash, queue *[]*externalapi.DomainHash) error {
parents, err := gh.dagTopologyManager.Parents(stagingArea, child)
if err != nil {
return err
@@ -346,8 +330,9 @@ func (gh *ghostdagHelper) insertParent(stagingArea *model.StagingArea, child *ex
return nil
}
/* ----------------findBlueSet------------------- */
func (gh *ghostdagHelper) findBlueSet(stagingArea *model.StagingArea, blueSet *[]*externalapi.DomainHash, selectedParent *externalapi.DomainHash) error {
func (gh *ghostdagHelper) findBlueSet(stagingArea *model.StagingArea, blueSet *[]*externalapi.DomainHash,
selectedParent *externalapi.DomainHash) error {
for selectedParent != nil {
if !contains(selectedParent, *blueSet) {
*blueSet = append(*blueSet, selectedParent)
@@ -368,44 +353,41 @@ func (gh *ghostdagHelper) findBlueSet(stagingArea *model.StagingArea, blueSet *[
return nil
}
/* ----------------sortByBlueScore------------------- */
func (gh *ghostdagHelper) sortByBlueWork(stagingArea *model.StagingArea, arr []*externalapi.DomainHash) error {
var err error = nil
var err error
sort.Slice(arr, func(i, j int) bool {
blockLeft, error := gh.dataStore.Get(gh.dbAccess, stagingArea, arr[i])
if error != nil {
err = error
blockLeft, err := gh.dataStore.Get(gh.dbAccess, stagingArea, arr[i])
if err != nil {
return false
}
blockRight, error := gh.dataStore.Get(gh.dbAccess, stagingArea, arr[j])
if error != nil {
err = error
blockRight, err := gh.dataStore.Get(gh.dbAccess, stagingArea, arr[j])
if err != nil {
return false
}
if blockLeft.BlueWork().Cmp(blockRight.BlueWork()) == -1 {
return true
}
if blockLeft.BlueWork().Cmp(blockRight.BlueWork()) == 0 {
return ismoreHash(arr[j], arr[i])
return isMoreHash(arr[j], arr[i])
}
return false
})
return err
}
/* --------------------------------------------- */
func (gh *ghostdagHelper) BlockData(stagingArea *model.StagingArea, blockHash *externalapi.DomainHash) (*model.BlockGHOSTDAGData, error) {
return gh.dataStore.Get(gh.dbAccess, stagingArea, blockHash)
}
func (gh *ghostdagHelper) ChooseSelectedParent(stagingArea *model.StagingArea, blockHashes ...*externalapi.DomainHash) (*externalapi.DomainHash, error) {
func (gh *ghostdagHelper) ChooseSelectedParent(*model.StagingArea, ...*externalapi.DomainHash) (*externalapi.DomainHash, error) {
panic("implement me")
}
func (gh *ghostdagHelper) Less(blockHashA *externalapi.DomainHash, ghostdagDataA *model.BlockGHOSTDAGData, blockHashB *externalapi.DomainHash, ghostdagDataB *model.BlockGHOSTDAGData) bool {
func (gh *ghostdagHelper) Less(*externalapi.DomainHash, *model.BlockGHOSTDAGData, *externalapi.DomainHash, *model.BlockGHOSTDAGData) bool {
panic("implement me")
}
func (gh *ghostdagHelper) GetSortedMergeSet(*model.StagingArea, *externalapi.DomainHash) ([]*externalapi.DomainHash, error) {
panic("implement me")
}

View File

@@ -189,7 +189,7 @@ func (v *transactionValidator) validateTransactionScripts(tx *externalapi.Domain
}
scriptPubKey := utxoEntry.ScriptPublicKey()
vm, err := txscript.NewEngine(scriptPubKey, tx, i, txscript.ScriptNoFlags, v.sigCache, sighashReusedValues)
vm, err := txscript.NewEngine(scriptPubKey, tx, i, txscript.ScriptNoFlags, v.sigCache, v.sigCacheECDSA, sighashReusedValues)
if err != nil {
return errors.Wrapf(ruleerrors.ErrScriptMalformed, "failed to parse input "+
"%d which references output %s - "+

View File

@@ -21,6 +21,7 @@ type transactionValidator struct {
massPerSigOp uint64
maxCoinbasePayloadLength uint64
sigCache *txscript.SigCache
sigCacheECDSA *txscript.SigCacheECDSA
}
// New instantiates a new TransactionValidator
@@ -47,5 +48,6 @@ func New(blockCoinbaseMaturity uint64,
ghostdagDataStore: ghostdagDataStore,
daaBlocksStore: daaBlocksStore,
sigCache: txscript.NewSigCache(sigCacheSize),
sigCacheECDSA: txscript.NewSigCacheECDSA(sigCacheSize),
}
}

View File

@@ -58,9 +58,9 @@ func TestValidateTransactionInContextAndPopulateMassAndFee(t *testing.T) {
if err != nil {
t.Fatalf("Failed to serialize public key: %v", err)
}
addr, err := util.NewAddressPubKeyHashFromPublicKey(publicKeySerialized[:], params.Prefix)
addr, err := util.NewAddressPublicKey(publicKeySerialized[:], params.Prefix)
if err != nil {
t.Fatalf("Failed to generate p2pkh address: %v", err)
t.Fatalf("Failed to generate p2pk address: %v", err)
}
scriptPublicKey, err := txscript.PayToAddrScript(addr)
if err != nil {
@@ -261,9 +261,9 @@ func TestSigningTwoInputs(t *testing.T) {
if err != nil {
t.Fatalf("Failed to serialize public key: %v", err)
}
addr, err := util.NewAddressPubKeyHashFromPublicKey(publicKeySerialized[:], params.Prefix)
addr, err := util.NewAddressPublicKey(publicKeySerialized[:], params.Prefix)
if err != nil {
t.Fatalf("Failed to generate p2pkh address: %v", err)
t.Fatalf("Failed to generate p2pk address: %v", err)
}
scriptPublicKey, err := txscript.PayToAddrScript(addr)
@@ -362,3 +362,127 @@ func TestSigningTwoInputs(t *testing.T) {
}
})
}
func TestSigningTwoInputsECDSA(t *testing.T) {
testutils.ForAllNets(t, true, func(t *testing.T, params *dagconfig.Params) {
params.BlockCoinbaseMaturity = 0
factory := consensus.NewFactory()
tc, teardown, err := factory.NewTestConsensus(params, false, "TestSigningTwoInputsECDSA")
if err != nil {
t.Fatalf("Error setting up consensus: %+v", err)
}
defer teardown(false)
privateKey, err := secp256k1.GenerateECDSAPrivateKey()
if err != nil {
t.Fatalf("Failed to generate a private key: %v", err)
}
publicKey, err := privateKey.ECDSAPublicKey()
if err != nil {
t.Fatalf("Failed to generate a public key: %v", err)
}
publicKeySerialized, err := publicKey.Serialize()
if err != nil {
t.Fatalf("Failed to serialize public key: %v", err)
}
addr, err := util.NewAddressPublicKeyECDSA(publicKeySerialized[:], params.Prefix)
if err != nil {
t.Fatalf("Failed to generate p2pk address: %v", err)
}
scriptPublicKey, err := txscript.PayToAddrScript(addr)
if err != nil {
t.Fatalf("PayToAddrScript: unexpected error: %v", err)
}
coinbaseData := &externalapi.DomainCoinbaseData{
ScriptPublicKey: scriptPublicKey,
}
block1Hash, _, err := tc.AddBlock([]*externalapi.DomainHash{params.GenesisHash}, coinbaseData, nil)
if err != nil {
t.Fatalf("AddBlock: %+v", err)
}
block2Hash, _, err := tc.AddBlock([]*externalapi.DomainHash{block1Hash}, coinbaseData, nil)
if err != nil {
t.Fatalf("AddBlock: %+v", err)
}
block3Hash, _, err := tc.AddBlock([]*externalapi.DomainHash{block2Hash}, coinbaseData, nil)
if err != nil {
t.Fatalf("AddBlock: %+v", err)
}
block2, err := tc.GetBlock(block2Hash)
if err != nil {
t.Fatalf("Error getting block2: %+v", err)
}
block3, err := tc.GetBlock(block3Hash)
if err != nil {
t.Fatalf("Error getting block3: %+v", err)
}
block2Tx := block2.Transactions[0]
block2TxOut := block2Tx.Outputs[0]
block3Tx := block3.Transactions[0]
block3TxOut := block3Tx.Outputs[0]
tx := &externalapi.DomainTransaction{
Version: constants.MaxTransactionVersion,
Inputs: []*externalapi.DomainTransactionInput{
{
PreviousOutpoint: externalapi.DomainOutpoint{
TransactionID: *consensushashing.TransactionID(block2.Transactions[0]),
Index: 0,
},
Sequence: constants.MaxTxInSequenceNum,
UTXOEntry: utxo.NewUTXOEntry(block2TxOut.Value, block2TxOut.ScriptPublicKey, true, 0),
},
{
PreviousOutpoint: externalapi.DomainOutpoint{
TransactionID: *consensushashing.TransactionID(block3.Transactions[0]),
Index: 0,
},
Sequence: constants.MaxTxInSequenceNum,
UTXOEntry: utxo.NewUTXOEntry(block3TxOut.Value, block3TxOut.ScriptPublicKey, true, 0),
},
},
Outputs: []*externalapi.DomainTransactionOutput{{
Value: 1,
ScriptPublicKey: &externalapi.ScriptPublicKey{
Script: nil,
Version: 0,
},
}},
SubnetworkID: subnetworks.SubnetworkIDNative,
Gas: 0,
LockTime: 0,
}
sighashReusedValues := &consensushashing.SighashReusedValues{}
for i, input := range tx.Inputs {
signatureScript, err := txscript.SignatureScriptECDSA(tx, i, consensushashing.SigHashAll, privateKey,
sighashReusedValues)
if err != nil {
t.Fatalf("Failed to create a sigScript: %v", err)
}
input.SignatureScript = signatureScript
}
_, insertionResult, err := tc.AddBlock([]*externalapi.DomainHash{block3Hash}, nil, []*externalapi.DomainTransaction{tx})
if err != nil {
t.Fatalf("AddBlock: %+v", err)
}
txOutpoint := &externalapi.DomainOutpoint{
TransactionID: *consensushashing.TransactionID(tx),
Index: 0,
}
if !insertionResult.VirtualUTXODiff.ToAdd().Contains(txOutpoint) {
t.Fatalf("tx was not accepted by the DAG")
}
})
}

View File

@@ -21,6 +21,8 @@ type testConsensus struct {
testReachabilityManager testapi.TestReachabilityManager
testConsensusStateManager testapi.TestConsensusStateManager
testTransactionValidator testapi.TestTransactionValidator
buildBlockConsensus *consensus
}
func (tc *testConsensus) DAGParams() *dagconfig.Params {

View File

@@ -2,7 +2,6 @@ package consensushashing
import (
"github.com/kaspanet/kaspad/domain/consensus/model/externalapi"
"github.com/kaspanet/kaspad/domain/consensus/utils/constants"
"github.com/kaspanet/kaspad/domain/consensus/utils/hashes"
"github.com/kaspanet/kaspad/domain/consensus/utils/serialization"
"github.com/kaspanet/kaspad/domain/consensus/utils/subnetworks"
@@ -58,10 +57,10 @@ type SighashReusedValues struct {
payloadHash *externalapi.DomainHash
}
// CalculateSignatureHash will, given a script and hash type calculate the signature hash
// to be used for signing and verification.
// CalculateSignatureHashSchnorr will, given a script and hash type calculate the signature hash
// to be used for signing and verification for Schnorr.
// This returns error only if one of the provided parameters are consensus-invalid.
func CalculateSignatureHash(tx *externalapi.DomainTransaction, inputIndex int, hashType SigHashType,
func CalculateSignatureHashSchnorr(tx *externalapi.DomainTransaction, inputIndex int, hashType SigHashType,
reusedValues *SighashReusedValues) (*externalapi.DomainHash, error) {
if !hashType.IsStandardSigHashType() {
@@ -70,18 +69,26 @@ func CalculateSignatureHash(tx *externalapi.DomainTransaction, inputIndex int, h
txIn := tx.Inputs[inputIndex]
prevScriptPublicKey := txIn.UTXOEntry.ScriptPublicKey()
if tx.Version > constants.MaxTransactionVersion {
return nil, errors.Errorf("Transaction version is unknown.")
}
if prevScriptPublicKey.Version > constants.MaxScriptPublicKeyVersion {
return nil, errors.Errorf("Script version is unknown.")
}
return calculateSignatureHash(tx, inputIndex, txIn, prevScriptPublicKey, hashType, reusedValues)
}
// CalculateSignatureHashECDSA will, given a script and hash type calculate the signature hash
// to be used for signing and verification for ECDSA.
// This returns error only if one of the provided parameters are consensus-invalid.
func CalculateSignatureHashECDSA(tx *externalapi.DomainTransaction, inputIndex int, hashType SigHashType,
reusedValues *SighashReusedValues) (*externalapi.DomainHash, error) {
hash, err := CalculateSignatureHashSchnorr(tx, inputIndex, hashType, reusedValues)
if err != nil {
return nil, err
}
hashWriter := hashes.NewTransactionSigningHashECDSAWriter()
hashWriter.InfallibleWrite(hash.ByteSlice())
return hashWriter.Finalize(), nil
}
func calculateSignatureHash(tx *externalapi.DomainTransaction, inputIndex int, txIn *externalapi.DomainTransactionInput,
prevScriptPublicKey *externalapi.ScriptPublicKey, hashType SigHashType, reusedValues *SighashReusedValues) (
*externalapi.DomainHash, error) {

View File

@@ -88,7 +88,7 @@ func modifySubnetworkID(tx *externalapi.DomainTransaction) *externalapi.DomainTr
return clone
}
func TestCalculateSignatureHash(t *testing.T) {
func TestCalculateSignatureHashSchnorr(t *testing.T) {
nativeTx, subnetworkTx, err := generateTxs()
if err != nil {
t.Fatalf("Error from generateTxs: %+v", err)
@@ -108,86 +108,86 @@ func TestCalculateSignatureHash(t *testing.T) {
// sigHashAll
{name: "native-all-0", tx: nativeTx, hashType: all, inputIndex: 0,
expectedSignatureHash: "c899a5ea7414f0bbfd77e50674f46da34ce8722b928d4362a4b4b727c69c6499"},
expectedSignatureHash: "3e4a8effa6903dea5f762754ec410d732114ec2907e5bcbae7b6dd8d3f309a10"},
{name: "native-all-0-modify-input-1", tx: nativeTx, hashType: all, inputIndex: 0,
modificationFunction: modifyInput(1), // should change the hash
expectedSignatureHash: "faf3b9db2e07b1c14b2df02002d3e40f1e430f177ac5cd3354c84dad8fbe72ce"},
expectedSignatureHash: "0bd2947383101f9708d94d5799626c69c1b8472d2de66523c90c4a674e99cc51"},
{name: "native-all-0-modify-output-1", tx: nativeTx, hashType: all, inputIndex: 0,
modificationFunction: modifyOutput(1), // should change the hash
expectedSignatureHash: "3a557c5b873aab72dcb81649642e1d7a63b75dcdcc74e19d340964a9e0eac76c"},
expectedSignatureHash: "ae69aa1372e958f069bf52d2628ead7ee789f195340b615ff0bcb6f01a995810"},
{name: "native-all-0-modify-sequence-1", tx: nativeTx, hashType: all, inputIndex: 0,
modificationFunction: modifySequence(1), // should change the hash
expectedSignatureHash: "2dd5fe8f9fa4bf551ea2f080a26e07b2462083e12d3b2ed01cb9369a61920665"},
expectedSignatureHash: "51295575e7efc1fe1c2e741ade49089e3d780d15d26e9a0b18b3d90e35caf795"},
{name: "native-all-anyonecanpay-0", tx: nativeTx, hashType: allAnyoneCanPay, inputIndex: 0,
expectedSignatureHash: "19fe2e0db681017f318fda705a39bbbad9c1085514cfbcff6fac01e1725f758b"},
expectedSignatureHash: "0a13d3cab42a6cf3a7ef96f7b28c9bb0d98c209a3032ff85a6bfa7dac520f2c2"},
{name: "native-all-anyonecanpay-0-modify-input-0", tx: nativeTx, hashType: allAnyoneCanPay, inputIndex: 0,
modificationFunction: modifyInput(0), // should change the hash
expectedSignatureHash: "5b21d492560a1c794595f769b3ae3c151775b9cfc4029d17c53f1856e1005da4"},
expectedSignatureHash: "aeaa56f5bc99daea0be5ed4b047808ce0123a81ac89c6089b72fa7199f5cd1c6"},
{name: "native-all-anyonecanpay-0-modify-input-1", tx: nativeTx, hashType: allAnyoneCanPay, inputIndex: 0,
modificationFunction: modifyInput(1), // shouldn't change the hash
expectedSignatureHash: "19fe2e0db681017f318fda705a39bbbad9c1085514cfbcff6fac01e1725f758b"},
expectedSignatureHash: "0a13d3cab42a6cf3a7ef96f7b28c9bb0d98c209a3032ff85a6bfa7dac520f2c2"},
{name: "native-all-anyonecanpay-0-modify-sequence", tx: nativeTx, hashType: allAnyoneCanPay, inputIndex: 0,
modificationFunction: modifySequence(1), // shouldn't change the hash
expectedSignatureHash: "19fe2e0db681017f318fda705a39bbbad9c1085514cfbcff6fac01e1725f758b"},
expectedSignatureHash: "0a13d3cab42a6cf3a7ef96f7b28c9bb0d98c209a3032ff85a6bfa7dac520f2c2"},
// sigHashNone
{name: "native-none-0", tx: nativeTx, hashType: none, inputIndex: 0,
expectedSignatureHash: "fafabaabf6349fee4e18626b4eff015472f2317576a8f4bf7b0eea1df6f3e32b"},
expectedSignatureHash: "da53f7c726b55357adb1b644265fe7b9b7897cadde91a942d6127195c2ce99dc"},
{name: "native-none-0-modify-output-1", tx: nativeTx, hashType: none, inputIndex: 0,
modificationFunction: modifyOutput(1), // shouldn't change the hash
expectedSignatureHash: "fafabaabf6349fee4e18626b4eff015472f2317576a8f4bf7b0eea1df6f3e32b"},
expectedSignatureHash: "da53f7c726b55357adb1b644265fe7b9b7897cadde91a942d6127195c2ce99dc"},
{name: "native-none-0-modify-sequence-0", tx: nativeTx, hashType: none, inputIndex: 0,
modificationFunction: modifySequence(0), // should change the hash
expectedSignatureHash: "daee0700e0ed4ab9f50de24d83e0bfce62999474ec8ceeb537ea35980662b601"},
expectedSignatureHash: "72c07c152a3792fb863d2de219ab4e863fe6779dc970a5c3958e26b3a3b9f139"},
{name: "native-none-0-modify-sequence-1", tx: nativeTx, hashType: none, inputIndex: 0,
modificationFunction: modifySequence(1), // shouldn't change the hash
expectedSignatureHash: "fafabaabf6349fee4e18626b4eff015472f2317576a8f4bf7b0eea1df6f3e32b"},
expectedSignatureHash: "da53f7c726b55357adb1b644265fe7b9b7897cadde91a942d6127195c2ce99dc"},
{name: "native-none-anyonecanpay-0", tx: nativeTx, hashType: noneAnyoneCanPay, inputIndex: 0,
expectedSignatureHash: "4e5c2d895f9711dc89c19d49ba478e9c8f4be0d82c9bd6b60d0361eb9b5296bc"},
expectedSignatureHash: "18eb79df328a516708f792cadfc4bf2be21b6add35fb6637a20347d532b1dce1"},
{name: "native-none-anyonecanpay-0-modify-amount-spent", tx: nativeTx, hashType: noneAnyoneCanPay, inputIndex: 0,
modificationFunction: modifyAmountSpent(0), // should change the hash
expectedSignatureHash: "9ce2f75eafc85b8e19133942c3143d14b61f2e7cc479fbc6d2fca026e50897f1"},
expectedSignatureHash: "674a8e6effa0bbfdb3df3ca45a7b17ab695cc0f9b3e0519e5bff165de13604e2"},
{name: "native-none-anyonecanpay-0-modify-script-public-key", tx: nativeTx, hashType: noneAnyoneCanPay, inputIndex: 0,
modificationFunction: modifyScriptPublicKey(0), // should change the hash
expectedSignatureHash: "c6c364190520fe6c0419c2f45e25bf084356333b03ac7aaec28251126398bda3"},
expectedSignatureHash: "7a879d339c9b948c5264f1c0b8463f551221b2dcd575623f178cbbfcf7e01987"},
// sigHashSingle
{name: "native-single-0", tx: nativeTx, hashType: single, inputIndex: 0,
expectedSignatureHash: "6ff01d5d7cd82e24bc9ca0edec8bd6931ffb5aa1d303f07ca05dc89757343a92"},
expectedSignatureHash: "99c0a4a381fed9dbd651450d6ca969a6e8cdd941e9d73be086a1ba6c90fb630f"},
{name: "native-single-0-modify-output-0", tx: nativeTx, hashType: single, inputIndex: 0,
modificationFunction: modifyOutput(0), // should change the hash
expectedSignatureHash: "d62af956aea369365bacc7e7f1aac106836994f1648311e82dd38da822c8771e"},
expectedSignatureHash: "92189a32391ca50a454d1853efed55acb1c49993911a1201df9891c866c483ee"},
{name: "native-single-0-modify-output-1", tx: nativeTx, hashType: single, inputIndex: 0,
modificationFunction: modifyOutput(1), // shouldn't change the hash
expectedSignatureHash: "6ff01d5d7cd82e24bc9ca0edec8bd6931ffb5aa1d303f07ca05dc89757343a92"},
expectedSignatureHash: "99c0a4a381fed9dbd651450d6ca969a6e8cdd941e9d73be086a1ba6c90fb630f"},
{name: "native-single-0-modify-sequence-0", tx: nativeTx, hashType: single, inputIndex: 0,
modificationFunction: modifySequence(0), // should change the hash
expectedSignatureHash: "46692229d45bf2ceacb18960faba29753e325c0ade26ecf94495b91daacb828d"},
expectedSignatureHash: "765e2289df98b3a5269f112d4b36d57fe32c74d42e04a3046c6a3c8dd78a4121"},
{name: "native-single-0-modify-sequence-1", tx: nativeTx, hashType: single, inputIndex: 0,
modificationFunction: modifySequence(1), // shouldn't change the hash
expectedSignatureHash: "6ff01d5d7cd82e24bc9ca0edec8bd6931ffb5aa1d303f07ca05dc89757343a92"},
expectedSignatureHash: "99c0a4a381fed9dbd651450d6ca969a6e8cdd941e9d73be086a1ba6c90fb630f"},
{name: "native-single-2-no-corresponding-output", tx: nativeTx, hashType: single, inputIndex: 2,
expectedSignatureHash: "d3cc385082a7f272ec2c8aae7f3a96ab2f49a4a4e1ed44d61af34058a7721281"},
expectedSignatureHash: "43de18c04d7fde81f49a40228d8730b4ceb0c66c77841c22622f59554769dd13"},
{name: "native-single-2-no-corresponding-output-modify-output-1", tx: nativeTx, hashType: single, inputIndex: 2,
modificationFunction: modifyOutput(1), // shouldn't change the hash
expectedSignatureHash: "d3cc385082a7f272ec2c8aae7f3a96ab2f49a4a4e1ed44d61af34058a7721281"},
expectedSignatureHash: "43de18c04d7fde81f49a40228d8730b4ceb0c66c77841c22622f59554769dd13"},
{name: "native-single-anyonecanpay-0", tx: nativeTx, hashType: singleAnyoneCanPay, inputIndex: 0,
expectedSignatureHash: "408fcfd8ceca135c0f54569ccf8ac727e1aa6b5a15f87ccca765f1d5808aa4ea"},
expectedSignatureHash: "2e270f86fd68f780a5aa8d250364ab6786d990040268e5d561d09dde365dfab7"},
{name: "native-single-anyonecanpay-2-no-corresponding-output", tx: nativeTx, hashType: singleAnyoneCanPay, inputIndex: 2,
expectedSignatureHash: "685fac0d0b9dd3c5556f266714c4f7f93475d49fa12befb18e8297bc062aeaba"},
expectedSignatureHash: "afc05dd6b4a530cc3a4d7d23126548bd1f31c793e9282cbbfa27ff5566219501"},
// subnetwork transaction
{name: "subnetwork-all-0", tx: subnetworkTx, hashType: all, inputIndex: 0,
expectedSignatureHash: "0e8b1433b761a220a61c0dc1f0fda909d49cef120d98d9f87344fef52dac0d8b"},
expectedSignatureHash: "5a67423ee048e067b94e2d4fc2960f62eceff6aa8b6c5ad71e3b7b7e5ff6bad7"},
{name: "subnetwork-all-modify-payload", tx: subnetworkTx, hashType: all, inputIndex: 0,
modificationFunction: modifyPayload, // should change the hash
expectedSignatureHash: "087315acb9193eaa14929dbe3d0ace80238aebe13eab3bf8db6c0a0d7ddb782e"},
expectedSignatureHash: "0d3bc5914da8dc8df081945fea1255359f380ca9baa8b31dfb3657c1e3c6038a"},
{name: "subnetwork-all-modify-gas", tx: subnetworkTx, hashType: all, inputIndex: 0,
modificationFunction: modifyGas, // should change the hash
expectedSignatureHash: "07a90408ef45864ae8354b07a74cf826a4621391425ba417470a6e680af4ce70"},
expectedSignatureHash: "70abe9f947d0a0f5d735f9a063db8af41fe5902940f2693a1782119063097094"},
{name: "subnetwork-all-subnetwork-id", tx: subnetworkTx, hashType: all, inputIndex: 0,
modificationFunction: modifySubnetworkID, // should change the hash
expectedSignatureHash: "4ca44c2e35729ae5efe831a77027f1a58a41dbdd853459c26cbfe7d6c88783fb"},
expectedSignatureHash: "571a0b7ea905b7a6ff7ab825b72d23f911bac0bfaa7c4c97a4887a3d090925d4"},
}
for _, test := range tests {
@@ -196,10 +196,132 @@ func TestCalculateSignatureHash(t *testing.T) {
tx = test.modificationFunction(tx)
}
actualSignatureHash, err := consensushashing.CalculateSignatureHash(
actualSignatureHash, err := consensushashing.CalculateSignatureHashSchnorr(
tx, test.inputIndex, test.hashType, &consensushashing.SighashReusedValues{})
if err != nil {
t.Errorf("%s: Error from CalculateSignatureHash: %+v", test.name, err)
t.Errorf("%s: Error from CalculateSignatureHashSchnorr: %+v", test.name, err)
continue
}
if actualSignatureHash.String() != test.expectedSignatureHash {
t.Errorf("%s: expected signature hash: '%s'; but got: '%s'",
test.name, test.expectedSignatureHash, actualSignatureHash)
}
}
}
func TestCalculateSignatureHashECDSA(t *testing.T) {
nativeTx, subnetworkTx, err := generateTxs()
if err != nil {
t.Fatalf("Error from generateTxs: %+v", err)
}
// Note: Expected values were generated by the same code that they test,
// As long as those were not verified using 3rd-party code they only check for regression, not correctness
tests := []struct {
name string
tx *externalapi.DomainTransaction
hashType consensushashing.SigHashType
inputIndex int
modificationFunction func(*externalapi.DomainTransaction) *externalapi.DomainTransaction
expectedSignatureHash string
}{
// native transactions
// sigHashAll
{name: "native-all-0", tx: nativeTx, hashType: all, inputIndex: 0,
expectedSignatureHash: "150a2bcd0296f76a3395a4a9982df46bf24ce93f955bc39c10ffc95b6c524eb3"},
{name: "native-all-0-modify-input-1", tx: nativeTx, hashType: all, inputIndex: 0,
modificationFunction: modifyInput(1), // should change the hash
expectedSignatureHash: "8fb5304e181b003e0c123ea6f6677abc3704feec47054e8a1c218b827bf57ca0"},
{name: "native-all-0-modify-output-1", tx: nativeTx, hashType: all, inputIndex: 0,
modificationFunction: modifyOutput(1), // should change the hash
expectedSignatureHash: "180cb36454aa80822694decde4fc711104e35a4bddf92286a83877f2b8d0aabb"},
{name: "native-all-0-modify-sequence-1", tx: nativeTx, hashType: all, inputIndex: 0,
modificationFunction: modifySequence(1), // should change the hash
expectedSignatureHash: "5b5f1c42a3c3c16bb4922777e2963c00e6a2cce39afa1980d2288053378f9632"},
{name: "native-all-anyonecanpay-0", tx: nativeTx, hashType: allAnyoneCanPay, inputIndex: 0,
expectedSignatureHash: "9473ffbe0db4914f2cd8fe5d67479224a02eb031882d9170b785d0d2c7bfcd1b"},
{name: "native-all-anyonecanpay-0-modify-input-0", tx: nativeTx, hashType: allAnyoneCanPay, inputIndex: 0,
modificationFunction: modifyInput(0), // should change the hash
expectedSignatureHash: "1208491d564c138d613f51b997394dbad23feca7c0ca88c7f36cdf6b9173327d"},
{name: "native-all-anyonecanpay-0-modify-input-1", tx: nativeTx, hashType: allAnyoneCanPay, inputIndex: 0,
modificationFunction: modifyInput(1), // shouldn't change the hash
expectedSignatureHash: "9473ffbe0db4914f2cd8fe5d67479224a02eb031882d9170b785d0d2c7bfcd1b"},
{name: "native-all-anyonecanpay-0-modify-sequence", tx: nativeTx, hashType: allAnyoneCanPay, inputIndex: 0,
modificationFunction: modifySequence(1), // shouldn't change the hash
expectedSignatureHash: "9473ffbe0db4914f2cd8fe5d67479224a02eb031882d9170b785d0d2c7bfcd1b"},
// sigHashNone
{name: "native-none-0", tx: nativeTx, hashType: none, inputIndex: 0,
expectedSignatureHash: "6e427f26e4a9c1a7fc556a8aabdedb8799a897bc5d42a0a18615e5a0f7639d8f"},
{name: "native-none-0-modify-output-1", tx: nativeTx, hashType: none, inputIndex: 0,
modificationFunction: modifyOutput(1), // shouldn't change the hash
expectedSignatureHash: "6e427f26e4a9c1a7fc556a8aabdedb8799a897bc5d42a0a18615e5a0f7639d8f"},
{name: "native-none-0-modify-sequence-0", tx: nativeTx, hashType: none, inputIndex: 0,
modificationFunction: modifySequence(0), // should change the hash
expectedSignatureHash: "57d76e2568cd3fc3426b4f8836fe900a2d20e740fad744949126651fd549f75e"},
{name: "native-none-0-modify-sequence-1", tx: nativeTx, hashType: none, inputIndex: 0,
modificationFunction: modifySequence(1), // shouldn't change the hash
expectedSignatureHash: "6e427f26e4a9c1a7fc556a8aabdedb8799a897bc5d42a0a18615e5a0f7639d8f"},
{name: "native-none-anyonecanpay-0", tx: nativeTx, hashType: noneAnyoneCanPay, inputIndex: 0,
expectedSignatureHash: "ef97a0f89d623302619f9aa2a00fce1522e72d4d255e6c6d3ed225ffc02f38ff"},
{name: "native-none-anyonecanpay-0-modify-amount-spent", tx: nativeTx, hashType: noneAnyoneCanPay, inputIndex: 0,
modificationFunction: modifyAmountSpent(0), // should change the hash
expectedSignatureHash: "043a2a943f02607be126ac6609ab2324aae389d784a4147f27101e7da379311a"},
{name: "native-none-anyonecanpay-0-modify-script-public-key", tx: nativeTx, hashType: noneAnyoneCanPay, inputIndex: 0,
modificationFunction: modifyScriptPublicKey(0), // should change the hash
expectedSignatureHash: "f2cd43d0d047cdcfbf8b6e12a86cfbf250f1e2c34dc5e631675a5f5b867bd9e6"},
// sigHashSingle
{name: "native-single-0", tx: nativeTx, hashType: single, inputIndex: 0,
expectedSignatureHash: "1cf376b9f180f59a1b9a5e420390198c20e1ba79c39349271632145fda175247"},
{name: "native-single-0-modify-output-0", tx: nativeTx, hashType: single, inputIndex: 0,
modificationFunction: modifyOutput(0), // should change the hash
expectedSignatureHash: "c2c7e77516a15f0f47f886b14cc47af2045eea15f176a9a560a9d47d8866958f"},
{name: "native-single-0-modify-output-1", tx: nativeTx, hashType: single, inputIndex: 0,
modificationFunction: modifyOutput(1), // shouldn't change the hash
expectedSignatureHash: "1cf376b9f180f59a1b9a5e420390198c20e1ba79c39349271632145fda175247"},
{name: "native-single-0-modify-sequence-0", tx: nativeTx, hashType: single, inputIndex: 0,
modificationFunction: modifySequence(0), // should change the hash
expectedSignatureHash: "2034eec2acc08c49d3896cc1bda214904ca850fc5989518885465b5a3154ee7f"},
{name: "native-single-0-modify-sequence-1", tx: nativeTx, hashType: single, inputIndex: 0,
modificationFunction: modifySequence(1), // shouldn't change the hash
expectedSignatureHash: "1cf376b9f180f59a1b9a5e420390198c20e1ba79c39349271632145fda175247"},
{name: "native-single-2-no-corresponding-output", tx: nativeTx, hashType: single, inputIndex: 2,
expectedSignatureHash: "84ae3bb03202efc587d97e5aea7b80581b82242b969e6dea13b8daa32d24c0c1"},
{name: "native-single-2-no-corresponding-output-modify-output-1", tx: nativeTx, hashType: single, inputIndex: 2,
modificationFunction: modifyOutput(1), // shouldn't change the hash
expectedSignatureHash: "84ae3bb03202efc587d97e5aea7b80581b82242b969e6dea13b8daa32d24c0c1"},
{name: "native-single-anyonecanpay-0", tx: nativeTx, hashType: singleAnyoneCanPay, inputIndex: 0,
expectedSignatureHash: "b2ccf259a65c3231d741a03420967b95563c3928cc15d3d15e8e795f383ab48b"},
{name: "native-single-anyonecanpay-2-no-corresponding-output", tx: nativeTx, hashType: singleAnyoneCanPay, inputIndex: 2,
expectedSignatureHash: "652c8cd0ac050e41aad347ea09ee788360eec70908ba22fe5bba5bdde49b8ae1"},
// subnetwork transaction
{name: "subnetwork-all-0", tx: subnetworkTx, hashType: all, inputIndex: 0,
expectedSignatureHash: "2e828c04f5f03e4ce4b3de1fa5303400da5fa504291b760f5f6d4e98fc24597f"},
{name: "subnetwork-all-modify-payload", tx: subnetworkTx, hashType: all, inputIndex: 0,
modificationFunction: modifyPayload, // should change the hash
expectedSignatureHash: "d5f3993aa8b7f47df52f78f2be9965f928c9cca9ac9e9542f1190b9d5ed6c17d"},
{name: "subnetwork-all-modify-gas", tx: subnetworkTx, hashType: all, inputIndex: 0,
modificationFunction: modifyGas, // should change the hash
expectedSignatureHash: "e74d4a9fa5cdf476299ebdfa03f3c8021a157f814731ea11f6a6d606dc5cd439"},
{name: "subnetwork-all-subnetwork-id", tx: subnetworkTx, hashType: all, inputIndex: 0,
modificationFunction: modifySubnetworkID, // should change the hash
expectedSignatureHash: "ca8bf9bc42cda2ec3ce8bee090011072e56ff4d0d8616d5c20cefe5f84d7fb37"},
}
for _, test := range tests {
tx := test.tx
if test.modificationFunction != nil {
tx = test.modificationFunction(tx)
}
actualSignatureHash, err := consensushashing.CalculateSignatureHashECDSA(
tx, test.inputIndex, test.hashType, &consensushashing.SighashReusedValues{})
if err != nil {
t.Errorf("%s: Error from CalculateSignatureHashECDSA: %+v", test.name, err)
continue
}
@@ -283,7 +405,7 @@ func generateTxs() (nativeTx, subnetworkTx *externalapi.DomainTransaction, err e
return nativeTx, subnetworkTx, nil
}
func BenchmarkCalculateSignatureHash(b *testing.B) {
func BenchmarkCalculateSignatureHashSchnorr(b *testing.B) {
sigHashTypes := []consensushashing.SigHashType{
consensushashing.SigHashAll,
consensushashing.SigHashNone,
@@ -300,9 +422,9 @@ func BenchmarkCalculateSignatureHash(b *testing.B) {
reusedValues := &consensushashing.SighashReusedValues{}
for inputIndex := range tx.Inputs {
sigHashType := sigHashTypes[inputIndex%len(sigHashTypes)]
_, err := consensushashing.CalculateSignatureHash(tx, inputIndex, sigHashType, reusedValues)
_, err := consensushashing.CalculateSignatureHashSchnorr(tx, inputIndex, sigHashType, reusedValues)
if err != nil {
b.Fatalf("Error from CalculateSignatureHash: %+v", err)
b.Fatalf("Error from CalculateSignatureHashSchnorr: %+v", err)
}
}
}

View File

@@ -22,28 +22,6 @@ const (
txEncodingExcludeSignatureScript = 1 << iota
)
// TransactionHashForSigning hashes the transaction and the given hash type in a way that is intended for
// signatures.
func TransactionHashForSigning(tx *externalapi.DomainTransaction, hashType uint32) *externalapi.DomainHash {
// Encode the header and hash everything prior to the number of
// transactions.
writer := hashes.NewTransactionSigningHashWriter()
err := serializeTransaction(writer, tx, txEncodingFull)
if err != nil {
// It seems like this could only happen if the writer returned an error.
// and this writer should never return an error (no allocations or possible failures)
// the only non-writer error path here is unknown types in `WriteElement`
panic(errors.Wrap(err, "TransactionHashForSigning() failed. this should never fail for structurally-valid transactions"))
}
err = serialization.WriteElement(writer, hashType)
if err != nil {
panic(errors.Wrap(err, "this should never happen. Hash digest should never return an error"))
}
return writer.Finalize()
}
// TransactionHash returns the transaction hash.
func TransactionHash(tx *externalapi.DomainTransaction) *externalapi.DomainHash {
// Encode the header and hash everything prior to the number of

View File

@@ -1,19 +1,26 @@
package hashes
import (
"crypto/sha256"
"github.com/pkg/errors"
"golang.org/x/crypto/blake2b"
)
const (
transcationHashDomain = "TransactionHash"
transcationIDDomain = "TransactionID"
transcationSigningDomain = "TransactionSigningHash"
blockDomain = "BlockHash"
proofOfWorkDomain = "ProofOfWorkHash"
merkleBranchDomain = "MerkleBranchHash"
transcationHashDomain = "TransactionHash"
transcationIDDomain = "TransactionID"
transcationSigningDomain = "TransactionSigningHash"
transcationSigningECDSADomain = "TransactionSigningHashECDSA"
blockDomain = "BlockHash"
proofOfWorkDomain = "ProofOfWorkHash"
merkleBranchDomain = "MerkleBranchHash"
)
// transactionSigningECDSADomainHash is a hashed version of transcationSigningECDSADomain that is used
// to make it a constant size. This is needed because this domain is used by sha256 hash writer, and
// sha256 doesn't support variable size domain separation.
var transactionSigningECDSADomainHash = sha256.Sum256([]byte(transcationSigningECDSADomain))
// NewTransactionHashWriter Returns a new HashWriter used for transaction hashes
func NewTransactionHashWriter() HashWriter {
blake, err := blake2b.New256([]byte(transcationHashDomain))
@@ -41,6 +48,13 @@ func NewTransactionSigningHashWriter() HashWriter {
return HashWriter{blake}
}
// NewTransactionSigningHashECDSAWriter Returns a new HashWriter used for signing on a transaction with ECDSA
func NewTransactionSigningHashECDSAWriter() HashWriter {
hashWriter := HashWriter{sha256.New()}
hashWriter.InfallibleWrite(transactionSigningECDSADomainHash[:])
return hashWriter
}
// NewBlockHashWriter Returns a new HashWriter used for hashing blocks
func NewBlockHashWriter() HashWriter {
blake, err := blake2b.New256([]byte(blockDomain))

View File

@@ -1,10 +1,9 @@
package hashes
import (
"hash"
"github.com/kaspanet/kaspad/domain/consensus/model/externalapi"
"github.com/pkg/errors"
"hash"
)
// HashWriter is used to incrementally hash data without concatenating all of the data to a single buffer

View File

@@ -15,7 +15,7 @@ although it is still fairly powerful.
## Examples
* [Standard Pay-to-pubkey-hash Script](http://godoc.org/github.com/kaspanet/kaspad/txscript#example-PayToAddrScript)
* [Standard Pay-to-pubkey Script](http://godoc.org/github.com/kaspanet/kaspad/txscript#example-PayToAddrScript)
Demonstrates creating a script which pays to a kaspa address. It also
prints the created script hex and uses the DisasmString function to display
the disassembled script.

View File

@@ -53,6 +53,7 @@ type Engine struct {
numOps int
flags ScriptFlags
sigCache *SigCache
sigCacheECDSA *SigCacheECDSA
sigHashReusedValues *consensushashing.SighashReusedValues
isP2SH bool // treat execution as pay-to-script-hash
savedFirstStack [][]byte // stack from first script for ps2h scripts
@@ -369,6 +370,14 @@ func (vm *Engine) checkPubKeyEncoding(pubKey []byte) error {
return scriptError(ErrPubKeyFormat, "unsupported public key type")
}
func (vm *Engine) checkPubKeyEncodingECDSA(pubKey []byte) error {
if len(pubKey) == 33 {
return nil
}
return scriptError(ErrPubKeyFormat, "unsupported public key type")
}
// checkSignatureLength returns whether or not the passed signature is
// in the correct Schnorr format.
func (vm *Engine) checkSignatureLength(sig []byte) error {
@@ -379,6 +388,14 @@ func (vm *Engine) checkSignatureLength(sig []byte) error {
return nil
}
func (vm *Engine) checkSignatureLengthECDSA(sig []byte) error {
if len(sig) != 64 {
message := fmt.Sprintf("invalid signature length %d", len(sig))
return scriptError(ErrSigLength, message)
}
return nil
}
// getStack returns the contents of stack as a byte array bottom up
func getStack(stack *stack) [][]byte {
array := make([][]byte, stack.Depth())
@@ -428,7 +445,7 @@ func (vm *Engine) SetAltStack(data [][]byte) {
// transaction, and input index. The flags modify the behavior of the script
// engine according to the description provided by each flag.
func NewEngine(scriptPubKey *externalapi.ScriptPublicKey, tx *externalapi.DomainTransaction, txIdx int, flags ScriptFlags,
sigCache *SigCache, sighashReusedValues *consensushashing.SighashReusedValues) (*Engine, error) {
sigCache *SigCache, sigCacheECDSA *SigCacheECDSA, sighashReusedValues *consensushashing.SighashReusedValues) (*Engine, error) {
// The provided transaction input index must refer to a valid input.
if txIdx < 0 || txIdx >= len(tx.Inputs) {
@@ -446,7 +463,7 @@ func NewEngine(scriptPubKey *externalapi.ScriptPublicKey, tx *externalapi.Domain
return nil, scriptError(ErrEvalFalse,
"false stack entry at end of script execution")
}
vm := Engine{scriptVersion: scriptPubKey.Version, flags: flags, sigCache: sigCache}
vm := Engine{scriptVersion: scriptPubKey.Version, flags: flags, sigCache: sigCache, sigCacheECDSA: sigCacheECDSA}
if vm.scriptVersion > constants.MaxScriptPublicKeyVersion {
return &vm, nil

View File

@@ -56,7 +56,7 @@ func TestBadPC(t *testing.T) {
scriptPubKey := &externalapi.ScriptPublicKey{Script: mustParseShortForm("NOP", 0), Version: 0}
for _, test := range tests {
vm, err := NewEngine(scriptPubKey, tx, 0, 0, nil, &consensushashing.SighashReusedValues{})
vm, err := NewEngine(scriptPubKey, tx, 0, 0, nil, nil, &consensushashing.SighashReusedValues{})
if err != nil {
t.Errorf("Failed to create script: %v", err)
}
@@ -124,7 +124,7 @@ func TestCheckErrorCondition(t *testing.T) {
scriptPubKey := &externalapi.ScriptPublicKey{Script: mustParseShortForm(test.script, 0), Version: 0}
vm, err := NewEngine(scriptPubKey, tx, 0, 0, nil, &consensushashing.SighashReusedValues{})
vm, err := NewEngine(scriptPubKey, tx, 0, 0, nil, nil, &consensushashing.SighashReusedValues{})
if err != nil {
t.Errorf("TestCheckErrorCondition: %d: failed to create script: %v", i, err)
}
@@ -252,7 +252,7 @@ func TestDisasmPC(t *testing.T) {
scriptPubKey := &externalapi.ScriptPublicKey{Script: mustParseShortForm("OP_DROP NOP TRUE", 0), Version: 0}
vm, err := NewEngine(scriptPubKey, tx, 0, 0, nil, &consensushashing.SighashReusedValues{})
vm, err := NewEngine(scriptPubKey, tx, 0, 0, nil, nil, &consensushashing.SighashReusedValues{})
if err != nil {
t.Fatalf("failed to create script: %v", err)
}
@@ -315,7 +315,7 @@ func TestDisasmScript(t *testing.T) {
}
scriptPubKey := &externalapi.ScriptPublicKey{Script: mustParseShortForm("OP_DROP NOP TRUE", 0), Version: 0}
vm, err := NewEngine(scriptPubKey, tx, 0, 0, nil, &consensushashing.SighashReusedValues{})
vm, err := NewEngine(scriptPubKey, tx, 0, 0, nil, nil, &consensushashing.SighashReusedValues{})
if err != nil {
t.Fatalf("failed to create script: %v", err)
}

View File

@@ -22,7 +22,7 @@ func ExamplePayToAddrScript() {
// which is useful to ensure the accuracy of the address and determine
// the address type. It is also required for the upcoming call to
// PayToAddrScript.
addressStr := "kaspa:qqfgqp8l9l90zwetj84k2jcac2m8falvvyy8xjtnhdac2m8falvvyvc9fuvqt"
addressStr := "kaspa:qqj9fg59mptxkr9j0y53j5mwurcmda5mtza9n6v9pm9uj8h0wgk6uma5pvumr"
address, err := util.DecodeAddress(addressStr, util.Bech32PrefixKaspa)
if err != nil {
fmt.Println(err)
@@ -45,15 +45,15 @@ func ExamplePayToAddrScript() {
fmt.Println("Script Disassembly:", disasm)
// Output:
// Script Hex: 76aa20128004ff2fcaf13b2b91eb654b1dc2b674f7ec6108734973bb7b856ce9efd8c288ac
// Script Disassembly: OP_DUP OP_BLAKE2B 128004ff2fcaf13b2b91eb654b1dc2b674f7ec6108734973bb7b856ce9efd8c2 OP_EQUALVERIFY OP_CHECKSIG
// Script Hex: 202454a285d8566b0cb2792919536ee0f1b6f69b58ba59e9850ecbc91eef722daeac
// Script Disassembly: 2454a285d8566b0cb2792919536ee0f1b6f69b58ba59e9850ecbc91eef722dae OP_CHECKSIG
}
// This example demonstrates extracting information from a standard public key
// script.
func ExampleExtractScriptPubKeyAddress() {
// Start with a standard pay-to-pubkey-hash script.
scriptHex := "76aa20128004ff2fcaf13b2b91eb65128004ff2fcaf13b2b91eb654b1dc2b674f7ec6188ac"
// Start with a standard pay-to-pubkey script.
scriptHex := "2089ac24ea10bb751af4939623ccc5e550d96842b64e8fca0f63e94b4373fd555eac"
script, err := hex.DecodeString(scriptHex)
if err != nil {
fmt.Println(err)
@@ -74,6 +74,6 @@ func ExampleExtractScriptPubKeyAddress() {
fmt.Println("Address:", address)
// Output:
// Script Class: pubkeyhash
// Address: kaspa:qqfgqp8l9l90zwetj84k2y5qqnljljh38v4er6m9fvwu9dn57lkxztu8quj75
// Script Class: pubkey
// Address: kaspa:qzy6cf82zzah2xh5jwtz8nx9u4gdj6zzke8gljs0v055ksmnl424u6fv7ajrs
}

View File

@@ -204,9 +204,9 @@ const (
OpUnknown166 = 0xa6 // 166
OpUnknown167 = 0xa7 // 167
OpSHA256 = 0xa8 // 168
OpUnknown169 = 0xa9 // 169
OpCheckMultiSigECDSA = 0xa9 // 169
OpBlake2b = 0xaa // 170
OpUnknown171 = 0xab // 171
OpCheckSigECDSA = 0xab // 171
OpCheckSig = 0xac // 172
OpCheckSigVerify = 0xad // 173
OpCheckMultiSig = 0xae // 174
@@ -485,8 +485,10 @@ var opcodeArray = [256]opcode{
OpWithin: {OpWithin, "OP_WITHIN", 1, opcodeWithin},
// Crypto opcodes.
OpCheckMultiSigECDSA: {OpCheckMultiSigECDSA, "OP_CHECKMULTISIGECDSA", 1, opcodeCheckMultiSigECDSA},
OpSHA256: {OpSHA256, "OP_SHA256", 1, opcodeSha256},
OpBlake2b: {OpBlake2b, "OP_BLAKE2B", 1, opcodeBlake2b},
OpCheckSigECDSA: {OpCheckSigECDSA, "OP_CHECKSIGECDSA", 1, opcodeCheckSigECDSA},
OpCheckSig: {OpCheckSig, "OP_CHECKSIG", 1, opcodeCheckSig},
OpCheckSigVerify: {OpCheckSigVerify, "OP_CHECKSIGVERIFY", 1, opcodeCheckSigVerify},
OpCheckMultiSig: {OpCheckMultiSig, "OP_CHECKMULTISIG", 1, opcodeCheckMultiSig},
@@ -507,8 +509,6 @@ var opcodeArray = [256]opcode{
// Undefined opcodes.
OpUnknown166: {OpUnknown166, "OP_UNKNOWN166", 1, opcodeInvalid},
OpUnknown167: {OpUnknown167, "OP_UNKNOWN167", 1, opcodeInvalid},
OpUnknown169: {OpUnknown169, "OP_UNKNOWN169", 1, opcodeInvalid},
OpUnknown171: {OpUnknown171, "OP_UNKNOWN171", 1, opcodeInvalid},
OpUnknown188: {OpUnknown188, "OP_UNKNOWN188", 1, opcodeInvalid},
OpUnknown189: {OpUnknown189, "OP_UNKNOWN189", 1, opcodeInvalid},
OpUnknown190: {OpUnknown190, "OP_UNKNOWN190", 1, opcodeInvalid},
@@ -1988,7 +1988,7 @@ func opcodeCheckSig(op *parsedOpcode, vm *Engine) error {
}
// Generate the signature hash based on the signature hash type.
sigHash, err := consensushashing.CalculateSignatureHash(&vm.tx, vm.txIdx, hashType, vm.sigHashReusedValues)
sigHash, err := consensushashing.CalculateSignatureHashSchnorr(&vm.tx, vm.txIdx, hashType, vm.sigHashReusedValues)
if err != nil {
vm.dstack.PushBool(false)
return nil
@@ -2027,6 +2027,89 @@ func opcodeCheckSig(op *parsedOpcode, vm *Engine) error {
return nil
}
func opcodeCheckSigECDSA(op *parsedOpcode, vm *Engine) error {
pkBytes, err := vm.dstack.PopByteArray()
if err != nil {
return err
}
fullSigBytes, err := vm.dstack.PopByteArray()
if err != nil {
return err
}
// The signature actually needs needs to be longer than this, but at
// least 1 byte is needed for the hash type below. The full length is
// checked depending on the script flags and upon parsing the signature.
if len(fullSigBytes) < 1 {
vm.dstack.PushBool(false)
return nil
}
// Trim off hashtype from the signature string and check if the
// signature and pubkey conform to the strict encoding requirements
// depending on the flags.
//
// NOTE: When the strict encoding flags are set, any errors in the
// signature or public encoding here result in an immediate script error
// (and thus no result bool is pushed to the data stack). This differs
// from the logic below where any errors in parsing the signature is
// treated as the signature failure resulting in false being pushed to
// the data stack. This is required because the more general script
// validation consensus rules do not have the new strict encoding
// requirements enabled by the flags.
hashType := consensushashing.SigHashType(fullSigBytes[len(fullSigBytes)-1])
sigBytes := fullSigBytes[:len(fullSigBytes)-1]
if !hashType.IsStandardSigHashType() {
return scriptError(ErrInvalidSigHashType, fmt.Sprintf("invalid hash type 0x%x", hashType))
}
if err := vm.checkSignatureLengthECDSA(sigBytes); err != nil {
return err
}
if err := vm.checkPubKeyEncodingECDSA(pkBytes); err != nil {
return err
}
// Generate the signature hash based on the signature hash type.
sigHash, err := consensushashing.CalculateSignatureHashECDSA(&vm.tx, vm.txIdx, hashType, vm.sigHashReusedValues)
if err != nil {
vm.dstack.PushBool(false)
return nil
}
pubKey, err := secp256k1.DeserializeECDSAPubKey(pkBytes)
if err != nil {
vm.dstack.PushBool(false)
return nil
}
signature, err := secp256k1.DeserializeECDSASignatureFromSlice(sigBytes)
if err != nil {
vm.dstack.PushBool(false)
return nil
}
var valid bool
secpHash := secp256k1.Hash(*sigHash.ByteArray())
if vm.sigCacheECDSA != nil {
valid = vm.sigCacheECDSA.Exists(secpHash, signature, pubKey)
if !valid && pubKey.ECDSAVerify(&secpHash, signature) {
vm.sigCacheECDSA.Add(secpHash, signature, pubKey)
valid = true
}
} else {
valid = pubKey.ECDSAVerify(&secpHash, signature)
}
if !valid && len(sigBytes) > 0 {
str := "signature not empty on failed checksig"
return scriptError(ErrNullFail, str)
}
vm.dstack.PushBool(valid)
return nil
}
// opcodeCheckSigVerify is a combination of opcodeCheckSig and opcodeVerify.
// The opcodeCheckSig function is invoked followed by opcodeVerify. See the
// documentation for each of those opcodes for more details.
@@ -2049,6 +2132,12 @@ type parsedSigInfo struct {
parsed bool
}
type parsedSigInfoECDSA struct {
signature []byte
parsedSignature *secp256k1.ECDSASignature
parsed bool
}
// opcodeCheckMultiSig treats the top item on the stack as an integer number of
// public keys, followed by that many entries as raw data representing the public
// keys, followed by the integer number of signatures, followed by that many
@@ -2194,7 +2283,7 @@ func opcodeCheckMultiSig(op *parsedOpcode, vm *Engine) error {
}
// Generate the signature hash based on the signature hash type.
sigHash, err := consensushashing.CalculateSignatureHash(&vm.tx, vm.txIdx, hashType, vm.sigHashReusedValues)
sigHash, err := consensushashing.CalculateSignatureHashSchnorr(&vm.tx, vm.txIdx, hashType, vm.sigHashReusedValues)
if err != nil {
return err
}
@@ -2231,6 +2320,175 @@ func opcodeCheckMultiSig(op *parsedOpcode, vm *Engine) error {
return nil
}
func opcodeCheckMultiSigECDSA(op *parsedOpcode, vm *Engine) error {
numKeys, err := vm.dstack.PopInt()
if err != nil {
return err
}
numPubKeys := int(numKeys.Int32())
if numPubKeys < 0 {
str := fmt.Sprintf("number of pubkeys %d is negative",
numPubKeys)
return scriptError(ErrInvalidPubKeyCount, str)
}
if numPubKeys > MaxPubKeysPerMultiSig {
str := fmt.Sprintf("too many pubkeys: %d > %d",
numPubKeys, MaxPubKeysPerMultiSig)
return scriptError(ErrInvalidPubKeyCount, str)
}
vm.numOps += numPubKeys
if vm.numOps > MaxOpsPerScript {
str := fmt.Sprintf("exceeded max operation limit of %d",
MaxOpsPerScript)
return scriptError(ErrTooManyOperations, str)
}
pubKeys := make([][]byte, 0, numPubKeys)
for i := 0; i < numPubKeys; i++ {
pubKey, err := vm.dstack.PopByteArray()
if err != nil {
return err
}
pubKeys = append(pubKeys, pubKey)
}
numSigs, err := vm.dstack.PopInt()
if err != nil {
return err
}
numSignatures := int(numSigs.Int32())
if numSignatures < 0 {
str := fmt.Sprintf("number of signatures %d is negative",
numSignatures)
return scriptError(ErrInvalidSignatureCount, str)
}
if numSignatures > numPubKeys {
str := fmt.Sprintf("more signatures than pubkeys: %d > %d",
numSignatures, numPubKeys)
return scriptError(ErrInvalidSignatureCount, str)
}
signatures := make([]*parsedSigInfoECDSA, 0, numSignatures)
for i := 0; i < numSignatures; i++ {
signature, err := vm.dstack.PopByteArray()
if err != nil {
return err
}
sigInfo := &parsedSigInfoECDSA{signature: signature}
signatures = append(signatures, sigInfo)
}
success := true
numPubKeys++
pubKeyIdx := -1
signatureIdx := 0
for numSignatures > 0 {
// When there are more signatures than public keys remaining,
// there is no way to succeed since too many signatures are
// invalid, so exit early.
pubKeyIdx++
numPubKeys--
if numSignatures > numPubKeys {
success = false
break
}
sigInfo := signatures[signatureIdx]
pubKey := pubKeys[pubKeyIdx]
// The order of the signature and public key evaluation is
// important here since it can be distinguished by an
// OP_CHECKMULTISIG NOT when the strict encoding flag is set.
rawSig := sigInfo.signature
if len(rawSig) == 0 {
// Skip to the next pubkey if signature is empty.
continue
}
// Split the signature into hash type and signature components.
hashType := consensushashing.SigHashType(rawSig[len(rawSig)-1])
signature := rawSig[:len(rawSig)-1]
// Only parse and check the signature encoding once.
var parsedSig *secp256k1.ECDSASignature
if !sigInfo.parsed {
if !hashType.IsStandardSigHashType() {
return scriptError(ErrInvalidSigHashType, fmt.Sprintf("invalid hash type 0x%x", hashType))
}
if err := vm.checkSignatureLengthECDSA(signature); err != nil {
return err
}
// Parse the signature.
parsedSig, err = secp256k1.DeserializeECDSASignatureFromSlice(signature)
sigInfo.parsed = true
if err != nil {
continue
}
sigInfo.parsedSignature = parsedSig
} else {
// Skip to the next pubkey if the signature is invalid.
if sigInfo.parsedSignature == nil {
continue
}
// Use the already parsed signature.
parsedSig = sigInfo.parsedSignature
}
if err := vm.checkPubKeyEncodingECDSA(pubKey); err != nil {
return err
}
// Parse the pubkey.
parsedPubKey, err := secp256k1.DeserializeECDSAPubKey(pubKey)
if err != nil {
continue
}
// Generate the signature hash based on the signature hash type.
sigHash, err := consensushashing.CalculateSignatureHashECDSA(&vm.tx, vm.txIdx, hashType, vm.sigHashReusedValues)
if err != nil {
return err
}
secpHash := secp256k1.Hash(*sigHash.ByteArray())
var valid bool
if vm.sigCacheECDSA != nil {
valid = vm.sigCacheECDSA.Exists(secpHash, parsedSig, parsedPubKey)
if !valid && parsedPubKey.ECDSAVerify(&secpHash, parsedSig) {
vm.sigCacheECDSA.Add(secpHash, parsedSig, parsedPubKey)
valid = true
}
} else {
valid = parsedPubKey.ECDSAVerify(&secpHash, parsedSig)
}
if valid {
// PubKey verified, move on to the next signature.
signatureIdx++
numSignatures--
}
}
if !success {
for _, sig := range signatures {
if len(sig.signature) > 0 {
str := "not all signatures empty on failed checkmultisig"
return scriptError(ErrNullFail, str)
}
}
}
vm.dstack.PushBool(success)
return nil
}
// opcodeCheckMultiSigVerify is a combination of opcodeCheckMultiSig and
// opcodeVerify. The opcodeCheckMultiSig is invoked followed by opcodeVerify.
// See the documentation for each of those opcodes for more details.

View File

@@ -71,9 +71,9 @@ func TestOpcodeDisasm(t *testing.T) {
0x9f: "OP_LESSTHAN", 0xa0: "OP_GREATERTHAN",
0xa1: "OP_LESSTHANOREQUAL", 0xa2: "OP_GREATERTHANOREQUAL",
0xa3: "OP_MIN", 0xa4: "OP_MAX", 0xa5: "OP_WITHIN",
0xa8: "OP_SHA256",
0xa8: "OP_SHA256", 0xa9: "OP_CHECKMULTISIGECDSA",
0xaa: "OP_BLAKE2B",
0xac: "OP_CHECKSIG", 0xad: "OP_CHECKSIGVERIFY",
0xab: "OP_CHECKSIGECDSA", 0xac: "OP_CHECKSIG", 0xad: "OP_CHECKSIGVERIFY",
0xae: "OP_CHECKMULTISIG", 0xaf: "OP_CHECKMULTISIGVERIFY",
0xb0: "OP_CHECKLOCKTIMEVERIFY", 0xb1: "OP_CHECKSEQUENCEVERIFY",
0xfa: "OP_SMALLINTEGER", 0xfb: "OP_PUBKEYS",
@@ -186,8 +186,8 @@ func TestOpcodeDisasm(t *testing.T) {
}
func isOpUnknown(opcodeVal int) bool {
return opcodeVal >= 0xba && opcodeVal <= 0xf9 || opcodeVal == 0xfc || opcodeVal == 0xab ||
opcodeVal == 0xa6 || opcodeVal == 0xa7 || opcodeVal == 0xa9
return opcodeVal >= 0xba && opcodeVal <= 0xf9 || opcodeVal == 0xfc ||
opcodeVal == 0xa6 || opcodeVal == 0xa7
}
func isNumberedNop(opcodeVal int) bool {

View File

@@ -260,8 +260,10 @@ func createSpendingTx(sigScript []byte, scriptPubKey *externalapi.ScriptPublicKe
func testScripts(t *testing.T, tests [][]interface{}, useSigCache bool) {
// Create a signature cache to use only if requested.
var sigCache *SigCache
var sigCacheECDSA *SigCacheECDSA
if useSigCache {
sigCache = NewSigCache(10)
sigCacheECDSA = NewSigCacheECDSA(10)
}
for i, test := range tests {
@@ -343,7 +345,7 @@ func testScripts(t *testing.T, tests [][]interface{}, useSigCache bool) {
// used, then create a new engine to execute the scripts.
tx := createSpendingTx(scriptSig, scriptPubKey)
vm, err := NewEngine(scriptPubKey, tx, 0, flags, sigCache, &consensushashing.SighashReusedValues{})
vm, err := NewEngine(scriptPubKey, tx, 0, flags, sigCache, sigCacheECDSA, &consensushashing.SighashReusedValues{})
if err == nil {
err = vm.Execute()
}

View File

@@ -19,7 +19,7 @@ type sigCacheEntry struct {
pubKey *secp256k1.SchnorrPublicKey
}
// SigCache implements an ECDSA signature verification cache with a randomized
// SigCache implements an Schnorr signature verification cache with a randomized
// entry eviction policy. Only valid signatures will be added to the cache. The
// benefits of SigCache are two fold. Firstly, usage of SigCache mitigates a DoS
// attack wherein an attack causes a victim's client to hang due to worst-case

View File

@@ -0,0 +1,90 @@
// Copyright (c) 2015-2016 The btcsuite developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
package txscript
import (
"github.com/kaspanet/go-secp256k1"
)
// sigCacheEntryECDSA represents an entry in the SigCache. Entries within the
// SigCache are keyed according to the sigHash of the signature. In the
// scenario of a cache-hit (according to the sigHash), an additional comparison
// of the signature, and public key will be executed in order to ensure a complete
// match. In the occasion that two sigHashes collide, the newer sigHash will
// simply overwrite the existing entry.
type sigCacheEntryECDSA struct {
sig *secp256k1.ECDSASignature
pubKey *secp256k1.ECDSAPublicKey
}
// SigCacheECDSA implements an ECDSA signature verification cache with a randomized
// entry eviction policy. Only valid signatures will be added to the cache. The
// benefits of SigCache are two fold. Firstly, usage of SigCache mitigates a DoS
// attack wherein an attack causes a victim's client to hang due to worst-case
// behavior triggered while processing attacker crafted invalid transactions. A
// detailed description of the mitigated DoS attack can be found here:
// https://bitslog.wordpress.com/2013/01/23/fixed-bitcoin-vulnerability-explanation-why-the-signature-cache-is-a-dos-protection/.
// Secondly, usage of the SigCache introduces a signature verification
// optimization which speeds up the validation of transactions within a block,
// if they've already been seen and verified within the mempool.
type SigCacheECDSA struct {
validSigs map[secp256k1.Hash]sigCacheEntryECDSA
maxEntries uint
}
// NewSigCacheECDSA creates and initializes a new instance of SigCache. Its sole
// parameter 'maxEntries' represents the maximum number of entries allowed to
// exist in the SigCache at any particular moment. Random entries are evicted
// to make room for new entries that would cause the number of entries in the
// cache to exceed the max.
func NewSigCacheECDSA(maxEntries uint) *SigCacheECDSA {
return &SigCacheECDSA{
validSigs: make(map[secp256k1.Hash]sigCacheEntryECDSA, maxEntries),
maxEntries: maxEntries,
}
}
// Exists returns true if an existing entry of 'sig' over 'sigHash' for public
// key 'pubKey' is found within the SigCache. Otherwise, false is returned.
//
// NOTE: This function is safe for concurrent access. Readers won't be blocked
// unless there exists a writer, adding an entry to the SigCache.
func (s *SigCacheECDSA) Exists(sigHash secp256k1.Hash, sig *secp256k1.ECDSASignature, pubKey *secp256k1.ECDSAPublicKey) bool {
entry, ok := s.validSigs[sigHash]
return ok && entry.pubKey.IsEqual(pubKey) && entry.sig.IsEqual(sig)
}
// Add adds an entry for a signature over 'sigHash' under public key 'pubKey'
// to the signature cache. In the event that the SigCache is 'full', an
// existing entry is randomly chosen to be evicted in order to make space for
// the new entry.
//
// NOTE: This function is safe for concurrent access. Writers will block
// simultaneous readers until function execution has concluded.
func (s *SigCacheECDSA) Add(sigHash secp256k1.Hash, sig *secp256k1.ECDSASignature, pubKey *secp256k1.ECDSAPublicKey) {
if s.maxEntries == 0 {
return
}
// If adding this new entry will put us over the max number of allowed
// entries, then evict an entry.
if uint(len(s.validSigs)+1) > s.maxEntries {
// Remove a random entry from the map. Relying on the random
// starting point of Go's map iteration. It's worth noting that
// the random iteration starting point is not 100% guaranteed
// by the spec, however most Go compilers support it.
// Ultimately, the iteration order isn't important here because
// in order to manipulate which items are evicted, an adversary
// would need to be able to execute preimage attacks on the
// hashing function in order to start eviction at a specific
// entry.
for sigEntry := range s.validSigs {
delete(s.validSigs, sigEntry)
break
}
}
s.validSigs[sigHash] = sigCacheEntryECDSA{sig, pubKey}
}

View File

@@ -19,7 +19,7 @@ import (
func RawTxInSignature(tx *externalapi.DomainTransaction, idx int, hashType consensushashing.SigHashType,
key *secp256k1.SchnorrKeyPair, sighashReusedValues *consensushashing.SighashReusedValues) ([]byte, error) {
hash, err := consensushashing.CalculateSignatureHash(tx, idx, hashType, sighashReusedValues)
hash, err := consensushashing.CalculateSignatureHashSchnorr(tx, idx, hashType, sighashReusedValues)
if err != nil {
return nil, err
}
@@ -32,8 +32,26 @@ func RawTxInSignature(tx *externalapi.DomainTransaction, idx int, hashType conse
return append(signature.Serialize()[:], byte(hashType)), nil
}
// RawTxInSignatureECDSA returns the serialized ECDSA signature for the input idx of
// the given transaction, with hashType appended to it.
func RawTxInSignatureECDSA(tx *externalapi.DomainTransaction, idx int, hashType consensushashing.SigHashType,
key *secp256k1.ECDSAPrivateKey, sighashReusedValues *consensushashing.SighashReusedValues) ([]byte, error) {
hash, err := consensushashing.CalculateSignatureHashECDSA(tx, idx, hashType, sighashReusedValues)
if err != nil {
return nil, err
}
secpHash := secp256k1.Hash(*hash.ByteArray())
signature, err := key.ECDSASign(&secpHash)
if err != nil {
return nil, errors.Errorf("cannot sign tx input: %s", err)
}
return append(signature.Serialize()[:], byte(hashType)), nil
}
// SignatureScript creates an input signature script for tx to spend KAS sent
// from a previous output to the owner of privKey. tx must include all
// from a previous output to the owner of a Schnorr private key. tx must include all
// transaction inputs and outputs, however txin scripts are allowed to be filled
// or empty. The returned script is calculated to be used as the idx'th txin
// sigscript for tx. script is the ScriptPublicKey of the previous output being used
@@ -48,16 +66,26 @@ func SignatureScript(tx *externalapi.DomainTransaction, idx int, hashType consen
return nil, err
}
pk, err := privKey.SchnorrPublicKey()
if err != nil {
return nil, err
}
pkData, err := pk.Serialize()
return NewScriptBuilder().AddData(sig).Script()
}
// SignatureScriptECDSA creates an input signature script for tx to spend KAS sent
// from a previous output to the owner of an ECDSA private key. tx must include all
// transaction inputs and outputs, however txin scripts are allowed to be filled
// or empty. The returned script is calculated to be used as the idx'th txin
// sigscript for tx. script is the ScriptPublicKey of the previous output being used
// as the idx'th input. privKey is serialized in either a compressed or
// uncompressed format based on compress. This format must match the same format
// used to generate the payment address, or the script validation will fail.
func SignatureScriptECDSA(tx *externalapi.DomainTransaction, idx int, hashType consensushashing.SigHashType,
privKey *secp256k1.ECDSAPrivateKey, sighashReusedValues *consensushashing.SighashReusedValues) ([]byte, error) {
sig, err := RawTxInSignatureECDSA(tx, idx, hashType, privKey, sighashReusedValues)
if err != nil {
return nil, err
}
return NewScriptBuilder().AddData(sig).AddData(pkData[:]).Script()
return NewScriptBuilder().AddData(sig).Script()
}
func sign(dagParams *dagconfig.Params, tx *externalapi.DomainTransaction, idx int,
@@ -71,7 +99,7 @@ func sign(dagParams *dagconfig.Params, tx *externalapi.DomainTransaction, idx in
}
switch class {
case PubKeyHashTy:
case PubKeyTy:
// look up key for address
key, err := kdb.GetKey(address)
if err != nil {

View File

@@ -54,7 +54,7 @@ func checkScripts(msg string, tx *externalapi.DomainTransaction, idx int, sigScr
tx.Inputs[idx].SignatureScript = sigScript
var flags ScriptFlags
vm, err := NewEngine(scriptPubKey, tx, idx,
flags, nil, &consensushashing.SighashReusedValues{})
flags, nil, nil, &consensushashing.SighashReusedValues{})
if err != nil {
return errors.Errorf("failed to make script engine for %s: %v",
msg, err)
@@ -143,7 +143,7 @@ func TestSignTxOutput(t *testing.T) {
if err != nil {
t.Fatal(err)
}
// Pay to Pubkey Hash (merging with correct)
// Pay to Pubkey (merging with correct)
for _, hashType := range hashTypes {
for _, input := range tx.Inputs {
input.UTXOEntry = utxo.NewUTXOEntry(500, scriptPubKey, false, 100)
@@ -180,14 +180,14 @@ func TestSignTxOutput(t *testing.T) {
err = checkScripts(msg, tx, i, sigScript, scriptPubKey)
if err != nil {
t.Errorf("twice signed script invalid for "+
t.Fatalf("twice signed script invalid for "+
"%s: %v", msg, err)
break
}
}
}
// Pay to Pubkey Hash (compressed)
// Pay to Pubkey
for _, hashType := range hashTypes {
for i := range tx.Inputs {
msg := fmt.Sprintf("%d:%d", hashType, i)
@@ -213,8 +213,7 @@ func TestSignTxOutput(t *testing.T) {
break
}
address, err := util.NewAddressPubKeyHash(
util.HashBlake2b(serializedPubKey[:]), util.Bech32PrefixKaspaTest)
address, err := util.NewAddressPublicKey(serializedPubKey[:], util.Bech32PrefixKaspaTest)
if err != nil {
t.Errorf("failed to make address for %s: %v",
msg, err)
@@ -238,7 +237,7 @@ func TestSignTxOutput(t *testing.T) {
}
}
// Pay to Pubkey Hash with duplicate merge
// Pay to Pubkey with duplicate merge
for _, hashType := range hashTypes {
for i := range tx.Inputs {
msg := fmt.Sprintf("%d:%d", hashType, i)
@@ -264,8 +263,7 @@ func TestSignTxOutput(t *testing.T) {
break
}
address, err := util.NewAddressPubKeyHash(
util.HashBlake2b(serializedPubKey[:]), util.Bech32PrefixKaspaTest)
address, err := util.NewAddressPublicKey(serializedPubKey[:], util.Bech32PrefixKaspaTest)
if err != nil {
t.Errorf("failed to make address for %s: %v",
msg, err)
@@ -316,7 +314,7 @@ func TestSignTxOutput(t *testing.T) {
// As before, but with p2sh now.
// Pay to Pubkey Hash
// Pay to Pubkey
for _, hashType := range hashTypes {
for i := range tx.Inputs {
msg := fmt.Sprintf("%d:%d", hashType, i)
@@ -342,8 +340,7 @@ func TestSignTxOutput(t *testing.T) {
break
}
address, err := util.NewAddressPubKeyHash(
util.HashBlake2b(serializedPubKey[:]), util.Bech32PrefixKaspaTest)
address, err := util.NewAddressPublicKey(serializedPubKey[:], util.Bech32PrefixKaspaTest)
if err != nil {
t.Errorf("failed to make address for %s: %v",
msg, err)
@@ -381,7 +378,7 @@ func TestSignTxOutput(t *testing.T) {
}
}
// Pay to Pubkey Hash (compressed) with duplicate merge
// Pay to Pubkey with duplicate merge
for _, hashType := range hashTypes {
for i := range tx.Inputs {
msg := fmt.Sprintf("%d:%d", hashType, i)
@@ -407,8 +404,7 @@ func TestSignTxOutput(t *testing.T) {
break
}
address, err := util.NewAddressPubKeyHash(
util.HashBlake2b(serializedPubKey[:]), util.Bech32PrefixKaspaTest)
address, err := util.NewAddressPublicKey(serializedPubKey[:], util.Bech32PrefixKaspaTest)
if err != nil {
t.Errorf("failed to make address for %s: %v",
msg, err)
@@ -474,7 +470,7 @@ func TestSignTxOutput(t *testing.T) {
}
func generateKeys() (keyPair *secp256k1.SchnorrKeyPair, scriptPublicKey *externalapi.ScriptPublicKey,
addressPubKeyHash *util.AddressPubKeyHash, err error) {
addressPubKeyHash *util.AddressPublicKey, err error) {
key, err := secp256k1.GenerateSchnorrKeyPair()
if err != nil {
@@ -490,8 +486,7 @@ func generateKeys() (keyPair *secp256k1.SchnorrKeyPair, scriptPublicKey *externa
if err != nil {
return nil, nil, nil, errors.Errorf("failed to serialize a pubkey for %s: %s", pubKey, err)
}
address, err := util.NewAddressPubKeyHash(
util.HashBlake2b(serializedPubKey[:]), util.Bech32PrefixKaspaTest)
address, err := util.NewAddressPublicKey(serializedPubKey[:], util.Bech32PrefixKaspaTest)
if err != nil {
return nil, nil, nil, errors.Errorf("failed to make address for %s: %s", serializedPubKey, err)
}
@@ -534,12 +529,10 @@ var (
oldCompressedScriptPubKey = &externalapi.ScriptPublicKey{[]byte{0x76, 0xa9, 0x14, 0x27, 0x4d, 0x9f, 0x7f,
0x61, 0x7e, 0x7c, 0x7a, 0x1c, 0x1f, 0xb2, 0x75, 0x79, 0x10,
0x43, 0x65, 0x68, 0x27, 0x9d, 0x86, 0x88, 0xac}, 0}
p2pkhScriptPubKey = &externalapi.ScriptPublicKey{[]byte{0x76, 0xaa, 0x20,
0x51, 0x9c, 0x25, 0xca, 0x95, 0xa0, 0xd8, 0xcd,
0xf5, 0xb8, 0x3f, 0x96, 0xa1, 0x5e, 0x8c, 0x1a,
0xae, 0x33, 0xeb, 0x50, 0xc8, 0x66, 0xc9, 0xd0,
0xa5, 0xce, 0x3e, 0x5f, 0x6b, 0x3b, 0x38, 0x8d,
0x88, 0xac}, 0}
p2pkScriptPubKey = &externalapi.ScriptPublicKey{[]byte{0x20, 0xb2, 0x52, 0xf0, 0x49, 0x85, 0x78, 0x03, 0x03,
0xc8, 0x7d, 0xce, 0x51, 0x7f, 0xa8, 0x69, 0x0b,
0x91, 0x95, 0xf4, 0xf3, 0x5c, 0x26, 0x73, 0x05,
0x05, 0xa2, 0xee, 0xbc, 0x09, 0x38, 0x34, 0x3a, 0xac}, 0}
shortScriptPubKey = &externalapi.ScriptPublicKey{[]byte{0x76, 0xa9, 0x14, 0xd1, 0x7c, 0xb5,
0xeb, 0xa4, 0x02, 0xcb, 0x68, 0xe0, 0x69, 0x56, 0xbf, 0x32,
0x53, 0x90, 0x0e, 0x0a, 0x88, 0xac}, 0}
@@ -638,7 +631,7 @@ var sigScriptTests = []tstSigScript{
{
txout: &externalapi.DomainTransactionOutput{
Value: coinbaseVal,
ScriptPublicKey: p2pkhScriptPubKey,
ScriptPublicKey: p2pkScriptPubKey,
},
sigscriptGenerates: true,
inputValidates: true,
@@ -654,7 +647,7 @@ var sigScriptTests = []tstSigScript{
{
txout: &externalapi.DomainTransactionOutput{
Value: coinbaseVal,
ScriptPublicKey: p2pkhScriptPubKey,
ScriptPublicKey: p2pkScriptPubKey,
},
sigscriptGenerates: true,
inputValidates: true,
@@ -663,7 +656,7 @@ var sigScriptTests = []tstSigScript{
{
txout: &externalapi.DomainTransactionOutput{
Value: coinbaseVal + fee,
ScriptPublicKey: p2pkhScriptPubKey,
ScriptPublicKey: p2pkScriptPubKey,
},
sigscriptGenerates: true,
inputValidates: true,
@@ -679,7 +672,7 @@ var sigScriptTests = []tstSigScript{
{
txout: &externalapi.DomainTransactionOutput{
Value: coinbaseVal,
ScriptPublicKey: p2pkhScriptPubKey,
ScriptPublicKey: p2pkScriptPubKey,
},
sigscriptGenerates: true,
inputValidates: true,
@@ -695,7 +688,7 @@ var sigScriptTests = []tstSigScript{
{
txout: &externalapi.DomainTransactionOutput{
Value: coinbaseVal,
ScriptPublicKey: p2pkhScriptPubKey,
ScriptPublicKey: p2pkScriptPubKey,
},
sigscriptGenerates: true,
inputValidates: true,
@@ -711,7 +704,7 @@ var sigScriptTests = []tstSigScript{
{
txout: &externalapi.DomainTransactionOutput{
Value: coinbaseVal,
ScriptPublicKey: p2pkhScriptPubKey,
ScriptPublicKey: p2pkScriptPubKey,
},
sigscriptGenerates: true,
inputValidates: true,
@@ -727,7 +720,7 @@ var sigScriptTests = []tstSigScript{
{
txout: &externalapi.DomainTransactionOutput{
Value: coinbaseVal,
ScriptPublicKey: p2pkhScriptPubKey,
ScriptPublicKey: p2pkScriptPubKey,
},
sigscriptGenerates: false,
inputValidates: false,
@@ -743,7 +736,7 @@ var sigScriptTests = []tstSigScript{
{
txout: &externalapi.DomainTransactionOutput{
Value: coinbaseVal,
ScriptPublicKey: p2pkhScriptPubKey,
ScriptPublicKey: p2pkScriptPubKey,
},
sigscriptGenerates: false,
inputValidates: false,
@@ -759,7 +752,7 @@ var sigScriptTests = []tstSigScript{
{
txout: &externalapi.DomainTransactionOutput{
Value: coinbaseVal,
ScriptPublicKey: p2pkhScriptPubKey,
ScriptPublicKey: p2pkScriptPubKey,
},
sigscriptGenerates: true,
inputValidates: true,
@@ -768,7 +761,7 @@ var sigScriptTests = []tstSigScript{
{
txout: &externalapi.DomainTransactionOutput{
Value: coinbaseVal + fee,
ScriptPublicKey: p2pkhScriptPubKey,
ScriptPublicKey: p2pkScriptPubKey,
},
sigscriptGenerates: true,
inputValidates: true,
@@ -784,7 +777,7 @@ var sigScriptTests = []tstSigScript{
{
txout: &externalapi.DomainTransactionOutput{
Value: coinbaseVal,
ScriptPublicKey: p2pkhScriptPubKey,
ScriptPublicKey: p2pkScriptPubKey,
},
sigscriptGenerates: true,
inputValidates: true,
@@ -793,7 +786,7 @@ var sigScriptTests = []tstSigScript{
{
txout: &externalapi.DomainTransactionOutput{
Value: coinbaseVal + fee,
ScriptPublicKey: p2pkhScriptPubKey,
ScriptPublicKey: p2pkScriptPubKey,
},
sigscriptGenerates: true,
inputValidates: true,
@@ -877,7 +870,7 @@ nexttest:
// Validate tx input scripts
var scriptFlags ScriptFlags
for j := range tx.Inputs {
vm, err := NewEngine(sigScriptTests[i].inputs[j].txout.ScriptPublicKey, tx, j, scriptFlags, nil,
vm, err := NewEngine(sigScriptTests[i].inputs[j].txout.ScriptPublicKey, tx, j, scriptFlags, nil, nil,
&consensushashing.SighashReusedValues{})
if err != nil {
t.Errorf("cannot create script vm for test %v: %v",

View File

@@ -21,15 +21,24 @@ type ScriptClass byte
// Classes of script payment known about in the blockDAG.
const (
NonStandardTy ScriptClass = iota // None of the recognized forms.
PubKeyHashTy // Pay pubkey hash.
PubKeyTy // Pay to pubkey.
PubKeyECDSATy // Pay to pubkey ECDSA.
ScriptHashTy // Pay to script hash.
)
// Script public key versions for address types.
const (
addressPublicKeyScriptPublicKeyVersion = 0
addressPublicKeyECDSAScriptPublicKeyVersion = 0
addressScriptHashScriptPublicKeyVersion = 0
)
// scriptClassToName houses the human-readable strings which describe each
// script class.
var scriptClassToName = []string{
NonStandardTy: "nonstandard",
PubKeyHashTy: "pubkeyhash",
PubKeyTy: "pubkey",
PubKeyECDSATy: "pubkeyecdsa",
ScriptHashTy: "scripthash",
}
@@ -43,24 +52,32 @@ func (t ScriptClass) String() string {
return scriptClassToName[t]
}
// isPubkeyHash returns true if the script passed is a pay-to-pubkey-hash
// isPayToPubkey returns true if the script passed is a pay-to-pubkey
// transaction, false otherwise.
func isPubkeyHash(pops []parsedOpcode) bool {
return len(pops) == 5 &&
pops[0].opcode.value == OpDup &&
pops[1].opcode.value == OpBlake2b &&
pops[2].opcode.value == OpData32 &&
pops[3].opcode.value == OpEqualVerify &&
pops[4].opcode.value == OpCheckSig
func isPayToPubkey(pops []parsedOpcode) bool {
return len(pops) == 2 &&
pops[0].opcode.value == OpData32 &&
pops[1].opcode.value == OpCheckSig
}
// isPayToPubkeyECDSA returns true if the script passed is an ECDSA pay-to-pubkey
// transaction, false otherwise.
func isPayToPubkeyECDSA(pops []parsedOpcode) bool {
return len(pops) == 2 &&
pops[0].opcode.value == OpData33 &&
pops[1].opcode.value == OpCheckSigECDSA
}
// scriptType returns the type of the script being inspected from the known
// standard types.
func typeOfScript(pops []parsedOpcode) ScriptClass {
if isPubkeyHash(pops) {
return PubKeyHashTy
} else if isScriptHash(pops) {
switch {
case isPayToPubkey(pops):
return PubKeyTy
case isPayToPubkeyECDSA(pops):
return PubKeyECDSATy
case isScriptHash(pops):
return ScriptHashTy
}
return NonStandardTy
@@ -85,8 +102,8 @@ func GetScriptClass(script []byte) ScriptClass {
func expectedInputs(pops []parsedOpcode, class ScriptClass) int {
switch class {
case PubKeyHashTy:
return 2
case PubKeyTy:
return 1
case ScriptHashTy:
// Not including script. That is handled by the caller.
@@ -169,12 +186,21 @@ func CalcScriptInfo(sigScript, scriptPubKey []byte, isP2SH bool) (*ScriptInfo, e
return si, nil
}
// payToPubKeyHashScript creates a new script to pay a transaction
// output to a 20-byte pubkey hash. It is expected that the input is a valid
// hash.
func payToPubKeyHashScript(pubKeyHash []byte) ([]byte, error) {
return NewScriptBuilder().AddOp(OpDup).AddOp(OpBlake2b).
AddData(pubKeyHash).AddOp(OpEqualVerify).AddOp(OpCheckSig).
// payToPubKeyScript creates a new script to pay a transaction
// output to a 32-byte pubkey.
func payToPubKeyScript(pubKey []byte) ([]byte, error) {
return NewScriptBuilder().
AddData(pubKey).
AddOp(OpCheckSig).
Script()
}
// payToPubKeyScript creates a new script to pay a transaction
// output to a 33-byte pubkey.
func payToPubKeyScriptECDSA(pubKey []byte) ([]byte, error) {
return NewScriptBuilder().
AddData(pubKey).
AddOp(OpCheckSigECDSA).
Script()
}
@@ -190,16 +216,29 @@ func payToScriptHashScript(scriptHash []byte) ([]byte, error) {
func PayToAddrScript(addr util.Address) (*externalapi.ScriptPublicKey, error) {
const nilAddrErrStr = "unable to generate payment script for nil address"
switch addr := addr.(type) {
case *util.AddressPubKeyHash:
case *util.AddressPublicKey:
if addr == nil {
return nil, scriptError(ErrUnsupportedAddress,
nilAddrErrStr)
}
script, err := payToPubKeyHashScript(addr.ScriptAddress())
script, err := payToPubKeyScript(addr.ScriptAddress())
if err != nil {
return nil, err
}
return &externalapi.ScriptPublicKey{script, constants.MaxScriptPublicKeyVersion}, err
return &externalapi.ScriptPublicKey{script, addressPublicKeyScriptPublicKeyVersion}, err
case *util.AddressPublicKeyECDSA:
if addr == nil {
return nil, scriptError(ErrUnsupportedAddress,
nilAddrErrStr)
}
script, err := payToPubKeyScriptECDSA(addr.ScriptAddress())
if err != nil {
return nil, err
}
return &externalapi.ScriptPublicKey{script, addressPublicKeyECDSAScriptPublicKeyVersion}, err
case *util.AddressScriptHash:
if addr == nil {
@@ -210,7 +249,8 @@ func PayToAddrScript(addr util.Address) (*externalapi.ScriptPublicKey, error) {
if err != nil {
return nil, err
}
return &externalapi.ScriptPublicKey{script, constants.MaxScriptPublicKeyVersion}, err
return &externalapi.ScriptPublicKey{script, addressScriptHashScriptPublicKeyVersion}, err
}
str := fmt.Sprintf("unable to generate payment script for unsupported "+
@@ -276,12 +316,24 @@ func ExtractScriptPubKeyAddress(scriptPubKey *externalapi.ScriptPublicKey, dagPa
scriptClass := typeOfScript(pops)
switch scriptClass {
case PubKeyHashTy:
// A pay-to-pubkey-hash script is of the form:
// OP_DUP OP_BLAKE2B <hash> OP_EQUALVERIFY OP_CHECKSIG
// Therefore the pubkey hash is the 3rd item on the stack.
// If the pubkey hash is invalid for some reason, return a nil address.
addr, err := util.NewAddressPubKeyHash(pops[2].data,
case PubKeyTy:
// A pay-to-pubkey script is of the form:
// <pubkey> OP_CHECKSIG
// Therefore the pubkey is the first item on the stack.
// If the pubkey is invalid for some reason, return a nil address.
addr, err := util.NewAddressPublicKey(pops[0].data,
dagParams.Prefix)
if err != nil {
return scriptClass, nil, nil
}
return scriptClass, addr, nil
case PubKeyECDSATy:
// A pay-to-pubkey script is of the form:
// <pubkey> OP_CHECKSIGECDSA
// Therefore the pubkey is the first item on the stack.
// If the pubkey is invalid for some reason, return a nil address.
addr, err := util.NewAddressPublicKeyECDSA(pops[0].data,
dagParams.Prefix)
if err != nil {
return scriptClass, nil, nil

View File

@@ -28,14 +28,27 @@ func mustParseShortForm(script string, version uint16) []byte {
return s
}
// newAddressPubKeyHash returns a new util.AddressPubKeyHash from the
// provided hash. It panics if an error occurs. This is only used in the tests
// newAddressPublicKey returns a new util.AddressPublicKey from the
// provided public key. It panics if an error occurs. This is only used in the tests
// as a helper since the only way it can fail is if there is an error in the
// test source code.
func newAddressPubKeyHash(pkHash []byte) util.Address {
addr, err := util.NewAddressPubKeyHash(pkHash, util.Bech32PrefixKaspa)
func newAddressPublicKey(publicKey []byte) util.Address {
addr, err := util.NewAddressPublicKey(publicKey, util.Bech32PrefixKaspa)
if err != nil {
panic("invalid public key hash in test source")
panic("invalid public key in test source")
}
return addr
}
// newAddressPublicKeyECDSA returns a new util.AddressPublicKeyECDSA from the
// provided public key. It panics if an error occurs. This is only used in the tests
// as a helper since the only way it can fail is if there is an error in the
// test source code.
func newAddressPublicKeyECDSA(publicKey []byte) util.Address {
addr, err := util.NewAddressPublicKeyECDSA(publicKey, util.Bech32PrefixKaspa)
if err != nil {
panic("invalid public key in test source")
}
return addr
@@ -67,15 +80,22 @@ func TestExtractScriptPubKeyAddrs(t *testing.T) {
class ScriptClass
}{
{
name: "standard p2pkh",
name: "standard p2pk",
script: &externalapi.ScriptPublicKey{
Script: hexToBytes("76aa20ad06dd6ddee55cbca9a9e3713bd" +
"7587509a30564ad06dd6ddee55cbca9a9e37188ac"),
Script: hexToBytes("202454a285d8566b0cb2792919536ee0f1b6f69b58ba59e9850ecbc91eef722daeac"),
Version: 0,
},
addr: newAddressPubKeyHash(hexToBytes("ad06dd6ddee5" +
"5cbca9a9e3713bd7587509a30564ad06dd6ddee55cbca9a9e371")),
class: PubKeyHashTy,
addr: newAddressPublicKey(hexToBytes("2454a285d8566b0cb2792919536ee0f1b6f69b58ba59e9850ecbc91eef722dae")),
class: PubKeyTy,
},
{
name: "standard p2pk ECDSA",
script: &externalapi.ScriptPublicKey{
Script: hexToBytes("212454a285d8566b0cb2792919536ee0f1b6f69b58ba59e9850ecbc91eef722daeaaab"),
Version: 0,
},
addr: newAddressPublicKeyECDSA(hexToBytes("2454a285d8566b0cb2792919536ee0f1b6f69b58ba59e9850ecbc91eef722daeaa")),
class: PubKeyECDSATy,
},
{
name: "standard p2sh",
@@ -216,8 +236,8 @@ func TestCalcScriptInfo(t *testing.T) {
{
// Invented scripts, the hashes do not match
name: "p2sh standard script",
sigScript: "1 81 DATA_37 DUP BLAKE2B DATA_32 0x010203" +
"0405060708090a0b0c0d0e0f1011121314fe441065b6532231de2fac56 EQUALVERIFY " +
sigScript: "1 81 DATA_34 DATA_32 0x010203" +
"0405060708090a0b0c0d0e0f1011121314fe441065b6532231de2fac56 " +
"CHECKSIG",
scriptPubKey: "BLAKE2B DATA_32 0xfe441065b6532231de2fac56" +
"3152205ec4f59c74fe441065b6532231de2fac56 EQUAL",
@@ -225,7 +245,7 @@ func TestCalcScriptInfo(t *testing.T) {
scriptInfo: ScriptInfo{
ScriptPubKeyClass: ScriptHashTy,
NumInputs: 3,
ExpectedInputs: 3, // nonstandard p2sh.
ExpectedInputs: 2, // nonstandard p2sh.
SigOps: 1,
},
},
@@ -303,10 +323,10 @@ func (b *bogusAddress) Prefix() util.Bech32Prefix {
func TestPayToAddrScript(t *testing.T) {
t.Parallel()
p2pkhMain, err := util.NewAddressPubKeyHash(hexToBytes("e34cce70c86"+
p2pkMain, err := util.NewAddressPublicKey(hexToBytes("e34cce70c86"+
"373273efcc54ce7d2a491bb4a0e84e34cce70c86373273efcc54c"), util.Bech32PrefixKaspa)
if err != nil {
t.Fatalf("Unable to create public key hash address: %v", err)
t.Fatalf("Unable to create public key address: %v", err)
}
p2shMain, err := util.NewAddressScriptHashFromHash(hexToBytes("e8c300"+
@@ -325,11 +345,11 @@ func TestPayToAddrScript(t *testing.T) {
expectedVersion uint16
err error
}{
// pay-to-pubkey-hash address on mainnet
// pay-to-pubkey address on mainnet
{
p2pkhMain,
"DUP BLAKE2B DATA_32 0xe34cce70c86373273efcc54ce7d2a4" +
"91bb4a0e84e34cce70c86373273efcc54c EQUALVERIFY CHECKSIG",
p2pkMain,
"DATA_32 0xe34cce70c86373273efcc54ce7d2a4" +
"91bb4a0e84e34cce70c86373273efcc54c CHECKSIG",
0,
nil,
},
@@ -343,7 +363,7 @@ func TestPayToAddrScript(t *testing.T) {
},
// Supported address types with nil pointers.
{(*util.AddressPubKeyHash)(nil), "", 0, errUnsupportedAddress},
{(*util.AddressPublicKey)(nil), "", 0, errUnsupportedAddress},
{(*util.AddressScriptHash)(nil), "", 0, errUnsupportedAddress},
// Unsupported address type.
@@ -392,17 +412,21 @@ var scriptClassTests = []struct {
}{
// p2pk
{
name: "Pay Pubkey",
script: "DATA_65 0x0411db93e1dcdb8a016b49840f8c53bc1eb68a382e" +
"97b1482ecad7b148a6909a5cb2e0eaddfb84ccf9744464f82e16" +
"0bfa9b8b64f9d4c03f999b8643f656b412a3 CHECKSIG",
class: NonStandardTy,
name: "Pay Pubkey",
script: "DATA_32 0x89ac24ea10bb751af4939623ccc5e550d96842b64e8fca0f63e94b4373fd555e CHECKSIG",
class: PubKeyTy,
},
// p2pk ECDSA
{
name: "Pay Pubkey ECDSA",
script: "DATA_33 0x89ac24ea10bb751af4939623ccc5e550d96842b64e8fca0f63e94b4373fd555eab CHECKSIGECDSA",
class: PubKeyECDSATy,
},
{
name: "Pay PubkeyHash",
script: "DUP BLAKE2B DATA_32 0x660d4ef3a743e3e696ad990364e55543e3e696ad990364e555e555" +
"c271ad504b EQUALVERIFY CHECKSIG",
class: PubKeyHashTy,
class: NonStandardTy,
},
// mutlisig
{
@@ -513,9 +537,14 @@ func TestStringifyClass(t *testing.T) {
stringed: "nonstandard",
},
{
name: "pubkeyhash",
class: PubKeyHashTy,
stringed: "pubkeyhash",
name: "pubkey",
class: PubKeyTy,
stringed: "pubkey",
},
{
name: "pubkeyecdsa",
class: PubKeyECDSATy,
stringed: "pubkeyecdsa",
},
{
name: "scripthash",

View File

@@ -38,8 +38,8 @@ func main() {
// later...
// Create and print new payment address, specific to the active network.
pubKeyHash := make([]byte, 20)
addr, err := util.NewAddressPubKeyHash(pubKeyHash, dagParams)
pubKey := make([]byte, 32)
addr, err := util.NewAddressPubKey(pubKey, dagParams)
if err != nil {
log.Fatal(err)
}

View File

@@ -46,8 +46,8 @@ variable (either directly, or hidden in a library call).
// later...
// Create and print new payment address, specific to the active network.
pubKeyHash := make([]byte, 20)
addr, err := util.NewAddressPubKeyHash(pubKeyHash, dagParams)
pubKey := make([]byte, 32)
addr, err := util.NewAddressPubKey(pubKey, dagParams)
if err != nil {
log.Fatal(err)
}

View File

@@ -127,43 +127,18 @@ func isDust(txOut *consensusexternalapi.DomainTransactionOutput, minRelayTxFee u
// input script to redeem it. Since there is no input script
// to redeem it yet, use the minimum size of a typical input script.
//
// Pay-to-pubkey-hash bytes breakdown:
//
// Output to hash (34 bytes):
// 8 value, 1 script len, 25 script [1 OP_DUP, 1 OP_HASH_160,
// 1 OP_DATA_20, 20 hash, 1 OP_EQUALVERIFY, 1 OP_CHECKSIG]
//
// Input with compressed pubkey (148 bytes):
// 36 prev outpoint, 1 script len, 107 script [1 OP_DATA_72, 72 sig,
// 1 OP_DATA_33, 33 compressed pubkey], 4 sequence
//
// Input with uncompressed pubkey (180 bytes):
// 36 prev outpoint, 1 script len, 139 script [1 OP_DATA_72, 72 sig,
// 1 OP_DATA_65, 65 compressed pubkey], 4 sequence
//
// Pay-to-pubkey bytes breakdown:
//
// Output to compressed pubkey (44 bytes):
// 8 value, 1 script len, 35 script [1 OP_DATA_33,
// 33 compressed pubkey, 1 OP_CHECKSIG]
// Output to pubkey (43 bytes):
// 8 value, 1 script len, 34 script [1 OP_DATA_32,
// 32 pubkey, 1 OP_CHECKSIG]
//
// Output to uncompressed pubkey (76 bytes):
// 8 value, 1 script len, 67 script [1 OP_DATA_65, 65 pubkey,
// 1 OP_CHECKSIG]
// Input (105 bytes):
// 36 prev outpoint, 1 script len, 64 script [1 OP_DATA_64,
// 64 sig], 4 sequence
//
// Input (114 bytes):
// 36 prev outpoint, 1 script len, 73 script [1 OP_DATA_72,
// 72 sig], 4 sequence
//
// Theoretically this could examine the script type of the output script
// and use a different size for the typical input script size for
// pay-to-pubkey vs pay-to-pubkey-hash inputs per the above breakdowns,
// but the only combination which is less than the value chosen is
// a pay-to-pubkey script with a compressed pubkey, which is not very
// common.
//
// The most common scripts are pay-to-pubkey-hash, and as per the above
// breakdown, the minimum size of a p2pkh input script is 148 bytes. So
// The most common scripts are pay-to-pubkey, and as per the above
// breakdown, the minimum size of a p2pk input script is 148 bytes. So
// that figure is used.
totalSize := estimatedsize.TransactionOutputEstimatedSerializedSize(txOut) + 148
@@ -172,7 +147,7 @@ func isDust(txOut *consensusexternalapi.DomainTransactionOutput, minRelayTxFee u
// minFreeTxRelayFee is in sompi/KB, so multiply by 1000 to
// convert to bytes.
//
// Using the typical values for a pay-to-pubkey-hash transaction from
// Using the typical values for a pay-to-pubkey transaction from
// the breakdown above and the default minimum free transaction relay
// fee of 1000, this equates to values less than 546 sompi being
// considered dust.

View File

@@ -179,9 +179,9 @@ func TestCheckTransactionStandard(t *testing.T) {
Sequence: constants.MaxTxInSequenceNum,
}
addrHash := [32]byte{0x01}
addr, err := util.NewAddressPubKeyHash(addrHash[:], util.Bech32PrefixKaspaTest)
addr, err := util.NewAddressPublicKey(addrHash[:], util.Bech32PrefixKaspaTest)
if err != nil {
t.Fatalf("NewAddressPubKeyHash: unexpected error: %v", err)
t.Fatalf("NewAddressPublicKey: unexpected error: %v", err)
}
dummyScriptPublicKey, err := txscript.PayToAddrScript(addr)
if err != nil {
@@ -200,7 +200,7 @@ func TestCheckTransactionStandard(t *testing.T) {
code RejectCode
}{
{
name: "Typical pay-to-pubkey-hash transaction",
name: "Typical pay-to-pubkey transaction",
tx: consensusexternalapi.DomainTransaction{Version: 0, Inputs: []*consensusexternalapi.DomainTransactionInput{&dummyTxIn}, Outputs: []*consensusexternalapi.DomainTransactionOutput{&dummyTxOut}},
height: 300000,
isStandard: true,

View File

@@ -15,6 +15,8 @@ import (
"github.com/pkg/errors"
)
const maxAddresses = 4096
// addressRandomizer is the interface for the randomizer needed for the AddressManager.
type addressRandomizer interface {
RandomAddress(addresses []*appmessage.NetAddress) *appmessage.NetAddress
@@ -27,6 +29,11 @@ type addressKey struct {
address ipv6
}
type address struct {
netAddress *appmessage.NetAddress
connectionFailedCount uint64
}
type ipv6 [net.IPv6len]byte
func (i ipv6) equal(other ipv6) bool {
@@ -45,17 +52,6 @@ func netAddressKey(netAddress *appmessage.NetAddress) addressKey {
return key
}
// netAddressKeys returns a key of the ip address to use it in maps.
func netAddressesKeys(netAddresses []*appmessage.NetAddress) map[addressKey]bool {
result := make(map[addressKey]bool, len(netAddresses))
for _, netAddress := range netAddresses {
key := netAddressKey(netAddress)
result[key] = true
}
return result
}
// AddressManager provides a concurrency safe address manager for caching potential
// peers on the Kaspa network.
type AddressManager struct {
@@ -85,13 +81,42 @@ func New(cfg *Config, database database.Database) (*AddressManager, error) {
}, nil
}
func (am *AddressManager) addAddressNoLock(address *appmessage.NetAddress) error {
if !IsRoutable(address, am.cfg.AcceptUnroutable) {
func (am *AddressManager) addAddressNoLock(netAddress *appmessage.NetAddress) error {
if !IsRoutable(netAddress, am.cfg.AcceptUnroutable) {
return nil
}
key := netAddressKey(netAddress)
address := &address{netAddress: netAddress, connectionFailedCount: 0}
err := am.store.add(key, address)
if err != nil {
return err
}
if am.store.notBannedCount() > maxAddresses {
allAddresses := am.store.getAllNotBanned()
maxConnectionFailedCount := uint64(0)
toRemove := allAddresses[0]
for _, address := range allAddresses[1:] {
if address.connectionFailedCount > maxConnectionFailedCount {
maxConnectionFailedCount = address.connectionFailedCount
toRemove = address
}
}
toRemoveKey := netAddressKey(toRemove.netAddress)
err := am.store.remove(toRemoveKey)
if err != nil {
return err
}
}
return nil
}
func (am *AddressManager) removeAddressNoLock(address *appmessage.NetAddress) error {
key := netAddressKey(address)
return am.store.add(key, address)
return am.store.remove(key)
}
// AddAddress adds address to the address manager
@@ -121,8 +146,37 @@ func (am *AddressManager) RemoveAddress(address *appmessage.NetAddress) error {
am.mutex.Lock()
defer am.mutex.Unlock()
return am.removeAddressNoLock(address)
}
// MarkConnectionFailure notifies the address manager that the given address
// has failed to connect
func (am *AddressManager) MarkConnectionFailure(address *appmessage.NetAddress) error {
am.mutex.Lock()
defer am.mutex.Unlock()
key := netAddressKey(address)
return am.store.remove(key)
entry, ok := am.store.getNotBanned(key)
if !ok {
return errors.Errorf("address %s is not registered with the address manager", address.TCPAddress())
}
entry.connectionFailedCount = entry.connectionFailedCount + 1
return am.store.updateNotBanned(key, entry)
}
// MarkConnectionSuccess notifies the address manager that the given address
// has successfully connected
func (am *AddressManager) MarkConnectionSuccess(address *appmessage.NetAddress) error {
am.mutex.Lock()
defer am.mutex.Unlock()
key := netAddressKey(address)
entry, ok := am.store.getNotBanned(key)
if !ok {
return errors.Errorf("address %s is not registered with the address manager", address.TCPAddress())
}
entry.connectionFailedCount = 0
return am.store.updateNotBanned(key, entry)
}
// Addresses returns all addresses
@@ -130,7 +184,7 @@ func (am *AddressManager) Addresses() []*appmessage.NetAddress {
am.mutex.Lock()
defer am.mutex.Unlock()
return am.store.getAllNotBanned()
return am.store.getAllNotBannedNetAddresses()
}
// BannedAddresses returns all banned addresses
@@ -138,7 +192,7 @@ func (am *AddressManager) BannedAddresses() []*appmessage.NetAddress {
am.mutex.Lock()
defer am.mutex.Unlock()
return am.store.getAllBanned()
return am.store.getAllBannedNetAddresses()
}
// notBannedAddressesWithException returns all not banned addresses with excpetion
@@ -146,7 +200,7 @@ func (am *AddressManager) notBannedAddressesWithException(exceptions []*appmessa
am.mutex.Lock()
defer am.mutex.Unlock()
return am.store.getAllNotBannedWithout(exceptions)
return am.store.getAllNotBannedNetAddressesWithout(exceptions)
}
// RandomAddress returns a random address that isn't banned and isn't in exceptions
@@ -174,7 +228,7 @@ func (am *AddressManager) Ban(addressToBan *appmessage.NetAddress) error {
keyToBan := netAddressKey(addressToBan)
keysToDelete := make([]addressKey, 0)
for _, address := range am.store.getAllNotBanned() {
for _, address := range am.store.getAllNotBannedNetAddresses() {
key := netAddressKey(address)
if key.address.equal(keyToBan.address) {
keysToDelete = append(keysToDelete, key)
@@ -187,7 +241,8 @@ func (am *AddressManager) Ban(addressToBan *appmessage.NetAddress) error {
}
}
return am.store.addBanned(keyToBan, addressToBan)
address := &address{netAddress: addressToBan}
return am.store.addBanned(keyToBan, address)
}
// Unban unmarks the given address as banned
@@ -223,7 +278,6 @@ func (am *AddressManager) IsBanned(address *appmessage.NetAddress) (bool, error)
}
return true, nil
}
func (am *AddressManager) unbanIfOldEnough(key addressKey) error {
@@ -233,7 +287,7 @@ func (am *AddressManager) unbanIfOldEnough(key addressKey) error {
}
const maxBanTime = 24 * time.Hour
if mstime.Since(address.Timestamp) > maxBanTime {
if mstime.Since(address.netAddress.Timestamp) > maxBanTime {
err := am.store.removeBanned(key)
if err != nil {
return err

View File

@@ -303,3 +303,71 @@ func TestRestoreAddressManager(t *testing.T) {
t.Fatalf("Banned address %s not returned from BannedAddresses()", addressToBan.IP)
}
}
func TestOverfillAddressManager(t *testing.T) {
addressManager, teardown := newAddressManagerForTest(t, "TestAddressManager")
defer teardown()
generateTestAddresses := func(amount int) []*appmessage.NetAddress {
testAddresses := make([]*appmessage.NetAddress, 0, amount)
for i := byte(0); i < 128; i++ {
for j := byte(0); j < 128; j++ {
testAddress := &appmessage.NetAddress{IP: net.IP{1, 2, i, j}, Timestamp: mstime.Now()}
testAddresses = append(testAddresses, testAddress)
if len(testAddresses) == amount {
break
}
}
if len(testAddresses) == amount {
break
}
}
return testAddresses
}
// Add a single test address to the address manager
testAddress := &appmessage.NetAddress{IP: net.IP{5, 6, 0, 0}, Timestamp: mstime.Now()}
err := addressManager.AddAddress(testAddress)
if err != nil {
t.Fatalf("AddAddress: %s", err)
}
// Add `maxAddresses-1` addresses to the address manager
addresses := generateTestAddresses(maxAddresses - 1)
err = addressManager.AddAddresses(addresses...)
if err != nil {
t.Fatalf("AddAddresses: %s", err)
}
// Make sure that it now contains exactly `maxAddresses` entries
returnedAddresses := addressManager.Addresses()
if len(returnedAddresses) != maxAddresses {
t.Fatalf("Unexpected address amount. Want: %d, got: %d", maxAddresses, len(returnedAddresses))
}
// Mark the first test address as a connection failure
err = addressManager.MarkConnectionFailure(testAddress)
if err != nil {
t.Fatalf("MarkConnectionFailure: %s", err)
}
// Add one more address to the address manager
err = addressManager.AddAddress(&appmessage.NetAddress{IP: net.IP{7, 8, 0, 0}, Timestamp: mstime.Now()})
if err != nil {
t.Fatalf("AddAddress: %s", err)
}
// Make sure that it now still contains exactly `maxAddresses` entries
returnedAddresses = addressManager.Addresses()
if len(returnedAddresses) != maxAddresses {
t.Fatalf("Unexpected address amount. Want: %d, got: %d", maxAddresses, len(returnedAddresses))
}
// Make sure that the first address is no longer in the
// connection manager
for _, address := range returnedAddresses {
if address.IP.Equal(testAddress.IP) {
t.Fatalf("Unexpectedly found testAddress returned addresses")
}
}
}

View File

@@ -5,6 +5,7 @@ import (
"github.com/kaspanet/kaspad/app/appmessage"
"github.com/kaspanet/kaspad/infrastructure/db/database"
"github.com/kaspanet/kaspad/util/mstime"
"github.com/pkg/errors"
"net"
)
@@ -13,15 +14,15 @@ var bannedAddressBucket = database.MakeBucket([]byte("banned-addresses"))
type addressStore struct {
database database.Database
notBannedAddresses map[addressKey]*appmessage.NetAddress
bannedAddresses map[ipv6]*appmessage.NetAddress
notBannedAddresses map[addressKey]*address
bannedAddresses map[ipv6]*address
}
func newAddressStore(database database.Database) (*addressStore, error) {
addressStore := &addressStore{
database: database,
notBannedAddresses: map[addressKey]*appmessage.NetAddress{},
bannedAddresses: map[ipv6]*appmessage.NetAddress{},
notBannedAddresses: map[addressKey]*address{},
bannedAddresses: map[ipv6]*address{},
}
err := addressStore.restoreNotBannedAddresses()
if err != nil {
@@ -56,7 +57,7 @@ func (as *addressStore) restoreNotBannedAddresses() error {
if err != nil {
return err
}
netAddress := as.deserializeNetAddress(serializedNetAddress)
netAddress := as.deserializeAddress(serializedNetAddress)
as.notBannedAddresses[key] = netAddress
}
return nil
@@ -80,13 +81,17 @@ func (as *addressStore) restoreBannedAddresses() error {
if err != nil {
return err
}
netAddress := as.deserializeNetAddress(serializedNetAddress)
netAddress := as.deserializeAddress(serializedNetAddress)
as.bannedAddresses[ipv6] = netAddress
}
return nil
}
func (as *addressStore) add(key addressKey, address *appmessage.NetAddress) error {
func (as *addressStore) notBannedCount() int {
return len(as.notBannedAddresses)
}
func (as *addressStore) add(key addressKey, address *address) error {
if _, ok := as.notBannedAddresses[key]; ok {
return nil
}
@@ -94,10 +99,28 @@ func (as *addressStore) add(key addressKey, address *appmessage.NetAddress) erro
as.notBannedAddresses[key] = address
databaseKey := as.notBannedDatabaseKey(key)
serializedAddress := as.serializeNetAddress(address)
serializedAddress := as.serializeAddress(address)
return as.database.Put(databaseKey, serializedAddress)
}
// updateNotBanned updates the not-banned address collection
func (as *addressStore) updateNotBanned(key addressKey, address *address) error {
if _, ok := as.notBannedAddresses[key]; !ok {
return errors.Errorf("address %s is not in the store", address.netAddress.TCPAddress())
}
as.notBannedAddresses[key] = address
databaseKey := as.notBannedDatabaseKey(key)
serializedAddress := as.serializeAddress(address)
return as.database.Put(databaseKey, serializedAddress)
}
func (as *addressStore) getNotBanned(key addressKey) (*address, bool) {
address, ok := as.notBannedAddresses[key]
return address, ok
}
func (as *addressStore) remove(key addressKey) error {
delete(as.notBannedAddresses, key)
@@ -105,21 +128,29 @@ func (as *addressStore) remove(key addressKey) error {
return as.database.Delete(databaseKey)
}
func (as *addressStore) getAllNotBanned() []*appmessage.NetAddress {
addresses := make([]*appmessage.NetAddress, 0, len(as.notBannedAddresses))
func (as *addressStore) getAllNotBanned() []*address {
addresses := make([]*address, 0, len(as.notBannedAddresses))
for _, address := range as.notBannedAddresses {
addresses = append(addresses, address)
}
return addresses
}
func (as *addressStore) getAllNotBannedWithout(ignoredAddresses []*appmessage.NetAddress) []*appmessage.NetAddress {
func (as *addressStore) getAllNotBannedNetAddresses() []*appmessage.NetAddress {
addresses := make([]*appmessage.NetAddress, 0, len(as.notBannedAddresses))
for _, address := range as.notBannedAddresses {
addresses = append(addresses, address.netAddress)
}
return addresses
}
func (as *addressStore) getAllNotBannedNetAddressesWithout(ignoredAddresses []*appmessage.NetAddress) []*appmessage.NetAddress {
ignoredKeys := netAddressesKeys(ignoredAddresses)
addresses := make([]*appmessage.NetAddress, 0, len(as.notBannedAddresses))
for key, address := range as.notBannedAddresses {
if !ignoredKeys[key] {
addresses = append(addresses, address)
addresses = append(addresses, address.netAddress)
}
}
return addresses
@@ -130,7 +161,7 @@ func (as *addressStore) isNotBanned(key addressKey) bool {
return ok
}
func (as *addressStore) addBanned(key addressKey, address *appmessage.NetAddress) error {
func (as *addressStore) addBanned(key addressKey, address *address) error {
if _, ok := as.bannedAddresses[key.address]; ok {
return nil
}
@@ -138,7 +169,7 @@ func (as *addressStore) addBanned(key addressKey, address *appmessage.NetAddress
as.bannedAddresses[key.address] = address
databaseKey := as.bannedDatabaseKey(key)
serializedAddress := as.serializeNetAddress(address)
serializedAddress := as.serializeAddress(address)
return as.database.Put(databaseKey, serializedAddress)
}
@@ -149,10 +180,10 @@ func (as *addressStore) removeBanned(key addressKey) error {
return as.database.Delete(databaseKey)
}
func (as *addressStore) getAllBanned() []*appmessage.NetAddress {
func (as *addressStore) getAllBannedNetAddresses() []*appmessage.NetAddress {
bannedAddresses := make([]*appmessage.NetAddress, 0, len(as.bannedAddresses))
for _, bannedAddress := range as.bannedAddresses {
bannedAddresses = append(bannedAddresses, bannedAddress)
bannedAddresses = append(bannedAddresses, bannedAddress.netAddress)
}
return bannedAddresses
}
@@ -162,11 +193,21 @@ func (as *addressStore) isBanned(key addressKey) bool {
return ok
}
func (as *addressStore) getBanned(key addressKey) (*appmessage.NetAddress, bool) {
func (as *addressStore) getBanned(key addressKey) (*address, bool) {
bannedAddress, ok := as.bannedAddresses[key.address]
return bannedAddress, ok
}
// netAddressKeys returns a key of the ip address to use it in maps.
func netAddressesKeys(netAddresses []*appmessage.NetAddress) map[addressKey]bool {
result := make(map[addressKey]bool, len(netAddresses))
for _, netAddress := range netAddresses {
key := netAddressKey(netAddress)
result[key] = true
}
return result
}
func (as *addressStore) notBannedDatabaseKey(key addressKey) *database.Key {
serializedKey := as.serializeAddressKey(key)
return notBannedAddressBucket.Key(serializedKey)
@@ -198,27 +239,32 @@ func (as *addressStore) deserializeAddressKey(serializedKey []byte) addressKey {
}
}
func (as *addressStore) serializeNetAddress(netAddress *appmessage.NetAddress) []byte {
serializedSize := 16 + 2 + 8 // ipv6 + port + timestamp
func (as *addressStore) serializeAddress(address *address) []byte {
serializedSize := 16 + 2 + 8 + 8 // ipv6 + port + timestamp + connectionFailedCount
serializedNetAddress := make([]byte, serializedSize)
copy(serializedNetAddress[:], netAddress.IP[:])
binary.LittleEndian.PutUint16(serializedNetAddress[16:], netAddress.Port)
binary.LittleEndian.PutUint64(serializedNetAddress[18:], uint64(netAddress.Timestamp.UnixMilliseconds()))
copy(serializedNetAddress[:], address.netAddress.IP[:])
binary.LittleEndian.PutUint16(serializedNetAddress[16:], address.netAddress.Port)
binary.LittleEndian.PutUint64(serializedNetAddress[18:], uint64(address.netAddress.Timestamp.UnixMilliseconds()))
binary.LittleEndian.PutUint64(serializedNetAddress[26:], uint64(address.connectionFailedCount))
return serializedNetAddress
}
func (as *addressStore) deserializeNetAddress(serializedNetAddress []byte) *appmessage.NetAddress {
func (as *addressStore) deserializeAddress(serializedAddress []byte) *address {
ip := make(net.IP, 16)
copy(ip[:], serializedNetAddress[:])
copy(ip[:], serializedAddress[:])
port := binary.LittleEndian.Uint16(serializedNetAddress[16:])
timestamp := mstime.UnixMilliseconds(int64(binary.LittleEndian.Uint64(serializedNetAddress[18:])))
port := binary.LittleEndian.Uint16(serializedAddress[16:])
timestamp := mstime.UnixMilliseconds(int64(binary.LittleEndian.Uint64(serializedAddress[18:])))
connectionFailedCount := binary.LittleEndian.Uint64(serializedAddress[26:])
return &appmessage.NetAddress{
IP: ip,
Port: port,
Timestamp: timestamp,
return &address{
netAddress: &appmessage.NetAddress{
IP: ip,
Port: port,
Timestamp: timestamp,
},
connectionFailedCount: connectionFailedCount,
}
}

View File

@@ -24,21 +24,24 @@ func TestAddressKeySerialization(t *testing.T) {
}
}
func TestNetAddressSerialization(t *testing.T) {
addressManager, teardown := newAddressManagerForTest(t, "TestNetAddressSerialization")
func TestAddressSerialization(t *testing.T) {
addressManager, teardown := newAddressManagerForTest(t, "TestAddressSerialization")
defer teardown()
addressStore := addressManager.store
testAddress := &appmessage.NetAddress{
IP: net.ParseIP("2602:100:abcd::102"),
Port: 12345,
Timestamp: mstime.Now(),
testAddress := &address{
netAddress: &appmessage.NetAddress{
IP: net.ParseIP("2602:100:abcd::102"),
Port: 12345,
Timestamp: mstime.Now(),
},
connectionFailedCount: 98465,
}
serializedTestNetAddress := addressStore.serializeNetAddress(testAddress)
deserializedTestNetAddress := addressStore.deserializeNetAddress(serializedTestNetAddress)
if !reflect.DeepEqual(testAddress, deserializedTestNetAddress) {
t.Fatalf("testAddress and deserializedTestNetAddress are not equal\n"+
"testAddress:%+v\ndeserializedTestNetAddress:%+v", testAddress, deserializedTestNetAddress)
serializedTestAddress := addressStore.serializeAddress(testAddress)
deserializedTestAddress := addressStore.deserializeAddress(serializedTestAddress)
if !reflect.DeepEqual(testAddress, deserializedTestAddress) {
t.Fatalf("testAddress and deserializedTestAddress are not equal\n"+
"testAddress:%+v\ndeserializedTestAddress:%+v", testAddress, deserializedTestAddress)
}
}

View File

@@ -42,9 +42,10 @@ func (c *ConnectionManager) checkOutgoingConnections(connSet connectionSet) {
err := c.initiateConnection(addressString)
if err != nil {
log.Infof("Couldn't connect to %s: %s", addressString, err)
c.addressManager.RemoveAddress(netAddress)
c.addressManager.MarkConnectionFailure(netAddress)
continue
}
c.addressManager.MarkConnectionSuccess(netAddress)
c.activeOutgoing[addressString] = struct{}{}
}

View File

@@ -16,10 +16,14 @@ import (
// OnErrorHandler defines a handler function for when errors occur
type OnErrorHandler func(err error)
// OnDisconnectedHandler defines a handler function for when the client disconnected
type OnDisconnectedHandler func()
// GRPCClient is a gRPC-based RPC client
type GRPCClient struct {
stream protowire.RPC_MessageStreamClient
onErrorHandler OnErrorHandler
stream protowire.RPC_MessageStreamClient
onErrorHandler OnErrorHandler
onDisconnectedHandler OnDisconnectedHandler
}
// Connect connects to the RPC server with the given address
@@ -52,6 +56,11 @@ func (c *GRPCClient) SetOnErrorHandler(onErrorHandler OnErrorHandler) {
c.onErrorHandler = onErrorHandler
}
// SetOnDisconnectedHandler sets the client's onDisconnectedHandler
func (c *GRPCClient) SetOnDisconnectedHandler(onDisconnectedHandler OnDisconnectedHandler) {
c.onDisconnectedHandler = onDisconnectedHandler
}
// AttachRouter attaches the given router to the client and starts
// sending/receiving messages via it
func (c *GRPCClient) AttachRouter(router *router.Router) {
@@ -103,6 +112,9 @@ func (c *GRPCClient) receive() (appmessage.Message, error) {
func (c *GRPCClient) handleError(err error) {
if errors.Is(err, io.EOF) {
if c.onDisconnectedHandler != nil {
c.onDisconnectedHandler()
}
return
}
if errors.Is(err, router.ErrRouteClosed) {

View File

@@ -7,6 +7,7 @@ import (
"github.com/kaspanet/kaspad/infrastructure/network/rpcclient/grpcclient"
"github.com/kaspanet/kaspad/util/panics"
"github.com/pkg/errors"
"sync/atomic"
"time"
)
@@ -16,32 +17,112 @@ const defaultTimeout = 30 * time.Second
type RPCClient struct {
*grpcclient.GRPCClient
rpcAddress string
rpcRouter *rpcRouter
rpcAddress string
rpcRouter *rpcRouter
isConnected uint32
isClosed uint32
isReconnecting uint32
timeout time.Duration
}
// NewRPCClient creates a new RPC client
func NewRPCClient(rpcAddress string) (*RPCClient, error) {
rpcClient, err := grpcclient.Connect(rpcAddress)
if err != nil {
return nil, errors.Wrapf(err, "error connecting to address %s", rpcAddress)
rpcClient := &RPCClient{
rpcAddress: rpcAddress,
timeout: defaultTimeout,
}
err := rpcClient.connect()
if err != nil {
return nil, err
}
return rpcClient, nil
}
func (c *RPCClient) connect() error {
rpcClient, err := grpcclient.Connect(c.rpcAddress)
if err != nil {
return errors.Wrapf(err, "error connecting to address %s", c.rpcAddress)
}
rpcClient.SetOnDisconnectedHandler(c.handleClientDisconnected)
rpcClient.SetOnErrorHandler(c.handleClientError)
rpcRouter, err := buildRPCRouter()
if err != nil {
return nil, errors.Wrapf(err, "error creating the RPC router")
return errors.Wrapf(err, "error creating the RPC router")
}
atomic.StoreUint32(&c.isConnected, 1)
rpcClient.AttachRouter(rpcRouter.router)
log.Infof("Connected to server %s", rpcAddress)
c.GRPCClient = rpcClient
c.rpcRouter = rpcRouter
return &RPCClient{
GRPCClient: rpcClient,
rpcAddress: rpcAddress,
rpcRouter: rpcRouter,
timeout: defaultTimeout,
}, nil
log.Infof("Connected to %s", c.rpcAddress)
return nil
}
func (c *RPCClient) disconnect() error {
c.rpcRouter.router.Close()
err := c.GRPCClient.Disconnect()
if err != nil {
return err
}
log.Infof("Disconnected from %s", c.rpcAddress)
return nil
}
// Reconnect forces the client to attempt to reconnect to the address
// this client initially was connected to
func (c *RPCClient) Reconnect() error {
if atomic.LoadUint32(&c.isClosed) == 1 {
return errors.Errorf("Cannot reconnect from a closed client")
}
// Protect against multiple threads attempting to reconnect at the same time
swapped := atomic.CompareAndSwapUint32(&c.isReconnecting, 0, 1)
if !swapped {
// Already reconnecting
return nil
}
defer atomic.StoreUint32(&c.isReconnecting, 0)
log.Warnf("Attempting to reconnect to %s", c.rpcAddress)
// Disconnect if we're connected
if atomic.LoadUint32(&c.isConnected) == 1 {
err := c.disconnect()
if err != nil {
return err
}
}
// Attempt to connect until we succeed
for {
err := c.connect()
if err == nil {
return nil
}
log.Warnf("Could not automatically reconnect to %s: %s", c.rpcAddress, err)
const retryDelay = 10 * time.Second
log.Warnf("Retrying in %s", retryDelay)
time.Sleep(retryDelay)
}
}
func (c *RPCClient) handleClientDisconnected() {
atomic.StoreUint32(&c.isConnected, 0)
if atomic.LoadUint32(&c.isClosed) == 0 {
err := c.Reconnect()
if err != nil {
panic(err)
}
}
}
func (c *RPCClient) handleClientError(err error) {
log.Warnf("Received error from client: %s", err)
c.handleClientDisconnected()
}
// SetTimeout sets the timeout by which to wait for RPC responses
@@ -50,8 +131,13 @@ func (c *RPCClient) SetTimeout(timeout time.Duration) {
}
// Close closes the RPC client
func (c *RPCClient) Close() {
func (c *RPCClient) Close() error {
swapped := atomic.CompareAndSwapUint32(&c.isClosed, 0, 1)
if !swapped {
return errors.Errorf("Cannot close a client that had already been closed")
}
c.rpcRouter.router.Close()
return nil
}
// Address returns the address the RPC client connected to

View File

@@ -15,7 +15,6 @@ import (
"github.com/kaspanet/kaspad/util"
"github.com/kaspanet/kaspad/util/panics"
"github.com/pkg/errors"
"golang.org/x/crypto/blake2b"
)
const (
@@ -178,8 +177,8 @@ func useDirOrCreateTemp(dataDir, tempName string) (string, error) {
}
func mineOnTips(client *rpc.Client) (appmessage.RejectReason, error) {
fakePublicKeyHash := make([]byte, blake2b.Size256)
addr, err := util.NewAddressPubKeyHash(fakePublicKeyHash, activeConfig().NetParams().Prefix)
fakePublicKey := make([]byte, util.PublicKeySize)
addr, err := util.NewAddressPublicKey(fakePublicKey, activeConfig().NetParams().Prefix)
if err != nil {
return appmessage.RejectReasonNone, err
}

View File

@@ -97,7 +97,7 @@ func generateAddress() (util.Address, error) {
return nil, err
}
return util.NewAddressPubKeyHashFromPublicKey(pubKeySerialized[:], activeConfig().ActiveNetParams.Prefix)
return util.NewAddressPublicKey(pubKeySerialized[:], activeConfig().ActiveNetParams.Prefix)
}
func areTipsAreEqual(resultA, resultB *appmessage.GetBlockDAGInfoResponseMessage) bool {

View File

@@ -18,14 +18,14 @@ const (
rpcAddress2 = "127.0.0.1:12346"
rpcAddress3 = "127.0.0.1:12347"
miningAddress1 = "kaspasim:qr79e37hxdgkn4xjjmfxvqvayc5gsmsql2660d08u9ej9vnc8lzcywr265u64"
miningAddress1PrivateKey = "0ec5d7308f65717f3f0c3e4d962d73056c1c255a16593b3989589281b51ad5bc"
miningAddress1 = "kaspasim:qqqqnc0pxg7qw3qkc7l6sge8kfhsvvyt7mkw8uamtndqup27ftnd6c769gn66"
miningAddress1PrivateKey = "0d81045b0deb2af36a25403c2154c87aa82d89dd337b575bae27ce7f5de53cee"
miningAddress2 = "kaspasim:qpvr825ypd2fzq779yl83zvte2r4wlgxwra625rgthk9jj96d4cxgsegwryhg"
miningAddress2PrivateKey = "2a2e99d4a5c3e6d4add69e7baf66b9c7a2f17e74fad86cbd36a3a6815cecc10e"
miningAddress2 = "kaspasim:qqqqnc0pxg7qw3qkc7l6sge8kfhsvvyt7mkw8uamtndqup27ftnd6c769gn66"
miningAddress2PrivateKey = "0d81045b0deb2af36a25403c2154c87aa82d89dd337b575bae27ce7f5de53cee"
miningAddress3 = "kaspasim:qpvr825ypd2fzq779yl83zvte2r4wlgxwra625rgthk9jj96d4cxgsegwryhg"
miningAddress3PrivateKey = "2a2e99d4a5c3e6d4add69e7baf66b9c7a2f17e74fad86cbd36a3a6815cecc10e"
miningAddress3 = "kaspasim:qqq754f2gdcjcnykwuwwr60c82rh5u6mxxe7yqxljnrxz9fu0h95kduq9ezng"
miningAddress3PrivateKey = "f6c8f31fd359cbb97007034780bc4021f6ad01c6bc10499b79849efd4cc7ca39"
defaultTimeout = 10 * time.Second
)

View File

@@ -20,8 +20,11 @@ var (
)
const (
// PubKeyHash addresses always have the version byte set to 0.
pubKeyHashAddrID = 0x00
// PubKey addresses always have the version byte set to 0.
pubKeyAddrID = 0x00
// PubKey addresses always have the version byte set to 1.
pubKeyECDSAAddrID = 0x01
// ScriptHash addresses always have the version byte set to 8.
scriptHashAddrID = 0x08
@@ -79,16 +82,16 @@ func (prefix Bech32Prefix) String() string {
}
// encodeAddress returns a human-readable payment address given a network prefix
// and a blake2b hash which encodes the kaspa network and address type. It is used
// in both pay-to-pubkey-hash (P2PKH) and pay-to-script-hash (P2SH) address
// and a payload which encodes the kaspa network and address type. It is used
// in both pay-to-pubkey (P2PK) and pay-to-script-hash (P2SH) address
// encoding.
func encodeAddress(prefix Bech32Prefix, hash256 []byte, version byte) string {
return bech32.Encode(prefix.String(), hash256[:blake2b.Size256], version)
func encodeAddress(prefix Bech32Prefix, payload []byte, version byte) string {
return bech32.Encode(prefix.String(), payload, version)
}
// Address is an interface type for any type of destination a transaction
// output may spend to. This includes pay-to-pubkey (P2PK), pay-to-pubkey-hash
// (P2PKH), and pay-to-script-hash (P2SH). Address is designed to be generic
// output may spend to. This includes pay-to-pubkey (P2PK)
// and pay-to-script-hash (P2SH). Address is designed to be generic
// enough that other kinds of addresses may be added in the future without
// changing the decoding and encoding API.
type Address interface {
@@ -98,7 +101,7 @@ type Address interface {
// Please note that String differs subtly from EncodeAddress: String
// will return the value as a string without any conversion, while
// EncodeAddress may convert destination types (for example,
// converting pubkeys to P2PKH addresses) before encoding as a
// converting pubkeys to P2PK addresses) before encoding as a
// payment address string.
String() string
@@ -139,95 +142,143 @@ func DecodeAddress(addr string, expectedPrefix Bech32Prefix) (Address, error) {
prefix)
}
// Switch on decoded length to determine the type.
switch len(decoded) {
case blake2b.Size256: // P2PKH or P2SH
switch version {
case pubKeyHashAddrID:
return newAddressPubKeyHash(prefix, decoded)
case scriptHashAddrID:
return newAddressScriptHashFromHash(prefix, decoded)
default:
return nil, ErrUnknownAddressType
}
switch version {
case pubKeyAddrID:
return newAddressPubKey(prefix, decoded)
case pubKeyECDSAAddrID:
return newAddressPubKeyECDSA(prefix, decoded)
case scriptHashAddrID:
return newAddressScriptHashFromHash(prefix, decoded)
default:
return nil, errors.New("decoded address is of unknown size")
return nil, ErrUnknownAddressType
}
}
// AddressPubKeyHash is an Address for a pay-to-pubkey-hash (P2PKH)
// PublicKeySize is the public key size for a schnorr public key
const PublicKeySize = 32
// AddressPublicKey is an Address for a pay-to-pubkey (P2PK)
// transaction.
type AddressPubKeyHash struct {
prefix Bech32Prefix
hash [blake2b.Size256]byte
type AddressPublicKey struct {
prefix Bech32Prefix
publicKey [PublicKeySize]byte
}
// NewAddressPubKeyHashFromPublicKey returns a new AddressPubKeyHash from given public key
func NewAddressPubKeyHashFromPublicKey(publicKey []byte, prefix Bech32Prefix) (*AddressPubKeyHash, error) {
pkHash := HashBlake2b(publicKey)
return newAddressPubKeyHash(prefix, pkHash)
}
// NewAddressPubKeyHash returns a new AddressPubKeyHash. pkHash mustbe 20
// NewAddressPublicKey returns a new AddressPublicKey. publicKey must be 32
// bytes.
func NewAddressPubKeyHash(pkHash []byte, prefix Bech32Prefix) (*AddressPubKeyHash, error) {
return newAddressPubKeyHash(prefix, pkHash)
func NewAddressPublicKey(publicKey []byte, prefix Bech32Prefix) (*AddressPublicKey, error) {
return newAddressPubKey(prefix, publicKey)
}
// newAddressPubKeyHash is the internal API to create a pubkey hash address
// newAddressPubKey is the internal API to create a pubkey address
// with a known leading identifier byte for a network, rather than looking
// it up through its parameters. This is useful when creating a new address
// structure from a string encoding where the identifier byte is already
// known.
func newAddressPubKeyHash(prefix Bech32Prefix, pkHash []byte) (*AddressPubKeyHash, error) {
// Check for a valid pubkey hash length.
if len(pkHash) != blake2b.Size256 {
return nil, errors.Errorf("pkHash must be %d bytes", blake2b.Size256)
func newAddressPubKey(prefix Bech32Prefix, publicKey []byte) (*AddressPublicKey, error) {
// Check for a valid pubkey length.
if len(publicKey) != PublicKeySize {
return nil, errors.Errorf("publicKey must be %d bytes", PublicKeySize)
}
addr := &AddressPubKeyHash{prefix: prefix}
copy(addr.hash[:], pkHash)
addr := &AddressPublicKey{prefix: prefix}
copy(addr.publicKey[:], publicKey)
return addr, nil
}
// EncodeAddress returns the string encoding of a pay-to-pubkey-hash
// EncodeAddress returns the string encoding of a pay-to-pubkey
// address. Part of the Address interface.
func (a *AddressPubKeyHash) EncodeAddress() string {
return encodeAddress(a.prefix, a.hash[:], pubKeyHashAddrID)
func (a *AddressPublicKey) EncodeAddress() string {
return encodeAddress(a.prefix, a.publicKey[:], pubKeyAddrID)
}
// ScriptAddress returns the bytes to be included in a txout script to pay
// to a pubkey hash. Part of the Address interface.
func (a *AddressPubKeyHash) ScriptAddress() []byte {
return a.hash[:]
// to a pubkey. Part of the Address interface.
func (a *AddressPublicKey) ScriptAddress() []byte {
return a.publicKey[:]
}
// IsForPrefix returns whether or not the pay-to-pubkey-hash address is associated
// IsForPrefix returns whether or not the pay-to-pubkey address is associated
// with the passed kaspa network.
func (a *AddressPubKeyHash) IsForPrefix(prefix Bech32Prefix) bool {
func (a *AddressPublicKey) IsForPrefix(prefix Bech32Prefix) bool {
return a.prefix == prefix
}
// Prefix returns the prefix for this address
func (a *AddressPubKeyHash) Prefix() Bech32Prefix {
func (a *AddressPublicKey) Prefix() Bech32Prefix {
return a.prefix
}
// String returns a human-readable string for the pay-to-pubkey-hash address.
// String returns a human-readable string for the pay-to-pubkey address.
// This is equivalent to calling EncodeAddress, but is provided so the type can
// be used as a fmt.Stringer.
func (a *AddressPubKeyHash) String() string {
func (a *AddressPublicKey) String() string {
return a.EncodeAddress()
}
// HashBlake2b returns the underlying array of the pubkey hash. This can be useful
// when an array is more appropiate than a slice (for example, when used as map
// keys).
func (a *AddressPubKeyHash) HashBlake2b() *[blake2b.Size256]byte {
return &a.hash
// PublicKeySizeECDSA is the public key size for an ECDSA public key
const PublicKeySizeECDSA = 33
// AddressPublicKeyECDSA is an Address for a pay-to-pubkey (P2PK)
// ECDSA transaction.
type AddressPublicKeyECDSA struct {
prefix Bech32Prefix
publicKey [PublicKeySizeECDSA]byte
}
// AddressScriptHash is an Address for a pay-to-script-hash (P2SH)
// NewAddressPublicKeyECDSA returns a new AddressPublicKeyECDSA. publicKey must be 33
// bytes.
func NewAddressPublicKeyECDSA(publicKey []byte, prefix Bech32Prefix) (*AddressPublicKeyECDSA, error) {
return newAddressPubKeyECDSA(prefix, publicKey)
}
// newAddressPubKeyECDSA is the internal API to create an ECDSA pubkey address
// with a known leading identifier byte for a network, rather than looking
// it up through its parameters. This is useful when creating a new address
// structure from a string encoding where the identifier byte is already
// known.
func newAddressPubKeyECDSA(prefix Bech32Prefix, publicKey []byte) (*AddressPublicKeyECDSA, error) {
// Check for a valid pubkey length.
if len(publicKey) != PublicKeySizeECDSA {
return nil, errors.Errorf("publicKey must be %d bytes", PublicKeySizeECDSA)
}
addr := &AddressPublicKeyECDSA{prefix: prefix}
copy(addr.publicKey[:], publicKey)
return addr, nil
}
// EncodeAddress returns the string encoding of a pay-to-pubkey
// address. Part of the Address interface.
func (a *AddressPublicKeyECDSA) EncodeAddress() string {
return encodeAddress(a.prefix, a.publicKey[:], pubKeyECDSAAddrID)
}
// ScriptAddress returns the bytes to be included in a txout script to pay
// to a pubkey. Part of the Address interface.
func (a *AddressPublicKeyECDSA) ScriptAddress() []byte {
return a.publicKey[:]
}
// IsForPrefix returns whether or not the pay-to-pubkey address is associated
// with the passed kaspa network.
func (a *AddressPublicKeyECDSA) IsForPrefix(prefix Bech32Prefix) bool {
return a.prefix == prefix
}
// Prefix returns the prefix for this address
func (a *AddressPublicKeyECDSA) Prefix() Bech32Prefix {
return a.prefix
}
// String returns a human-readable string for the pay-to-pubkey address.
// This is equivalent to calling EncodeAddress, but is provided so the type can
// be used as a fmt.Stringer.
func (a *AddressPublicKeyECDSA) String() string {
return a.EncodeAddress()
}
// AddressScriptHash is an Address for a pay-to-script-publicKey (P2SH)
// transaction.
type AddressScriptHash struct {
prefix Bech32Prefix

View File

@@ -26,99 +26,125 @@ func TestAddresses(t *testing.T) {
passedPrefix util.Bech32Prefix
expectedPrefix util.Bech32Prefix
}{
// Positive P2PKH tests.
// Positive P2PK tests.
{
name: "mainnet p2pkh",
name: "mainnet p2pk",
addr: "kaspa:qr35ennsep3hxfe7lnz5ee7j5jgmkjswsn35ennsep3hxfe7ln35cdv0dy335",
encoded: "kaspa:qr35ennsep3hxfe7lnz5ee7j5jgmkjswsn35ennsep3hxfe7ln35cdv0dy335",
valid: true,
result: util.TstAddressPubKeyHash(
result: util.TstAddressPubKey(
util.Bech32PrefixKaspa,
[blake2b.Size256]byte{
[util.PublicKeySize]byte{
0xe3, 0x4c, 0xce, 0x70, 0xc8, 0x63, 0x73, 0x27, 0x3e, 0xfc,
0xc5, 0x4c, 0xe7, 0xd2, 0xa4, 0x91, 0xbb, 0x4a, 0x0e, 0x84,
0xe3, 0x4c, 0xce, 0x70, 0xc8, 0x63, 0x73, 0x27, 0x3e, 0xfc,
0xe3, 0x4c,
}),
f: func() (util.Address, error) {
pkHash := []byte{
publicKey := []byte{
0xe3, 0x4c, 0xce, 0x70, 0xc8, 0x63, 0x73, 0x27, 0x3e, 0xfc,
0xc5, 0x4c, 0xe7, 0xd2, 0xa4, 0x91, 0xbb, 0x4a, 0x0e, 0x84,
0xe3, 0x4c, 0xce, 0x70, 0xc8, 0x63, 0x73, 0x27, 0x3e, 0xfc,
0xe3, 0x4c}
return util.NewAddressPubKeyHash(pkHash, util.Bech32PrefixKaspa)
return util.NewAddressPublicKey(publicKey, util.Bech32PrefixKaspa)
},
passedPrefix: util.Bech32PrefixUnknown,
expectedPrefix: util.Bech32PrefixKaspa,
},
{
name: "mainnet p2pkh 2",
name: "mainnet p2pk 2",
addr: "kaspa:qq80qvqs0lfxuzmt7sz3909ze6camq9d4t35ennsep3hxfe7ln35cvfqgz3z8",
encoded: "kaspa:qq80qvqs0lfxuzmt7sz3909ze6camq9d4t35ennsep3hxfe7ln35cvfqgz3z8",
valid: true,
result: util.TstAddressPubKeyHash(
result: util.TstAddressPubKey(
util.Bech32PrefixKaspa,
[blake2b.Size256]byte{
[util.PublicKeySize]byte{
0x0e, 0xf0, 0x30, 0x10, 0x7f, 0xd2, 0x6e, 0x0b, 0x6b, 0xf4,
0x05, 0x12, 0xbc, 0xa2, 0xce, 0xb1, 0xdd, 0x80, 0xad, 0xaa,
0xe3, 0x4c, 0xce, 0x70, 0xc8, 0x63, 0x73, 0x27, 0x3e, 0xfc,
0xe3, 0x4c,
}),
f: func() (util.Address, error) {
pkHash := []byte{
publicKey := []byte{
0x0e, 0xf0, 0x30, 0x10, 0x7f, 0xd2, 0x6e, 0x0b, 0x6b, 0xf4,
0x05, 0x12, 0xbc, 0xa2, 0xce, 0xb1, 0xdd, 0x80, 0xad, 0xaa,
0xe3, 0x4c, 0xce, 0x70, 0xc8, 0x63, 0x73, 0x27, 0x3e, 0xfc,
0xe3, 0x4c,
}
return util.NewAddressPubKeyHash(pkHash, util.Bech32PrefixKaspa)
return util.NewAddressPublicKey(publicKey, util.Bech32PrefixKaspa)
},
passedPrefix: util.Bech32PrefixKaspa,
expectedPrefix: util.Bech32PrefixKaspa,
},
{
name: "testnet p2pkh",
name: "testnet p2pk",
addr: "kaspatest:qputx94qseratdmjs0j395mq8u03er0x3l35ennsep3hxfe7ln35ckquw528z",
encoded: "kaspatest:qputx94qseratdmjs0j395mq8u03er0x3l35ennsep3hxfe7ln35ckquw528z",
valid: true,
result: util.TstAddressPubKeyHash(
result: util.TstAddressPubKey(
util.Bech32PrefixKaspaTest,
[blake2b.Size256]byte{
[util.PublicKeySize]byte{
0x78, 0xb3, 0x16, 0xa0, 0x86, 0x47, 0xd5, 0xb7, 0x72, 0x83,
0xe5, 0x12, 0xd3, 0x60, 0x3f, 0x1f, 0x1c, 0x8d, 0xe6, 0x8f,
0xe3, 0x4c, 0xce, 0x70, 0xc8, 0x63, 0x73, 0x27, 0x3e, 0xfc,
0xe3, 0x4c,
}),
f: func() (util.Address, error) {
pkHash := []byte{
publicKey := []byte{
0x78, 0xb3, 0x16, 0xa0, 0x86, 0x47, 0xd5, 0xb7, 0x72, 0x83,
0xe5, 0x12, 0xd3, 0x60, 0x3f, 0x1f, 0x1c, 0x8d, 0xe6, 0x8f,
0xe3, 0x4c, 0xce, 0x70, 0xc8, 0x63, 0x73, 0x27, 0x3e, 0xfc,
0xe3, 0x4c,
}
return util.NewAddressPubKeyHash(pkHash, util.Bech32PrefixKaspaTest)
return util.NewAddressPublicKey(publicKey, util.Bech32PrefixKaspaTest)
},
passedPrefix: util.Bech32PrefixKaspaTest,
expectedPrefix: util.Bech32PrefixKaspaTest,
},
// Negative P2PKH tests.
// ECDSA P2PK tests.
{
name: "p2pkh wrong hash length",
name: "mainnet ecdsa p2pk",
addr: "kaspa:q835ennsep3hxfe7lnz5ee7j5jgmkjswsn35ennsep3hxfe7ln35e2sm7yrlr4w",
encoded: "kaspa:q835ennsep3hxfe7lnz5ee7j5jgmkjswsn35ennsep3hxfe7ln35e2sm7yrlr4w",
valid: true,
result: util.TstAddressPubKeyECDSA(
util.Bech32PrefixKaspa,
[util.PublicKeySizeECDSA]byte{
0xe3, 0x4c, 0xce, 0x70, 0xc8, 0x63, 0x73, 0x27, 0x3e, 0xfc,
0xc5, 0x4c, 0xe7, 0xd2, 0xa4, 0x91, 0xbb, 0x4a, 0x0e, 0x84,
0xe3, 0x4c, 0xce, 0x70, 0xc8, 0x63, 0x73, 0x27, 0x3e, 0xfc,
0xe3, 0x4c, 0xaa,
}),
f: func() (util.Address, error) {
publicKey := []byte{
0xe3, 0x4c, 0xce, 0x70, 0xc8, 0x63, 0x73, 0x27, 0x3e, 0xfc,
0xc5, 0x4c, 0xe7, 0xd2, 0xa4, 0x91, 0xbb, 0x4a, 0x0e, 0x84,
0xe3, 0x4c, 0xce, 0x70, 0xc8, 0x63, 0x73, 0x27, 0x3e, 0xfc,
0xe3, 0x4c, 0xaa}
return util.NewAddressPublicKeyECDSA(publicKey, util.Bech32PrefixKaspa)
},
passedPrefix: util.Bech32PrefixUnknown,
expectedPrefix: util.Bech32PrefixKaspa,
},
// Negative P2PK tests.
{
name: "p2pk wrong public key length",
addr: "",
valid: false,
f: func() (util.Address, error) {
pkHash := []byte{
publicKey := []byte{
0x00, 0x0e, 0xf0, 0x30, 0x10, 0x7f, 0xd2, 0x6e, 0x0b, 0x6b,
0xf4, 0x05, 0x12, 0xbc, 0xa2, 0xce, 0xb1, 0xdd, 0x80, 0xad,
0xaa}
return util.NewAddressPubKeyHash(pkHash, util.Bech32PrefixKaspa)
return util.NewAddressPublicKey(publicKey, util.Bech32PrefixKaspa)
},
passedPrefix: util.Bech32PrefixKaspa,
expectedPrefix: util.Bech32PrefixKaspa,
},
{
name: "p2pkh bad checksum",
name: "p2pk bad checksum",
addr: "kaspa:qr35ennsep3hxfe7lnz5ee7j5jgmkjswss74as46gx",
valid: false,
passedPrefix: util.Bech32PrefixKaspa,
@@ -168,12 +194,9 @@ func TestAddresses(t *testing.T) {
expectedPrefix: util.Bech32PrefixKaspa,
},
{
// Taken from transactions:
// output: b0539a45de13b3e0403909b8bd1a555b8cbe45fd4e3f3fda76f3a5f52835c29d
// input: (not yet redeemed at time test was written)
name: "mainnet p2sh 2",
addr: "kaspa:pr5vxqxg0xrwl2zvxlq9rxffqx00sm44ksqqqqqqqqqqqqqqqqqqq33flv3je",
encoded: "kaspa:pr5vxqxg0xrwl2zvxlq9rxffqx00sm44ksqqqqqqqqqqqqqqqqqqq33flv3je",
addr: "kaspa:pr5vxqxg0xrwl2zvxlq9rxffqx00sm44kn5vxqxg0xrwl2zvxl5vxyhvsake2",
encoded: "kaspa:pr5vxqxg0xrwl2zvxlq9rxffqx00sm44kn5vxqxg0xrwl2zvxl5vxyhvsake2",
valid: true,
result: util.TstAddressScriptHash(
util.Bech32PrefixKaspa,
@@ -197,8 +220,8 @@ func TestAddresses(t *testing.T) {
},
{
name: "testnet p2sh",
addr: "kaspatest:przhjdpv93xfygpqtckdc2zkzuzqeyj2pgqqqqqqqqqqqqqqqqqqqyjpt4duk",
encoded: "kaspatest:przhjdpv93xfygpqtckdc2zkzuzqeyj2pgqqqqqqqqqqqqqqqqqqqyjpt4duk",
addr: "kaspatest:przhjdpv93xfygpqtckdc2zkzuzqeyj2pt5vxqxg0xrwl2zvxl5vx35yyy2h9",
encoded: "kaspatest:przhjdpv93xfygpqtckdc2zkzuzqeyj2pt5vxqxg0xrwl2zvxl5vx35yyy2h9",
valid: true,
result: util.TstAddressScriptHash(
util.Bech32PrefixKaspaTest,
@@ -270,23 +293,25 @@ func TestAddresses(t *testing.T) {
// Perform type-specific calculations.
var saddr []byte
switch decoded.(type) {
case *util.AddressPubKeyHash:
saddr = util.TstAddressSAddr(encoded)
case *util.AddressPublicKey:
saddr = util.TstAddressSAddrP2PK(encoded)
case *util.AddressPublicKeyECDSA:
saddr = util.TstAddressSAddrP2PKECDSA(encoded)
case *util.AddressScriptHash:
saddr = util.TstAddressSAddr(encoded)
saddr = util.TstAddressSAddrP2SH(encoded)
}
// Check script address, as well as the HashBlake2b method for P2PKH and
// P2SH addresses.
// Check script address, as well as the HashBlake2b method for P2SH addresses.
if !bytes.Equal(saddr, decoded.ScriptAddress()) {
t.Errorf("%v: script addresses do not match:\n%x != \n%x",
test.name, saddr, decoded.ScriptAddress())
return
}
switch a := decoded.(type) {
case *util.AddressPubKeyHash:
if h := a.HashBlake2b()[:]; !bytes.Equal(saddr, h) {
case *util.AddressPublicKey:
if h := a.ScriptAddress()[:]; !bytes.Equal(saddr, h) {
t.Errorf("%v: hashes do not match:\n%x != \n%x",
test.name, saddr, h)
return
@@ -335,6 +360,18 @@ func TestAddresses(t *testing.T) {
test.name)
return
}
if !reflect.DeepEqual(addr, decoded) {
t.Errorf("%v: created address does not match the decoded address",
test.name)
return
}
if !reflect.DeepEqual(addr, decoded) {
t.Errorf("%v: created address does not match the decoded address",
test.name)
return
}
}
}
@@ -357,7 +394,7 @@ func TestDecodeAddressErrorConditions(t *testing.T) {
{
"kaspasim:raskzcg58mth0an",
util.Bech32PrefixKaspaSim,
"decoded address is of unknown size",
"unknown address type",
},
{
"kaspatest:qqq65mvpxcmajeq44n2n8vfn6u9f8l4zsy0xez0tzw",

View File

@@ -18,14 +18,14 @@ expensive hashing operations.
Address Overview
The Address interface provides an abstraction for a kaspa address. While the
most common type is a pay-to-pubkey-hash, kaspa already supports others and
most common type is a pay-to-pubkey, kaspa already supports others and
may well support more in the future. This package currently provides
implementations for the pay-to-pubkey-hash, and pay-to-script-hash address
implementations for the pay-to-pubkey, and pay-to-script-hash address
types.
To decode/encode an address:
addrString := "kaspa:qqfgqp8l9l90zwetj84k2jcac2m8falvvyy8xjtnhd"
addrString := "kaspa:qqj9fg59mptxkr9j0y53j5mwurcmda5mtza9n6v9pm9uj8h0wgk6uma5pvumr"
defaultPrefix := util.Bech32PrefixKaspa
addr, err := util.DecodeAddress(addrString, defaultPrefix)
if err != nil {

View File

@@ -22,10 +22,17 @@ func TstAppDataDir(goos, appName string, roaming bool) string {
return appDir(goos, appName, roaming)
}
func TstAddressPubKeyHash(prefix Bech32Prefix, hash [blake2b.Size256]byte) *AddressPubKeyHash {
return &AddressPubKeyHash{
prefix: prefix,
hash: hash,
func TstAddressPubKey(prefix Bech32Prefix, hash [PublicKeySize]byte) *AddressPublicKey {
return &AddressPublicKey{
prefix: prefix,
publicKey: hash,
}
}
func TstAddressPubKeyECDSA(prefix Bech32Prefix, hash [PublicKeySizeECDSA]byte) *AddressPublicKeyECDSA {
return &AddressPublicKeyECDSA{
prefix: prefix,
publicKey: hash,
}
}
@@ -40,8 +47,22 @@ func TstAddressScriptHash(prefix Bech32Prefix, hash [blake2b.Size256]byte) *Addr
}
// TstAddressSAddr returns the expected script address bytes for
// P2PKH and P2SH kaspa addresses.
func TstAddressSAddr(addr string) []byte {
// P2PK kaspa addresses.
func TstAddressSAddrP2PK(addr string) []byte {
_, decoded, _, _ := bech32.Decode(addr)
return decoded[:PublicKeySize]
}
// TstAddressSAddr returns the expected script address bytes for
// ECDSA P2PK kaspa addresses.
func TstAddressSAddrP2PKECDSA(addr string) []byte {
_, decoded, _, _ := bech32.Decode(addr)
return decoded[:PublicKeySizeECDSA]
}
// TstAddressSAddrP2SH returns the expected script address bytes for
// P2SH kaspa addresses.
func TstAddressSAddrP2SH(addr string) []byte {
_, decoded, _, _ := bech32.Decode(addr)
return decoded[:blake2b.Size256]
}