Repository: oasisprotocol/oasis-sdk (rust)

Issue:
Implement missing secp256k1 signer
Currently the secp256k1 signer implementation in the [Go client SDK](https://github.com/oasisprotocol/oasis-sdk/tree/main/client-sdk/go/crypto/signature/secp256k1) is missing and should be filled in. We could use the `github.com/btcsuite/btcd/btcec` library (unless there are any better alternatives).

Pull request:
client-sdk/go/crypto: Implement secp256k1 signer
Closes #95.

Patch:
diff --git a/Cargo.lock b/Cargo.lock
index b2f16cb259..737ed59541 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -642,7 +642,6 @@ dependencies = [
  "cfg-if 1.0.0",
  "ecdsa",
  "elliptic-curve",
- "sha2",
 ]
 
 [[package]]
@@ -794,6 +793,7 @@ dependencies = [
  "once_cell",
  "serde",
  "serde_bytes",
+ "sha2",
  "thiserror",
 ]
 
diff --git a/client-sdk/go/crypto/signature/secp256k1/secp256k1.go b/client-sdk/go/crypto/signature/secp256k1/secp256k1.go
index 199bed6dd3..0ec59ea544 100644
--- a/client-sdk/go/crypto/signature/secp256k1/secp256k1.go
+++ b/client-sdk/go/crypto/signature/secp256k1/secp256k1.go
@@ -1,65 +1,91 @@
 package secp256k1
 
 import (
+	"encoding/base64"
 	"encoding/json"
 
+	"github.com/btcsuite/btcd/btcec"
+
 	"github.com/oasisprotocol/oasis-core/go/common/cbor"
 
-	"github.com/oasisprotocol/oasis-sdk/client-sdk/go/crypto/signature"
+	sdkSignature "github.com/oasisprotocol/oasis-sdk/client-sdk/go/crypto/signature"
 )
 
 // PublicKey is a Secp256k1 public key.
-type PublicKey []byte
+type PublicKey btcec.PublicKey
 
 type serializedPublicKey struct {
 	Secp256k1 []byte `json:"secp256k1"`
 }
 
 func (pk PublicKey) MarshalCBOR() ([]byte, error) {
-	return cbor.Marshal(serializedPublicKey{Secp256k1: []byte(pk)}), nil
+	bpk := btcec.PublicKey(pk)
+	return cbor.Marshal(serializedPublicKey{Secp256k1: bpk.SerializeCompressed()}), nil
 }
 
 func (pk PublicKey) MarshalJSON() ([]byte, error) {
-	return json.Marshal(serializedPublicKey{Secp256k1: []byte(pk)})
+	bpk := btcec.PublicKey(pk)
+	return json.Marshal(serializedPublicKey{Secp256k1: bpk.SerializeCompressed()})
 }
 
 // MarshalBinary encodes a public key into binary form.
 func (pk PublicKey) MarshalBinary() ([]byte, error) {
-	panic("not implemented")
+	bpk := btcec.PublicKey(pk)
+	return bpk.SerializeCompressed(), nil
 }
 
 // UnMarshalBinary decodes a binary marshaled public key.
 func (pk *PublicKey) UnmarshalBinary(data []byte) error {
-	panic("not implemented")
+	parsedPK, err := btcec.ParsePubKey(data, btcec.S256())
+	if err != nil {
+		return err
+	}
+	*pk = PublicKey(*parsedPK)
+	return nil
 }
 
 // MarshalText encodes a public key into text form.
 func (pk PublicKey) MarshalText() ([]byte, error) {
-	panic("not implemented")
+	serialized, _ := pk.MarshalBinary()
+	return []byte(base64.StdEncoding.EncodeToString(serialized)), nil
 }
 
 // UnmarshalText decodes a text marshaled public key.
 func (pk *PublicKey) UnmarshalText(text []byte) error {
-	panic("not implemented")
+	decodedPK, err := base64.StdEncoding.DecodeString(string(text))
+	if err != nil {
+		return err
+	}
+	return pk.UnmarshalBinary(decodedPK)
 }
 
 // String returns a string representation of the public key.
 func (pk PublicKey) String() string {
-	panic("not implemented")
+	str, _ := pk.MarshalText()
+	return string(str)
 }
 
 // Equal compares vs another public key for equality.
-func (pk PublicKey) Equal(other signature.PublicKey) bool {
+func (pk PublicKey) Equal(other sdkSignature.PublicKey) bool {
 	opk, ok := other.(PublicKey)
 	if !ok {
 		return false
 	}
-	_ = opk
-	panic("not implemented")
+	obpk := btcec.PublicKey(opk)
+	bpk := btcec.PublicKey(pk)
+	return bpk.IsEqual(&obpk)
 }
 
 // Verify returns true iff the signature is valid for the public key over the context and message.
 func (pk PublicKey) Verify(context, message, signature []byte) bool {
-	// TODO
-	return false
+	sig, err := btcec.ParseSignature(signature, btcec.S256())
+	if err != nil {
+		return false
+	}
+	data, err := PrepareSignerMessage(sdkSignature.Context(context), message)
+	if err != nil {
+		return false
+	}
+	bpk := btcec.PublicKey(pk)
+	return sig.Verify(data, &bpk)
 }
diff --git a/client-sdk/go/crypto/signature/secp256k1/signer.go b/client-sdk/go/crypto/signature/secp256k1/signer.go
new file mode 100644
index 0000000000..a249e11a6a
--- /dev/null
+++ b/client-sdk/go/crypto/signature/secp256k1/signer.go
@@ -0,0 +1,55 @@
+package secp256k1
+
+import (
+	"runtime"
+
+	"github.com/btcsuite/btcd/btcec"
+
+	"github.com/oasisprotocol/oasis-core/go/common/crypto/hash"
+
+	sdkSignature "github.com/oasisprotocol/oasis-sdk/client-sdk/go/crypto/signature"
+)
+
+type Signer struct {
+	privateKey btcec.PrivateKey
+}
+
+func (s Signer) Public() sdkSignature.PublicKey {
+	return PublicKey(*s.privateKey.PubKey())
+}
+
+func (s Signer) ContextSign(context, message []byte) ([]byte, error) {
+	data, err := PrepareSignerMessage(sdkSignature.Context(context), message)
+	if err != nil {
+		return nil, err
+	}
+
+	sig, err := s.privateKey.Sign(data)
+	if err != nil {
+		return nil, err
+	}
+	return sig.Serialize(), nil
+}
+
+func (s Signer) String() string {
+	return s.Public().String()
+}
+
+func (s Signer) Reset() {
+	s.privateKey.D.SetBytes([]byte{})
+	s.privateKey.X.SetBytes([]byte{})
+	s.privateKey.Y.SetBytes([]byte{})
+	runtime.GC()
+}
+
+// NewSigner creates a new Secp256k1 signer using the given private key (S256 curve is assumed).
+func NewSigner(pk []byte) sdkSignature.Signer {
+	privKey, _ := btcec.PrivKeyFromBytes(btcec.S256(), pk)
+	return Signer{privateKey: *privKey}
+}
+
+// PrepareSignerMessage prepares a context and message for signing by a Signer.
+func PrepareSignerMessage(context sdkSignature.Context, message []byte) ([]byte, error) {
+	h := hash.NewFromBytes([]byte(context), message)
+	return h.MarshalBinary()
+}
diff --git a/client-sdk/go/go.mod b/client-sdk/go/go.mod
index 1f75935e6f..e24a2febdf 100644
--- a/client-sdk/go/go.mod
+++ b/client-sdk/go/go.mod
@@ -10,6 +10,7 @@ replace (
 )
 
 require (
+	github.com/btcsuite/btcd v0.21.0-beta
 	github.com/oasisprotocol/oasis-core/go v0.2100.1
 	github.com/stretchr/testify v1.7.0
 	google.golang.org/grpc v1.36.0
diff --git a/client-sdk/go/go.sum b/client-sdk/go/go.sum
index 7389eebf34..e2e10d8ca1 100644
--- a/client-sdk/go/go.sum
+++ b/client-sdk/go/go.sum
@@ -70,6 +70,7 @@ github.com/btcsuite/btcd v0.0.0-20190213025234-306aecffea32/go.mod h1:DrZx5ec/dm
 github.com/btcsuite/btcd v0.0.0-20190523000118-16327141da8c/go.mod h1:3J08xEfcugPacsc34/LKRU2yO7YmuT8yt28J8k2+rrI=
 github.com/btcsuite/btcd v0.0.0-20190824003749-130ea5bddde3/go.mod h1:3J08xEfcugPacsc34/LKRU2yO7YmuT8yt28J8k2+rrI=
 github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ=
+github.com/btcsuite/btcd v0.21.0-beta h1:At9hIZdJW0s9E/fAz28nrz6AmcNlSVucCH796ZteX1M=
 github.com/btcsuite/btcd v0.21.0-beta/go.mod h1:ZSWyehm27aAuS9bvkATT+Xte3hjHZ+MRgMY/8NJ7K94=
 github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA=
 github.com/btcsuite/btcutil v0.0.0-20190207003914-4c204d697803/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg=
@@ -198,7 +199,6 @@ github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7a
 github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
 github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
 github.com/gogo/protobuf v1.3.0/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
-github.com/gogo/protobuf v1.3.1 h1:DqDEcV5aeaTmdFBePNpYsp3FlcVH/2ISVVM9Qf8PSls=
 github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
 github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
 github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
@@ -235,7 +235,6 @@ github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw
 github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
 github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.1 h1:JFrFEBb2xKufg6XkJsJr+WbKb4FQlURi5RUcBveYu9k=
 github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.4 h1:L8R9j+yAqZuZjsqh/z+F1NCffTKKLShY6zXTItVIZ8M=
 github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
@@ -708,7 +707,6 @@ github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y8
 github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA=
 github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
 github.com/prometheus/common v0.14.0/go.mod h1:U+gB1OBLb1lF3O42bTCL+FK18tX9Oar16Clt/msog/s=
-github.com/prometheus/common v0.15.0 h1:4fgOnadei3EZvgRwxJ7RMpG1k1pOZth5Pc13tyspaKM=
 github.com/prometheus/common v0.15.0/go.mod h1:U+gB1OBLb1lF3O42bTCL+FK18tX9Oar16Clt/msog/s=
 github.com/prometheus/common v0.19.0 h1:Itb4+NjG9wRdkAWgVucbM/adyIXxEhbw0866e0uZE6A=
 github.com/prometheus/common v0.19.0/go.mod h1:U+gB1OBLb1lF3O42bTCL+FK18tX9Oar16Clt/msog/s=
@@ -718,7 +716,6 @@ github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7z
 github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
 github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
 github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
-github.com/prometheus/procfs v0.2.0 h1:wH4vA7pcjKuZzjF7lM8awk4fnuJO6idemZXoKnULUx4=
 github.com/prometheus/procfs v0.2.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
 github.com/prometheus/procfs v0.6.0 h1:mxy4L2jP6qMonqmq+aTtOx1ifVWUgG/TAmntgbh3xv4=
 github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
@@ -783,7 +780,6 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf
 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
 github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
 github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
-github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
 github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
@@ -933,7 +929,6 @@ golang.org/x/net v0.0.0-20200421231249-e086a090c8fd/go.mod h1:qpuaurCH72eLCgpAm/
 golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
 golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
 golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
-golang.org/x/net v0.0.0-20200822124328-c89045814202 h1:VvcQYSHwXgi7W+TpUR6A9g6Up98WAHf3f/ulnJ62IyA=
 golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
 golang.org/x/net v0.0.0-20201021035429-f5854403a974 h1:IX6qOQeG5uLjB/hjjwjedwfjND0hgjPMMyO1RoIXQNI=
 golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
@@ -999,7 +994,6 @@ golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20201015000850-e3ed0017c211/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20201214210602-f9fddec55a1e h1:AyodaIpKjppX+cBfTASF2E1US3H2JFBj920Ot3rtDjs=
 golang.org/x/sys v0.0.0-20201214210602-f9fddec55a1e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c h1:VwygUrnw9jn88c4u8GD3rZQbqrP/tgas88tPUbBxQrk=
 golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -1099,7 +1093,6 @@ google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM
 google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
 google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
 google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
-google.golang.org/grpc v1.35.0 h1:TwIQcH3es+MojMVojxxfQ3l3OF2KzlRxML2xZq0kRo8=
 google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
 google.golang.org/grpc v1.36.0 h1:o1bcQ6imQMIOpdrO3SWf2z5RV72WbDwdXuK0MDlc8As=
 google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
diff --git a/runtime-sdk/Cargo.toml b/runtime-sdk/Cargo.toml
index ddb7015ef1..962aa06284 100644
--- a/runtime-sdk/Cargo.toml
+++ b/runtime-sdk/Cargo.toml
@@ -13,7 +13,8 @@ oasis-runtime-sdk-macros = { path = "../runtime-sdk-macros", optional = true }
 serde = { version = "1.0.118", features = ["derive"] }
 serde_bytes = "0.11.5"
 ed25519-dalek = { version = "1.0.1", features = ["serde"] }
-k256 = { version = "0.6.0", features = ["ecdsa", "sha256"] }
+sha2 = "0.9.3"
+k256 = { version = "0.6.0", features = ["ecdsa"] }
 thiserror = "1.0.24"
 hex = "0.4.2"
 io-context = "0.2.0"
diff --git a/runtime-sdk/src/crypto/signature/secp256k1.rs b/runtime-sdk/src/crypto/signature/secp256k1.rs
index 47e1d917d5..1368759e69 100644
--- a/runtime-sdk/src/crypto/signature/secp256k1.rs
+++ b/runtime-sdk/src/crypto/signature/secp256k1.rs
@@ -1,12 +1,11 @@
 //! Secp256k1 signatures.
-use std::{convert::TryFrom, fmt};
+use std::fmt;
 
 use k256::{
     self,
-    ecdsa::{self, signature::Verifier},
+    ecdsa::{self, digest::Digest, signature::DigestVerifier},
 };
-
-use oasis_core_runtime::common::crypto::hash::Hash;
+use sha2::Sha512Trunc256;
 
 use crate::crypto::signature::{Error, Signature};
 
@@ -40,14 +39,21 @@ impl PublicKey {
         message: &[u8],
         signature: &Signature,
     ) -> Result<(), Error> {
-        let digest = Hash::digest_bytes_list(&[context, message]);
-        let sig = ecdsa::Signature::try_from(signature.0.as_ref())
+        // Note that we must use Sha512Trunc256 instead of our Hash here,
+        // even though it's the same thing, because it implements the Digest
+        // trait, so we can use verify_digest() below, which doesn't pre-hash
+        // the data (verify() does).
+        let mut digest = Sha512Trunc256::new();
+        for byte in &[context, message] {
+            digest.update(byte);
+        }
+        let sig = ecdsa::Signature::from_asn1(signature.0.as_ref())
             .map_err(|_| Error::MalformedSignature)?;
         let verify_key = ecdsa::VerifyingKey::from_encoded_point(&self.0)
             .map_err(|_| Error::MalformedPublicKey)?;
 
         verify_key
-            .verify(digest.as_ref(), &sig)
+            .verify_digest(digest, &sig)
             .map_err(|_| Error::VerificationFailed)
     }
 }


Test patch:
diff --git a/client-sdk/go/crypto/signature/secp256k1/secp256k1_test.go b/client-sdk/go/crypto/signature/secp256k1/secp256k1_test.go
new file mode 100644
index 0000000000..4fa7690833
--- /dev/null
+++ b/client-sdk/go/crypto/signature/secp256k1/secp256k1_test.go
@@ -0,0 +1,145 @@
+package secp256k1
+
+import (
+	"encoding/hex"
+	"testing"
+
+	"github.com/stretchr/testify/require"
+
+	sdkSignature "github.com/oasisprotocol/oasis-sdk/client-sdk/go/crypto/signature"
+)
+
+// A helper method that creates a new test secp256k1 signer.
+func newTestSigner(t *testing.T) sdkSignature.Signer {
+	require := require.New(t)
+
+	// Use the same test private key as in the btcec examples.
+	hexPrivateKey := "22a47fa09a223f2aa079edf85a7c2d4f87" + "20ee63e502ee2869afab7de234b80c"
+
+	rawPrivateKey, err := hex.DecodeString(hexPrivateKey)
+	require.NoError(err, "DecodeString")
+	require.NotNil(rawPrivateKey, "DecodeString")
+
+	signer := NewSigner(rawPrivateKey)
+	require.NotNil(signer.Public(), "signer public key should not be nil")
+
+	return signer
+}
+
+func TestSecp256k1SignAndVerify(t *testing.T) {
+	require := require.New(t)
+	s := newTestSigner(t)
+
+	ctx1 := []byte("ctx1")
+	msg1 := []byte("msg1")
+	sig1, err := s.ContextSign(ctx1, msg1)
+	require.NoError(err, "ContextSign")
+	require.NotNil(sig1, "signature should not be nil")
+
+	ver1 := s.Public().Verify(ctx1, msg1, sig1)
+	require.True(ver1, "verification should succeed")
+
+	ctx2 := []byte("ctx2")
+	msg2 := []byte("msg2")
+	sig2, err := s.ContextSign(ctx2, msg2)
+	require.NoError(err, "ContextSign")
+	require.NotNil(sig2, "signature should not be nil")
+
+	ver2 := s.Public().Verify(ctx2, msg2, sig2)
+	require.True(ver2, "verification should succeed")
+
+	require.False(s.Public().Verify(ctx1, msg2, sig1))
+	require.False(s.Public().Verify(ctx1, msg2, sig2))
+	require.False(s.Public().Verify(ctx2, msg2, sig1))
+	require.False(s.Public().Verify(ctx2, msg1, sig2))
+	require.False(s.Public().Verify(ctx1, msg1, sig2))
+	require.False(s.Public().Verify(ctx2, msg2, sig1))
+	require.False(s.Public().Verify(ctx2, msg1, sig1))
+
+	require.False(s.Public().Verify(ctx2, []byte("foo"), sig2))
+	require.False(s.Public().Verify([]byte("bar"), msg2, sig2))
+
+	// Try the example from btcec too, the signature should match.
+	ctx3 := []byte("")
+	msg3 := []byte("test message")
+	sig3, err := s.ContextSign(ctx3, msg3)
+	require.NoError(err, "ContextSign")
+	require.NotNil(sig3, "signature should not be nil")
+	require.EqualValues(hex.EncodeToString(sig3), "304502210082fa505a49af65ba0e90450dfb9a03e69947840bf49bba04ee0091fa5173124902205b33fae275be8e59d851568151c95fb15f18e5d23d4bff39e57333df0814bce4")
+
+	ver3 := s.Public().Verify(ctx3, msg3, sig3)
+	require.True(ver3, "verification should succeed")
+
+	ver4 := s.Public().Verify(ctx3, msg3, []byte("asdfghjkl"))
+	require.False(ver4, "verification should fail")
+
+	ver5 := s.Public().Verify(ctx3, msg3, []byte(""))
+	require.False(ver5, "verification should fail")
+}
+
+func TestSecp256k1PubKeySerDes(t *testing.T) {
+	require := require.New(t)
+	s := newTestSigner(t)
+
+	spk := s.Public()
+	require.NotNil(spk, "signer public key should not be nil")
+
+	pk, ok := spk.(PublicKey)
+	require.True(ok, "signer public key should be a secp256k1 public key")
+
+	mcbor, err := pk.MarshalCBOR()
+	require.NoError(err, "MarshalCBOR")
+	require.NotNil(mcbor, "MarshalCBOR")
+
+	mjson, err := pk.MarshalJSON()
+	require.NoError(err, "MarshalJSON")
+	require.NotNil(mjson, "MarshalJSON")
+
+	mstr := pk.String()
+	require.NotNil(mstr, "String")
+
+	sstr := s.String()
+	require.NotNil(sstr, "String")
+	require.EqualValues(mstr, sstr)
+
+	mbin, err := pk.MarshalBinary()
+	require.NoError(err, "MarshalBinary")
+
+	var upk PublicKey
+	err = upk.UnmarshalBinary(mbin)
+	require.NoError(err, "UnmarshalBinary")
+	require.True(pk.Equal(upk))
+	require.True(upk.Equal(pk))
+
+	mtxt, err := pk.MarshalText()
+	require.NoError(err, "MarshalText")
+
+	var utpk PublicKey
+	err = utpk.UnmarshalText(mtxt)
+	require.NoError(err, "UnmarshalText")
+	require.True(pk.Equal(utpk))
+	require.True(utpk.Equal(pk))
+
+	var x PublicKey
+	require.Error(x.UnmarshalText([]byte("asdf")))
+	require.Error(x.UnmarshalBinary([]byte("ghij")))
+}
+
+func TestSecp256k1Reset(t *testing.T) {
+	require := require.New(t)
+	s := newTestSigner(t)
+
+	ctx1 := []byte("ctx1")
+	msg1 := []byte("msg1")
+	sig1, err := s.ContextSign(ctx1, msg1)
+	require.NoError(err, "ContextSign")
+	require.NotNil(sig1, "signature should not be nil")
+
+	ver1 := s.Public().Verify(ctx1, msg1, sig1)
+	require.True(ver1, "verification should succeed")
+
+	s.Reset()
+
+	ver2 := s.Public().Verify(ctx1, msg1, sig1)
+	require.False(ver2, "verification should fail after reset")
+}
diff --git a/client-sdk/go/testing/testing.go b/client-sdk/go/testing/testing.go
index 92ec922ef2..74004b6777 100644
--- a/client-sdk/go/testing/testing.go
+++ b/client-sdk/go/testing/testing.go
@@ -1,10 +1,13 @@
 package testing
 
 import (
+	"crypto/sha512"
+
 	memorySigner "github.com/oasisprotocol/oasis-core/go/common/crypto/signature/signers/memory"
 
 	"github.com/oasisprotocol/oasis-sdk/client-sdk/go/crypto/signature"
 	"github.com/oasisprotocol/oasis-sdk/client-sdk/go/crypto/signature/ed25519"
+	"github.com/oasisprotocol/oasis-sdk/client-sdk/go/crypto/signature/secp256k1"
 	"github.com/oasisprotocol/oasis-sdk/client-sdk/go/types"
 )
 
@@ -14,7 +17,7 @@ type TestKey struct {
 	Address types.Address
 }
 
-func newTestKey(seed string) TestKey {
+func newEd25519TestKey(seed string) TestKey {
 	signer := ed25519.WrapSigner(memorySigner.NewTestSigner(seed))
 	return TestKey{
 		Signer:  signer,
@@ -22,11 +25,22 @@ func newTestKey(seed string) TestKey {
 	}
 }
 
+func newSecp256k1TestKey(seed string) TestKey {
+	pk := sha512.Sum512_256([]byte(seed))
+	signer := secp256k1.NewSigner(pk[:])
+	return TestKey{
+		Signer:  signer,
+		Address: types.NewAddress(signer.Public()),
+	}
+}
+
 var (
 	// Alice is the test key A.
-	Alice = newTestKey("oasis-runtime-sdk/test-keys: alice")
+	Alice = newEd25519TestKey("oasis-runtime-sdk/test-keys: alice")
 	// Bob is the test key A.
-	Bob = newTestKey("oasis-runtime-sdk/test-keys: bob")
+	Bob = newEd25519TestKey("oasis-runtime-sdk/test-keys: bob")
 	// Charlie is the test key C.
-	Charlie = newTestKey("oasis-runtime-sdk/test-keys: charlie")
+	Charlie = newEd25519TestKey("oasis-runtime-sdk/test-keys: charlie")
+	// Dave is the test key D.
+	Dave = newSecp256k1TestKey("oasis-runtime-sdk/test-keys: dave")
 )
diff --git a/client-sdk/go/testing/testing_test.go b/client-sdk/go/testing/testing_test.go
new file mode 100644
index 0000000000..96c7786396
--- /dev/null
+++ b/client-sdk/go/testing/testing_test.go
@@ -0,0 +1,13 @@
+package testing
+
+import (
+	"fmt"
+	"testing"
+)
+
+func TestPrintTestKeys(t *testing.T) {
+	fmt.Printf("A: %v\n", Alice.Signer.Public().String())
+	fmt.Printf("B: %v\n", Bob.Signer.Public().String())
+	fmt.Printf("C: %v\n", Charlie.Signer.Public().String())
+	fmt.Printf("D: %v\n", Dave.Signer.Public().String())
+}
diff --git a/runtime-sdk/src/testing/keys.rs b/runtime-sdk/src/testing/keys.rs
index f1b48feb2d..9593959aef 100644
--- a/runtime-sdk/src/testing/keys.rs
+++ b/runtime-sdk/src/testing/keys.rs
@@ -2,8 +2,8 @@
 
 // TODO: Should be derived from seeds once implemented in the Rust version.
 
-/// Define a test key.
-macro_rules! test_key {
+/// Define an ed25519 test key.
+macro_rules! test_key_ed25519 {
     ($doc:expr, $name:ident, $pk:expr) => {
         #[doc=" Test key "]
         #[doc=$doc]
@@ -36,6 +36,41 @@ macro_rules! test_key {
     }
 }
 
-test_key!("A", alice, "NcPzNW3YU2T+ugNUtUWtoQnRvbOL9dYSaBfbjHLP1pE=");
-test_key!("B", bob, "YgkEiVSR4SMQdfXw+ppuFYlqH0seutnCKk8KG8PyAx0=");
-test_key!("C", charlie, "8l1AQE+ETOPLckiNJ7NOD+AfZdaPw6wguir/vSF11YI=");
+/// Define a secp256k1 test key.
+macro_rules! test_key_secp256k1 {
+    ($doc:expr, $name:ident, $pk:expr) => {
+        #[doc=" Test key "]
+        #[doc=$doc]
+        #[doc="."]
+        pub mod $name {
+            use crate::crypto::signature::{PublicKey, secp256k1};
+            use crate::types::address::Address;
+
+            #[doc=" Test public key "]
+            #[doc=$doc]
+            #[doc="."]
+            pub fn pk() -> PublicKey {
+                PublicKey::Secp256k1(pk_secp256k1())
+            }
+
+            #[doc=" Test Secp256k1 public key "]
+            #[doc=$doc]
+            #[doc="."]
+            pub fn pk_secp256k1() -> secp256k1::PublicKey {
+                $pk.into()
+            }
+
+            #[doc=" Test address "]
+            #[doc=$doc]
+            #[doc="."]
+            pub fn address() -> Address {
+                Address::from_pk(&pk())
+            }
+        }
+    }
+}
+
+test_key_ed25519!("A", alice, "NcPzNW3YU2T+ugNUtUWtoQnRvbOL9dYSaBfbjHLP1pE=");
+test_key_ed25519!("B", bob, "YgkEiVSR4SMQdfXw+ppuFYlqH0seutnCKk8KG8PyAx0=");
+test_key_ed25519!("C", charlie, "8l1AQE+ETOPLckiNJ7NOD+AfZdaPw6wguir/vSF11YI=");
+test_key_secp256k1!("D", dave, "AwF6GNjbybMzhi3XRj5R1oTiMMkO1nAwB7NZAlH1X4BE");
diff --git a/tests/e2e/scenarios.go b/tests/e2e/scenarios.go
index 3a0ffcea45..c3651cee33 100644
--- a/tests/e2e/scenarios.go
+++ b/tests/e2e/scenarios.go
@@ -7,7 +7,7 @@ import (
 
 var (
 	// SimpleKVRuntime is the basic network + client test case with runtime support.
-	SimpleKVRuntime scenario.Scenario = NewRuntimeScenario("test-runtime-simple-keyvalue", []RunTestFunction{SimpleKVTest, KVEventTest, KVBalanceTest, KVTransferTest})
+	SimpleKVRuntime scenario.Scenario = NewRuntimeScenario("test-runtime-simple-keyvalue", []RunTestFunction{SimpleKVTest, KVEventTest, KVBalanceTest, KVTransferTest, KVDaveTest})
 )
 
 // RegisterScenarios registers all oasis-sdk end-to-end runtime tests.
diff --git a/tests/e2e/simplekvtest.go b/tests/e2e/simplekvtest.go
index 457b457d35..59e0d54a07 100644
--- a/tests/e2e/simplekvtest.go
+++ b/tests/e2e/simplekvtest.go
@@ -323,6 +323,19 @@ func KVBalanceTest(log *logging.Logger, rtc client.RuntimeClient) error {
 		return fmt.Errorf("Charlie's account is missing native denomination balance")
 	}
 
+	log.Info("checking Dave's account balance")
+	db, err := ac.Balances(ctx, client.RoundLatest, testing.Dave.Address)
+	if err != nil {
+		return err
+	}
+	if q, ok := db.Balances[types.NativeDenomination]; ok {
+		if q.Cmp(quantity.NewFromUint64(100)) != 0 {
+			return fmt.Errorf("Dave's account balance is wrong (expected 100, got %s)", q.String())
+		}
+	} else {
+		return fmt.Errorf("Dave's account is missing native denomination balance")
+	}
+
 	return nil
 }
 
@@ -384,3 +397,62 @@ func KVTransferTest(log *logging.Logger, rtc client.RuntimeClient) error {
 
 	return nil
 }
+
+func KVDaveTest(log *logging.Logger, rtc client.RuntimeClient) error {
+	ctx := context.Background()
+	ac := accounts.NewV1(rtc)
+
+	chainCtx, err := GetChainContext(ctx, rtc)
+	if err != nil {
+		return err
+	}
+
+	nonce, err := ac.Nonce(ctx, client.RoundLatest, testing.Dave.Address)
+	if err != nil {
+		return err
+	}
+
+	log.Info("transferring 10 units from Dave to Alice")
+	tx := types.NewTransaction(nil, "accounts.Transfer", struct {
+		To     types.Address   `json:"to"`
+		Amount types.BaseUnits `json:"amount"`
+	}{
+		To:     testing.Alice.Address,
+		Amount: types.NewBaseUnits(*quantity.NewFromUint64(10), types.NativeDenomination),
+	})
+	tx.AppendSignerInfo(testing.Dave.Signer.Public(), nonce)
+	stx := tx.PrepareForSigning()
+	stx.AppendSign(chainCtx, testing.Dave.Signer)
+
+	if _, err := rtc.SubmitTx(ctx, stx.UnverifiedTransaction()); err != nil {
+		return err
+	}
+
+	log.Info("checking Dave's account balance")
+	db, err := ac.Balances(ctx, client.RoundLatest, testing.Dave.Address)
+	if err != nil {
+		return err
+	}
+	if q, ok := db.Balances[types.NativeDenomination]; ok {
+		if q.Cmp(quantity.NewFromUint64(90)) != 0 {
+			return fmt.Errorf("Dave's account balance is wrong (expected 90, got %s)", q.String())
+		}
+	} else {
+		return fmt.Errorf("Dave's account is missing native denomination balance")
+	}
+
+	log.Info("checking Alice's account balance")
+	ab, err := ac.Balances(ctx, client.RoundLatest, testing.Alice.Address)
+	if err != nil {
+		return err
+	}
+	if q, ok := ab.Balances[types.NativeDenomination]; ok {
+		if q.Cmp(quantity.NewFromUint64(2910)) != 0 {
+			return fmt.Errorf("Alice's account balance is wrong (expected 2910, got %s)", q.String())
+		}
+	} else {
+		return fmt.Errorf("Alice's account is missing native denomination balance")
+	}
+
+	return nil
+}
diff --git a/tests/run-e2e.sh b/tests/run-e2e.sh
index fd946da478..de3d705428 100755
--- a/tests/run-e2e.sh
+++ b/tests/run-e2e.sh
@@ -73,6 +73,7 @@ ${TAR} -xf oasis-core.tar.gz --strip-components=1 oasis_core_${OASIS_RELEASE}_li
 
 printf "${CYAN}### Running end-to-end tests...${OFF}\n"
 ./e2e --log.level=INFO \
+	--basedir.no_cleanup \
 	--e2e.node.binary="${TEST_BASE_DIR}"/oasis-node \
 	--e2e.runtime.binary_dir.default="${TEST_BASE_DIR}" \
 	--e2e.runtime.loader="${TEST_BASE_DIR}"/oasis-core-runtime-loader
diff --git a/tests/runtimes/simple-keyvalue/src/lib.rs b/tests/runtimes/simple-keyvalue/src/lib.rs
index 85cb4a21a8..5bf69d432b 100644
--- a/tests/runtimes/simple-keyvalue/src/lib.rs
+++ b/tests/runtimes/simple-keyvalue/src/lib.rs
@@ -41,11 +41,17 @@ impl sdk::Runtime for Runtime {
                         d.insert(Denomination::NATIVE, 1_000.into());
                         d
                     });
+                    // Dave.
+                    b.insert(sdk::testing::keys::dave::address(), {
+                        let mut d = BTreeMap::new();
+                        d.insert(Denomination::NATIVE, 100.into());
+                        d
+                    });
                     b
                 },
                 total_supplies: {
                     let mut ts = BTreeMap::new();
-                    ts.insert(Denomination::NATIVE, 6_000.into());
+                    ts.insert(Denomination::NATIVE, 6_100.into());
                     ts
                 },
                 ..Default::default()