From d2cccd2829995c65e00c39ea0009627382ca3dd2 Mon Sep 17 00:00:00 2001 From: Ori Newman Date: Tue, 6 Apr 2021 17:25:09 +0300 Subject: [PATCH] Add ECDSA support to the wallet (#1664) * Add ECDSA support to the wallet * Fix genkeypair * Fix typo and rename var --- cmd/genkeypair/main.go | 2 +- cmd/kaspawallet/balance.go | 2 +- cmd/kaspawallet/config.go | 1 + cmd/kaspawallet/create.go | 6 +- cmd/kaspawallet/create_unsigned_tx.go | 19 +- cmd/kaspawallet/keys/create.go | 4 +- cmd/kaspawallet/keys/keys.go | 23 +- cmd/kaspawallet/libkaspawallet/keypair.go | 60 ++- cmd/kaspawallet/libkaspawallet/sign.go | 146 ++++++ cmd/kaspawallet/libkaspawallet/transaction.go | 92 +--- .../libkaspawallet/transaction_test.go | 463 +++++++++--------- cmd/kaspawallet/send.go | 22 +- cmd/kaspawallet/show_address.go | 2 +- cmd/kaspawallet/sign.go | 2 +- 14 files changed, 484 insertions(+), 360 deletions(-) create mode 100644 cmd/kaspawallet/libkaspawallet/sign.go diff --git a/cmd/genkeypair/main.go b/cmd/genkeypair/main.go index be81d81ab..e54e0aed4 100644 --- a/cmd/genkeypair/main.go +++ b/cmd/genkeypair/main.go @@ -12,7 +12,7 @@ func main() { panic(err) } - privateKey, publicKey, err := libkaspawallet.CreateKeyPair() + privateKey, publicKey, err := libkaspawallet.CreateKeyPair(false) if err != nil { panic(err) } diff --git a/cmd/kaspawallet/balance.go b/cmd/kaspawallet/balance.go index ad22e5799..64458d09d 100644 --- a/cmd/kaspawallet/balance.go +++ b/cmd/kaspawallet/balance.go @@ -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 } diff --git a/cmd/kaspawallet/config.go b/cmd/kaspawallet/config.go index 6bb7a0320..a4715299a 100644 --- a/cmd/kaspawallet/config.go +++ b/cmd/kaspawallet/config.go @@ -28,6 +28,7 @@ type createConfig struct { MinimumSignatures uint32 `long:"min-signatures" short:"m" description:"Minimum required signatures" default:"1"` NumPrivateKeys uint32 `long:"num-private-keys" short:"k" description:"Number of private keys" default:"1"` NumPublicKeys uint32 `long:"num-public-keys" short:"n" description:"Total number of keys" default:"1"` + 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 } diff --git a/cmd/kaspawallet/create.go b/cmd/kaspawallet/create.go index b0644e459..ac51ab894 100644 --- a/cmd/kaspawallet/create.go +++ b/cmd/kaspawallet/create.go @@ -15,7 +15,7 @@ func create(conf *createConfig) error { var publicKeys [][]byte var err error if !conf.Import { - encryptedPrivateKeys, publicKeys, err = keys.CreateKeyPairs(conf.NumPrivateKeys) + encryptedPrivateKeys, publicKeys, err = keys.CreateKeyPairs(conf.NumPrivateKeys, conf.ECDSA) } else { encryptedPrivateKeys, publicKeys, err = keys.ImportKeyPairs(conf.NumPrivateKeys) } @@ -49,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 } @@ -59,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 } diff --git a/cmd/kaspawallet/create_unsigned_tx.go b/cmd/kaspawallet/create_unsigned_tx.go index 3f6c7cb11..fceb01331 100644 --- a/cmd/kaspawallet/create_unsigned_tx.go +++ b/cmd/kaspawallet/create_unsigned_tx.go @@ -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 } diff --git a/cmd/kaspawallet/keys/create.go b/cmd/kaspawallet/keys/create.go index 3e12ec568..6a278272f 100644 --- a/cmd/kaspawallet/keys/create.go +++ b/cmd/kaspawallet/keys/create.go @@ -12,9 +12,9 @@ import ( ) // 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() + return libkaspawallet.CreateKeyPair(ecdsa) }) } diff --git a/cmd/kaspawallet/keys/keys.go b/cmd/kaspawallet/keys/keys.go index a2cbf7d1d..0c4a9533c 100644 --- a/cmd/kaspawallet/keys/keys.go +++ b/cmd/kaspawallet/keys/keys.go @@ -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) diff --git a/cmd/kaspawallet/libkaspawallet/keypair.go b/cmd/kaspawallet/libkaspawallet/keypair.go index 7385d803f..07577662c 100644 --- a/cmd/kaspawallet/libkaspawallet/keypair.go +++ b/cmd/kaspawallet/libkaspawallet/keypair.go @@ -8,7 +8,15 @@ import ( ) // 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() if err != nil { 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 } +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 func PublicKeyFromPrivateKey(privateKeyBytes []byte) ([]byte, error) { keyPair, err := secp256k1.DeserializeSchnorrPrivateKeyFromSlice(privateKeyBytes) 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() @@ -45,40 +70,21 @@ func PublicKeyFromPrivateKey(privateKeyBytes []byte) ([]byte, error) { 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. -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 } diff --git a/cmd/kaspawallet/libkaspawallet/sign.go b/cmd/kaspawallet/libkaspawallet/sign.go new file mode 100644 index 000000000..48ed27063 --- /dev/null +++ b/cmd/kaspawallet/libkaspawallet/sign.go @@ -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 +} diff --git a/cmd/kaspawallet/libkaspawallet/transaction.go b/cmd/kaspawallet/libkaspawallet/transaction.go index 75c34c871..93759dff9 100644 --- a/cmd/kaspawallet/libkaspawallet/transaction.go +++ b/cmd/kaspawallet/libkaspawallet/transaction.go @@ -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) diff --git a/cmd/kaspawallet/libkaspawallet/transaction_test.go b/cmd/kaspawallet/libkaspawallet/transaction_test.go index a5e960fb8..5c75a8514 100644 --- a/cmd/kaspawallet/libkaspawallet/transaction_test.go +++ b/cmd/kaspawallet/libkaspawallet/transaction_test.go @@ -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 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.AddressPublicKey); !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") + } + }) }) } diff --git a/cmd/kaspawallet/send.go b/cmd/kaspawallet/send.go index f0eb4b90f..502b30ab0 100644 --- a/cmd/kaspawallet/send.go +++ b/cmd/kaspawallet/send.go @@ -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 } diff --git a/cmd/kaspawallet/show_address.go b/cmd/kaspawallet/show_address.go index 3077b83ce..a309309fa 100644 --- a/cmd/kaspawallet/show_address.go +++ b/cmd/kaspawallet/show_address.go @@ -12,7 +12,7 @@ func showAddress(conf *showAddressConfig) error { 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 { return err } diff --git a/cmd/kaspawallet/sign.go b/cmd/kaspawallet/sign.go index 2bd7b1378..e2add8365 100644 --- a/cmd/kaspawallet/sign.go +++ b/cmd/kaspawallet/sign.go @@ -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 }