From e74fbfea80c96d4427dc08a61f16e5ea6fe8544f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bastian=20M=C3=BCller?= <bastian@turbolent.com>
Date: Thu, 17 Oct 2024 13:01:13 -0700
Subject: [PATCH 1/6] add Crypto contract

---
 contracts/Crypto.cdc                       | 172 +++++++++++++++++++++
 lib/go/contracts/contracts.go              |  28 ++--
 lib/go/contracts/contracts_test.go         |   6 +
 lib/go/contracts/internal/assets/assets.go |  23 +++
 lib/go/test/crypto_test.go                 | 168 ++++++++++++++++++++
 5 files changed, 380 insertions(+), 17 deletions(-)
 create mode 100644 contracts/Crypto.cdc
 create mode 100644 lib/go/test/crypto_test.go

diff --git a/contracts/Crypto.cdc b/contracts/Crypto.cdc
new file mode 100644
index 000000000..631bf1709
--- /dev/null
+++ b/contracts/Crypto.cdc
@@ -0,0 +1,172 @@
+
+access(all)
+contract Crypto {
+
+    access(all)
+    fun hash(_ data: [UInt8], algorithm: HashAlgorithm): [UInt8] {
+        return algorithm.hash(data)
+    }
+
+    access(all)
+    fun hashWithTag(_ data: [UInt8], tag: String, algorithm: HashAlgorithm): [UInt8] {
+        return algorithm.hashWithTag(data, tag: tag)
+    }
+
+    access(all)
+    struct KeyListEntry {
+
+        access(all)
+        let keyIndex: Int
+
+        access(all)
+        let publicKey: PublicKey
+
+        access(all)
+        let hashAlgorithm: HashAlgorithm
+
+        access(all)
+        let weight: UFix64
+
+        access(all)
+        let isRevoked: Bool
+
+        init(
+            keyIndex: Int,
+            publicKey: PublicKey,
+            hashAlgorithm: HashAlgorithm,
+            weight: UFix64,
+            isRevoked: Bool
+        ) {
+            self.keyIndex = keyIndex
+            self.publicKey = publicKey
+            self.hashAlgorithm = hashAlgorithm
+            self.weight = weight
+            self.isRevoked = isRevoked
+        }
+    }
+
+    access(all)
+    struct KeyList {
+
+        access(self)
+        let entries: [KeyListEntry]
+
+        init() {
+            self.entries = []
+        }
+
+        /// Adds a new key with the given weight
+        access(all)
+        fun add(
+            _ publicKey: PublicKey,
+            hashAlgorithm: HashAlgorithm,
+            weight: UFix64
+        ): KeyListEntry {
+
+            let keyIndex = self.entries.length
+            let entry = KeyListEntry(
+                keyIndex: keyIndex,
+                publicKey: publicKey,
+                hashAlgorithm: hashAlgorithm,
+                weight: weight,
+                isRevoked: false
+            )
+            self.entries.append(entry)
+            return entry
+        }
+
+        /// Returns the key at the given index, if it exists.
+        /// Revoked keys are always returned, but they have the `isRevoked` field set to true
+        access(all)
+        fun get(keyIndex: Int): KeyListEntry? {
+            if keyIndex >= self.entries.length {
+                return nil
+            }
+
+            return self.entries[keyIndex]
+        }
+
+        /// Marks the key at the given index revoked, but does not delete it
+        access(all)
+        fun revoke(keyIndex: Int) {
+            if keyIndex >= self.entries.length {
+                return
+            }
+
+            let currentEntry = self.entries[keyIndex]
+            self.entries[keyIndex] = KeyListEntry(
+                keyIndex: currentEntry.keyIndex,
+                publicKey: currentEntry.publicKey,
+                hashAlgorithm: currentEntry.hashAlgorithm,
+                weight: currentEntry.weight,
+                isRevoked: true
+            )
+        }
+
+        /// Returns true if the given signatures are valid for the given signed data
+        access(all)
+        fun verify(
+            signatureSet: [KeyListSignature],
+            signedData: [UInt8],
+            domainSeparationTag: String
+        ): Bool {
+
+            var validWeights: UFix64 = 0.0
+
+            let seenKeyIndices: {Int: Bool} = {}
+
+            for signature in signatureSet {
+
+                // Ensure the key index is valid
+                if signature.keyIndex >= self.entries.length {
+                    return false
+                }
+
+                // Ensure this key index has not already been seen
+                if seenKeyIndices[signature.keyIndex] ?? false {
+                    return false
+                }
+
+                // Record the key index was seen
+                seenKeyIndices[signature.keyIndex] = true
+
+                // Get the actual key
+                let key = self.entries[signature.keyIndex]
+
+                // Ensure the key is not revoked
+                if key.isRevoked {
+                    return false
+                }
+
+                // Ensure the signature is valid
+                if !key.publicKey.verify(
+                    signature: signature.signature,
+                    signedData: signedData,
+                    domainSeparationTag: domainSeparationTag,
+                    hashAlgorithm:key.hashAlgorithm
+                ) {
+                    return false
+                }
+
+                validWeights = validWeights + key.weight
+            }
+
+            return validWeights >= 1.0
+        }
+    }
+
+    access(all)
+    struct KeyListSignature {
+
+        access(all)
+        let keyIndex: Int
+
+        access(all)
+        let signature: [UInt8]
+
+        init(keyIndex: Int, signature: [UInt8]) {
+            self.keyIndex = keyIndex
+            self.signature = signature
+        }
+    }
+}
\ No newline at end of file
diff --git a/lib/go/contracts/contracts.go b/lib/go/contracts/contracts.go
index 59b22a199..c2b16c6f5 100644
--- a/lib/go/contracts/contracts.go
+++ b/lib/go/contracts/contracts.go
@@ -40,6 +40,7 @@ const (
 	flowContractAuditsFilename      = "FlowContractAudits.cdc"
 	flowNodeVersionBeaconFilename   = "NodeVersionBeacon.cdc"
 	flowRandomBeaconHistoryFilename = "RandomBeaconHistory.cdc"
+	cryptoFilename                  = "Crypto.cdc"
 
 	// Test contracts
 	// only used for testing
@@ -193,8 +194,7 @@ func FlowIDTableStaking(env templates.Environment) []byte {
 
 // FlowStakingProxy returns the StakingProxy contract.
 func FlowStakingProxy() []byte {
-	code := assets.MustAssetString(flowStakingProxyFilename)
-	return []byte(code)
+	return assets.MustAsset(flowStakingProxyFilename)
 }
 
 // FlowStakingCollection returns the StakingCollection contract.
@@ -223,16 +223,12 @@ func FlowLockedTokens(
 
 // FlowQC returns the FlowClusterQCs contract.
 func FlowQC() []byte {
-	code := assets.MustAssetString(flowQCFilename)
-
-	return []byte(code)
+	return assets.MustAsset(flowQCFilename)
 }
 
 // FlowDKG returns the FlowDKG contract.
 func FlowDKG() []byte {
-	code := assets.MustAssetString(flowDKGFilename)
-
-	return []byte(code)
+	return assets.MustAsset(flowDKGFilename)
 }
 
 // FlowEpoch returns the FlowEpoch contract.
@@ -246,23 +242,17 @@ func FlowEpoch(env templates.Environment) []byte {
 
 // NodeVersionBeacon returns the NodeVersionBeacon contract content.
 func NodeVersionBeacon() []byte {
-	code := assets.MustAssetString(flowNodeVersionBeaconFilename)
-
-	return []byte(code)
+	return assets.MustAsset(flowNodeVersionBeaconFilename)
 }
 
 func RandomBeaconHistory() []byte {
-	code := assets.MustAssetString(flowRandomBeaconHistoryFilename)
-
-	return []byte(code)
+	return assets.MustAsset(flowRandomBeaconHistoryFilename)
 }
 
 // FlowContractAudits returns the deprecated FlowContractAudits contract.
 // This contract is no longer used on any network
 func FlowContractAudits() []byte {
-	code := assets.MustAssetString(flowContractAuditsFilename)
-
-	return []byte(code)
+	return assets.MustAsset(flowContractAuditsFilename)
 }
 
 /******************** Test contracts *********************/
@@ -331,3 +321,7 @@ func TestFlowFees(fungibleTokenAddress, flowTokenAddress, storageFeesAddress str
 
 	return []byte(code)
 }
+
+func Crypto() []byte {
+	return assets.MustAsset(cryptoFilename)
+}
diff --git a/lib/go/contracts/contracts_test.go b/lib/go/contracts/contracts_test.go
index 2b568da19..c42e7095b 100644
--- a/lib/go/contracts/contracts_test.go
+++ b/lib/go/contracts/contracts_test.go
@@ -89,3 +89,9 @@ func TestNodeVersionBeacon(t *testing.T) {
 	contract := contracts.NodeVersionBeacon()
 	assert.NotNil(t, contract)
 }
+
+func TestCrypto(t *testing.T) {
+	contract := contracts.Crypto()
+
+	assert.NotNil(t, contract)
+}
diff --git a/lib/go/contracts/internal/assets/assets.go b/lib/go/contracts/internal/assets/assets.go
index 03e9775ca..31f66f325 100644
--- a/lib/go/contracts/internal/assets/assets.go
+++ b/lib/go/contracts/internal/assets/assets.go
@@ -1,5 +1,6 @@
 // Code generated by go-bindata. DO NOT EDIT.
 // sources:
+// Crypto.cdc (4.588kB)
 // FlowFees.cdc (9.634kB)
 // FlowIDTableStaking.cdc (101.556kB)
 // FlowServiceAccount.cdc (8.509kB)
@@ -82,6 +83,26 @@ func (fi bindataFileInfo) Sys() interface{} {
 	return nil
 }
 
+var _cryptoCdc = "\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xb4\x57\x5d\x6f\xdb\x36\x14\x7d\xf7\xaf\xb8\x7b\x73\x30\x43\xcd\x80\x61\x18\x04\xa8\x41\xb7\x75\x5b\x90\x0d\x18\x92\x16\x7d\x30\x8c\x96\x91\xae\x24\xc2\x2a\x65\x90\x57\x76\x84\xc0\xff\x7d\xa0\x68\x49\x24\x45\x7f\xa4\x48\xf5\x62\xc9\x3a\xf7\xeb\xf0\xdc\x2b\x72\xc6\xd2\x14\x95\x9a\xb3\xaa\xba\x9a\xa5\xb5\x20\xc9\x52\x82\xdf\x65\xbb\xa1\x1a\x9e\x67\x33\x00\x00\x1b\xa2\x9f\xf3\x46\x40\xc9\x54\x39\xff\x0c\x19\x23\x16\xc3\xf2\xe3\xad\xa0\x5f\x57\x0b\x60\x55\x51\x4b\x4e\xe5\xd7\x18\xfe\x66\xaa\x7c\xd7\x3f\x5e\x0d\x18\x78\xee\x5c\xe8\x4b\x22\x35\x52\x8c\x36\x51\xe7\x53\x7b\x34\x61\xf6\xa7\xa3\x7f\xe2\x54\x7e\x60\xc5\x34\x09\x62\x45\x0c\x0f\x24\xb9\x28\x5e\x23\xa3\x3e\x8e\x8e\x72\x70\x4e\xac\x38\x99\xa2\x22\xd9\xa4\x04\x77\xd8\xfe\xc3\x15\xbd\x17\x24\xdb\x9e\xcb\x10\x5c\x5f\x15\x12\xac\xb1\xbd\x15\x19\x3e\xc5\x70\x2b\xe8\x3c\x7c\xd3\x3c\x56\x3c\xbd\xc3\x36\x86\xff\xfa\xdb\xf3\x56\xa5\xcd\x82\x47\xca\x79\xeb\x1d\xf2\xa2\xa4\x18\x3e\xfe\xc9\x9f\x7e\xf9\xf9\x3c\x9e\xab\x7b\xdc\xd6\x6b\xcc\x62\xf8\xad\xae\xab\xd1\x80\x0b\x4e\xf3\xe1\x49\x5f\x4e\xf5\x0b\xe7\x55\xa8\x52\x17\x71\xaa\x2a\x17\xe9\x56\xe0\xbe\xf3\xb3\xed\xff\xbf\xb2\x34\xd2\xad\x2f\x56\x79\xd4\xe7\x0b\xc9\x90\xfa\x14\x34\x64\x0e\xc9\x58\xc5\x14\xe6\xa4\x0f\x89\x5b\xce\x14\x6e\x6a\x80\xe4\x50\xcc\x14\x30\x14\x02\xc9\x58\xd4\x00\xdb\x5f\xae\xdd\x80\x6c\x75\x00\x77\x91\x51\x90\xe4\xa8\x62\x58\xda\x8a\x5f\x79\x8b\x1d\x24\xf1\x60\x0a\x09\x2c\x57\x56\x7e\xc3\xed\x9b\x37\x6f\xe0\x5d\x96\x29\x60\x20\x70\xa7\x89\x86\x1d\xa7\x12\xa8\x44\x28\xf8\x16\x85\x4f\x41\x48\x88\x7a\x62\xb0\x2c\x73\xe5\xf6\xf9\x3b\xaa\x6a\x14\x4e\x7c\x74\x08\x80\xd7\xf4\x90\x38\x8c\x44\x15\x8a\x82\xca\x09\x1c\x3b\x3f\x89\xe3\xd6\x2d\x0c\x9c\x5e\xea\xef\x16\x13\x8c\x55\xfe\x26\x5c\x7e\x80\x82\xf2\x38\x05\x36\x0d\xe6\x77\xfa\xde\x6a\xb0\x9c\x55\x0a\x1d\xc0\xd5\x51\x79\x44\x6c\xb3\x41\x91\xcd\xbb\xe2\x5d\xd8\x61\x64\x77\x6f\x8e\x09\xe8\xbe\xc3\xa8\x4e\x34\x5a\x41\x8c\x2c\xfd\xf0\x8e\x1d\xe0\x39\x70\x02\x7c\xe2\x8a\x54\xe4\x59\x9b\x4e\x5a\x63\xab\x80\x49\x04\x56\xed\x58\xab\x0e\x91\x31\x5b\xc0\x63\xd3\x39\x6c\xa1\x64\x5b\xec\x5c\x7f\x19\x0a\xfd\x02\x39\xc7\x2a\x03\x85\x04\x54\x03\xc9\x06\xcf\x6a\xb5\x40\x9a\x3b\xd3\xd0\x93\xd1\x8d\xd7\x49\x3c\x1f\x55\xf4\x36\x28\x23\xcf\xc0\x22\x4e\xf0\xca\x79\xb5\x9f\x85\xe8\xb5\x5d\x2e\xfb\x58\x47\x1b\xf6\x5f\x26\xd7\xa7\xd8\x06\x69\xb8\x31\xcc\x65\x35\x2a\x10\x35\x41\x86\x15\x12\x02\x3f\xdf\xcc\xc6\xde\xe3\xe8\xf5\x48\x39\x45\x88\xee\xc1\xb4\x91\x12\xc5\xa1\xa5\x93\x73\xe4\x80\x27\xe6\x11\xf2\x82\x36\xb6\x43\x46\x17\xf5\xb4\x63\x71\x79\x83\x3b\x66\x17\x76\xbb\x63\x73\x41\xeb\x3b\x4d\x00\x4e\xe7\x1f\xeb\x5c\xd9\xa0\x5e\xd1\x51\x48\x8a\x17\x82\x51\x23\xd1\xf4\xe4\x96\x55\x3c\x83\xbc\x96\x1e\x04\xb3\x6e\x77\x78\x56\x51\x5b\x94\x3c\xf7\x56\x60\x08\xf1\x80\x34\x7e\xdb\x1e\xfa\x7f\x57\x8b\x09\x1a\xb3\x3f\x9c\xad\xa8\x03\xc8\xea\xaf\x8c\x8b\x07\xdc\x30\xc9\x88\xd7\xe2\xc3\xb8\x4d\xb5\xbf\x17\x7a\xe3\xe1\x7f\x27\xb6\x4c\x9a\x0a\x3f\x75\xe4\xaa\xfe\x3b\x03\x09\x5c\x47\xd7\x53\x81\x2a\x44\x71\xd7\x89\x84\xa7\xfa\xbb\xfc\x7c\x2b\xc8\x78\xde\x43\x02\xcf\x9e\xa4\x35\x6b\x43\xad\xc0\x85\x53\xb8\x9f\x8a\x59\x19\x78\x2f\x94\x06\xf7\x3d\x6e\xda\x9a\x2b\x93\xe5\x74\xed\xf3\xd1\x67\xf4\xf2\xae\x84\x71\x10\x4d\xbf\x1a\x30\xed\x51\x3f\x47\xae\xac\x24\x4b\x66\xa6\x0d\xab\x24\xb2\xac\x85\x47\xd4\x52\x41\x14\xc1\xac\x1d\x22\x97\xd3\x22\x56\x70\x73\x63\x92\x7a\xbd\xbc\xef\x31\xad\x65\xe6\x71\xbb\x63\x2a\x9c\xe5\x05\x29\x26\xa6\xe3\x42\xb1\xfe\x42\x33\x9d\x59\x4a\x0d\xab\x74\xbc\x09\xea\xb0\x49\xf1\x07\x5d\x20\xd0\x45\x4a\x31\xec\x4b\x6f\x4b\x6a\x71\xbe\xc6\xd6\xda\xbf\xbe\xbe\x1a\xd0\x16\xfb\x09\xc5\xfe\xa0\x13\x19\x06\x67\x14\x9a\x10\xc3\x1a\xf4\x0e\x63\x4b\xe7\xc3\xdd\x74\x16\x82\x37\x2f\xc6\xfb\x30\x36\x38\x3a\x02\x7f\x86\xad\xdd\x09\xaf\xab\x3a\x7e\xbc\x80\xc0\x69\xa7\xbf\x5e\x4a\xba\x3d\xb1\x20\x71\x1f\x7f\xec\x96\x39\x70\x7c\x09\xef\x3f\x1c\xdb\xb7\x09\xfc\x14\x5d\x7f\xcb\x69\x66\x98\xdd\xdf\xe1\x34\x6e\x69\xe0\x30\xfe\xbd\xf3\x8f\x7b\xbe\x0d\xe0\xbf\xfd\x94\x39\x0a\x3a\x19\xfd\x4e\x08\xda\xff\x1f\x00\x00\xff\xff\xc0\xa0\xe3\x76\xec\x11\x00\x00"
+
+func cryptoCdcBytes() ([]byte, error) {
+	return bindataRead(
+		_cryptoCdc,
+		"Crypto.cdc",
+	)
+}
+
+func cryptoCdc() (*asset, error) {
+	bytes, err := cryptoCdcBytes()
+	if err != nil {
+		return nil, err
+	}
+
+	info := bindataFileInfo{name: "Crypto.cdc", size: 0, mode: os.FileMode(0), modTime: time.Unix(0, 0)}
+	a := &asset{bytes: bytes, info: info, digest: [32]uint8{0xb3, 0x3c, 0x3a, 0xd, 0x4a, 0x76, 0xb6, 0x38, 0x96, 0x40, 0x54, 0x48, 0xce, 0x1f, 0xa6, 0x53, 0x31, 0xb4, 0x7a, 0x22, 0xd9, 0x8, 0xea, 0xd9, 0x28, 0x4, 0xc, 0x5f, 0x75, 0x8c, 0x39, 0xc0}}
+	return a, nil
+}
+
 var _flowfeesCdc = "\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xb4\x5a\x5f\x73\xdb\x36\x12\x7f\xf7\xa7\xd8\xfa\xa1\x47\xb6\xb2\x9c\x87\x9b\x7b\xf0\x58\x69\x1d\xd7\xbe\xe9\xcc\xdd\x5c\x26\x75\xdd\xc7\x0c\x4c\x2e\x45\x4c\x28\x40\x07\x80\x92\xd5\x8c\xbf\xfb\x0d\xfe\x12\x20\x41\x5b\xf6\xa5\x7e\x88\x22\x72\x01\xec\xdf\xdf\x2e\x76\x45\x37\x5b\x2e\x14\xdc\xf6\x6c\x4d\x1f\x3a\xbc\xe3\x5f\x90\x41\x23\xf8\x06\x4e\x93\x67\xa7\x27\x9e\xb2\xe3\xfb\x84\xca\x7f\x4f\x28\x7e\x53\x5c\x90\x35\xde\x22\xca\x88\x2e\x7a\x7a\x7a\x72\x42\xaa\x0a\xa5\x2c\x48\xd7\x95\x50\x71\xa6\x04\xa9\xec\x62\xb3\xea\xeb\xc9\x09\x00\xc0\xf9\x39\xdc\xec\x90\x29\x50\x2d\x51\x40\x25\xe0\x86\x2a\x85\x35\xec\x5b\x64\xa0\xf4\xc1\x12\x88\x40\xa8\x71\xcb\x25\xd5\x6f\x14\x07\xd5\x22\x34\x88\xb0\x23\x7d\xa7\xcc\x3e\xf1\x61\x68\x36\x34\x4c\xcb\x5f\xfc\xb2\x82\x6c\x78\xcf\xd4\x05\xfc\x7e\x4b\x1f\xff\xf1\xf7\xf2\x95\xc7\xef\xa9\x6a\x6b\x41\xf6\x4e\x2d\xc7\x33\xf0\x87\x5f\xf8\x16\x06\x1a\xad\x29\x2b\x7d\xdd\x57\x0a\xeb\x99\xa3\xb4\x46\x7f\x71\x24\xa3\x73\x16\x40\x59\xd5\xf5\x92\x72\x76\xd3\x34\x5c\x44\x2f\xf0\x11\xab\x5e\x4d\x5e\x1c\xcb\x19\x6c\x89\x20\x1b\x54\x28\x24\x54\x2d\x61\x6b\x9c\xe7\xee\x63\x20\xbd\x36\x94\x75\x21\x7b\xb1\xc6\x5b\x52\x29\x2e\x66\x59\xbd\xe6\x72\x9e\xdd\xf8\xe5\xc0\xf2\x47\x41\x77\x44\x39\xc3\x18\xab\xc1\xb6\x7f\xe8\x68\xe5\x1d\x08\x9a\x9e\x55\x7a\x97\x98\x59\x89\x5d\x53\xc2\x8e\x08\xbb\xee\x02\x7e\x0e\x6e\xbf\xbc\x37\x26\x9e\x88\xd6\xf4\xcc\x6f\x59\x68\x97\xb8\x80\x9f\xbf\x26\x11\x65\x17\x3e\x95\xf0\xd5\xac\xd5\x7f\x1d\x2a\xeb\x3e\x97\x67\xf6\x93\xc8\xef\xa6\x47\xc5\xd4\x0f\xa4\x23\xac\x42\x58\x19\xfa\xa5\xfb\x1a\x48\x34\xdf\x4b\xc3\xf2\x32\xe5\xe5\xf2\x4c\x7f\x96\x81\x50\xdb\x6e\x36\x22\xdc\xae\x96\xfa\xc9\xab\xf2\x1c\xfe\x89\xca\x38\xba\x67\x82\x37\xe6\xab\x09\xdf\xfb\xac\xe3\x6b\xa5\xac\x51\xdd\x22\x7e\xb0\x6b\x8a\xd2\x9b\x28\x52\x83\x40\xd5\x0b\x16\x33\x1f\xcb\xf5\x34\xd5\xb5\x40\xc9\x7b\x51\x21\x5c\xd5\x1b\xca\xa8\x54\x82\x28\x2e\xa2\x1d\xcf\xcf\x43\x80\x46\xcf\xe2\xd7\x57\x5d\xc7\xf7\xd2\xf0\x4f\x92\x4d\x14\x0f\x4b\x7d\xbc\xcf\x44\x78\x4e\x58\xbf\xd4\xaa\xf6\x56\xf0\xcd\x2d\xa2\xd1\xcd\x38\xdc\xe7\xfc\x23\x92\xc2\x1b\xdd\xfa\xee\xe5\x59\x00\x4b\xa7\x25\x7f\x58\xd8\xda\x7e\x96\xc9\x06\x91\xa5\xa7\xd0\x93\x5b\xe0\xac\x71\x79\x96\x4a\xea\xcc\xe0\x7d\xe1\x39\xfd\xd9\xe0\x07\xd2\x75\x41\x6b\x11\x34\x10\x05\x3c\xf6\xd9\xb1\x0a\xa5\xf1\x97\x01\x20\xbe\x31\x32\x64\xf4\xcb\x70\x3f\x1c\x07\x2b\x78\xe6\xf8\xe8\xcb\x0c\x0f\x99\x87\x33\x0c\x65\x1e\xa6\x96\x08\xe6\x9e\xa8\x24\xe1\xb8\xfc\x3f\x6c\xe4\xed\x63\xe4\x82\xc6\x08\xf6\x82\x65\x7e\x1b\x54\x50\x7c\x86\x8c\x71\x72\x2a\xe6\x5d\x9d\xaa\xd8\x8b\xb6\x1e\x8b\x56\xfe\xb5\xe6\x49\x18\x59\x1e\x6f\xac\x74\xdd\x5f\x61\x3a\x48\xc1\xf6\x0a\xa4\x12\x7d\xa5\xa0\xe5\x5d\x4d\xd9\x3a\x17\x4a\x0c\xb1\xb6\x05\x50\x45\xba\xaa\xef\x74\xa2\x73\x64\x72\x02\x9a\x6e\xbb\x84\x9b\x04\x32\xcf\xe1\xae\x4d\x3d\x41\x67\xf9\x5e\xda\x13\x36\xe4\x0b\x82\x12\x84\x49\x62\xd2\xa5\xad\x45\x04\xca\x2d\x67\x86\xa0\xa5\xeb\x16\x3a\x4e\x6a\x09\x9c\x19\x36\x18\xaa\x3d\x17\x5f\xb2\xfe\xa4\x73\x6b\xc6\x77\x26\xec\xdc\xfe\xeb\x3f\x7f\x40\xc5\xa5\xd2\xd9\x86\x33\x84\x9e\x51\xf3\xff\x60\x3a\x40\x63\x83\xa5\x25\xbf\xff\xb7\x66\xda\xb2\x25\x35\xb6\x42\xc3\x05\x18\x69\xb5\x12\x27\xab\x66\x99\x7b\x06\x60\x8e\x67\x32\xf8\xc9\xab\x98\x9c\xac\x9a\x65\xf2\x19\xa0\x1b\xa0\x80\x32\xaa\xbe\x31\x88\xa6\x01\x6e\x12\x77\x74\x00\xac\x62\xd3\x4e\x49\x33\xc7\xc2\x2a\xc7\xcc\x74\x69\x86\x29\x58\xe5\x58\x9d\x0b\x2c\xb8\x47\x41\x9b\xc3\x47\x72\x40\xe1\x6a\x92\x4f\x28\x75\x7e\x35\x16\xd1\xa9\x0f\x6b\x78\x38\x18\x07\xde\x0d\xb4\xd2\x11\xdf\x72\x71\x37\x04\xc1\x4d\xb0\x54\xae\x8a\x8c\xc3\x6e\xf6\xd8\xa4\x68\xb9\x13\x3d\x02\xb5\x65\xd5\x56\xd3\x42\x4b\x24\xc8\xbe\x69\x68\x45\x75\xe9\xec\x4b\x2f\xed\x2f\x9a\x28\x0e\xc8\xc1\x6d\x34\x20\x70\xa6\x28\xeb\xf3\x69\x56\xa3\x6a\x45\x1c\xf7\x18\xc9\x73\x01\x1f\x38\xef\x12\x8e\x5a\x04\x9d\x3a\x36\xfd\xc6\x71\xe4\x59\x10\xf8\xdf\x9e\x0a\xac\x8f\xe7\x65\x99\x6e\x4c\x25\xec\x48\xa7\x05\x96\x50\x63\x43\x9d\xe2\x8f\x57\x7a\x3e\x2e\xb4\x70\x9e\x37\xb7\x3e\x13\xb8\x56\x30\xf2\x68\x04\x9b\xc0\x5a\x31\xe0\x84\xf9\xfe\x63\x24\x90\x7e\x50\x4e\x04\xae\x08\xd3\x1e\xdc\x8b\xf8\x8c\x82\x36\xa6\x0a\x22\x3b\x42\x3b\xa2\xe3\x7c\x1c\xda\x1e\x61\xcb\x59\x51\x1c\x8f\x91\xfc\x3a\xaf\xcc\x44\xf9\x33\x46\x5d\xcc\x29\x65\x01\x2f\x9c\x91\x8b\xf6\xec\x41\xb0\xca\x7b\xd5\x74\xf9\x88\x15\x58\x8d\x99\x9b\x2e\xc9\xf3\x08\xab\x19\xe6\xc7\x35\xd1\x00\x00\xaf\x08\x6a\x2a\x75\x62\xed\x06\x40\xd0\xc8\xfd\x80\x0d\x17\xc1\x92\x6c\x0d\x24\x76\x84\xa5\x3f\xe6\x57\x65\x4f\xa2\x28\xed\x8d\x79\xec\x31\x26\x9a\xfe\x26\x43\x3c\x51\x69\x53\x28\x32\xde\xaf\xdb\x38\x6e\xf2\x91\xb5\xf0\x27\x11\x56\x3b\xe4\xb2\xb5\x9e\xf7\xea\x2d\x97\x36\xb9\x8c\xdd\x3b\xf0\x58\x90\xaa\xe2\xc2\x56\x17\xb6\x89\x32\xce\x8f\x66\x73\xbf\xe1\xc4\x79\xdd\x05\x30\xda\xbf\xf4\x7b\x9f\x44\x61\x36\xb6\x76\x24\xb1\x8f\x7b\xe2\x58\x77\x48\xa3\xf9\xea\x23\xb8\xfb\xd1\x6f\xf7\xf6\x90\xd5\xf7\x8e\x0d\x79\x9c\x08\xe1\x38\x1e\xd7\xba\xc7\x7b\x49\x11\x5c\xed\xb3\xb5\xe9\x55\x55\xe9\xab\x55\xaf\xda\xe2\x03\x17\x82\xef\xef\x35\xca\x95\xf0\xfd\x95\x95\x6a\x11\xc5\x6c\xbe\x13\x13\x08\x36\xe4\xf1\x26\xdf\x94\x31\x24\xe5\xc5\x91\x79\xc5\xdf\xdc\xbd\xf6\x5c\xed\x36\x0f\xe0\x03\xb4\xea\x32\x63\x43\x1e\xd3\xf0\xd2\x19\xde\xc0\x00\xdf\x6c\x7b\x65\x1a\x7c\xc5\x44\x98\xd1\x83\x4c\x7f\x69\x2a\x5e\x79\x92\x65\xdb\xb9\x45\xe0\x78\x1c\x3b\xcf\x4a\x60\x50\xd4\xee\xf0\x69\x82\x3b\xa3\x36\xe5\xb2\xc6\x46\x5f\x7b\xcd\x85\xf9\x13\x4a\x14\xbb\x40\x5d\x04\xeb\x2e\x49\x5d\x0b\x94\x32\xe2\x96\x36\xb3\x67\xac\x9c\xc9\x8a\x77\xe3\x9b\x91\x86\x09\x1b\x42\x41\xb2\x28\x36\xfe\x44\xc1\x01\x1f\xa9\x02\x24\xa2\x3b\x2c\x73\xb7\xf4\x39\xf3\x17\x09\xb1\xfe\x9b\x49\x0d\x4a\xf4\xb8\x98\x10\x4f\x52\x45\x5e\xb6\xe9\xc2\xb9\x4c\x32\xf1\xa0\x64\xe5\xcc\x0d\x76\xae\xe1\x64\xec\x60\xb0\xc3\x34\x46\x6c\x33\x24\x5e\xf7\xab\xbd\x7f\x60\xbd\x46\xa8\x88\x44\xd8\xb7\x28\x30\xaa\xaa\x6a\x8e\x92\x29\x68\xc9\x0e\x81\xd8\x0d\x16\xa0\x04\x92\xf4\x3c\x22\xe1\x5d\x1a\x09\x43\xff\xed\xdd\xf2\x5d\x6c\x7c\xed\x64\xa6\x5d\x64\xba\x38\xb0\x1a\xa0\x60\x29\xad\x7b\x2d\x1f\x0c\x18\x5c\x7e\x3f\xea\xef\xbd\x77\x2d\xba\x73\x47\x77\xde\xf8\xf7\xe6\xf5\xd8\x67\x06\x0e\x86\xe3\x26\x7d\xc0\x48\x8d\xaf\x72\x14\x07\xd8\xe3\xc2\xe6\xc1\xa7\x3b\xac\x81\xaa\xd8\x06\x91\xbb\xae\xb5\xfa\x50\xc7\x21\x61\xf9\xa8\x75\xc4\xa9\x23\xcf\xf8\xa5\xdf\xf8\xfd\xea\x28\xdf\x7b\x93\xc3\x1e\xed\xac\x93\x4e\xe8\xb5\xad\x0a\x4c\xe7\x3b\x49\xff\x5a\x11\xb2\x7f\x70\x9d\x71\xc5\x5d\xa3\xde\xdf\xca\xc3\x06\xa1\xa5\x78\xd5\xab\xd6\xe5\x05\x5b\x26\x0c\xab\x69\xbe\x9f\x6a\x77\x4c\x19\x2c\x3e\x6b\xb2\xe7\xb3\xce\xeb\xfb\xfe\x91\xe7\x69\xdf\x6f\x10\xaf\x4c\xbf\xf0\x1b\xa1\x3f\xce\x42\x3f\x6d\xe2\xc3\x8e\xc0\x4f\x81\x66\x2a\xc2\xb8\xcd\x6c\x41\xef\x0b\xa8\x39\x30\xae\x42\x29\xb5\x18\xaf\x97\x3c\x54\x67\xc3\x26\x3d\x63\xa8\x95\x4e\x04\xed\x0e\x61\xce\x61\x86\x17\x32\x83\xc2\xb9\xb0\x9b\xe0\x01\xc9\x40\x81\xb1\x56\xda\x04\xf6\x3d\xda\x12\x5e\x8b\x13\x09\x63\x3f\xfd\x04\x5b\xc2\x68\x55\x9c\xfe\xce\xcc\xc5\x43\x71\xb0\x87\x82\xc0\x06\x05\xea\xd8\x72\x35\x9f\xc7\x51\xc3\xae\x05\xc3\xd3\xf2\x64\xc6\x1a\xef\x33\xb0\x93\xb1\x8a\x45\x80\xa6\x57\xbd\xc1\x5d\x5d\x48\xf3\x5a\xa3\x86\x6a\x61\x4f\xbb\x0e\x18\xee\xf4\x5d\x52\x67\x3e\x52\xb5\x58\x4f\x0c\x43\xa4\xc7\x18\x63\x1f\x6d\x99\x9e\xd5\x28\x42\x6d\xe8\xe1\xc5\x33\x61\xb7\xe5\x0e\xd5\x55\x8b\x54\xc4\x91\x29\xad\x47\xd6\x3a\xb0\x74\xd0\x56\xbc\xeb\x70\x7a\x3d\x89\xa6\x04\x8e\x91\xeb\x5e\x08\x64\xaa\x3b\x58\x39\xa8\x34\x87\xf8\xb6\x58\x43\x68\x37\x29\xed\x0b\x22\xc3\x94\xcc\x66\x77\xd7\xe9\xa1\x1d\x55\x07\x9f\xc6\xe6\x8a\xdd\x8e\xaf\x69\x55\x4e\x14\xf2\xa1\x57\x71\x3f\x4e\x68\x07\xc7\x5c\x7a\xdc\x39\x6b\xea\x14\xb5\xf0\xdd\x04\xfb\xb0\x25\x12\x3a\x94\x32\xba\x91\xfa\xc5\x01\xb6\xf3\xb7\x05\xff\x17\x83\xc0\x71\x09\xc8\x0c\xb9\xdc\x10\x04\x2e\xcf\xe2\x55\x93\x01\x46\xd8\xbd\x3c\x6a\xa4\xe5\x76\x4d\x8b\xc6\x3b\xd7\x27\xf5\x2d\xd1\x28\x89\x09\xac\x38\xb3\x0d\x19\xac\xa1\x97\xbe\xad\x5a\x13\x45\x3c\x24\x53\xe9\xa6\x94\xfa\xe6\xe3\xc6\x5b\x1f\x93\xe9\x85\x51\x79\xc7\xab\x2f\x6e\x28\xac\x2b\x0d\xb3\xa2\x25\xdb\x2d\x32\x37\x94\x05\x3f\x7b\xc9\xce\x62\x83\xa0\x19\x58\x7e\x03\x72\x42\x7e\x4e\xb6\xa3\xb8\x8f\x66\x70\x71\x97\xfd\x62\xb6\x11\x1c\x4f\xe3\x9c\x6b\x06\xdc\xaa\xf8\xf6\x70\x99\x2c\x9c\xa0\x92\x41\xad\xc7\x84\xa6\x8c\xe0\xe8\x46\x08\x2e\x34\x3f\xe6\xea\xac\x1e\x47\x2d\x6d\xd3\x20\x3d\x98\xc6\xb6\x81\x2c\x34\x7d\x0d\x4a\x3a\xfa\xa7\xbe\xa8\x50\x21\xd5\x77\xa7\x39\x79\xed\xc4\x36\x3b\x3f\xfa\xac\xcf\x18\xbe\x8f\x24\x2f\xd3\x9b\x12\x6e\xb6\xea\x00\x4e\x18\x7f\xd7\xdf\x0b\x6a\xd8\x65\xb8\x1f\xa9\x4d\x71\x9f\xa6\x83\xaf\x8e\x75\xd6\x71\x52\xbf\x41\x67\xcf\x6f\x2a\xc9\x0e\x8b\x44\xac\x85\xe2\xc7\xee\xe8\xdd\xf2\x85\x21\x7c\xb2\xfd\xf2\xe5\xd1\x4a\x4a\x7f\xfc\x68\x25\x5d\x37\x3b\x5a\x79\x3a\x09\x2d\x1c\x57\x78\x64\xe1\xca\xce\xf8\xf5\x9b\xca\x02\xf7\x64\xfe\xe8\xe2\x7a\x4d\x77\xc8\xc6\x92\x98\xb7\x23\x16\xe6\xc3\xea\xd9\x02\xe8\xa5\xea\x2a\x33\x08\xd7\x50\x69\x38\x95\xbe\xc4\xca\x8c\xc7\x46\x25\x86\x22\x9d\x6b\x84\xd9\x95\x49\x23\xfe\x07\x28\x26\x12\xfe\xe0\x09\x73\x6d\xf8\x1f\xc7\xec\x0e\xe4\xb9\xd6\x7b\x39\x46\x8d\xc0\x4f\x1c\xa2\xa6\x35\x39\x8a\xb3\x6b\x73\x5b\x00\x62\x63\x2a\xfc\xc6\xc8\x66\x09\x6d\x04\xed\xe1\xfa\xb2\x41\x99\x8f\xc6\x4c\x4a\xf0\x43\x71\x5b\x26\x55\x66\xcf\x1b\x1d\xc1\x76\xe4\x6e\x68\xee\x0e\x5b\xbc\x00\xfd\xef\xe5\xf8\xa7\x15\xef\x8b\xb2\xcc\xff\xe6\x22\x51\xb2\x19\x9e\xea\xa3\xec\x01\xe9\xaf\x0e\x8a\x63\x22\xf5\xf2\xcc\xec\xb1\x80\x24\x48\x1b\x37\x24\x34\xfb\x79\x27\x7f\xfa\x5f\x00\x00\x00\xff\xff\x35\xea\xb4\x22\xa2\x25\x00\x00"
 
 func flowfeesCdcBytes() ([]byte, error) {
@@ -453,6 +474,7 @@ func AssetNames() []string {
 
 // _bindata is a table, holding each asset generator, mapped to its name.
 var _bindata = map[string]func() (*asset, error){
+	"Crypto.cdc":                               cryptoCdc,
 	"FlowFees.cdc":                             flowfeesCdc,
 	"FlowIDTableStaking.cdc":                   flowidtablestakingCdc,
 	"FlowServiceAccount.cdc":                   flowserviceaccountCdc,
@@ -515,6 +537,7 @@ type bintree struct {
 }
 
 var _bintree = &bintree{nil, map[string]*bintree{
+	"Crypto.cdc": {cryptoCdc, map[string]*bintree{}},
 	"FlowFees.cdc": {flowfeesCdc, map[string]*bintree{}},
 	"FlowIDTableStaking.cdc": {flowidtablestakingCdc, map[string]*bintree{}},
 	"FlowServiceAccount.cdc": {flowserviceaccountCdc, map[string]*bintree{}},
diff --git a/lib/go/test/crypto_test.go b/lib/go/test/crypto_test.go
new file mode 100644
index 000000000..36be9faa9
--- /dev/null
+++ b/lib/go/test/crypto_test.go
@@ -0,0 +1,168 @@
+package test
+
+import (
+	"context"
+	"encoding/hex"
+	"fmt"
+	"testing"
+
+	"github.com/onflow/cadence"
+	jsoncdc "github.com/onflow/cadence/encoding/json"
+	"github.com/onflow/flow-go-sdk"
+	sdktemplates "github.com/onflow/flow-go-sdk/templates"
+	"github.com/onflow/flow-go-sdk/test"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+
+	"github.com/onflow/flow-core-contracts/lib/go/contracts"
+)
+
+func TestCrypto(t *testing.T) {
+	b, adapter := newBlockchain()
+
+	accountKeys := test.AccountKeyGenerator()
+
+	// Create new keys for the Crypto account and deploy
+	cryptoAccountKey, _ := accountKeys.NewWithSigner()
+	cryptoCode := contracts.Crypto()
+
+	cryptoAddress, err := adapter.CreateAccount(
+		context.Background(),
+		[]*flow.AccountKey{cryptoAccountKey},
+		[]sdktemplates.Contract{
+			{
+				Name:   "Crypto",
+				Source: string(cryptoCode),
+			},
+		},
+	)
+	assert.NoError(t, err)
+
+	script := []byte(fmt.Sprintf(
+		`
+          import Crypto from %s
+
+          access(all)
+          fun main(
+            rawPublicKeys: [String],
+            weights: [UFix64],
+            domainSeparationTag: String,
+            signatures: [String],
+            toAddress: Address,
+            fromAddress: Address,
+            amount: UFix64
+          ): Bool {
+            let keyList = Crypto.KeyList()
+
+            var i = 0
+            for rawPublicKey in rawPublicKeys {
+              keyList.add(
+                PublicKey(
+                  publicKey: rawPublicKey.decodeHex(),
+                  signatureAlgorithm: SignatureAlgorithm.ECDSA_P256
+                ),
+                hashAlgorithm: HashAlgorithm.SHA3_256,
+                weight: weights[i],
+              )
+              i = i + 1
+            }
+
+            let signatureSet: [Crypto.KeyListSignature] = []
+
+            var j = 0
+            for signature in signatures {
+              signatureSet.append(
+                Crypto.KeyListSignature(
+                  keyIndex: j,
+                  signature: signature.decodeHex()
+                )
+              )
+              j = j + 1
+            }
+
+            // assemble the same message in cadence
+            let message = toAddress.toBytes()
+              .concat(fromAddress.toBytes())
+              .concat(amount.toBigEndianBytes())
+
+            return keyList.verify(
+              signatureSet: signatureSet,
+              signedData: message,
+              domainSeparationTag: domainSeparationTag
+            )
+          }
+        `,
+		cryptoAddress.HexWithPrefix(),
+	))
+
+	// create the keys
+	keyAlice, signerAlice := accountKeys.NewWithSigner()
+	keyBob, signerBob := accountKeys.NewWithSigner()
+
+	// create the message that will be signed
+	addresses := test.AddressGenerator()
+
+	toAddress := cadence.Address(addresses.New())
+	fromAddress := cadence.Address(addresses.New())
+
+	amount, err := cadence.NewUFix64("100.00")
+	require.NoError(t, err)
+
+	var message []byte
+	message = append(message, toAddress.Bytes()...)
+	message = append(message, fromAddress.Bytes()...)
+	message = append(message, amount.ToBigEndianBytes()...)
+
+	// sign the message with Alice and Bob
+	signatureAlice, err := flow.SignUserMessage(signerAlice, message)
+	require.NoError(t, err)
+
+	signatureBob, err := flow.SignUserMessage(signerBob, message)
+	require.NoError(t, err)
+
+	publicKeys := cadence.NewArray([]cadence.Value{
+		cadence.String(hex.EncodeToString(keyAlice.PublicKey.Encode())),
+		cadence.String(hex.EncodeToString(keyBob.PublicKey.Encode())),
+	})
+
+	// each signature has half weight
+	weightAlice, err := cadence.NewUFix64("0.5")
+	require.NoError(t, err)
+
+	weightBob, err := cadence.NewUFix64("0.5")
+	require.NoError(t, err)
+
+	weights := cadence.NewArray([]cadence.Value{
+		weightAlice,
+		weightBob,
+	})
+
+	signatures := cadence.NewArray([]cadence.Value{
+		cadence.String(hex.EncodeToString(signatureAlice)),
+		cadence.String(hex.EncodeToString(signatureBob)),
+	})
+
+	domainSeparationTag := cadence.String("FLOW-V0.0-user")
+
+	arguments := []cadence.Value{
+		publicKeys,
+		weights,
+		domainSeparationTag,
+		signatures,
+		toAddress,
+		fromAddress,
+		amount,
+	}
+
+	encodedArguments := make([][]byte, 0, len(arguments))
+	for _, argument := range arguments {
+		encodedArguments = append(encodedArguments, jsoncdc.MustEncode(argument))
+	}
+
+	result := executeScriptAndCheck(t, b, script, encodedArguments)
+
+	assert.Equal(t,
+		cadence.NewBool(true),
+		result,
+	)
+}

From 79fdc6c8ba530234e9784cedf91f7df962f9e0da Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bastian=20M=C3=BCller?= <bastian@turbolent.com>
Date: Thu, 17 Oct 2024 15:04:55 -0700
Subject: [PATCH 2/6] add Crypto contract to templates code

---
 lib/go/templates/templates.go | 8 ++++++++
 1 file changed, 8 insertions(+)

diff --git a/lib/go/templates/templates.go b/lib/go/templates/templates.go
index 208f7bcf5..906f63305 100644
--- a/lib/go/templates/templates.go
+++ b/lib/go/templates/templates.go
@@ -19,6 +19,7 @@ const (
 	placeholderFungibleTokenMVAddress     = "\"FungibleTokenMetadataViews\""
 	placeholderMetadataViewsAddress       = "\"MetadataViews\""
 	placeholderBurnerAddress              = "\"Burner\""
+	placeholderCryptoAddress              = "\"Crypto\""
 	placeholderFlowTokenAddress           = "\"FlowToken\""
 	placeholderIDTableAddress             = "\"FlowIDTableStaking\""
 	placeholderLockedTokensAddress        = "\"LockedTokens\""
@@ -38,6 +39,7 @@ type Environment struct {
 	Network                           string
 	ViewResolverAddress               string
 	BurnerAddress                     string
+	CryptoAddress                     string
 	FungibleTokenAddress              string
 	NonFungibleTokenAddress           string
 	MetadataViewsAddress              string
@@ -90,6 +92,12 @@ func ReplaceAddresses(code string, env Environment) string {
 		withHexPrefix(env.BurnerAddress),
 	)
 
+	code = strings.ReplaceAll(
+		code,
+		placeholderCryptoAddress,
+		withHexPrefix(env.CryptoAddress),
+	)
+
 	code = strings.ReplaceAll(
 		code,
 		placeholderViewResolverAddress,

From 0b1b28434b48f8900793d9c156777c8eca968d2f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bastian=20M=C3=BCller?= <bastian@turbolent.com>
Date: Thu, 17 Oct 2024 17:07:33 -0700
Subject: [PATCH 3/6] add tests

---
 contracts/Crypto_test.cdc | 342 ++++++++++++++++++++++++++++++++++++++
 contracts/flow.json       |  30 ++++
 2 files changed, 372 insertions(+)
 create mode 100644 contracts/Crypto_test.cdc
 create mode 100644 contracts/flow.json

diff --git a/contracts/Crypto_test.cdc b/contracts/Crypto_test.cdc
new file mode 100644
index 000000000..0cdd42e61
--- /dev/null
+++ b/contracts/Crypto_test.cdc
@@ -0,0 +1,342 @@
+import Test
+import Crypto from "Crypto.cdc"
+
+access(all)
+fun setup() {
+    let err = Test.deployContract(
+        name: "Crypto",
+        path: "Crypto.cdc",
+        arguments: []
+    )
+
+    Test.expect(err, Test.beNil())
+}
+
+access(all)
+fun testCryptoHash() {
+    let hash = Crypto.hash([1, 2, 3], algorithm: HashAlgorithm.SHA3_256)
+    Test.assertEqual(32, hash.length)
+}
+
+access(all)
+fun testCryptoHashWithTag() {
+    let hash = Crypto.hashWithTag(
+        [1, 2, 3],
+        tag: "v0.1.tag",
+        algorithm: HashAlgorithm.SHA3_256
+    )
+    Test.assertEqual(32, hash.length)
+}
+
+access(all)
+fun testAddKeyToKeyList() {
+    let keyList = Crypto.KeyList()
+
+    let publicKey = PublicKey(
+        publicKey:
+            "db04940e18ec414664ccfd31d5d2d4ece3985acb8cb17a2025b2f1673427267968e52e2bbf3599059649d4b2cce98fdb8a3048e68abf5abe3e710129e90696ca".decodeHex(),
+        signatureAlgorithm: SignatureAlgorithm.ECDSA_P256
+    )
+    keyList.add(
+        publicKey,
+        hashAlgorithm: HashAlgorithm.SHA3_256,
+        weight: 1.0
+    )
+
+    Test.assert(keyList.get(keyIndex: 0) != nil)
+}
+
+access(all)
+fun testGetKeyFromList() {
+    let keyList = Crypto.KeyList()
+
+    let publicKey = PublicKey(
+        publicKey:
+            "db04940e18ec414664ccfd31d5d2d4ece3985acb8cb17a2025b2f1673427267968e52e2bbf3599059649d4b2cce98fdb8a3048e68abf5abe3e710129e90696ca".decodeHex(),
+        signatureAlgorithm: SignatureAlgorithm.ECDSA_P256
+    )
+    keyList.add(
+        publicKey,
+        hashAlgorithm: HashAlgorithm.SHA3_256,
+        weight: 1.0
+    )
+
+    Test.assert(keyList.get(keyIndex: 0) != nil)
+    Test.assert(keyList.get(keyIndex: 2) == nil)
+}
+
+access(all)
+fun testRevokeKeyFromList() {
+    let keyList = Crypto.KeyList()
+
+    let publicKey = PublicKey(
+        publicKey:
+            "db04940e18ec414664ccfd31d5d2d4ece3985acb8cb17a2025b2f1673427267968e52e2bbf3599059649d4b2cce98fdb8a3048e68abf5abe3e710129e90696ca".decodeHex(),
+        signatureAlgorithm: SignatureAlgorithm.ECDSA_P256
+    )
+    keyList.add(
+        publicKey,
+        hashAlgorithm: HashAlgorithm.SHA3_256,
+        weight: 0.5
+    )
+
+    keyList.revoke(keyIndex: 0)
+    keyList.revoke(keyIndex: 2)
+
+    Test.assert(keyList.get(keyIndex: 0)!.isRevoked)
+    Test.assert(keyList.get(keyIndex: 2) == nil)
+}
+
+access(all)
+fun testKeyListVerify() {
+    let keyList = Crypto.KeyList()
+
+    let publicKeyA = PublicKey(
+        publicKey:
+            "db04940e18ec414664ccfd31d5d2d4ece3985acb8cb17a2025b2f1673427267968e52e2bbf3599059649d4b2cce98fdb8a3048e68abf5abe3e710129e90696ca".decodeHex(),
+        signatureAlgorithm: SignatureAlgorithm.ECDSA_P256
+    )
+
+    keyList.add(
+        publicKeyA,
+        hashAlgorithm: HashAlgorithm.SHA3_256,
+        weight: 0.5
+    )
+
+    let publicKeyB = PublicKey(
+        publicKey:
+            "df9609ee588dd4a6f7789df8d56f03f545d4516f0c99b200d73b9a3afafc14de5d21a4fc7a2a2015719dc95c9e756cfa44f2a445151aaf42479e7120d83df956".decodeHex(),
+        signatureAlgorithm: SignatureAlgorithm.ECDSA_P256
+    )
+
+    keyList.add(
+        publicKeyB,
+        hashAlgorithm: HashAlgorithm.SHA3_256,
+        weight: 0.5
+    )
+
+    let signatureSet = [
+        Crypto.KeyListSignature(
+            keyIndex: 0,
+            signature:
+                "8870a8cbe6f44932ba59e0d15a706214cc4ad2538deb12c0cf718d86f32c47765462a92ce2da15d4a29eb4e2b6fa05d08c7db5d5b2a2cd8c2cb98ded73da31f6".decodeHex()
+        ),
+        Crypto.KeyListSignature(
+            keyIndex: 1,
+            signature:
+                "bbdc5591c3f937a730d4f6c0a6fde61a0a6ceaa531ccb367c3559335ab9734f4f2b9da8adbe371f1f7da913b5a3fdd96a871e04f078928ca89a83d841c72fadf".decodeHex()
+        )
+    ]
+
+    // "foo", encoded as UTF-8, in hex representation
+    let signedData = "666f6f".decodeHex()
+
+    let isValid = keyList.verify(
+        signatureSet: signatureSet,
+        signedData: signedData,
+        domainSeparationTag: "FLOW-V0.0-user"
+    )
+
+    Test.assert(isValid)
+}
+
+access(all)
+fun testKeyListVerifyInsufficientWeights() {
+    let keyList = Crypto.KeyList()
+
+    let publicKeyA = PublicKey(
+        publicKey:
+            "db04940e18ec414664ccfd31d5d2d4ece3985acb8cb17a2025b2f1673427267968e52e2bbf3599059649d4b2cce98fdb8a3048e68abf5abe3e710129e90696ca".decodeHex(),
+        signatureAlgorithm: SignatureAlgorithm.ECDSA_P256
+    )
+
+    keyList.add(
+        publicKeyA,
+        hashAlgorithm: HashAlgorithm.SHA3_256,
+        weight: 0.4
+    )
+
+    let publicKeyB = PublicKey(
+        publicKey:
+            "df9609ee588dd4a6f7789df8d56f03f545d4516f0c99b200d73b9a3afafc14de5d21a4fc7a2a2015719dc95c9e756cfa44f2a445151aaf42479e7120d83df956".decodeHex(),
+        signatureAlgorithm: SignatureAlgorithm.ECDSA_P256
+    )
+
+    keyList.add(
+        publicKeyB,
+        hashAlgorithm: HashAlgorithm.SHA3_256,
+        weight: 0.5
+    )
+
+    let signatureSet = [
+        Crypto.KeyListSignature(
+            keyIndex: 0,
+            signature:
+                "8870a8cbe6f44932ba59e0d15a706214cc4ad2538deb12c0cf718d86f32c47765462a92ce2da15d4a29eb4e2b6fa05d08c7db5d5b2a2cd8c2cb98ded73da31f6".decodeHex()
+        ),
+        Crypto.KeyListSignature(
+            keyIndex: 1,
+            signature:
+                "bbdc5591c3f937a730d4f6c0a6fde61a0a6ceaa531ccb367c3559335ab9734f4f2b9da8adbe371f1f7da913b5a3fdd96a871e04f078928ca89a83d841c72fadf".decodeHex()
+        )
+    ]
+
+    // "foo", encoded as UTF-8, in hex representation
+    let signedData = "666f6f".decodeHex()
+
+    let isValid = keyList.verify(
+        signatureSet: signatureSet,
+        signedData: signedData,
+        domainSeparationTag: "FLOW-V0.0-user"
+    )
+
+    Test.assert(!isValid)
+}
+
+access(all)
+fun testKeyListVerifyWithRevokedKey() {
+    let keyList = Crypto.KeyList()
+
+    let publicKey = PublicKey(
+        publicKey:
+            "db04940e18ec414664ccfd31d5d2d4ece3985acb8cb17a2025b2f1673427267968e52e2bbf3599059649d4b2cce98fdb8a3048e68abf5abe3e710129e90696ca".decodeHex(),
+        signatureAlgorithm: SignatureAlgorithm.ECDSA_P256
+    )
+    keyList.add(
+        publicKey,
+        hashAlgorithm: HashAlgorithm.SHA3_256,
+        weight: 0.5
+    )
+
+    let signatureSet = [
+        Crypto.KeyListSignature(
+            keyIndex: 0,
+            signature:
+                "8870a8cbe6f44932ba59e0d15a706214cc4ad2538deb12c0cf718d86f32c47765462a92ce2da15d4a29eb4e2b6fa05d08c7db5d5b2a2cd8c2cb98ded73da31f6".decodeHex()
+        )
+    ]
+
+    // "foo", encoded as UTF-8, in hex representation
+    let signedData = "666f6f".decodeHex()
+
+    keyList.revoke(keyIndex: 0)
+
+    let isValid = keyList.verify(
+        signatureSet: signatureSet,
+        signedData: signedData,
+        domainSeparationTag: "FLOW-V0.0-user"
+    )
+
+    Test.assert(!isValid)
+}
+
+access(all)
+fun testKeyListVerifyWithMissingSignature() {
+    let keyList = Crypto.KeyList()
+
+    let publicKey = PublicKey(
+        publicKey:
+            "db04940e18ec414664ccfd31d5d2d4ece3985acb8cb17a2025b2f1673427267968e52e2bbf3599059649d4b2cce98fdb8a3048e68abf5abe3e710129e90696ca".decodeHex(),
+        signatureAlgorithm: SignatureAlgorithm.ECDSA_P256
+    )
+    keyList.add(
+        publicKey,
+        hashAlgorithm: HashAlgorithm.SHA3_256,
+        weight: 0.5
+    )
+
+    let signatureSet = [
+        Crypto.KeyListSignature(
+            keyIndex: 1,
+            signature:
+                "8870a8cbe6f44932ba59e0d15a706214cc4ad2538deb12c0cf718d86f32c47765462a92ce2da15d4a29eb4e2b6fa05d08c7db5d5b2a2cd8c2cb98ded73da31f6".decodeHex()
+        )
+    ]
+
+    // "foo", encoded as UTF-8, in hex representation
+    let signedData = "666f6f".decodeHex()
+
+    let isValid = keyList.verify(
+        signatureSet: signatureSet,
+        signedData: signedData,
+        domainSeparationTag: "FLOW-V0.0-user"
+    )
+
+    Test.assert(!isValid)
+}
+
+access(all)
+fun testKeyListVerifyDuplicateSignature() {
+    let keyList = Crypto.KeyList()
+
+    let publicKey = PublicKey(
+        publicKey:
+            "db04940e18ec414664ccfd31d5d2d4ece3985acb8cb17a2025b2f1673427267968e52e2bbf3599059649d4b2cce98fdb8a3048e68abf5abe3e710129e90696ca".decodeHex(),
+        signatureAlgorithm: SignatureAlgorithm.ECDSA_P256
+    )
+    keyList.add(
+        publicKey,
+        hashAlgorithm: HashAlgorithm.SHA3_256,
+        weight: 0.5
+    )
+
+    let signatureSet = [
+        Crypto.KeyListSignature(
+            keyIndex: 0,
+            signature:
+                "8870a8cbe6f44932ba59e0d15a706214cc4ad2538deb12c0cf718d86f32c47765462a92ce2da15d4a29eb4e2b6fa05d08c7db5d5b2a2cd8c2cb98ded73da31f6".decodeHex()
+        ),
+        Crypto.KeyListSignature(
+            keyIndex: 0,
+            signature:
+                "8870a8cbe6f44932ba59e0d15a706214cc4ad2538deb12c0cf718d86f32c47765462a92ce2da15d4a29eb4e2b6fa05d08c7db5d5b2a2cd8c2cb98ded73da31f6".decodeHex()
+        )
+    ]
+
+    // "foo", encoded as UTF-8, in hex representation
+    let signedData = "666f6f".decodeHex()
+
+    let isValid = keyList.verify(
+        signatureSet: signatureSet,
+        signedData: signedData,
+        domainSeparationTag: "FLOW-V0.0-user"
+    )
+
+    Test.assert(!isValid)
+}
+
+access(all)
+fun testKeyListVerifyInvalidSignature() {
+    let keyList = Crypto.KeyList()
+
+    let publicKey = PublicKey(
+        publicKey:
+            "db04940e18ec414664ccfd31d5d2d4ece3985acb8cb17a2025b2f1673427267968e52e2bbf3599059649d4b2cce98fdb8a3048e68abf5abe3e710129e90696ca".decodeHex(),
+        signatureAlgorithm: SignatureAlgorithm.ECDSA_P256
+    )
+    keyList.add(
+        publicKey,
+        hashAlgorithm: HashAlgorithm.SHA3_256,
+        weight: 0.5
+    )
+
+    let signatureSet = [
+        Crypto.KeyListSignature(
+            keyIndex: 0,
+            signature:
+                "db70a8cbe6f44932ba59e0d15a706214cc4ad2538deb12c0cf718d86f32c47765462a92ce2da15d4a29eb4e2b6fa05d08c7db5d5b2a2cd8c2cb98ded73da31f6".decodeHex()
+        )
+    ]
+
+    // "foo", encoded as UTF-8, in hex representation
+    let signedData = "666f6f".decodeHex()
+
+    let isValid = keyList.verify(
+        signatureSet: signatureSet,
+        signedData: signedData,
+        domainSeparationTag: "FLOW-V0.0-user"
+    )
+
+    Test.assert(!isValid)
+}
+
diff --git a/contracts/flow.json b/contracts/flow.json
new file mode 100644
index 000000000..7604b6c30
--- /dev/null
+++ b/contracts/flow.json
@@ -0,0 +1,30 @@
+{
+  "networks": {
+    "emulator": "127.0.0.1:3569",
+    "testing": "127.0.0.1:3569",
+    "mainnet": "access.mainnet.nodes.onflow.org:9000",
+    "sandboxnet": "access.sandboxnet.nodes.onflow.org:9000",
+    "testnet": "access.devnet.nodes.onflow.org:9000"
+  },
+  "contracts": {
+    "Crypto": {
+      "source": "crypto.cdc",
+      "aliases": {
+        "testing": "0x0000000000000007"
+      }
+    }
+  },
+  "deployments": {
+    "emulator": {
+      "emulator-account": [
+        "Crypto"
+      ]
+    }
+  },
+  "accounts": {
+    "emulator-account": {
+      "address": "f8d6e0586b0a20c7",
+      "key": "b775d6da04a0b4819903d89a25395bfd90eb8559182255690c7c141edaeae923"
+    }
+  }
+}
\ No newline at end of file

From 0320e412595fae108d040efd1195d4928fb31da3 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bastian=20M=C3=BCller?= <bastian@turbolent.com>
Date: Thu, 17 Oct 2024 17:09:23 -0700
Subject: [PATCH 4/6] add Cadence-based tests to CI

---
 .github/workflows/ci.yml | 8 +++++++-
 contracts/flow.json      | 2 +-
 2 files changed, 8 insertions(+), 2 deletions(-)

diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 9f55a7812..ed45a24b0 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -18,5 +18,11 @@ jobs:
         run: flow version
       - name: Update PATH
         run: echo "/root/.local/bin" >> $GITHUB_PATH
-      - name: Run tests
+      - name: Run Go-based tests
         run: export GOPATH=$HOME/go && make ci
+
+      - name: Install Flow CLI
+        run: sh -ci "$(curl -fsSL https://raw.githubusercontent.com/onflow/flow-cli/master/install.sh)"
+
+      - name: Run Cadence-based tests
+        run: cd contracts && flow test --cover --covercode="contracts" Crypto_test.cdc
diff --git a/contracts/flow.json b/contracts/flow.json
index 7604b6c30..c9a8389a7 100644
--- a/contracts/flow.json
+++ b/contracts/flow.json
@@ -8,7 +8,7 @@
   },
   "contracts": {
     "Crypto": {
-      "source": "crypto.cdc",
+      "source": "Crypto.cdc",
       "aliases": {
         "testing": "0x0000000000000007"
       }

From 107455d3092ba49e923e2c93e199ab846dc15bca Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bastian=20M=C3=BCller?= <bastian@turbolent.com>
Date: Thu, 17 Oct 2024 17:14:33 -0700
Subject: [PATCH 5/6] ignore JSON and Cadence test files

---
 lib/go/contracts/contracts.go | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/lib/go/contracts/contracts.go b/lib/go/contracts/contracts.go
index c2b16c6f5..b3ac7593a 100644
--- a/lib/go/contracts/contracts.go
+++ b/lib/go/contracts/contracts.go
@@ -1,6 +1,6 @@
 package contracts
 
-//go:generate go run github.com/kevinburke/go-bindata/go-bindata -prefix ../../../contracts -o internal/assets/assets.go -pkg assets -nometadata -nomemcopy ../../../contracts/...
+//go:generate go run github.com/kevinburke/go-bindata/go-bindata -ignore=.*_test\.cdc -ignore=.*.json -prefix ../../../contracts -o internal/assets/assets.go -pkg assets -nometadata -nomemcopy ../../../contracts/...
 
 import (
 	"fmt"

From 8bb8f95eeedc5078d94a4092f2ef05b3be1f1938 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bastian=20M=C3=BCller?= <bastian@turbolent.com>
Date: Mon, 21 Oct 2024 09:20:49 -0700
Subject: [PATCH 6/6] move CLI configuration for Crypto contract into existing
 file

---
 .github/workflows/ci.yml |  2 +-
 contracts/flow.json      | 30 ------------------------------
 flow.json                |  6 ++++++
 3 files changed, 7 insertions(+), 31 deletions(-)
 delete mode 100644 contracts/flow.json

diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index ed45a24b0..5d49337a1 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -25,4 +25,4 @@ jobs:
         run: sh -ci "$(curl -fsSL https://raw.githubusercontent.com/onflow/flow-cli/master/install.sh)"
 
       - name: Run Cadence-based tests
-        run: cd contracts && flow test --cover --covercode="contracts" Crypto_test.cdc
+        run: flow test --cover --covercode="contracts" contracts/Crypto_test.cdc
diff --git a/contracts/flow.json b/contracts/flow.json
deleted file mode 100644
index c9a8389a7..000000000
--- a/contracts/flow.json
+++ /dev/null
@@ -1,30 +0,0 @@
-{
-  "networks": {
-    "emulator": "127.0.0.1:3569",
-    "testing": "127.0.0.1:3569",
-    "mainnet": "access.mainnet.nodes.onflow.org:9000",
-    "sandboxnet": "access.sandboxnet.nodes.onflow.org:9000",
-    "testnet": "access.devnet.nodes.onflow.org:9000"
-  },
-  "contracts": {
-    "Crypto": {
-      "source": "Crypto.cdc",
-      "aliases": {
-        "testing": "0x0000000000000007"
-      }
-    }
-  },
-  "deployments": {
-    "emulator": {
-      "emulator-account": [
-        "Crypto"
-      ]
-    }
-  },
-  "accounts": {
-    "emulator-account": {
-      "address": "f8d6e0586b0a20c7",
-      "key": "b775d6da04a0b4819903d89a25395bfd90eb8559182255690c7c141edaeae923"
-    }
-  }
-}
\ No newline at end of file
diff --git a/flow.json b/flow.json
index dc598c0f7..f4bd4cf5c 100644
--- a/flow.json
+++ b/flow.json
@@ -116,6 +116,12 @@
         "testnet": "0x7aad92e5a0715d21",
         "mainnet": "0x62430cf28c26d095"
       }
+    },
+    "Crypto": {
+      "source": "./contracts/Crypto.cdc",
+      "aliases": {
+        "testing": "0x0000000000000007"
+      }
     }
   },
   "networks": {