Add ECDSA support to the wallet (#1664)

* Add ECDSA support to the wallet

* Fix genkeypair

* Fix typo and rename var
This commit is contained in:
Ori Newman 2021-04-06 17:25:09 +03:00 committed by GitHub
parent 7186f83095
commit d2cccd2829
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 484 additions and 360 deletions

View File

@ -12,7 +12,7 @@ func main() {
panic(err) panic(err)
} }
privateKey, publicKey, err := libkaspawallet.CreateKeyPair() privateKey, publicKey, err := libkaspawallet.CreateKeyPair(false)
if err != nil { if err != nil {
panic(err) panic(err)
} }

View File

@ -18,7 +18,7 @@ func balance(conf *balanceConfig) error {
return err 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 { if err != nil {
return err return err
} }

View File

@ -28,6 +28,7 @@ type createConfig struct {
MinimumSignatures uint32 `long:"min-signatures" short:"m" description:"Minimum required signatures" default:"1"` MinimumSignatures uint32 `long:"min-signatures" short:"m" description:"Minimum required signatures" default:"1"`
NumPrivateKeys uint32 `long:"num-private-keys" short:"k" description:"Number of private keys" default:"1"` 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"` 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)"` Import bool `long:"import" short:"i" description:"Import private keys (as opposed to generating them)"`
config.NetworkFlags config.NetworkFlags
} }

View File

@ -15,7 +15,7 @@ func create(conf *createConfig) error {
var publicKeys [][]byte var publicKeys [][]byte
var err error var err error
if !conf.Import { if !conf.Import {
encryptedPrivateKeys, publicKeys, err = keys.CreateKeyPairs(conf.NumPrivateKeys) encryptedPrivateKeys, publicKeys, err = keys.CreateKeyPairs(conf.NumPrivateKeys, conf.ECDSA)
} else { } else {
encryptedPrivateKeys, publicKeys, err = keys.ImportKeyPairs(conf.NumPrivateKeys) encryptedPrivateKeys, publicKeys, err = keys.ImportKeyPairs(conf.NumPrivateKeys)
} }
@ -49,7 +49,7 @@ func create(conf *createConfig) error {
publicKeys = append(publicKeys, publicKey) 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 { if err != nil {
return err return err
} }
@ -59,7 +59,7 @@ func create(conf *createConfig) error {
return err 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 { if err != nil {
return err return err
} }

View File

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

View File

@ -12,9 +12,9 @@ import (
) )
// CreateKeyPairs generates `numKeys` number of key pairs. // 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 createKeyPairsFromFunction(numKeys, func(_ uint32) ([]byte, []byte, error) {
return libkaspawallet.CreateKeyPair() return libkaspawallet.CreateKeyPair(ecdsa)
}) })
} }

View File

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

View File

@ -8,7 +8,15 @@ import (
) )
// CreateKeyPair generates a private-public key pair // CreateKeyPair generates a private-public key pair
func CreateKeyPair() ([]byte, []byte, error) { func CreateKeyPair(ecdsa bool) ([]byte, []byte, error) {
if ecdsa {
return createKeyPairECDSA()
}
return createKeyPair()
}
func createKeyPair() ([]byte, []byte, error) {
keyPair, err := secp256k1.GenerateSchnorrKeyPair() keyPair, err := secp256k1.GenerateSchnorrKeyPair()
if err != nil { if err != nil {
return nil, nil, errors.Wrap(err, "Failed to generate private key") return nil, nil, errors.Wrap(err, "Failed to generate private key")
@ -25,11 +33,28 @@ func CreateKeyPair() ([]byte, []byte, error) {
return keyPair.SerializePrivateKey()[:], publicKeySerialized[:], nil return keyPair.SerializePrivateKey()[:], publicKeySerialized[:], nil
} }
func createKeyPairECDSA() ([]byte, []byte, error) {
keyPair, err := secp256k1.GenerateECDSAPrivateKey()
if err != nil {
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 keyPair.Serialize()[:], publicKeySerialized[:], nil
}
// PublicKeyFromPrivateKey returns the public key associated with a private key // PublicKeyFromPrivateKey returns the public key associated with a private key
func PublicKeyFromPrivateKey(privateKeyBytes []byte) ([]byte, error) { func PublicKeyFromPrivateKey(privateKeyBytes []byte) ([]byte, error) {
keyPair, err := secp256k1.DeserializeSchnorrPrivateKeyFromSlice(privateKeyBytes) keyPair, err := secp256k1.DeserializeSchnorrPrivateKeyFromSlice(privateKeyBytes)
if err != nil { if err != nil {
return nil, errors.Wrap(err, "Failed to deserialized private key") return nil, errors.Wrap(err, "Failed to deserialize private key")
} }
publicKey, err := keyPair.SchnorrPublicKey() publicKey, err := keyPair.SchnorrPublicKey()
@ -45,40 +70,21 @@ func PublicKeyFromPrivateKey(privateKeyBytes []byte) ([]byte, error) {
return publicKeySerialized[:], nil return publicKeySerialized[:], nil
} }
func keyPairBytes(keyPair *secp256k1.SchnorrKeyPair) ([]byte, []byte, error) {
publicKey, err := keyPair.SchnorrPublicKey()
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 keyPair.SerializePrivateKey()[:], publicKeySerialized[:], nil
}
func addressFromPublicKey(params *dagconfig.Params, publicKeySerialized []byte) (util.Address, error) {
addr, err := util.NewAddressPublicKey(publicKeySerialized[:], params.Prefix)
if err != nil {
return nil, errors.Wrap(err, "Failed to generate p2pk address")
}
return addr, nil
}
// Address returns the address associated with the given public keys and minimum signatures parameters. // 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) sortPublicKeys(pubKeys)
if uint32(len(pubKeys)) < minimumSignatures { if uint32(len(pubKeys)) < minimumSignatures {
return nil, errors.Errorf("The minimum amount of signatures (%d) is greater than the amount of "+ return nil, errors.Errorf("The minimum amount of signatures (%d) is greater than the amount of "+
"provided public keys (%d)", minimumSignatures, len(pubKeys)) "provided public keys (%d)", minimumSignatures, len(pubKeys))
} }
if len(pubKeys) == 1 { 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 { if err != nil {
return nil, err 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 ( import (
"bytes" "bytes"
"github.com/kaspanet/go-secp256k1"
"github.com/kaspanet/kaspad/cmd/kaspawallet/libkaspawallet/serialization" "github.com/kaspanet/kaspad/cmd/kaspawallet/libkaspawallet/serialization"
"github.com/kaspanet/kaspad/domain/consensus/model/externalapi" "github.com/kaspanet/kaspad/domain/consensus/model/externalapi"
"github.com/kaspanet/kaspad/domain/consensus/utils/consensushashing"
"github.com/kaspanet/kaspad/domain/consensus/utils/constants" "github.com/kaspanet/kaspad/domain/consensus/utils/constants"
"github.com/kaspanet/kaspad/domain/consensus/utils/subnetworks" "github.com/kaspanet/kaspad/domain/consensus/utils/subnetworks"
"github.com/kaspanet/kaspad/domain/consensus/utils/txscript" "github.com/kaspanet/kaspad/domain/consensus/utils/txscript"
"github.com/kaspanet/kaspad/domain/consensus/utils/utxo"
"github.com/kaspanet/kaspad/util" "github.com/kaspanet/kaspad/util"
"github.com/pkg/errors" "github.com/pkg/errors"
"sort" "sort"
@ -31,11 +28,12 @@ func sortPublicKeys(publicKeys [][]byte) {
func CreateUnsignedTransaction( func CreateUnsignedTransaction(
pubKeys [][]byte, pubKeys [][]byte,
minimumSignatures uint32, minimumSignatures uint32,
ecdsa bool,
payments []*Payment, payments []*Payment,
selectedUTXOs []*externalapi.OutpointAndUTXOEntryPair) ([]byte, error) { selectedUTXOs []*externalapi.OutpointAndUTXOEntryPair) ([]byte, error) {
sortPublicKeys(pubKeys) sortPublicKeys(pubKeys)
unsignedTransaction, err := createUnsignedTransaction(pubKeys, minimumSignatures, payments, selectedUTXOs) unsignedTransaction, err := createUnsignedTransaction(pubKeys, minimumSignatures, ecdsa, payments, selectedUTXOs)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -43,53 +41,34 @@ func CreateUnsignedTransaction(
return serialization.SerializePartiallySignedTransaction(unsignedTransaction) return serialization.SerializePartiallySignedTransaction(unsignedTransaction)
} }
// Sign signs the transaction with the given private keys func multiSigRedeemScript(pubKeys [][]byte, minimumSignatures uint32, ecdsa bool) ([]byte, error) {
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) {
scriptBuilder := txscript.NewScriptBuilder() scriptBuilder := txscript.NewScriptBuilder()
scriptBuilder.AddInt64(int64(minimumSignatures)) scriptBuilder.AddInt64(int64(minimumSignatures))
for _, key := range pubKeys { for _, key := range pubKeys {
scriptBuilder.AddData(key) scriptBuilder.AddData(key)
} }
scriptBuilder.AddInt64(int64(len(pubKeys))) scriptBuilder.AddInt64(int64(len(pubKeys)))
scriptBuilder.AddOp(txscript.OpCheckMultiSig)
if ecdsa {
scriptBuilder.AddOp(txscript.OpCheckMultiSigECDSA)
} else {
scriptBuilder.AddOp(txscript.OpCheckMultiSig)
}
return scriptBuilder.Script() return scriptBuilder.Script()
} }
func createUnsignedTransaction( func createUnsignedTransaction(
pubKeys [][]byte, pubKeys [][]byte,
minimumSignatures uint32, minimumSignatures uint32,
ecdsa bool,
payments []*Payment, payments []*Payment,
selectedUTXOs []*externalapi.OutpointAndUTXOEntryPair) (*serialization.PartiallySignedTransaction, error) { selectedUTXOs []*externalapi.OutpointAndUTXOEntryPair) (*serialization.PartiallySignedTransaction, error) {
var redeemScript []byte var redeemScript []byte
if len(pubKeys) > 1 { if len(pubKeys) > 1 {
var err error var err error
redeemScript, err = multiSigRedeemScript(pubKeys, minimumSignatures) redeemScript, err = multiSigRedeemScript(pubKeys, minimumSignatures, ecdsa)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -146,53 +125,6 @@ func createUnsignedTransaction(
}, nil }, 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. // IsTransactionFullySigned returns whether the transaction is fully signed and ready to broadcast.
func IsTransactionFullySigned(psTxBytes []byte) (bool, error) { func IsTransactionFullySigned(psTxBytes []byte) (bool, error) {
partiallySignedTransaction, err := serialization.DeserializePartiallySignedTransaction(psTxBytes) partiallySignedTransaction, err := serialization.DeserializePartiallySignedTransaction(psTxBytes)

View File

@ -15,254 +15,277 @@ import (
"testing" "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) { func TestMultisig(t *testing.T) {
testutils.ForAllNets(t, true, func(t *testing.T, params *dagconfig.Params) { testutils.ForAllNets(t, true, func(t *testing.T, params *dagconfig.Params) {
params.BlockCoinbaseMaturity = 0 forSchnorrAndECDSA(t, func(t *testing.T, ecdsa bool) {
tc, teardown, err := consensus.NewFactory().NewTestConsensus(params, false, "TestMultisig") params.BlockCoinbaseMaturity = 0
if err != nil { tc, teardown, err := consensus.NewFactory().NewTestConsensus(params, false, "TestMultisig")
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()
if err != nil { if err != nil {
t.Fatalf("CreateKeyPair: %+v", err) t.Fatalf("Error setting up tc: %+v", err)
} }
} defer teardown(false)
const minimumSignatures = 2 const numKeys = 3
address, err := libkaspawallet.Address(params, publicKeys, minimumSignatures) privateKeys := make([][]byte, numKeys)
if err != nil { publicKeys := make([][]byte, numKeys)
t.Fatalf("Address: %+v", err) 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 { const minimumSignatures = 2
t.Fatalf("The address is of unexpected type") address, err := libkaspawallet.Address(params, publicKeys, minimumSignatures, ecdsa)
} if err != nil {
t.Fatalf("Address: %+v", err)
}
scriptPublicKey, err := txscript.PayToAddrScript(address) if _, ok := address.(*util.AddressScriptHash); !ok {
if err != nil { t.Fatalf("The address is of unexpected type")
t.Fatalf("PayToAddrScript: %+v", err) }
}
coinbaseData := &externalapi.DomainCoinbaseData{ scriptPublicKey, err := txscript.PayToAddrScript(address)
ScriptPublicKey: scriptPublicKey, if err != nil {
ExtraData: nil, t.Fatalf("PayToAddrScript: %+v", err)
} }
fundingBlockHash, _, err := tc.AddBlock([]*externalapi.DomainHash{params.GenesisHash}, coinbaseData, nil) coinbaseData := &externalapi.DomainCoinbaseData{
if err != nil { ScriptPublicKey: scriptPublicKey,
t.Fatalf("AddBlock: %+v", err) ExtraData: nil,
} }
block1Hash, _, err := tc.AddBlock([]*externalapi.DomainHash{fundingBlockHash}, nil, nil) fundingBlockHash, _, err := tc.AddBlock([]*externalapi.DomainHash{params.GenesisHash}, coinbaseData, nil)
if err != nil { if err != nil {
t.Fatalf("AddBlock: %+v", err) t.Fatalf("AddBlock: %+v", err)
} }
block1, err := tc.GetBlock(block1Hash) block1Hash, _, err := tc.AddBlock([]*externalapi.DomainHash{fundingBlockHash}, nil, nil)
if err != nil { if err != nil {
t.Fatalf("GetBlock: %+v", err) t.Fatalf("AddBlock: %+v", err)
} }
block1Tx := block1.Transactions[0] block1, err := tc.GetBlock(block1Hash)
block1TxOut := block1Tx.Outputs[0] if err != nil {
selectedUTXOs := []*externalapi.OutpointAndUTXOEntryPair{{ t.Fatalf("GetBlock: %+v", err)
Outpoint: &externalapi.DomainOutpoint{ }
TransactionID: *consensushashing.TransactionID(block1.Transactions[0]),
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, Index: 0,
}, }
UTXOEntry: utxo.NewUTXOEntry(block1TxOut.Value, block1TxOut.ScriptPublicKey, true, 0), if !insertionResult.VirtualUTXODiff.ToAdd().Contains(addedUTXO) {
}} t.Fatalf("Transaction wasn't accepted in the DAG")
}
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")
}
}) })
} }
func TestP2PK(t *testing.T) { func TestP2PK(t *testing.T) {
testutils.ForAllNets(t, true, func(t *testing.T, params *dagconfig.Params) { testutils.ForAllNets(t, true, func(t *testing.T, params *dagconfig.Params) {
params.BlockCoinbaseMaturity = 0 forSchnorrAndECDSA(t, func(t *testing.T, ecdsa bool) {
tc, teardown, err := consensus.NewFactory().NewTestConsensus(params, false, "TestMultisig") params.BlockCoinbaseMaturity = 0
if err != nil { tc, teardown, err := consensus.NewFactory().NewTestConsensus(params, false, "TestMultisig")
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()
if err != nil { if err != nil {
t.Fatalf("CreateKeyPair: %+v", err) t.Fatalf("Error setting up tc: %+v", err)
} }
} defer teardown(false)
const minimumSignatures = 1 const numKeys = 1
address, err := libkaspawallet.Address(params, publicKeys, minimumSignatures) privateKeys := make([][]byte, numKeys)
if err != nil { publicKeys := make([][]byte, numKeys)
t.Fatalf("Address: %+v", err) 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.AddressPublicKey); !ok { const minimumSignatures = 1
t.Fatalf("The address is of unexpected type") address, err := libkaspawallet.Address(params, publicKeys, minimumSignatures, ecdsa)
} if err != nil {
t.Fatalf("Address: %+v", err)
}
scriptPublicKey, err := txscript.PayToAddrScript(address) if ecdsa {
if err != nil { if _, ok := address.(*util.AddressPublicKeyECDSA); !ok {
t.Fatalf("PayToAddrScript: %+v", err) 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, err := txscript.PayToAddrScript(address)
ScriptPublicKey: scriptPublicKey, if err != nil {
ExtraData: nil, t.Fatalf("PayToAddrScript: %+v", err)
} }
fundingBlockHash, _, err := tc.AddBlock([]*externalapi.DomainHash{params.GenesisHash}, coinbaseData, nil) coinbaseData := &externalapi.DomainCoinbaseData{
if err != nil { ScriptPublicKey: scriptPublicKey,
t.Fatalf("AddBlock: %+v", err) ExtraData: nil,
} }
block1Hash, _, err := tc.AddBlock([]*externalapi.DomainHash{fundingBlockHash}, nil, nil) fundingBlockHash, _, err := tc.AddBlock([]*externalapi.DomainHash{params.GenesisHash}, coinbaseData, nil)
if err != nil { if err != nil {
t.Fatalf("AddBlock: %+v", err) t.Fatalf("AddBlock: %+v", err)
} }
block1, err := tc.GetBlock(block1Hash) block1Hash, _, err := tc.AddBlock([]*externalapi.DomainHash{fundingBlockHash}, nil, nil)
if err != nil { if err != nil {
t.Fatalf("GetBlock: %+v", err) t.Fatalf("AddBlock: %+v", err)
} }
block1Tx := block1.Transactions[0] block1, err := tc.GetBlock(block1Hash)
block1TxOut := block1Tx.Outputs[0] if err != nil {
selectedUTXOs := []*externalapi.OutpointAndUTXOEntryPair{{ t.Fatalf("GetBlock: %+v", err)
Outpoint: &externalapi.DomainOutpoint{ }
TransactionID: *consensushashing.TransactionID(block1.Transactions[0]),
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, Index: 0,
}, }
UTXOEntry: utxo.NewUTXOEntry(block1TxOut.Value, block1TxOut.ScriptPublicKey, true, 0), if !insertionResult.VirtualUTXODiff.ToAdd().Contains(addedUTXO) {
}} t.Fatalf("Transaction wasn't accepted in the DAG")
}
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")
}
}) })
} }

View File

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

View File

@ -12,7 +12,7 @@ func showAddress(conf *showAddressConfig) error {
return err return err
} }
address, err := libkaspawallet.Address(conf.NetParams(), keysFile.PublicKeys, keysFile.MinimumSignatures) address, err := libkaspawallet.Address(conf.NetParams(), keysFile.PublicKeys, keysFile.MinimumSignatures, keysFile.ECDSA)
if err != nil { if err != nil {
return err return err
} }

View File

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