Repository: MystenLabs/seal
Pull request #115: feat: add versioning to KeyServer
Author: login: joyqvq
Created: 2025-05-16T16:59:00Z | Merged: 2025-05-21T22:30:47Z

## Description 

next step: validate on client side ts-sdks in retrieveKeyServers
## Test plan 

How did you test the new or updated feature?


Diff:
diff --git a/Cargo.lock b/Cargo.lock
index 8fb006906..d1d4a4113 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -3917,6 +3917,7 @@ dependencies = [
  "chrono",
  "crypto",
  "fastcrypto",
+ "futures",
  "hex",
  "jsonrpsee",
  "lru 0.13.0",
diff --git a/UsingSeal.md b/UsingSeal.md
index 4219f6cc6..c62e18ded 100644
--- a/UsingSeal.md
+++ b/UsingSeal.md
@@ -185,10 +185,10 @@ Currently, access to above key servers is permissive. However, rate limiting is
 ### Setup
 Use the `seal-cli` tool to generate a new master key using `cargo run --bin seal-cli genkey`.
 
-Key servers can be registered onchain to enable discoverability. To register a key server, call the `register_and_transfer` function in the `seal::key_server` module. For example:
+Key servers can be registered onchain to enable discoverability. To register a key server, call the `create_and_transfer_v1` function in the `seal::key_server` module. For example:
 
 ```shell
-sui client call --function register_and_transfer --module key_server --package 0xe126f08d71c79d3e5619fc034da698d9986a76e6b5f2e0d4a00e068e6668ab8f --args mysten-dev-1 https://seal-key-server-testnet-1.mystenlabs.com 0xa023acbf600401017ee17bf918106ea9911914ca017aa3ab9ab5c64beb9bb5236fd9d4d5b5645dc3bc0d4f732ed04fc60d14b9f37987fe5eeb4db07fc0982904ce1ed0b07607ae2e99086e141f6c6a1df6def5f5d434ca7c09856a3750c92969 --gas-budget 10000000
+sui client call --function create_and_transfer_v1 --module key_server --package 0x62c79dfeb0a2ca8c308a56bde530ccf3846535e1623949d45c90d23128afff52 --args mysten-dev-1 https://seal-key-server-testnet-1.mystenlabs.com 0 0xa023acbf600401017ee17bf918106ea9911914ca017aa3ab9ab5c64beb9bb5236fd9d4d5b5645dc3bc0d4f732ed04fc60d14b9f37987fe5eeb4db07fc0982904ce1ed0b07607ae2e99086e141f6c6a1df6def5f5d434ca7c09856a3750c92969 --gas-budget 10000000
 ```
 
 Run the server using `cargo run --bin key-server` with environment variables:
diff --git a/crates/key-server/Cargo.toml b/crates/key-server/Cargo.toml
index 7adef8161..210345125 100644
--- a/crates/key-server/Cargo.toml
+++ b/crates/key-server/Cargo.toml
@@ -43,3 +43,4 @@ jsonrpsee = "0.24.0"
 tracing-test = "0.2.5"
 test_cluster = { git = "https://github.com/mystenlabs/sui", rev = "1f5fef23d09fb697fff9e83907c5871c08fb6c87", package = "test-cluster" }
 sui_move_build = { git = "https://github.com/mystenlabs/sui", rev = "1f5fef23d09fb697fff9e83907c5871c08fb6c87", package = "sui-move-build" }
+futures = "0.3"
diff --git a/crates/key-server/src/tests/mod.rs b/crates/key-server/src/tests/mod.rs
index d9dd9eacc..7bc41d642 100644
--- a/crates/key-server/src/tests/mod.rs
+++ b/crates/key-server/src/tests/mod.rs
@@ -9,6 +9,7 @@ use fastcrypto::ed25519::Ed25519KeyPair;
 use fastcrypto::groups::bls12381::G1Element;
 use fastcrypto::groups::GroupElement;
 use fastcrypto::serde_helpers::ToFromByteArray;
+use futures::future::join_all;
 use rand::thread_rng;
 use semver::VersionReq;
 use serde_json::json;
@@ -202,7 +203,7 @@ impl SealTestCluster {
                 self.cluster.get_address_0(),
                 package_id,
                 "key_server",
-                "register_and_transfer",
+                "create_and_transfer_v1",
                 vec![],
                 vec![
                     SuiJsonValue::from_str(description).unwrap(),
@@ -236,35 +237,58 @@ impl SealTestCluster {
         service_objects[0].1
     }
 
-    /// Get the public keys of the key servers with the given Object IDs.
+    /// Get the public keys of the key servers v1 with the given Object IDs.
     pub async fn get_public_keys(&self, object_ids: &[ObjectID]) -> Vec<ibe::PublicKey> {
+        let futures = object_ids.iter().map(|id| {
+            self.cluster
+                .sui_client()
+                .read_api()
+                .get_dynamic_fields(*id, None, None)
+        });
+
+        let res = join_all(futures).await;
+
+        // filter df that has type KeyServerV1
+        let object_ids = res
+            .into_iter()
+            .filter_map(|page| {
+                page.ok().and_then(|p| {
+                    p.data
+                        .into_iter()
+                        .find(|df| df.object_type.ends_with("::key_server::KeyServerV1"))
+                        .map(|df| df.object_id)
+                })
+            })
+            .collect::<Vec<_>>();
         let objects = self
             .cluster
             .sui_client()
             .read_api()
-            .multi_get_object_with_options(
-                object_ids.to_vec(),
-                SuiObjectDataOptions::full_content(),
-            )
+            .multi_get_object_with_options(object_ids, SuiObjectDataOptions::full_content())
             .await
             .unwrap();
-
         objects
             .into_iter()
             .map(|o| {
-                o.data
+                let value = o
+                    .data
                     .unwrap()
                     .content
                     .unwrap()
                     .try_as_move()
                     .unwrap()
                     .fields
-                    .field_value("pk")
+                    .field_value("value")
                     .unwrap()
-                    .to_json_value()
-                    .as_array()
+                    .to_json_value();
+                let pk = value
+                    .as_object()
                     .unwrap()
-                    .iter()
+                    .get("pk")
+                    .unwrap()
+                    .as_array()
+                    .unwrap();
+                pk.iter()
                     .map(|v| v.as_u64().unwrap() as u8)
                     .collect::<Vec<_>>()
             })
diff --git a/move/patterns/sources/voting.move b/move/patterns/sources/voting.move
index 8cc025b1e..73083c06f 100644
--- a/move/patterns/sources/voting.move
+++ b/move/patterns/sources/voting.move
@@ -113,7 +113,7 @@ public fun finalize_vote(
 #[test]
 fun test_vote() {
     use seal::bf_hmac_encryption::{verify_derived_keys, get_public_key};
-    use seal::key_server::{register, destroy_cap, KeyServer};
+    use seal::key_server::{create_v1, destroy_cap, KeyServer};
     use std::string;
     use seal::bf_hmac_encryption::parse_encrypted_object;
     use sui::test_scenario::{Self, next_tx, ctx};
@@ -125,7 +125,7 @@ fun test_vote() {
     // Setup key servers.
     let pk0 =
         x"a6b8194ba6ffa1bf4c4e13ab1e56833f99f45f97874e77b845b361305ddaa741174febc307d3e07f7d4d5bb08c0adf3d11a5b8774c84006fb0ba7435f045f56a61905bc283049c2175984528e40a36e0096aabd401a67b1ccc442416c33b5df9";
-    let cap0 = register(
+    let cap0 = create_v1(
         string::utf8(b"mysten0"),
         string::utf8(b"https://mysten-labs.com"),
         0,
@@ -137,7 +137,7 @@ fun test_vote() {
 
     let pk1 =
         x"ac1c15fe6c5476ebc8b5bc432dcea06a30c87f89d21b89159ceab06afb84e0e7edefaadb896771ee281d25b6845aa3a20bda9324de39a9909c00f09b344b053da835dfde943c995576ec5e2fcf93221006bb2fcec8ef5096b4b88c36e1aa861c";
-    let cap1 = register(
+    let cap1 = create_v1(
         string::utf8(b"mysten1"),
         string::utf8(b"https://mysten-labs.com"),
         0,
@@ -149,7 +149,7 @@ fun test_vote() {
 
     let pk2 =
         x"a8750277f240eb4d94c159b2ec47c1c19396f6e33691fbf50514906b3e70c0454d9a79cf1f1f5562e4ddad9c4505bfb405a9901ac6ba2a51c24919d7599c74a5155f83606f80c1a302de9865deb4577911493dc1608754d67051f755cd44c391";
-    let cap2 = register(
+    let cap2 = create_v1(
         string::utf8(b"mysten2"),
         string::utf8(b"https://mysten-labs.com"),
         0,
diff --git a/move/seal/sources/bf_hmac_encryption.move b/move/seal/sources/bf_hmac_encryption.move
index 352a18c82..b6deb5fbf 100644
--- a/move/seal/sources/bf_hmac_encryption.move
+++ b/move/seal/sources/bf_hmac_encryption.move
@@ -381,7 +381,7 @@ fun test_parse_encrypted_object() {
 fun test_seal_decrypt() {
     use sui::bls12381::{g1_from_bytes};
     use sui::test_scenario::{Self, next_tx, ctx};
-    use seal::key_server::{register, destroy_cap};
+    use seal::key_server::{create_v1, destroy_cap};
     use std::string;
 
     let addr1 = @0xA;
@@ -389,7 +389,7 @@ fun test_seal_decrypt() {
 
     let pk0 =
         x"aeb258b9fb9a2f29f74eb0a1a895860bb1c6ba3f9ea7075366de159e4764413e9ec0597ac9c0dad409723935440a45f40eee4728630ae3ea40a68a819375bba1d78d7810f901d8a469d785d00cfed6bd28f01d41e49c5652d924e9d19fddcf62";
-    let cap0 = register(
+    let cap0 = create_v1(
         string::utf8(b"mysten0"),
         string::utf8(b"https://mysten-labs.com"),
         0,
@@ -401,7 +401,7 @@ fun test_seal_decrypt() {
 
     let pk1 =
         x"b1076a26f4f82f39d0e767fcd2118659362afe40bce4e8d553258c86756bb74f888bca79f2d6b71edf6e25af89efa83713a223b48a19d2e551897ac92ac7458336cd489be3be025e348ca93f4c94d22594f96f0e08990e51a7de9da8ff29c98f";
-    let cap1 = register(
+    let cap1 = create_v1(
         string::utf8(b"mysten1"),
         string::utf8(b"https://mysten-labs.com"),
         0,
@@ -413,7 +413,7 @@ fun test_seal_decrypt() {
 
     let pk2 =
         x"95fcb465af3791f31d53d80db6c8dcf9f83a419b2570614ecfbb068f47613da17cb9ffc66bb052b9546f17196929538f0bd2d38e1f515d9916e2db13dc43e0ccbd4cb3d7cbb13ffecc0b68b37481ebaaaa17cad18096a9c2c27a797f17d78623";
-    let cap2 = register(
+    let cap2 = create_v1(
         string::utf8(b"mysten2"),
         string::utf8(b"https://mysten-labs.com"),
         0,
diff --git a/move/seal/sources/key_server.move b/move/seal/sources/key_server.move
index 42a1472bf..d88440cba 100644
--- a/move/seal/sources/key_server.move
+++ b/move/seal/sources/key_server.move
@@ -10,14 +10,20 @@ module seal::key_server;
 
 use std::string::String;
 use sui::{bls12381::{G2, g2_from_bytes}, group_ops::Element};
+use sui::dynamic_field as df;
 
 const EInvalidCap: u64 = 0;
 const EInvalidKeyType: u64 = 1;
-
+const EInvalidVersion: u64 = 2;
 const KeyTypeBonehFranklinBLS12381: u8 = 0;
 
 public struct KeyServer has key {
     id: UID,
+    first_version: u64,
+    last_version: u64,
+}
+
+public struct KeyServerV1 has store {
     name: String,
     url: String,
     key_type: u8,
@@ -29,7 +35,7 @@ public struct Cap has key {
     key_server_id: ID,
 }
 
-public fun register(
+public fun create_v1(
     name: String,
     url: String,
     key_type: u8,
@@ -40,13 +46,19 @@ public fun register(
     assert!(key_type == KeyTypeBonehFranklinBLS12381, EInvalidKeyType);
     let _ = g2_from_bytes(&pk);
 
-    let key_server = KeyServer {
+    let mut key_server = KeyServer {
         id: object::new(ctx),
+        first_version: 1,
+        last_version: 1,
+    };
+
+    let key_server_v1 = KeyServerV1 {
         name,
         url,
         key_type,
         pk,
     };
+    df::add(&mut key_server.id, 1, key_server_v1);
 
     let cap = Cap {
         id: object::new(ctx),
@@ -58,31 +70,40 @@ public fun register(
 }
 
 // Helper function to register a key server and transfer the cap to the caller.
-entry fun register_and_transfer(
+entry fun create_and_transfer_v1(
     name: String,
     url: String,
     key_type: u8,
     pk: vector<u8>,
     ctx: &mut TxContext,
 ) {
-    let cap = register(name, url, key_type, pk, ctx);
+    let cap = create_v1(name, url, key_type, pk, ctx);
     transfer::transfer(cap, ctx.sender());
 }
 
+public fun v1(s: &KeyServer): &KeyServerV1 {
+    assert!(df::exists_(&s.id, 1), EInvalidVersion);
+    df::borrow(&s.id, 1)
+}
+
 public fun name(s: &KeyServer): String {
-    s.name
+    let v1 = v1(s);
+    v1.name
 }
 
 public fun url(s: &KeyServer): String {
-    s.url
+    let v1 = v1(s);
+    v1.url
 }
 
-public fun key_type(s: &KeyServer): u8 {
-    s.key_type
+public fun key_type(s: &mut KeyServer): u8 {
+    let v1 = v1(s);
+    v1.key_type
 }
 
 public fun pk(s: &KeyServer): &vector<u8> {
-    &s.pk
+    let v1 = v1(s);
+    &v1.pk
 }
 
 public fun id(s: &KeyServer): &UID {
@@ -90,13 +111,16 @@ public fun id(s: &KeyServer): &UID {
 }
 
 public fun pk_as_bf_bls12381(s: &KeyServer): Element<G2> {
-    assert!(s.key_type == KeyTypeBonehFranklinBLS12381, EInvalidKeyType);
-    g2_from_bytes(&s.pk)
+    let v1: &KeyServerV1 = v1(s);
+    assert!(v1.key_type == KeyTypeBonehFranklinBLS12381, EInvalidKeyType);
+    g2_from_bytes(&v1.pk)
 }
 
 public fun update(s: &mut KeyServer, cap: &Cap, url: String) {
     assert!(object::id(s) == cap.key_server_id, EInvalidCap);
-    s.url = url;
+    assert!(df::exists_(&s.id, 1), EInvalidVersion);
+    let v1: &mut KeyServerV1 = df::borrow_mut(&mut s.id, 1);
+    v1.url = url;
 }
 
 #[test_only]
@@ -116,7 +140,7 @@ fun test_flow() {
 
     let pk = g2_generator();
     let pk_bytes = *pk.bytes();
-    let cap = register(
+    let cap = create_v1(
         string::utf8(b"mysten"),
         string::utf8(b"https::/mysten-labs.com"),
         0,
@@ -135,4 +159,4 @@ fun test_flow() {
     test_scenario::return_shared(s);
     destroy_cap(cap);
     test_scenario::end(scenario);
-}
+}
\ No newline at end of file


Reviews:
user: login: joyqvq
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-16T17:00:06Z
user: login: benr-ml
body: I think that KeyServer should be much simpler, e.g., `struct KeyServer{ id: UID, first_version: u16 }` - why do we need the rest of the fields? Also, there should be a way to create V1 directly.
(There is a design proposal along those lines in the ticket.)
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-17T17:27:56Z
user: login: benr-ml
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-20T14:09:51Z
user: login: benr-ml
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-05-21T08:41:44Z

Review comments:
diff_hunk: @@ -22,6 +23,16 @@ public struct KeyServer has key {
     url: String,
     key_type: u8,
     pk: vector<u8>,
+    first_version: u64,
+}
+
+public struct KeyServerV1 has key, store {
+    id: UID,
+    name: String,
+    url: String,
+    key_type: u8,
+    pk: vector<u8>,
+    new_field: u64,
path: move/seal/sources/key_server.move
user: login: joyqvq
body: do we want to add any new fields to V1? 
created_at: 2025-05-16T16:59:31Z
author_association: COLLABORATOR
diff_hunk: @@ -46,6 +57,7 @@ public fun register(
         url,
         key_type,
         pk,
+        first_version: 0
path: move/seal/sources/key_server.move
user: login: joyqvq
body: this assumes that df[0] does not map to anything
created_at: 2025-05-16T17:00:04Z
author_association: COLLABORATOR
diff_hunk: @@ -58,45 +68,53 @@ public fun register(
 }
 
 // Helper function to register a key server and transfer the cap to the caller.
-entry fun register_and_transfer(
+entry fun register_and_transfer_v1(
     name: String,
     url: String,
     key_type: u8,
     pk: vector<u8>,
     ctx: &mut TxContext,
 ) {
-    let cap = register(name, url, key_type, pk, ctx);
+    let cap = create_v1(name, url, key_type, pk, ctx);
     transfer::transfer(cap, ctx.sender());
 }
 
+public fun v1(s: &KeyServer): &KeyServerV1 {
+    assert!(s.first_version == 1, EInvalidVersion);
path: move/seal/sources/key_server.move
user: login: benr-ml
body: let's check that df[1] is defined instead (that is what next versions will do)
created_at: 2025-05-20T14:07:30Z
author_association: COLLABORATOR
diff_hunk: @@ -58,45 +68,53 @@ public fun register(
 }
 
 // Helper function to register a key server and transfer the cap to the caller.
-entry fun register_and_transfer(
+entry fun register_and_transfer_v1(
     name: String,
     url: String,
     key_type: u8,
     pk: vector<u8>,
     ctx: &mut TxContext,
 ) {
-    let cap = register(name, url, key_type, pk, ctx);
+    let cap = create_v1(name, url, key_type, pk, ctx);
     transfer::transfer(cap, ctx.sender());
 }
 
+public fun v1(s: &KeyServer): &KeyServerV1 {
+    assert!(s.first_version == 1, EInvalidVersion);
+    df::borrow(&s.id, 1)
+}
+
 public fun name(s: &KeyServer): String {
-    s.name
+    let v1 = v1(s);
+    v1.name
 }
 
 public fun url(s: &KeyServer): String {
-    s.url
+    let v1 = v1(s);
+    v1.url
 }
 
-public fun key_type(s: &KeyServer): u8 {
-    s.key_type
+public fun key_type(s: &mut KeyServer): u8 {
+    let v1 = v1(s);
+    v1.key_type
 }
 
 public fun pk(s: &KeyServer): &vector<u8> {
-    &s.pk
-}
-
-public fun id(s: &KeyServer): &UID {
-    &s.id
+    let v1 = v1(s);
+    &v1.pk
 }
 
 public fun pk_as_bf_bls12381(s: &KeyServer): Element<G2> {
-    assert!(s.key_type == KeyTypeBonehFranklinBLS12381, EInvalidKeyType);
-    g2_from_bytes(&s.pk)
+    let v1: &KeyServerV1 = v1(s);
+    assert!(v1.key_type == KeyTypeBonehFranklinBLS12381, EInvalidKeyType);
+    g2_from_bytes(&v1.pk)
 }
 
 public fun update(s: &mut KeyServer, cap: &Cap, url: String) {
     assert!(object::id(s) == cap.key_server_id, EInvalidCap);
-    s.url = url;
+    assert!(s.first_version == 1, EInvalidVersion);
path: move/seal/sources/key_server.move
user: login: benr-ml
body: check the df[1] instead
created_at: 2025-05-20T14:09:21Z
author_association: COLLABORATOR
diff_hunk: @@ -58,45 +68,53 @@ public fun register(
 }
 
 // Helper function to register a key server and transfer the cap to the caller.
-entry fun register_and_transfer(
+entry fun register_and_transfer_v1(
path: move/seal/sources/key_server.move
user: login: benr-ml
body: ```suggestion
entry fun create_and_transfer_v1(
```
created_at: 2025-05-20T14:09:44Z
author_association: COLLABORATOR

PR comments:
user: login: vercel[bot]
created_at: 2025-05-16T16:59:01Z
body: [vc]: #Az7WKTWh7t2veSmATbPjsi73G7cgXz1vjuQ+K2vulb0=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJzZWFsLWV4YW1wbGUiLCJsaXZlRmVlZGJhY2siOnsicmVzb2x2ZWQiOjAsInVucmVzb2x2ZWQiOjAsInRvdGFsIjowLCJsaW5rIjoic2VhbC1leGFtcGxlLWdpdC1qb3kta2V5LXNlcnZlci11cGdyYWRlLW15c3Rlbi1sYWJzLnZlcmNlbC5hcHAifSwiaW5zcGVjdG9yVXJsIjoiaHR0cHM6Ly92ZXJjZWwuY29tL215c3Rlbi1sYWJzL3NlYWwtZXhhbXBsZS9GQlZtUE05Q2dBdXpjQkhwRk55dmRHSDV1THBVIiwicHJldmlld1VybCI6InNlYWwtZXhhbXBsZS1naXQtam95LWtleS1zZXJ2ZXItdXBncmFkZS1teXN0ZW4tbGFicy52ZXJjZWwuYXBwIiwibmV4dENvbW1pdFN0YXR1cyI6IkRFUExPWUVEIiwicm9vdERpcmVjdG9yeSI6ImV4YW1wbGVzL2Zyb250ZW5kIn1dfQ==
**The latest updates on your projects**. Learn more about [Vercel for Git ↗︎](https://vercel.link/github-learn-more)

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **seal-example** | ✅ Ready ([Inspect](https://vercel.com/mysten-labs/seal-example/FBVmPM9CgAuzcBHpFNyvdGH5uLpU)) | [Visit Preview](https://seal-example-git-joy-key-server-upgrade-mysten-labs.vercel.app) | 💬 [**Add feedback**](https://vercel.live/open-feedback/seal-example-git-joy-key-server-upgrade-mysten-labs.vercel.app?via=pr-comment-feedback-link) | May 21, 2025 10:25pm |


author_association: NONE
user: login: joyqvq
created_at: 2025-05-19T20:43:22Z
body: > I think that KeyServer should be much simpler, e.g., `struct KeyServer{ id: UID, first_version: u16 }` - why do we need the rest of the fields? Also, there should be a way to create V1 directly. (There is a design proposal along those lines in the ticket.)

1 - so we do not need to create a KeyServerV0 and all the getters are simpler. do we need these getters? 
2 - related, do you have an idea what new fields we should add for KeyServerV1 (i added a placeholder in this pr as an example what a new field will look like).
2 - that was my question for the ticket - why do we need to create v1 separately without upgrading? 
author_association: COLLABORATOR