From 93c2bb2b37309d6e459c8d9db28c3634069aeaeb Mon Sep 17 00:00:00 2001 From: Joe Polny Date: Mon, 22 Apr 2024 15:48:57 -0400 Subject: [PATCH 01/27] use network-pubkey-appid for suject --- client/cli/cmd/register.go | 4 +++- client/internal/main.go | 38 +++++++++++++++++++++++---------- client/internal/utils.go | 43 ++++++++++++++++++++++++++++++++++++-- 3 files changed, 71 insertions(+), 14 deletions(-) diff --git a/client/cli/cmd/register.go b/client/cli/cmd/register.go index eb4dae9..dafcc0e 100644 --- a/client/cli/cmd/register.go +++ b/client/cli/cmd/register.go @@ -74,8 +74,10 @@ func runRegisterCmd(_ *cobra.Command, args []string) error { return fmt.Errorf("there's already a DID with reference name: %s", name) } + // TODO: pass this in from cli + network := "testnet" // Generate base identifier instance - subject := fmt.Sprintf("%x-%d", account.PublicKey, appID) + subject := fmt.Sprintf("%s-%x-%d", network, account.PublicKey, appID) method := "algo" log.WithFields(xlog.Fields{ "subject": subject, diff --git a/client/internal/main.go b/client/internal/main.go index 638d307..474c973 100644 --- a/client/internal/main.go +++ b/client/internal/main.go @@ -114,14 +114,14 @@ func (c *AlgoClient) PublishDID(id *did.Identifier, sender *crypto.Account) erro contract := loadContract() signer := transaction.BasicAccountTransactionSigner{Account: *sender} doc, _ := json.Marshal(id.Document(true)) - pub, appID, err := parseSubjectString(id.Subject()) + pub, network, appID, err := parseSubjectString(id.Subject()) if err != nil { return err } if c.np.StoreProvider != "" { return c.submitToProvider(pub, appID, http.MethodPost, doc) } - return publishDID(c.algod, appID, contract, sender.Address, signer, doc, pub) + return publishDID(c.algod, appID, contract, sender.Address, signer, doc, pub, network) } // DeleteDID removes a DID document from the network. @@ -131,14 +131,14 @@ func (c *AlgoClient) DeleteDID(id *did.Identifier, sender *crypto.Account) error }).Info("deleting DID document") contract := loadContract() signer := transaction.BasicAccountTransactionSigner{Account: *sender} - pub, appID, err := parseSubjectString(id.Subject()) + pub, network, appID, err := parseSubjectString(id.Subject()) if err != nil { return err } if c.np.StoreProvider != "" { return c.submitToProvider(pub, appID, http.MethodDelete, nil) } - return deleteDID(appID, pub, sender.Address, c.algod, contract, signer) + return deleteDID(appID, pub, sender.Address, c.algod, contract, signer, network) } // Resolve retrieves a DID document from the network. @@ -152,13 +152,13 @@ func (c *AlgoClient) Resolve(id string) (*did.Document, error) { } // Extract the public key and application ID from the subject - pub, appID, err := parseSubjectString(subject.Subject()) + pub, network, appID, err := parseSubjectString(subject.Subject()) if err != nil { return nil, err } // Retrieve the data from the network - data, err := resolveDID(appID, pub, c.algod) + data, err := resolveDID(appID, pub, c.algod, network) if err != nil { return nil, err } @@ -202,18 +202,34 @@ func addressFromPub(pub []byte) (string, error) { return types.EncodeAddress(pub) } -func parseSubjectString(subject string) (pub []byte, appID uint64, err error) { +// pubkey-network-appID +func parseSubjectString(subject string) (pub []byte, network string, appID uint64, err error) { idSegments := strings.Split(subject, "-") - if len(idSegments) != 2 { - err = fmt.Errorf("invalid subject identifier") + if len(idSegments) != 3 { + err = fmt.Errorf("invalid subject identifier. Expected 3 segments, got %d", len(idSegments)) return } - pub, err = hex.DecodeString(idSegments[0]) + + network = idSegments[0] + matchFound := false + for _, a := range []string{"mainnet", "testnet"} { + if a == network { + matchFound = true + break + } + } + if !matchFound { + err = fmt.Errorf("invalid network in subject identifier: %s", network) + return + } + + pub, err = hex.DecodeString(idSegments[1]) if err != nil { err = fmt.Errorf("invalid public key in subject identifier") return } - app, err := strconv.Atoi(idSegments[1]) + + app, err := strconv.Atoi(idSegments[2]) if err != nil { err = fmt.Errorf("invalid storage app ID in subject identifier") return diff --git a/client/internal/utils.go b/client/internal/utils.go index 558eb4e..5170aba 100644 --- a/client/internal/utils.go +++ b/client/internal/utils.go @@ -22,6 +22,12 @@ const ( bytes_per_call = 2048 - 4 - 34 - 8 - 8 ) +const ( + mainnetGenesisHash = "wGHE2Pwdvd7S12BL5FaOP20EGYesN73ktiC1qzkkit8=" + testnetGenesisHash = "SGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/cOUJOiI=" + betanetGenesisHash = "mFgazF+2uRS1tMiL9dsj01hJGySEmPN28B/TjjvpVW0=" +) + // CreateApp is used to deploy the AlgoDID storage smart contract to the // Algorand network. func createApp( @@ -115,7 +121,7 @@ func createApp( return confirmedTxn.ApplicationIndex, nil } -// PublishDID is used to upload a new DID document to the AlgoDID +// publishDID is used to upload a new DID document to the AlgoDID // storage smart contract. func publishDID( algodClient *algod.Client, @@ -125,7 +131,13 @@ func publishDID( signer transaction.TransactionSigner, data []byte, pubKey []byte, + network string, ) error { + err := checkNetwork(network, algodClient) + if err != nil { + return err + } + ceilBoxes := int(math.Ceil(float64(len(data)) / float64(max_box_size))) endBoxSize := len(data) % max_box_size @@ -281,9 +293,30 @@ func publishDID( return err } +func checkNetwork(network string, algodClient *algod.Client) error { + sp, err := algodClient.SuggestedParams().Do(context.Background()) + + if err != nil { + return fmt.Errorf("failed to get suggested params: %w", err) + } + + if (network == "mainnet" && base64.StdEncoding.EncodeToString(sp.GenesisHash) != mainnetGenesisHash) || + (network == "testnet" && base64.StdEncoding.EncodeToString(sp.GenesisHash) != testnetGenesisHash) || + (network == "betanet" && base64.StdEncoding.EncodeToString(sp.GenesisHash) != betanetGenesisHash) { + return fmt.Errorf("The given network does not match the network of the Algorand node") + } + + return nil +} + // ResolveDID is used to read the DID document from the AlgoDID storage smart // contract. -func resolveDID(appID uint64, pubKey []byte, algodClient *algod.Client) ([]byte, error) { +func resolveDID(appID uint64, pubKey []byte, algodClient *algod.Client, network string) ([]byte, error) { + err := checkNetwork(network, algodClient) + if err != nil { + return nil, err + } + metadata, err := getMetadata(appID, pubKey, algodClient) if err != nil { return nil, err @@ -319,7 +352,13 @@ func deleteDID( algodClient *algod.Client, contract *abi.Contract, signer transaction.TransactionSigner, + network string, ) error { + err := checkNetwork(network, algodClient) + if err != nil { + return err + } + startAtc := transaction.AtomicTransactionComposer{} method, err := contract.GetMethodByName("startDelete") From 49408fc12bedc498a296684758b13ea5ad251c8b Mon Sep 17 00:00:00 2001 From: Joe Polny Date: Wed, 24 Apr 2024 13:02:58 -0400 Subject: [PATCH 02/27] feat: suport multiple networks at once --- client/cli/cmd/config.go | 4 +- client/cli/cmd/config_app_id.go | 9 +++- client/cli/cmd/config_switch.go | 36 ------------- client/cli/cmd/deploy.go | 10 ++-- client/cli/cmd/register.go | 9 ++-- client/cli/cmd/utils.go | 16 ++---- client/cli/cmd/wallet_info.go | 8 +-- client/cli/cmd/wallet_pay.go | 7 +-- client/internal/driver.go | 2 +- client/internal/main.go | 96 ++++++++++++++++++++------------- client/ui/api.go | 2 +- client/ui/provider.go | 19 +++++-- 12 files changed, 107 insertions(+), 111 deletions(-) delete mode 100644 client/cli/cmd/config_switch.go diff --git a/client/cli/cmd/config.go b/client/cli/cmd/config.go index 1ebd076..8ab49f9 100644 --- a/client/cli/cmd/config.go +++ b/client/cli/cmd/config.go @@ -33,9 +33,9 @@ func (ac *appConf) isProfileAvailable(name string) bool { return false } -func (ac *appConf) setAppID(appID uint) { +func (ac *appConf) setAppID(network string, appID uint) { for _, p := range ac.Network.Profiles { - if p.Name == ac.Network.Active { + if p.Name == network { p.AppID = appID } } diff --git a/client/cli/cmd/config_app_id.go b/client/cli/cmd/config_app_id.go index 958a9b4..10b1fc6 100644 --- a/client/cli/cmd/config_app_id.go +++ b/client/cli/cmd/config_app_id.go @@ -10,12 +10,17 @@ import ( var configAppIDCmd = &cobra.Command{ Use: "app-id", - Example: "algoid config app-id [app-id]", + Example: "algoid config app-id [app-id] [network]", Short: "Adjust the `app-id` setting for the active network profile", RunE: func(cmd *cobra.Command, args []string) error { if len(args) == 0 { return fmt.Errorf("appID name is required") } + + if len(args) == 1 { + return fmt.Errorf("network name is required") + } + conf := new(appConf) if err := viper.Unmarshal(&conf); err != nil { return err @@ -24,7 +29,7 @@ var configAppIDCmd = &cobra.Command{ if err != nil { return fmt.Errorf("invalid app-id: '%s'", args[0]) } - conf.setAppID(uint(appID)) + conf.setAppID(args[1], uint(appID)) err = conf.save() if err == nil { log.Info("configuration updated") diff --git a/client/cli/cmd/config_switch.go b/client/cli/cmd/config_switch.go deleted file mode 100644 index 5af948c..0000000 --- a/client/cli/cmd/config_switch.go +++ /dev/null @@ -1,36 +0,0 @@ -package cmd - -import ( - "fmt" - - "github.com/spf13/cobra" - "github.com/spf13/viper" -) - -var configSwithProfileCmd = &cobra.Command{ - Use: "switch", - Example: "algoid config switch [profile]", - Short: "Modify the active network profile", - RunE: func(cmd *cobra.Command, args []string) error { - if len(args) == 0 { - return fmt.Errorf("profile name is required") - } - conf := new(appConf) - if err := viper.Unmarshal(&conf); err != nil { - return err - } - if !conf.isProfileAvailable(args[0]) { - return fmt.Errorf("profile '%s' not found", args[0]) - } - conf.Network.Active = args[0] - err := conf.save() - if err == nil { - log.Info("configuration updated") - } - return err - }, -} - -func init() { - configCmd.AddCommand(configSwithProfileCmd) -} diff --git a/client/cli/cmd/deploy.go b/client/cli/cmd/deploy.go index 35d44fa..5bc9707 100644 --- a/client/cli/cmd/deploy.go +++ b/client/cli/cmd/deploy.go @@ -2,6 +2,7 @@ package cmd import ( "errors" + "fmt" ac "github.com/algorand/go-algorand-sdk/v2/crypto" "github.com/algorand/go-algorand-sdk/v2/mnemonic" @@ -13,7 +14,7 @@ var deployContractCmd = &cobra.Command{ Use: "deploy", Aliases: []string{"deploy-contract"}, Short: "Deploy the AlgoDID storage smart contract", - Example: "algoid deploy [wallet-name]", + Example: "algoid deploy [wallet-name] [network]", RunE: runDeployContractCmd, } @@ -23,7 +24,7 @@ func init() { func runDeployContractCmd(_ *cobra.Command, args []string) error { // Get parameters - if len(args) != 1 { + if len(args) != 2 { return errors.New("missing required parameters") } name := sanitize.Name(args[0]) @@ -60,11 +61,12 @@ func runDeployContractCmd(_ *cobra.Command, args []string) error { return err } + network := args[1] // Deploy contract - appID, err := cl.DeployContract(&account) + appID, err := cl.Networks[network].DeployContract(&account) if err != nil { return err } - log.WithField("app_id", appID).Info("storage contract deployed successfully") + log.WithField("app_id", appID).Info(fmt.Sprintf("storage contract deployed successfully to %s", network)) return nil } diff --git a/client/cli/cmd/register.go b/client/cli/cmd/register.go index dafcc0e..53b20d9 100644 --- a/client/cli/cmd/register.go +++ b/client/cli/cmd/register.go @@ -17,7 +17,7 @@ import ( var registerCmd = &cobra.Command{ Use: "register", Short: "Creates a new DID locally", - Example: "algoid register [wallet-name]", + Example: "algoid register [wallet-name] [network]", Aliases: []string{"create", "new"}, RunE: runRegisterCmd, } @@ -31,7 +31,7 @@ func init() { } func runRegisterCmd(_ *cobra.Command, args []string) error { - if len(args) != 1 { + if len(args) != 2 { return errors.New("missing required parameters") } name := sanitize.Name(args[0]) @@ -62,8 +62,9 @@ func runRegisterCmd(_ *cobra.Command, args []string) error { return err } + network := args[1] // Get storage application identifier - appID, err := getStorageAppID() + appID, err := getStorageAppID(network) if err != nil { return err } @@ -74,8 +75,6 @@ func runRegisterCmd(_ *cobra.Command, args []string) error { return fmt.Errorf("there's already a DID with reference name: %s", name) } - // TODO: pass this in from cli - network := "testnet" // Generate base identifier instance subject := fmt.Sprintf("%s-%x-%d", network, account.PublicKey, appID) method := "algo" diff --git a/client/cli/cmd/utils.go b/client/cli/cmd/utils.go index dd31d5c..3be8e69 100644 --- a/client/cli/cmd/utils.go +++ b/client/cli/cmd/utils.go @@ -17,14 +17,14 @@ func getClientStore() (*store.LocalStore, error) { } // Retrieve the application identifier for the active network profile. -func getStorageAppID() (uint, error) { +func getStorageAppID(network string) (uint, error) { conf := new(internal.ClientSettings) if err := viper.UnmarshalKey("network", &conf); err != nil { return 0, err } var profile *internal.NetworkProfile for _, p := range conf.Profiles { - if p.Name == conf.Active { + if p.Name == network { profile = p break } @@ -36,17 +36,11 @@ func getStorageAppID() (uint, error) { } // Get network client instance. -func getAlgoClient() (*internal.AlgoClient, error) { +func getAlgoClient() (*internal.AlgoDIDClient, error) { conf := new(internal.ClientSettings) if err := viper.UnmarshalKey("network", &conf); err != nil { return nil, err } - var profile *internal.NetworkProfile - for _, p := range conf.Profiles { - if p.Name == conf.Active { - profile = p - break - } - } - return internal.NewAlgoClient(profile, log) + + return internal.NewAlgoClient(conf.Profiles, log) } diff --git a/client/cli/cmd/wallet_info.go b/client/cli/cmd/wallet_info.go index f4949b4..c60300c 100644 --- a/client/cli/cmd/wallet_info.go +++ b/client/cli/cmd/wallet_info.go @@ -13,11 +13,11 @@ import ( var walletInfoCmd = &cobra.Command{ Use: "info", Short: "Get account information", - Example: "algoid wallet info [wallet-name]", + Example: "algoid wallet info [wallet-name] [network]", Aliases: []string{"details", "inspect", "more"}, RunE: func(cmd *cobra.Command, args []string) error { // Get parameters - if len(args) != 1 { + if len(args) != 2 { return errors.New("missing required parameters") } name := sanitize.Name(args[0]) @@ -54,13 +54,15 @@ var walletInfoCmd = &cobra.Command{ return err } + network := args[1] // Get account info - info, err := cl.AccountInformation(account.Address.String()) + info, err := cl.Networks[network].AccountInformation(account.Address.String()) if err != nil { return err } // Print results + fmt.Printf("network: %s\n", network) fmt.Printf("address: %s\n", account.Address.String()) fmt.Printf("public key: %x\n", account.PublicKey) fmt.Printf("status: %s\n", info.Status) diff --git a/client/cli/cmd/wallet_pay.go b/client/cli/cmd/wallet_pay.go index 2a6317c..247bdbc 100644 --- a/client/cli/cmd/wallet_pay.go +++ b/client/cli/cmd/wallet_pay.go @@ -17,7 +17,7 @@ var walletPayCmd = &cobra.Command{ Use: "pay", Short: "Create and submit a new transaction", Aliases: []string{"txn", "send"}, - Example: "algoid wallet pay [wallet-name]", + Example: "algoid wallet pay [wallet-name] [network]", RunE: runWalletPayCmd, } @@ -90,8 +90,9 @@ func runWalletPayCmd(_ *cobra.Command, args []string) (err error) { return err } + network := args[1] // Get transaction parameters - params, _ := cl.SuggestedParams() + params, _ := cl.Networks[network].SuggestedParams() // Get sender address sender := account.Address.String() @@ -115,7 +116,7 @@ func runWalletPayCmd(_ *cobra.Command, args []string) (err error) { // Submit transaction log.Debug("submitting signed transaction") - txID, err := cl.SubmitTx(stx) + txID, err := cl.Networks[network].SubmitTx(stx) if err != nil { return err } diff --git a/client/internal/driver.go b/client/internal/driver.go index edd56b7..6b1ffbf 100644 --- a/client/internal/driver.go +++ b/client/internal/driver.go @@ -9,7 +9,7 @@ import ( // Read a DID document from the Algorand network. The method complies // with the `resolver.Provider` interface. -func (c *AlgoClient) Read(id string) (*did.Document, *did.DocumentMetadata, error) { +func (c *AlgoDIDClient) Read(id string) (*did.Document, *did.DocumentMetadata, error) { if _, err := did.Parse(id); err != nil { return nil, nil, errors.New(resolver.ErrInvalidDID) } diff --git a/client/internal/main.go b/client/internal/main.go index 474c973..e842198 100644 --- a/client/internal/main.go +++ b/client/internal/main.go @@ -23,7 +23,6 @@ import ( // ClientSettings defines the configuration options available when // interacting with an AlgoDID network agent. type ClientSettings struct { - Active string `json:"active" yaml:"active" mapstructure:"active"` Profiles []*NetworkProfile `json:"profiles" yaml:"profiles" mapstructure:"profiles"` } @@ -46,60 +45,73 @@ type NetworkProfile struct { StoreProvider string `json:"store_provider,omitempty" yaml:"store_provider,omitempty" mapstructure:"store_provider"` } -// AlgoClient provides a simplified interface to interact with the +type NetworkClient struct { + profile *NetworkProfile + algod *algod.Client +} + +// AlgoDIDClient provides a simplified interface to interact with the // Algorand network. -type AlgoClient struct { - np *NetworkProfile - log xlog.Logger - httpC *http.Client - algod *algod.Client +type AlgoDIDClient struct { + log xlog.Logger + httpC *http.Client + Networks map[string]*NetworkClient } // NewAlgoClient creates a new instance of the AlgoClient. -func NewAlgoClient(profile *NetworkProfile, log xlog.Logger) (*AlgoClient, error) { - if profile == nil { +func NewAlgoClient(profiles []*NetworkProfile, log xlog.Logger) (*AlgoDIDClient, error) { + if len(profiles) == 0 { return nil, fmt.Errorf("no network profile provided") } - client, err := algod.MakeClient(profile.Node, profile.NodeToken) - if err != nil { - return nil, err + + client := &AlgoDIDClient{ + log: log, + httpC: &http.Client{}, + Networks: make(map[string]*NetworkClient), + } + + for _, p := range profiles { + algod, err := algod.MakeClient(p.Node, p.NodeToken) + if err != nil { + return nil, err + } + client.Networks[p.Name] = &NetworkClient{ + profile: p, + algod: algod, + } } - return &AlgoClient{ - np: profile, - log: log, - algod: client, - httpC: &http.Client{}, - }, nil + + return client, nil } // StorageAppID returns the application ID for the AlgoDID storage. -func (c *AlgoClient) StorageAppID() uint { - return c.np.AppID +func (c *NetworkClient) StorageAppID() uint { + return c.profile.AppID } // SuggestedParams returns the suggested transaction parameters. -func (c *AlgoClient) SuggestedParams() (types.SuggestedParams, error) { +func (c *NetworkClient) SuggestedParams() (types.SuggestedParams, error) { return c.algod.SuggestedParams().Do(context.TODO()) } // SubmitTx sends a raw signed transaction to the network. -func (c *AlgoClient) SubmitTx(stx []byte) (string, error) { +func (c *NetworkClient) SubmitTx(stx []byte) (string, error) { return c.algod.SendRawTransaction(stx).Do(context.TODO()) } // AccountInformation returns the account information for the given address. -func (c *AlgoClient) AccountInformation(address string) (models.Account, error) { +func (c *NetworkClient) AccountInformation(address string) (models.Account, error) { return c.algod.AccountInformation(address).Do(context.TODO()) } // Ready returns true if the network is available. -func (c *AlgoClient) Ready() bool { +func (c *NetworkClient) Ready() bool { return c.algod.HealthCheck().Do(context.TODO()) == nil } // DeployContract creates a new instance of the AlgoDID storage smart contract // on the network. -func (c *AlgoClient) DeployContract(sender *crypto.Account) (uint64, error) { +func (c *NetworkClient) DeployContract(sender *crypto.Account) (uint64, error) { contract := loadContract() signer := transaction.BasicAccountTransactionSigner{Account: *sender} return createApp(c.algod, contract, signer.Account.Address, signer) @@ -107,7 +119,7 @@ func (c *AlgoClient) DeployContract(sender *crypto.Account) (uint64, error) { // PublishDID sends a new DID document to the network // fot on-chain storage. -func (c *AlgoClient) PublishDID(id *did.Identifier, sender *crypto.Account) error { +func (c *AlgoDIDClient) PublishDID(id *did.Identifier, sender *crypto.Account) error { c.log.WithFields(map[string]interface{}{ "did": id.String(), }).Info("publishing DID document") @@ -118,14 +130,16 @@ func (c *AlgoClient) PublishDID(id *did.Identifier, sender *crypto.Account) erro if err != nil { return err } - if c.np.StoreProvider != "" { - return c.submitToProvider(pub, appID, http.MethodPost, doc) + + networkClient := c.Networks[network] + if networkClient.profile.StoreProvider != "" { + return c.submitToProvider(network, pub, appID, http.MethodPost, doc) } - return publishDID(c.algod, appID, contract, sender.Address, signer, doc, pub, network) + return publishDID(networkClient.algod, appID, contract, sender.Address, signer, doc, pub, network) } // DeleteDID removes a DID document from the network. -func (c *AlgoClient) DeleteDID(id *did.Identifier, sender *crypto.Account) error { +func (c *AlgoDIDClient) DeleteDID(id *did.Identifier, sender *crypto.Account) error { c.log.WithFields(map[string]interface{}{ "did": id.String(), }).Info("deleting DID document") @@ -135,14 +149,16 @@ func (c *AlgoClient) DeleteDID(id *did.Identifier, sender *crypto.Account) error if err != nil { return err } - if c.np.StoreProvider != "" { - return c.submitToProvider(pub, appID, http.MethodDelete, nil) + + networkClient := c.Networks[network] + if networkClient.profile.StoreProvider != "" { + return c.submitToProvider(network, pub, appID, http.MethodDelete, nil) } - return deleteDID(appID, pub, sender.Address, c.algod, contract, signer, network) + return deleteDID(appID, pub, sender.Address, networkClient.algod, contract, signer, network) } // Resolve retrieves a DID document from the network. -func (c *AlgoClient) Resolve(id string) (*did.Document, error) { +func (c *AlgoDIDClient) Resolve(id string) (*did.Document, error) { c.log.WithField("did", id).Info("retrieving DID document") // Parse the DID @@ -157,8 +173,10 @@ func (c *AlgoClient) Resolve(id string) (*did.Document, error) { return nil, err } + networkClient := c.Networks[network] + // Retrieve the data from the network - data, err := resolveDID(appID, pub, c.algod, network) + data, err := resolveDID(appID, pub, networkClient.algod, network) if err != nil { return nil, err } @@ -169,13 +187,15 @@ func (c *AlgoClient) Resolve(id string) (*did.Document, error) { return doc, nil } -func (c *AlgoClient) submitToProvider(pub []byte, appID uint64, method string, doc []byte) error { - c.log.Warning("using provider: ", c.np.StoreProvider) +func (c *AlgoDIDClient) submitToProvider(network string, pub []byte, appID uint64, method string, doc []byte) error { + networkClient := c.Networks[network] + + c.log.Warning("using provider: ", networkClient.profile.StoreProvider) addr, err := addressFromPub(pub) if err != nil { return err } - endpoint := fmt.Sprintf("%s/v1/%s/%d", c.np.StoreProvider, addr, appID) + endpoint := fmt.Sprintf("%s/v1/%s/%d", networkClient.profile.StoreProvider, addr, appID) var payload io.Reader if doc != nil { payload = bytes.NewReader(doc) diff --git a/client/ui/api.go b/client/ui/api.go index 5f71e40..b0c3d8e 100644 --- a/client/ui/api.go +++ b/client/ui/api.go @@ -22,7 +22,7 @@ type LocalAPI struct { } // LocalAPIServer creates a new instance of the local API server. -func LocalAPIServer(st *store.LocalStore, client *internal.AlgoClient, log xlog.Logger) (*LocalAPI, error) { +func LocalAPIServer(st *store.LocalStore, client *internal.AlgoDIDClient, log xlog.Logger) (*LocalAPI, error) { // provider instances p := &Provider{ st: st, diff --git a/client/ui/provider.go b/client/ui/provider.go index f2db5f2..267892d 100644 --- a/client/ui/provider.go +++ b/client/ui/provider.go @@ -20,7 +20,7 @@ import ( type Provider struct { st *store.LocalStore log xlog.Logger - client *internal.AlgoClient + client *internal.AlgoDIDClient } // LocalEntry represents a DID instance stored in the local @@ -47,7 +47,11 @@ type LocalEntry struct { // Ready returns true if the network is available. func (p *Provider) Ready() bool { - return p.client.Ready() + ready := true + for _, n := range p.client.Networks { + ready = ready && n.Ready() + } + return ready } // List all DID instances in the local store. @@ -69,7 +73,7 @@ func (p *Provider) List() []*LocalEntry { } // Register a new DID instance in the local store. -func (p *Provider) Register(name string, passphrase string) error { +func (p *Provider) Register(network string, name string, passphrase string) error { // Check for duplicates dup, _ := p.st.Get(name) if dup != nil { @@ -87,7 +91,7 @@ func (p *Provider) Register(name string, passphrase string) error { } // Generate base identifier instance - subject := fmt.Sprintf("%x-%d", account.PublicKey, p.client.StorageAppID()) + subject := fmt.Sprintf("%s-%x-%d", network, account.PublicKey, p.client.Networks[network].StorageAppID()) method := "algo" p.log.WithFields(xlog.Fields{ "subject": subject, @@ -255,7 +259,12 @@ func (p *Provider) registerHandlerFunc(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusBadRequest) return } - if err = p.Register(name, passphrase); err != nil { + network, ok := params["network"] + if !ok { + w.WriteHeader(http.StatusBadRequest) + return + } + if err = p.Register(network, name, passphrase); err != nil { w.WriteHeader(http.StatusInternalServerError) return } From 1295453089a7ada3666d088bffe5ed0b476e1eb9 Mon Sep 17 00:00:00 2001 From: Joe Polny Date: Wed, 24 Apr 2024 13:16:10 -0400 Subject: [PATCH 03/27] fix linting --- client/cli/cmd/config_app_id.go | 2 +- client/cli/cmd/config_view.go | 2 +- client/cli/cmd/delete.go | 2 +- client/cli/cmd/edit_activate.go | 2 +- client/cli/cmd/edit_deactivate.go | 2 +- client/cli/cmd/resolver.go | 4 ++-- client/cli/cmd/version.go | 2 +- client/cli/cmd/wallet_create.go | 2 +- client/cli/cmd/wallet_delete.go | 2 +- client/cli/cmd/wallet_disconnect.go | 2 +- client/cli/cmd/wallet_export.go | 2 +- client/cli/cmd/wallet_info.go | 2 +- client/cli/cmd/wallet_list.go | 2 +- client/cli/cmd/wallet_rename.go | 2 +- client/cli/cmd/wallet_restore.go | 2 +- client/internal/main.go | 13 +++++++------ client/store/local.go | 4 ++-- 17 files changed, 25 insertions(+), 24 deletions(-) diff --git a/client/cli/cmd/config_app_id.go b/client/cli/cmd/config_app_id.go index 10b1fc6..d2f92b2 100644 --- a/client/cli/cmd/config_app_id.go +++ b/client/cli/cmd/config_app_id.go @@ -12,7 +12,7 @@ var configAppIDCmd = &cobra.Command{ Use: "app-id", Example: "algoid config app-id [app-id] [network]", Short: "Adjust the `app-id` setting for the active network profile", - RunE: func(cmd *cobra.Command, args []string) error { + RunE: func(_ *cobra.Command, args []string) error { if len(args) == 0 { return fmt.Errorf("appID name is required") } diff --git a/client/cli/cmd/config_view.go b/client/cli/cmd/config_view.go index 5b514cc..fcb8a40 100644 --- a/client/cli/cmd/config_view.go +++ b/client/cli/cmd/config_view.go @@ -12,7 +12,7 @@ var configViewCmd = &cobra.Command{ Use: "view", Example: "algoid config view", Short: "View current configuration settings", - RunE: func(cmd *cobra.Command, args []string) error { + RunE: func(_ *cobra.Command, _ []string) error { conf := new(appConf) if err := viper.Unmarshal(&conf); err != nil { return err diff --git a/client/cli/cmd/delete.go b/client/cli/cmd/delete.go index dc2bb93..8d97b71 100644 --- a/client/cli/cmd/delete.go +++ b/client/cli/cmd/delete.go @@ -13,7 +13,7 @@ var deleteCmd = &cobra.Command{ Short: "Permanently delete a local identifier", Example: "algoid delete [DID reference name]", Aliases: []string{"del", "rm", "remove"}, - RunE: func(cmd *cobra.Command, args []string) error { + RunE: func(_ *cobra.Command, args []string) error { if len(args) != 1 { return errors.New("you must specify a DID reference name") } diff --git a/client/cli/cmd/edit_activate.go b/client/cli/cmd/edit_activate.go index 0ddf7a2..234cafb 100644 --- a/client/cli/cmd/edit_activate.go +++ b/client/cli/cmd/edit_activate.go @@ -13,7 +13,7 @@ var activateCmd = &cobra.Command{ Use: "activate", Short: "Mark a DID as active", Example: "algoid edit activate [DID reference name]", - RunE: func(cmd *cobra.Command, args []string) error { + RunE: func(_ *cobra.Command, args []string) error { if len(args) != 1 { return errors.New("you must specify a DID reference name") } diff --git a/client/cli/cmd/edit_deactivate.go b/client/cli/cmd/edit_deactivate.go index 44063cc..8db24f2 100644 --- a/client/cli/cmd/edit_deactivate.go +++ b/client/cli/cmd/edit_deactivate.go @@ -13,7 +13,7 @@ var deactivateCmd = &cobra.Command{ Use: "deactivate", Short: "Mark a DID as inactive", Example: "algoid edit activate [DID reference name]", - RunE: func(cmd *cobra.Command, args []string) error { + RunE: func(_ *cobra.Command, args []string) error { if len(args) != 1 { return errors.New("you must specify a DID reference name") } diff --git a/client/cli/cmd/resolver.go b/client/cli/cmd/resolver.go index 77f8cf3..87b4a77 100644 --- a/client/cli/cmd/resolver.go +++ b/client/cli/cmd/resolver.go @@ -56,10 +56,10 @@ func runResolverCmd(_ *cobra.Command, _ []string) error { } mux := http.NewServeMux() - mux.HandleFunc("/1.0/ping", func(w http.ResponseWriter, r *http.Request) { + mux.HandleFunc("/1.0/ping", func(w http.ResponseWriter, _ *http.Request) { _, _ = w.Write([]byte("pong")) }) - mux.HandleFunc("/1.0/ready", func(w http.ResponseWriter, r *http.Request) { + mux.HandleFunc("/1.0/ready", func(w http.ResponseWriter, _ *http.Request) { _, _ = w.Write([]byte("ok")) }) mux.HandleFunc("/1.0/identifiers/", rslv.ResolutionHandler) diff --git a/client/cli/cmd/version.go b/client/cli/cmd/version.go index 4e2c416..2acf500 100644 --- a/client/cli/cmd/version.go +++ b/client/cli/cmd/version.go @@ -12,7 +12,7 @@ import ( var versionCmd = &cobra.Command{ Use: "version", Short: "Display version information", - RunE: func(cmd *cobra.Command, args []string) error { + RunE: func(_ *cobra.Command, _ []string) error { var components = map[string]string{ "Version": info.CoreVersion, "Build code": info.BuildCode, diff --git a/client/cli/cmd/wallet_create.go b/client/cli/cmd/wallet_create.go index 3fd42b9..a0a6020 100644 --- a/client/cli/cmd/wallet_create.go +++ b/client/cli/cmd/wallet_create.go @@ -16,7 +16,7 @@ var walletCreateCmd = &cobra.Command{ Aliases: []string{"new"}, Short: "Create a new (standalone) ALGO wallet", Example: "algoid wallet new [wallet-name]", - RunE: func(cmd *cobra.Command, args []string) error { + RunE: func(_ *cobra.Command, args []string) error { // Get parameters if len(args) != 1 { return errors.New("you must provide a name for your wallet") diff --git a/client/cli/cmd/wallet_delete.go b/client/cli/cmd/wallet_delete.go index 9514eb3..3434134 100644 --- a/client/cli/cmd/wallet_delete.go +++ b/client/cli/cmd/wallet_delete.go @@ -23,7 +23,7 @@ var walletDeleteCmd = &cobra.Command{ Example: "algoid wallet delete [wallet-name]", Short: "Permanently delete an ALGO wallet", Long: walletDeleteCmdDesc, - RunE: func(cmd *cobra.Command, args []string) error { + RunE: func(_ *cobra.Command, args []string) error { if len(args) != 1 { return errors.New("you must specify the wallet name") } diff --git a/client/cli/cmd/wallet_disconnect.go b/client/cli/cmd/wallet_disconnect.go index c7ee747..bde901f 100644 --- a/client/cli/cmd/wallet_disconnect.go +++ b/client/cli/cmd/wallet_disconnect.go @@ -14,7 +14,7 @@ var walletDisconnectCmd = &cobra.Command{ Aliases: []string{"unlink"}, Short: "Remove a linked ALGO address from your DID", Example: "algoid wallet disconnect [did-name] [algo-address]", - RunE: func(cmd *cobra.Command, args []string) error { + RunE: func(_ *cobra.Command, args []string) error { if len(args) != 2 { return errors.New("missing required parameters") } diff --git a/client/cli/cmd/wallet_export.go b/client/cli/cmd/wallet_export.go index 97bb70e..22e573f 100644 --- a/client/cli/cmd/wallet_export.go +++ b/client/cli/cmd/wallet_export.go @@ -27,7 +27,7 @@ with the name "[wallet-name]-mnemonic.txt". Keep in mind that misplacing or sharing the mnemonic can result in catastrophic security issues and permanent loss of funds.`, - RunE: func(cmd *cobra.Command, args []string) error { + RunE: func(_ *cobra.Command, args []string) error { // Get parameters if len(args) != 1 { return errors.New("missing required parameters") diff --git a/client/cli/cmd/wallet_info.go b/client/cli/cmd/wallet_info.go index c60300c..9dd8669 100644 --- a/client/cli/cmd/wallet_info.go +++ b/client/cli/cmd/wallet_info.go @@ -15,7 +15,7 @@ var walletInfoCmd = &cobra.Command{ Short: "Get account information", Example: "algoid wallet info [wallet-name] [network]", Aliases: []string{"details", "inspect", "more"}, - RunE: func(cmd *cobra.Command, args []string) error { + RunE: func(_ *cobra.Command, args []string) error { // Get parameters if len(args) != 2 { return errors.New("missing required parameters") diff --git a/client/cli/cmd/wallet_list.go b/client/cli/cmd/wallet_list.go index 5b2eb87..7020538 100644 --- a/client/cli/cmd/wallet_list.go +++ b/client/cli/cmd/wallet_list.go @@ -8,7 +8,7 @@ var walletListCmd = &cobra.Command{ Use: "list", Aliases: []string{"ls"}, Short: "List your existing ALGO wallet(s)", - RunE: func(cmd *cobra.Command, args []string) error { + RunE: func(_ *cobra.Command, _ []string) error { store, err := getClientStore() if err != nil { return err diff --git a/client/cli/cmd/wallet_rename.go b/client/cli/cmd/wallet_rename.go index ff895cf..ca561fe 100644 --- a/client/cli/cmd/wallet_rename.go +++ b/client/cli/cmd/wallet_rename.go @@ -13,7 +13,7 @@ var walletRenameCmd = &cobra.Command{ Short: "Rename an existing ALGO wallet", Example: "algoid wallet rename [current-name] [new-name]", Aliases: []string{"mv"}, - RunE: func(cmd *cobra.Command, args []string) error { + RunE: func(_ *cobra.Command, args []string) error { // Get parameters if len(args) != 2 { return errors.New("missing required parameters") diff --git a/client/cli/cmd/wallet_restore.go b/client/cli/cmd/wallet_restore.go index 4d975b0..51b8ab8 100644 --- a/client/cli/cmd/wallet_restore.go +++ b/client/cli/cmd/wallet_restore.go @@ -18,7 +18,7 @@ var walletRestoreCmd = &cobra.Command{ Short: "Restore a wallet using an existing mnemonic file", Aliases: []string{"recover"}, Example: "algoid wallet restore [mnemonic-file]", - RunE: func(cmd *cobra.Command, args []string) error { + RunE: func(_ *cobra.Command, args []string) error { // Get parameters if len(args) != 1 { return errors.New("missing required parameters") diff --git a/client/internal/main.go b/client/internal/main.go index e842198..27b7d4e 100644 --- a/client/internal/main.go +++ b/client/internal/main.go @@ -45,6 +45,7 @@ type NetworkProfile struct { StoreProvider string `json:"store_provider,omitempty" yaml:"store_provider,omitempty" mapstructure:"store_provider"` } +// NetworkClient is an interface for easily interacting with algod. type NetworkClient struct { profile *NetworkProfile algod *algod.Client @@ -222,12 +223,12 @@ func addressFromPub(pub []byte) (string, error) { return types.EncodeAddress(pub) } -// pubkey-network-appID +// parseSubjectString extracts the network, public key and application ID from a DID (network-pubkey-appID). func parseSubjectString(subject string) (pub []byte, network string, appID uint64, err error) { idSegments := strings.Split(subject, "-") if len(idSegments) != 3 { err = fmt.Errorf("invalid subject identifier. Expected 3 segments, got %d", len(idSegments)) - return + return pub, network, appID, err } network = idSegments[0] @@ -240,20 +241,20 @@ func parseSubjectString(subject string) (pub []byte, network string, appID uint6 } if !matchFound { err = fmt.Errorf("invalid network in subject identifier: %s", network) - return + return pub, network, appID, err } pub, err = hex.DecodeString(idSegments[1]) if err != nil { err = fmt.Errorf("invalid public key in subject identifier") - return + return pub, network, appID, err } app, err := strconv.Atoi(idSegments[2]) if err != nil { err = fmt.Errorf("invalid storage app ID in subject identifier") - return + return pub, network, appID, err } appID = uint64(app) - return + return pub, network, appID, err } diff --git a/client/store/local.go b/client/store/local.go index 5ec396d..b65bfc2 100644 --- a/client/store/local.go +++ b/client/store/local.go @@ -88,7 +88,7 @@ func (ls *LocalStore) Get(name string) (*did.Identifier, error) { func (ls *LocalStore) List() map[string]*did.Identifier { // nolint: prealloc var list = make(map[string]*did.Identifier) - _ = filepath.Walk(ls.home, func(path string, info os.FileInfo, err error) error { + _ = filepath.Walk(ls.home, func(_ string, info os.FileInfo, err error) error { if err != nil { return err } @@ -167,7 +167,7 @@ func (ls *LocalStore) OpenWallet(name, passphrase string) (string, error) { // ListWallets returns a list of the names of all wallets locally stored. func (ls *LocalStore) ListWallets() (list []string) { - _ = filepath.Walk(filepath.Join(ls.home, "wallets"), func(path string, info os.FileInfo, err error) error { + _ = filepath.Walk(filepath.Join(ls.home, "wallets"), func(_ string, info os.FileInfo, err error) error { if err != nil { return err } From 17d3159b29e2fa556ba93a050834e666dde70289 Mon Sep 17 00:00:00 2001 From: Joe Polny Date: Tue, 30 Apr 2024 17:44:29 -0400 Subject: [PATCH 04/27] update TUTORIAL.md --- TUTORIAL.md | 76 ++++++++++++++++++++++++++--------------------------- 1 file changed, 37 insertions(+), 39 deletions(-) diff --git a/TUTORIAL.md b/TUTORIAL.md index 45e89d3..0ddb3fc 100644 --- a/TUTORIAL.md +++ b/TUTORIAL.md @@ -155,9 +155,7 @@ For example, to use Algorand testnet: ``` network: - active: testnet profiles: - # to deploy your own storage provider contract - name: testnet node: https://testnet-api.algonode.cloud node_token: "" @@ -206,7 +204,7 @@ encryption key required for secure storage. Finally, you'll get an output simila to this. ```shell -2024-03-06T12:00:15-05:00 INF new wallet created address=3ARALWACZXG2IHZOPYOKPHLT6KNJKI45RJSFQSYOJSJVD675DX42M4ZMRE name=sample-account +2024-04-30T17:33:49-04:00 INF new wallet created address=PVBONYHTY4OXO7PDNBE47FXCROASIUYZRTXC2LGCW6YZIOAGAAD2VXRI44 name=sample-account ``` ### 3.2 List existing Wallets @@ -221,7 +219,7 @@ algoid wallet list The list displays every wallet using its local alias for simpler usage. ```shell -2024-03-06T12:00:33-05:00 INF wallet found: sample-account +2024-04-30T17:34:09-04:00 INF wallet found: sample-account ``` ### 3.3 Get wallet details @@ -230,17 +228,18 @@ To get additional details such as your account balance, status, rewards, etc; si use the `wallet info` command. ```shell -algoid wallet info sample-account +algoid wallet info sample-account testnet ``` -The client application will reach out to the network and the account information will +The client application will reach out to the network specified and the account information will be printed. ```txt -address: 3ARALWACZXG2IHZOPYOKPHLT6KNJKI45RJSFQSYOJSJVD675DX42M4ZMRE -public key: d82205d802cdcda41f2e7e1ca79d73f29a95239d8a64584b0e4c9351fbfd1df9 +network: testnet +address: PVBONYHTY4OXO7PDNBE47FXCROASIUYZRTXC2LGCW6YZIOAGAAD2VXRI44 +public key: 7d42e6e0f3c71d777de36849cf96e28b812453198cee2d2cc2b7b19438060007 status: Offline -round: 37804729 +round: 39535146 current balance: 0 pending rewards: 0 total rewards: 0 @@ -249,7 +248,7 @@ total rewards: 0 ## 4. Deploy ```shell -algoid deploy sample-account +algoid deploy sample-account testnet 2024-03-06T12:01:40-05:00 INF storage contract deployed successfully app_id=613372790 ``` @@ -258,13 +257,12 @@ algoid deploy sample-account ```yml network: - active: testnet profiles: # to deploy your own storage provider contract - name: testnet node: https://testnet-api.algonode.cloud node_token: "" - app_id: 613372790 + app_id: 654583141 ``` ## 4. DID Managent @@ -283,17 +281,17 @@ To create a new DID, with a new passphrase-protected cryptographic key enabled for authentication simply run: ```shell -algoid create sample-account +algoid create sample-account testnet ``` You'll be asked to enter and confirm you passphrase and finally get an output similar to: ```shell -2024-03-06T12:02:54-05:00 INF generating new identifier method=algo subject=d82205d802cdcda41f2e7e1ca79d73f29a95239d8a64584b0e4c9351fbfd1df9-613372790 -2024-03-06T12:02:54-05:00 DBG adding master key -2024-03-06T12:02:54-05:00 DBG setting master key as authentication mechanism -2024-03-06T12:02:54-05:00 INF adding entry to local store +2024-04-30T17:41:55-04:00 INF generating new identifier method=algo subject=testnet-7d42e6e0f3c71d777de36849cf96e28b812453198cee2d2cc2b7b19438060007-654583141 +2024-04-30T17:41:55-04:00 DBG adding master key +2024-04-30T17:41:55-04:00 DBG setting master key as authentication mechanism +2024-04-30T17:41:55-04:00 INF adding entry to local store ``` ### 4.2 List existing DIDs @@ -309,7 +307,7 @@ The list displays every DID instance along it's local alias for simpler usage. ```txt Name DID -sample-account did:algo:d82205d802cdcda41f2e7e1ca79d73f29a95239d8a64584b0e4c9351fbfd1df9-613372790 +sample-account did:algo:testnet-7d42e6e0f3c71d777de36849cf96e28b812453198cee2d2cc2b7b19438060007-654583141 ``` ### 4.3 Inspect your local DID document @@ -328,9 +326,9 @@ The command will print on the screen the full contents of the associated DID document. ```json -2024-03-06T12:03:25-05:00 INF created: 2024-03-06T17:02:54Z -2024-03-06T12:03:25-05:00 INF updated: 2024-03-06T17:02:54Z -2024-03-06T12:03:25-05:00 INF active: true +2024-04-30T17:42:24-04:00 INF created: 2024-04-30T21:41:55Z +2024-04-30T17:42:24-04:00 INF updated: 2024-04-30T21:41:55Z +2024-04-30T17:42:24-04:00 INF active: true { "document": { "@context": [ @@ -338,17 +336,17 @@ document. "https://w3id.org/security/suites/ed25519-2020/v1", "https://w3id.org/security/suites/x25519-2020/v1" ], - "id": "did:algo:d82205d802cdcda41f2e7e1ca79d73f29a95239d8a64584b0e4c9351fbfd1df9-613372790", + "id": "did:algo:testnet-7d42e6e0f3c71d777de36849cf96e28b812453198cee2d2cc2b7b19438060007-654583141", "verificationMethod": [ { - "id": "did:algo:d82205d802cdcda41f2e7e1ca79d73f29a95239d8a64584b0e4c9351fbfd1df9-613372790#master", + "id": "did:algo:testnet-7d42e6e0f3c71d777de36849cf96e28b812453198cee2d2cc2b7b19438060007-654583141#master", "type": "Ed25519VerificationKey2020", - "controller": "did:algo:d82205d802cdcda41f2e7e1ca79d73f29a95239d8a64584b0e4c9351fbfd1df9-613372790", - "publicKeyMultibase": "zFYh9rcBz66DNU8UwzLNmyVAsLhJe17kCmFdEazHb9Qxg" + "controller": "did:algo:testnet-7d42e6e0f3c71d777de36849cf96e28b812453198cee2d2cc2b7b19438060007-654583141", + "publicKeyMultibase": "z9Ry8aFPMLKapvtYkNNSFsoNhkc4192j4ai17EzMquAZc" } ], "authentication": [ - "did:algo:d82205d802cdcda41f2e7e1ca79d73f29a95239d8a64584b0e4c9351fbfd1df9-613372790#master" + "did:algo:testnet-7d42e6e0f3c71d777de36849cf96e28b812453198cee2d2cc2b7b19438060007-654583141#master" ] } } @@ -422,10 +420,10 @@ For example, by running `algoid publish sample-account` you'll get an output sim the following. ```shell -2024-03-06T12:04:00-05:00 INF submitting request to the network -2024-03-06T12:04:00-05:00 INF publishing: sample-account -2024-03-06T12:04:00-05:00 INF publishing DID document did=did:algo:d82205d802cdcda41f2e7e1ca79d73f29a95239d8a64584b0e4c9351fbfd1df9-613372790 -2024-03-06T12:04:16-05:00 INF DID instance published +2024-04-30T17:42:54-04:00 INF submitting request to the network +2024-04-30T17:42:54-04:00 INF publishing: sample-account +2024-04-30T17:42:54-04:00 INF publishing DID document did=did:algo:testnet-7d42e6e0f3c71d777de36849cf96e28b812453198cee2d2cc2b7b19438060007-654583141 +2024-04-30T17:43:10-04:00 INF DID instance published ``` ### 5.1 Resolve a DID @@ -450,28 +448,28 @@ algoid retrieve [existing DID] For example, to resolve the DID created as part of this tutorial. ```shell -algoid resolve did:algo:d82205d802cdcda41f2e7e1ca79d73f29a95239d8a64584b0e4c9351fbfd1df9-613372790 +algoid resolve did:algo:testnet-7d42e6e0f3c71d777de36849cf96e28b812453198cee2d2cc2b7b19438060007-654583141 -2024-03-06T12:04:34-05:00 INF retrieving record -2024-03-06T12:04:34-05:00 INF retrieving DID document did=did:algo:d82205d802cdcda41f2e7e1ca79d73f29a95239d8a64584b0e4c9351fbfd1df9-613372790 -2024-03-06T12:04:35-05:00 WRN skipping validation +2024-04-30T17:43:41-04:00 INF retrieving record +2024-04-30T17:43:41-04:00 INF retrieving DID document did=did:algo:testnet-7d42e6e0f3c71d777de36849cf96e28b812453198cee2d2cc2b7b19438060007-654583141 +2024-04-30T17:43:43-04:00 WRN skipping validation { "@context": [ "https://www.w3.org/ns/did/v1", "https://w3id.org/security/suites/ed25519-2020/v1", "https://w3id.org/security/suites/x25519-2020/v1" ], - "id": "did:algo:d82205d802cdcda41f2e7e1ca79d73f29a95239d8a64584b0e4c9351fbfd1df9-613372790", + "id": "did:algo:testnet-7d42e6e0f3c71d777de36849cf96e28b812453198cee2d2cc2b7b19438060007-654583141", "verificationMethod": [ { - "id": "did:algo:d82205d802cdcda41f2e7e1ca79d73f29a95239d8a64584b0e4c9351fbfd1df9-613372790#master", + "id": "did:algo:testnet-7d42e6e0f3c71d777de36849cf96e28b812453198cee2d2cc2b7b19438060007-654583141#master", "type": "Ed25519VerificationKey2020", - "controller": "did:algo:d82205d802cdcda41f2e7e1ca79d73f29a95239d8a64584b0e4c9351fbfd1df9-613372790", - "publicKeyMultibase": "zFYh9rcBz66DNU8UwzLNmyVAsLhJe17kCmFdEazHb9Qxg" + "controller": "did:algo:testnet-7d42e6e0f3c71d777de36849cf96e28b812453198cee2d2cc2b7b19438060007-654583141", + "publicKeyMultibase": "z9Ry8aFPMLKapvtYkNNSFsoNhkc4192j4ai17EzMquAZc" } ], "authentication": [ - "did:algo:d82205d802cdcda41f2e7e1ca79d73f29a95239d8a64584b0e4c9351fbfd1df9-613372790#master" + "did:algo:testnet-7d42e6e0f3c71d777de36849cf96e28b812453198cee2d2cc2b7b19438060007-654583141#master" ] } ``` From b3f33835792ec74e751f5fbac45a0d465edb8d7e Mon Sep 17 00:00:00 2001 From: Joe Polny Date: Tue, 30 Apr 2024 18:13:43 -0400 Subject: [PATCH 05/27] use : as seperator, change order --- TUTORIAL.md | 26 +++++++++++++------------- client/cli/cmd/register.go | 2 +- client/internal/main.go | 8 ++++---- client/ui/provider.go | 2 +- 4 files changed, 19 insertions(+), 19 deletions(-) diff --git a/TUTORIAL.md b/TUTORIAL.md index 0ddb3fc..bd86e16 100644 --- a/TUTORIAL.md +++ b/TUTORIAL.md @@ -288,7 +288,7 @@ You'll be asked to enter and confirm you passphrase and finally get an output similar to: ```shell -2024-04-30T17:41:55-04:00 INF generating new identifier method=algo subject=testnet-7d42e6e0f3c71d777de36849cf96e28b812453198cee2d2cc2b7b19438060007-654583141 +2024-04-30T17:41:55-04:00 INF generating new identifier method=algo subject=testnet:654583141:7d42e6e0f3c71d777de36849cf96e28b812453198cee2d2cc2b7b19438060007 2024-04-30T17:41:55-04:00 DBG adding master key 2024-04-30T17:41:55-04:00 DBG setting master key as authentication mechanism 2024-04-30T17:41:55-04:00 INF adding entry to local store @@ -307,7 +307,7 @@ The list displays every DID instance along it's local alias for simpler usage. ```txt Name DID -sample-account did:algo:testnet-7d42e6e0f3c71d777de36849cf96e28b812453198cee2d2cc2b7b19438060007-654583141 +sample-account did:algo:testnet:654583141:7d42e6e0f3c71d777de36849cf96e28b812453198cee2d2cc2b7b19438060007 ``` ### 4.3 Inspect your local DID document @@ -336,17 +336,17 @@ document. "https://w3id.org/security/suites/ed25519-2020/v1", "https://w3id.org/security/suites/x25519-2020/v1" ], - "id": "did:algo:testnet-7d42e6e0f3c71d777de36849cf96e28b812453198cee2d2cc2b7b19438060007-654583141", + "id": "did:algo:testnet:654583141:7d42e6e0f3c71d777de36849cf96e28b812453198cee2d2cc2b7b19438060007", "verificationMethod": [ { - "id": "did:algo:testnet-7d42e6e0f3c71d777de36849cf96e28b812453198cee2d2cc2b7b19438060007-654583141#master", + "id": "did:algo:testnet:654583141:7d42e6e0f3c71d777de36849cf96e28b812453198cee2d2cc2b7b19438060007#master", "type": "Ed25519VerificationKey2020", - "controller": "did:algo:testnet-7d42e6e0f3c71d777de36849cf96e28b812453198cee2d2cc2b7b19438060007-654583141", + "controller": "did:algo:testnet:654583141:7d42e6e0f3c71d777de36849cf96e28b812453198cee2d2cc2b7b19438060007", "publicKeyMultibase": "z9Ry8aFPMLKapvtYkNNSFsoNhkc4192j4ai17EzMquAZc" } ], "authentication": [ - "did:algo:testnet-7d42e6e0f3c71d777de36849cf96e28b812453198cee2d2cc2b7b19438060007-654583141#master" + "did:algo:testnet:654583141:7d42e6e0f3c71d777de36849cf96e28b812453198cee2d2cc2b7b19438060007#master" ] } } @@ -422,7 +422,7 @@ the following. ```shell 2024-04-30T17:42:54-04:00 INF submitting request to the network 2024-04-30T17:42:54-04:00 INF publishing: sample-account -2024-04-30T17:42:54-04:00 INF publishing DID document did=did:algo:testnet-7d42e6e0f3c71d777de36849cf96e28b812453198cee2d2cc2b7b19438060007-654583141 +2024-04-30T17:42:54-04:00 INF publishing DID document did=did:algo:testnet:654583141:7d42e6e0f3c71d777de36849cf96e28b812453198cee2d2cc2b7b19438060007 2024-04-30T17:43:10-04:00 INF DID instance published ``` @@ -448,10 +448,10 @@ algoid retrieve [existing DID] For example, to resolve the DID created as part of this tutorial. ```shell -algoid resolve did:algo:testnet-7d42e6e0f3c71d777de36849cf96e28b812453198cee2d2cc2b7b19438060007-654583141 +algoid resolve did:algo:testnet:654583141:7d42e6e0f3c71d777de36849cf96e28b812453198cee2d2cc2b7b19438060007 2024-04-30T17:43:41-04:00 INF retrieving record -2024-04-30T17:43:41-04:00 INF retrieving DID document did=did:algo:testnet-7d42e6e0f3c71d777de36849cf96e28b812453198cee2d2cc2b7b19438060007-654583141 +2024-04-30T17:43:41-04:00 INF retrieving DID document did=did:algo:testnet:654583141:7d42e6e0f3c71d777de36849cf96e28b812453198cee2d2cc2b7b19438060007 2024-04-30T17:43:43-04:00 WRN skipping validation { "@context": [ @@ -459,17 +459,17 @@ algoid resolve did:algo:testnet-7d42e6e0f3c71d777de36849cf96e28b812453198cee2d2c "https://w3id.org/security/suites/ed25519-2020/v1", "https://w3id.org/security/suites/x25519-2020/v1" ], - "id": "did:algo:testnet-7d42e6e0f3c71d777de36849cf96e28b812453198cee2d2cc2b7b19438060007-654583141", + "id": "did:algo:testnet:654583141:7d42e6e0f3c71d777de36849cf96e28b812453198cee2d2cc2b7b19438060007", "verificationMethod": [ { - "id": "did:algo:testnet-7d42e6e0f3c71d777de36849cf96e28b812453198cee2d2cc2b7b19438060007-654583141#master", + "id": "did:algo:testnet:654583141:7d42e6e0f3c71d777de36849cf96e28b812453198cee2d2cc2b7b19438060007#master", "type": "Ed25519VerificationKey2020", - "controller": "did:algo:testnet-7d42e6e0f3c71d777de36849cf96e28b812453198cee2d2cc2b7b19438060007-654583141", + "controller": "did:algo:testnet:654583141:7d42e6e0f3c71d777de36849cf96e28b812453198cee2d2cc2b7b19438060007", "publicKeyMultibase": "z9Ry8aFPMLKapvtYkNNSFsoNhkc4192j4ai17EzMquAZc" } ], "authentication": [ - "did:algo:testnet-7d42e6e0f3c71d777de36849cf96e28b812453198cee2d2cc2b7b19438060007-654583141#master" + "did:algo:testnet:654583141:7d42e6e0f3c71d777de36849cf96e28b812453198cee2d2cc2b7b19438060007#master" ] } ``` diff --git a/client/cli/cmd/register.go b/client/cli/cmd/register.go index 53b20d9..31b1be9 100644 --- a/client/cli/cmd/register.go +++ b/client/cli/cmd/register.go @@ -76,7 +76,7 @@ func runRegisterCmd(_ *cobra.Command, args []string) error { } // Generate base identifier instance - subject := fmt.Sprintf("%s-%x-%d", network, account.PublicKey, appID) + subject := fmt.Sprintf("%s:%d:%x", network, appID, account.PublicKey) method := "algo" log.WithFields(xlog.Fields{ "subject": subject, diff --git a/client/internal/main.go b/client/internal/main.go index 27b7d4e..1da919e 100644 --- a/client/internal/main.go +++ b/client/internal/main.go @@ -223,9 +223,9 @@ func addressFromPub(pub []byte) (string, error) { return types.EncodeAddress(pub) } -// parseSubjectString extracts the network, public key and application ID from a DID (network-pubkey-appID). +// parseSubjectString extracts the network, public key and application ID from a DID (network:appID:pubkey). func parseSubjectString(subject string) (pub []byte, network string, appID uint64, err error) { - idSegments := strings.Split(subject, "-") + idSegments := strings.Split(subject, ":") if len(idSegments) != 3 { err = fmt.Errorf("invalid subject identifier. Expected 3 segments, got %d", len(idSegments)) return pub, network, appID, err @@ -244,13 +244,13 @@ func parseSubjectString(subject string) (pub []byte, network string, appID uint6 return pub, network, appID, err } - pub, err = hex.DecodeString(idSegments[1]) + pub, err = hex.DecodeString(idSegments[2]) if err != nil { err = fmt.Errorf("invalid public key in subject identifier") return pub, network, appID, err } - app, err := strconv.Atoi(idSegments[2]) + app, err := strconv.Atoi(idSegments[1]) if err != nil { err = fmt.Errorf("invalid storage app ID in subject identifier") return pub, network, appID, err diff --git a/client/ui/provider.go b/client/ui/provider.go index 267892d..f468c5f 100644 --- a/client/ui/provider.go +++ b/client/ui/provider.go @@ -91,7 +91,7 @@ func (p *Provider) Register(network string, name string, passphrase string) erro } // Generate base identifier instance - subject := fmt.Sprintf("%s-%x-%d", network, account.PublicKey, p.client.Networks[network].StorageAppID()) + subject := fmt.Sprintf("%s:%d:%x", network, p.client.Networks[network].StorageAppID(), account.PublicKey) method := "algo" p.log.WithFields(xlog.Fields{ "subject": subject, From fc1eee846023091c1270c539a61c1fd929b7b02b Mon Sep 17 00:00:00 2001 From: Joe Polny Date: Tue, 30 Apr 2024 18:35:41 -0400 Subject: [PATCH 06/27] add namespace --- TUTORIAL.md | 24 ++++++++++++------------ client/cli/cmd/register.go | 2 +- client/internal/main.go | 12 +++++++++--- client/ui/provider.go | 2 +- 4 files changed, 23 insertions(+), 17 deletions(-) diff --git a/TUTORIAL.md b/TUTORIAL.md index bd86e16..43acf06 100644 --- a/TUTORIAL.md +++ b/TUTORIAL.md @@ -307,7 +307,7 @@ The list displays every DID instance along it's local alias for simpler usage. ```txt Name DID -sample-account did:algo:testnet:654583141:7d42e6e0f3c71d777de36849cf96e28b812453198cee2d2cc2b7b19438060007 +sample-account did:algo:testnet:box:654583141:7d42e6e0f3c71d777de36849cf96e28b812453198cee2d2cc2b7b19438060007 ``` ### 4.3 Inspect your local DID document @@ -336,17 +336,17 @@ document. "https://w3id.org/security/suites/ed25519-2020/v1", "https://w3id.org/security/suites/x25519-2020/v1" ], - "id": "did:algo:testnet:654583141:7d42e6e0f3c71d777de36849cf96e28b812453198cee2d2cc2b7b19438060007", + "id": "did:algo:testnet:box:654583141:7d42e6e0f3c71d777de36849cf96e28b812453198cee2d2cc2b7b19438060007", "verificationMethod": [ { - "id": "did:algo:testnet:654583141:7d42e6e0f3c71d777de36849cf96e28b812453198cee2d2cc2b7b19438060007#master", + "id": "did:algo:testnet:box:654583141:7d42e6e0f3c71d777de36849cf96e28b812453198cee2d2cc2b7b19438060007#master", "type": "Ed25519VerificationKey2020", - "controller": "did:algo:testnet:654583141:7d42e6e0f3c71d777de36849cf96e28b812453198cee2d2cc2b7b19438060007", + "controller": "did:algo:testnet:box:654583141:7d42e6e0f3c71d777de36849cf96e28b812453198cee2d2cc2b7b19438060007", "publicKeyMultibase": "z9Ry8aFPMLKapvtYkNNSFsoNhkc4192j4ai17EzMquAZc" } ], "authentication": [ - "did:algo:testnet:654583141:7d42e6e0f3c71d777de36849cf96e28b812453198cee2d2cc2b7b19438060007#master" + "did:algo:testnet:box:654583141:7d42e6e0f3c71d777de36849cf96e28b812453198cee2d2cc2b7b19438060007#master" ] } } @@ -422,7 +422,7 @@ the following. ```shell 2024-04-30T17:42:54-04:00 INF submitting request to the network 2024-04-30T17:42:54-04:00 INF publishing: sample-account -2024-04-30T17:42:54-04:00 INF publishing DID document did=did:algo:testnet:654583141:7d42e6e0f3c71d777de36849cf96e28b812453198cee2d2cc2b7b19438060007 +2024-04-30T17:42:54-04:00 INF publishing DID document did=did:algo:testnet:box:654583141:7d42e6e0f3c71d777de36849cf96e28b812453198cee2d2cc2b7b19438060007 2024-04-30T17:43:10-04:00 INF DID instance published ``` @@ -448,10 +448,10 @@ algoid retrieve [existing DID] For example, to resolve the DID created as part of this tutorial. ```shell -algoid resolve did:algo:testnet:654583141:7d42e6e0f3c71d777de36849cf96e28b812453198cee2d2cc2b7b19438060007 +algoid resolve did:algo:testnet:box:654583141:7d42e6e0f3c71d777de36849cf96e28b812453198cee2d2cc2b7b19438060007 2024-04-30T17:43:41-04:00 INF retrieving record -2024-04-30T17:43:41-04:00 INF retrieving DID document did=did:algo:testnet:654583141:7d42e6e0f3c71d777de36849cf96e28b812453198cee2d2cc2b7b19438060007 +2024-04-30T17:43:41-04:00 INF retrieving DID document did=did:algo:testnet:box:654583141:7d42e6e0f3c71d777de36849cf96e28b812453198cee2d2cc2b7b19438060007 2024-04-30T17:43:43-04:00 WRN skipping validation { "@context": [ @@ -459,17 +459,17 @@ algoid resolve did:algo:testnet:654583141:7d42e6e0f3c71d777de36849cf96e28b812453 "https://w3id.org/security/suites/ed25519-2020/v1", "https://w3id.org/security/suites/x25519-2020/v1" ], - "id": "did:algo:testnet:654583141:7d42e6e0f3c71d777de36849cf96e28b812453198cee2d2cc2b7b19438060007", + "id": "did:algo:testnet:box:654583141:7d42e6e0f3c71d777de36849cf96e28b812453198cee2d2cc2b7b19438060007", "verificationMethod": [ { - "id": "did:algo:testnet:654583141:7d42e6e0f3c71d777de36849cf96e28b812453198cee2d2cc2b7b19438060007#master", + "id": "did:algo:testnet:box:654583141:7d42e6e0f3c71d777de36849cf96e28b812453198cee2d2cc2b7b19438060007#master", "type": "Ed25519VerificationKey2020", - "controller": "did:algo:testnet:654583141:7d42e6e0f3c71d777de36849cf96e28b812453198cee2d2cc2b7b19438060007", + "controller": "did:algo:testnet:box:654583141:7d42e6e0f3c71d777de36849cf96e28b812453198cee2d2cc2b7b19438060007", "publicKeyMultibase": "z9Ry8aFPMLKapvtYkNNSFsoNhkc4192j4ai17EzMquAZc" } ], "authentication": [ - "did:algo:testnet:654583141:7d42e6e0f3c71d777de36849cf96e28b812453198cee2d2cc2b7b19438060007#master" + "did:algo:testnet:box:654583141:7d42e6e0f3c71d777de36849cf96e28b812453198cee2d2cc2b7b19438060007#master" ] } ``` diff --git a/client/cli/cmd/register.go b/client/cli/cmd/register.go index 31b1be9..4140f04 100644 --- a/client/cli/cmd/register.go +++ b/client/cli/cmd/register.go @@ -76,7 +76,7 @@ func runRegisterCmd(_ *cobra.Command, args []string) error { } // Generate base identifier instance - subject := fmt.Sprintf("%s:%d:%x", network, appID, account.PublicKey) + subject := fmt.Sprintf("%s:box:%d:%x", network, appID, account.PublicKey) method := "algo" log.WithFields(xlog.Fields{ "subject": subject, diff --git a/client/internal/main.go b/client/internal/main.go index 1da919e..c344f6d 100644 --- a/client/internal/main.go +++ b/client/internal/main.go @@ -226,7 +226,7 @@ func addressFromPub(pub []byte) (string, error) { // parseSubjectString extracts the network, public key and application ID from a DID (network:appID:pubkey). func parseSubjectString(subject string) (pub []byte, network string, appID uint64, err error) { idSegments := strings.Split(subject, ":") - if len(idSegments) != 3 { + if len(idSegments) != 4 { err = fmt.Errorf("invalid subject identifier. Expected 3 segments, got %d", len(idSegments)) return pub, network, appID, err } @@ -244,13 +244,19 @@ func parseSubjectString(subject string) (pub []byte, network string, appID uint6 return pub, network, appID, err } - pub, err = hex.DecodeString(idSegments[2]) + namespace := idSegments[1] + if namespace != "box" { + err = fmt.Errorf("invalid namespace in subject identifier: %s", namespace) + return pub, network, appID, err + } + + pub, err = hex.DecodeString(idSegments[3]) if err != nil { err = fmt.Errorf("invalid public key in subject identifier") return pub, network, appID, err } - app, err := strconv.Atoi(idSegments[1]) + app, err := strconv.Atoi(idSegments[2]) if err != nil { err = fmt.Errorf("invalid storage app ID in subject identifier") return pub, network, appID, err diff --git a/client/ui/provider.go b/client/ui/provider.go index f468c5f..981df5e 100644 --- a/client/ui/provider.go +++ b/client/ui/provider.go @@ -91,7 +91,7 @@ func (p *Provider) Register(network string, name string, passphrase string) erro } // Generate base identifier instance - subject := fmt.Sprintf("%s:%d:%x", network, p.client.Networks[network].StorageAppID(), account.PublicKey) + subject := fmt.Sprintf("%s:box:%d:%x", network, p.client.Networks[network].StorageAppID(), account.PublicKey) method := "algo" p.log.WithFields(xlog.Fields{ "subject": subject, From 8c3b052f07da261e9a1ffd5db5aff00879fffc96 Mon Sep 17 00:00:00 2001 From: Joe Polny Date: Thu, 2 May 2024 16:44:58 -0400 Subject: [PATCH 07/27] add api_test --- .gitignore | 2 + client/internal/contracts.go | 2 +- client/internal/main.go | 12 +- client/internal/utils.go | 4 +- client/store/local.go | 1 + client/ui/api_test.go | 276 +++++++++++++++++++++++++++++++++++ client/ui/provider.go | 9 +- go.mod | 3 + go.sum | 4 +- 9 files changed, 297 insertions(+), 16 deletions(-) create mode 100644 client/ui/api_test.go diff --git a/.gitignore b/.gitignore index 6ea25ec..4aef901 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +.test/ + # Generals .DS_Store .idea diff --git a/client/internal/contracts.go b/client/internal/contracts.go index 8fb0f2b..5baec5d 100644 --- a/client/internal/contracts.go +++ b/client/internal/contracts.go @@ -50,7 +50,7 @@ func init() { _ = clearFile.Close() } -func loadContract() *abi.Contract { +func LoadContract() *abi.Contract { abiFile, _ := StorageContracts.Open("AlgoDID.abi.json") abiContents, _ := io.ReadAll(abiFile) contract := &abi.Contract{} diff --git a/client/internal/main.go b/client/internal/main.go index c344f6d..627554c 100644 --- a/client/internal/main.go +++ b/client/internal/main.go @@ -113,9 +113,9 @@ func (c *NetworkClient) Ready() bool { // DeployContract creates a new instance of the AlgoDID storage smart contract // on the network. func (c *NetworkClient) DeployContract(sender *crypto.Account) (uint64, error) { - contract := loadContract() + contract := LoadContract() signer := transaction.BasicAccountTransactionSigner{Account: *sender} - return createApp(c.algod, contract, signer.Account.Address, signer) + return CreateApp(c.algod, contract, signer.Account.Address, signer) } // PublishDID sends a new DID document to the network @@ -124,7 +124,7 @@ func (c *AlgoDIDClient) PublishDID(id *did.Identifier, sender *crypto.Account) e c.log.WithFields(map[string]interface{}{ "did": id.String(), }).Info("publishing DID document") - contract := loadContract() + contract := LoadContract() signer := transaction.BasicAccountTransactionSigner{Account: *sender} doc, _ := json.Marshal(id.Document(true)) pub, network, appID, err := parseSubjectString(id.Subject()) @@ -144,7 +144,7 @@ func (c *AlgoDIDClient) DeleteDID(id *did.Identifier, sender *crypto.Account) er c.log.WithFields(map[string]interface{}{ "did": id.String(), }).Info("deleting DID document") - contract := loadContract() + contract := LoadContract() signer := transaction.BasicAccountTransactionSigner{Account: *sender} pub, network, appID, err := parseSubjectString(id.Subject()) if err != nil { @@ -177,7 +177,7 @@ func (c *AlgoDIDClient) Resolve(id string) (*did.Document, error) { networkClient := c.Networks[network] // Retrieve the data from the network - data, err := resolveDID(appID, pub, networkClient.algod, network) + data, err := ResolveDID(appID, pub, networkClient.algod, network) if err != nil { return nil, err } @@ -233,7 +233,7 @@ func parseSubjectString(subject string) (pub []byte, network string, appID uint6 network = idSegments[0] matchFound := false - for _, a := range []string{"mainnet", "testnet"} { + for _, a := range []string{"mainnet", "testnet", "custom"} { if a == network { matchFound = true break diff --git a/client/internal/utils.go b/client/internal/utils.go index 5170aba..90138ed 100644 --- a/client/internal/utils.go +++ b/client/internal/utils.go @@ -30,7 +30,7 @@ const ( // CreateApp is used to deploy the AlgoDID storage smart contract to the // Algorand network. -func createApp( +func CreateApp( algodClient *algod.Client, contract *abi.Contract, sender types.Address, @@ -311,7 +311,7 @@ func checkNetwork(network string, algodClient *algod.Client) error { // ResolveDID is used to read the DID document from the AlgoDID storage smart // contract. -func resolveDID(appID uint64, pubKey []byte, algodClient *algod.Client, network string) ([]byte, error) { +func ResolveDID(appID uint64, pubKey []byte, algodClient *algod.Client, network string) ([]byte, error) { err := checkNetwork(network, algodClient) if err != nil { return nil, err diff --git a/client/store/local.go b/client/store/local.go index b65bfc2..f063d3d 100644 --- a/client/store/local.go +++ b/client/store/local.go @@ -43,6 +43,7 @@ func NewLocalStore(home string) (*LocalStore, error) { return nil, fmt.Errorf("failed to create new wallets directory: %w", err) } } + return &LocalStore{home: h}, nil } diff --git a/client/ui/api_test.go b/client/ui/api_test.go new file mode 100644 index 0000000..6be6218 --- /dev/null +++ b/client/ui/api_test.go @@ -0,0 +1,276 @@ +package ui + +import ( + "bytes" + "context" + "fmt" + "io" + "net/http" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/algorand/go-algorand-sdk/client/kmd" + "github.com/algorand/go-algorand-sdk/v2/client/v2/algod" + "github.com/algorand/go-algorand-sdk/v2/crypto" + + "github.com/algorand/go-algorand-sdk/v2/mnemonic" + "github.com/algorand/go-algorand-sdk/v2/transaction" + "github.com/algorandfoundation/did-algo/client/internal" + "github.com/algorandfoundation/did-algo/client/store" + + "github.com/stretchr/testify/require" + "go.bryk.io/pkg/log" +) + +var algodClient *algod.Client +var logger = log.WithZero(log.ZeroOptions{PrettyPrint: true}) +var localStore *store.LocalStore +var profile internal.NetworkProfile + +func getKmdClient() (kmd.Client, error) { + kmdClient, err := kmd.MakeClient( + "http://localhost:4002", + strings.Repeat("a", 64), + ) + + return kmdClient, err +} + +func getSandboxAccounts() ([]crypto.Account, error) { + client, err := getKmdClient() + if err != nil { + return nil, fmt.Errorf("Failed to create kmd client: %+v", err) + } + + resp, err := client.ListWallets() + if err != nil { + return nil, fmt.Errorf("Failed to list wallets: %+v", err) + } + + var walletId string + for _, wallet := range resp.Wallets { + if wallet.Name == "unencrypted-default-wallet" { + walletId = wallet.ID + } + } + + if walletId == "" { + return nil, fmt.Errorf("No wallet named %s", "unencrypted-default-wallet") + } + + whResp, err := client.InitWalletHandle(walletId, "") + if err != nil { + return nil, fmt.Errorf("Failed to init wallet handle: %+v", err) + } + + addrResp, err := client.ListKeys(whResp.WalletHandleToken) + if err != nil { + return nil, fmt.Errorf("Failed to list keys: %+v", err) + } + + var accts []crypto.Account + for _, addr := range addrResp.Addresses { + expResp, err := client.ExportKey(whResp.WalletHandleToken, "", addr) + if err != nil { + return nil, fmt.Errorf("Failed to export key: %+v", err) + } + + acct, err := crypto.AccountFromPrivateKey(expResp.PrivateKey) + if err != nil { + return nil, fmt.Errorf("Failed to create account from private key: %+v", err) + } + + accts = append(accts, acct) + } + + return accts, nil +} + +func req(t *testing.T, method string, endpoint string, reqBody string) *http.Response { + jsonBody := []byte(reqBody) + bodyReader := bytes.NewReader(jsonBody) + + req, err := http.NewRequest(method, fmt.Sprintf("http://localhost:9090/%s", endpoint), bodyReader) + require.NoError(t, err) + + res, err := http.DefaultClient.Do(req) + + return res +} + +func TestMain(m *testing.M) { + home, err := filepath.Abs(fmt.Sprintf("./.test/%d", time.Now().Unix())) + if err != nil { + panic(err) + } + + localStore, err = store.NewLocalStore(home) + if err != nil { + panic(err) + } + + algodClient, err = algod.MakeClient("http://localhost:4001", strings.Repeat("a", 64)) + if err != nil { + panic(err) + } + + status, err := algodClient.Status().Do(context.Background()) + + // type NetworkProfile struct { + // // Profile name. + // Name string `json:"name" yaml:"name" mapstructure:"name"` + + // // Algod node address. + // Node string `json:"node" yaml:"node" mapstructure:"node"` + + // // Algod node access token. + // NodeToken string `json:"node_token,omitempty" yaml:"node_token,omitempty" mapstructure:"node_token"` + + // // Application ID for the AlgoDID storage smart contract. + // AppID uint `json:"app_id" yaml:"app_id" mapstructure:"app_id"` + + // // Storage contract provider server, if any. + // StoreProvider string `json:"store_provider,omitempty" yaml:"store_provider,omitempty" mapstructure:"store_provider"` + // } + + block, err := algodClient.Block(status.LastRound).Do(context.Background()) + if err != nil { + panic(err) + } + + profile = internal.NetworkProfile{ + Name: "custom", + Node: "http://localhost:4001", + NodeToken: strings.Repeat("a", 64), + AppID: uint(block.TxnCounter) + 2, + } + + fmt.Printf("Using app ID: %d\n", profile.AppID) + + profiles := []*internal.NetworkProfile{&profile} + + // Get network client + cl, err := internal.NewAlgoClient(profiles, logger) + if err != nil { + panic(err) + } + + srv, err := LocalAPIServer(localStore, cl, logger) + + logger.Debug("starting local API server on localhost:9090") + go func() { + _ = srv.Start() + }() + + m.Run() +} + +func TestReady(t *testing.T) { + res := req(t, http.MethodGet, "ready", "") + + require.Equal(t, http.StatusOK, res.StatusCode) +} + +func TestList(t *testing.T) { + res := req(t, http.MethodGet, "list", "") + + require.Equal(t, http.StatusOK, res.StatusCode) + + body, err := io.ReadAll(res.Body) + require.NoError(t, err) + + require.Equal(t, "[]\n", string(body)) +} + +func TestRegister(t *testing.T) { + res := req(t, http.MethodPost, "register", `{"name": "TestRegister", "recovery_key": "test", "network": "custom"}`) + + require.Equal(t, http.StatusOK, res.StatusCode) +} + +func TestListAfterRegister(t *testing.T) { + res := req(t, http.MethodPost, "register", `{"name": "TestListAfterRegister", "recovery_key": "test", "network": "custom"}`) + + require.Equal(t, http.StatusOK, res.StatusCode) + + res = req(t, http.MethodGet, "list", "") + + require.Equal(t, http.StatusOK, res.StatusCode) + + body, err := io.ReadAll(res.Body) + require.NoError(t, err) + + require.Regexp(t, `{"name":"TestListAfterRegister","did":"did:algo:custom:box:`, string(body)) +} + +func TestUpdate(t *testing.T) { + res := req(t, http.MethodPost, "register", `{"name": "TestUpdate", "recovery_key": "test", "network": "custom"}`) + + require.Equal(t, http.StatusOK, res.StatusCode) + + // Get account + + mn, err := localStore.OpenWallet("TestUpdate", "test") + require.NoError(t, err) + + sk, err := mnemonic.ToPrivateKey(mn) + require.NoError(t, err) + + acct, err := crypto.AccountFromPrivateKey(sk) + require.NoError(t, err) + + signer := transaction.BasicAccountTransactionSigner{Account: acct} + + require.NoError(t, err) + + // Fund account + + localAccounts, err := getSandboxAccounts() + require.NoError(t, err) + + funder := localAccounts[0] + + sp, err := algodClient.SuggestedParams().Do(context.Background()) + require.NoError(t, err) + + fundTxn, err := transaction.MakePaymentTxn(funder.Address.String(), acct.Address.String(), 1_000_000, nil, "", sp) + require.NoError(t, err) + + txid, stxn, err := crypto.SignTransaction(funder.PrivateKey, fundTxn) + require.NoError(t, err) + + _, err = algodClient.SendRawTransaction(stxn).Do(context.Background()) + require.NoError(t, err) + + _, err = transaction.WaitForConfirmation(algodClient, txid, 5, context.Background()) + require.NoError(t, err) + + // Create the app + + createdAppId, err := internal.CreateApp(algodClient, internal.LoadContract(), acct.Address, signer) + require.NoError(t, err) + require.Equal(t, uint64(profile.AppID), createdAppId) + fmt.Printf("Created app ID: %d\n", createdAppId) + + appInfo, err := algodClient.GetApplicationByID(createdAppId).Do(context.Background()) + require.NoError(t, err) + require.False(t, appInfo.Deleted) + + // Sync with the network + + res = req(t, http.MethodPost, "update", `{"name": "TestUpdate", "passphrase": "test"}`) + require.NoError(t, err) + require.Equal(t, http.StatusOK, res.StatusCode) + + did, err := localStore.Get("TestUpdate") + require.NoError(t, err) + + resolvedDid, err := internal.ResolveDID(createdAppId, acct.PublicKey, algodClient, "custom") + require.NoError(t, err) + + fmt.Println(string(resolvedDid)) + + require.Regexp(t, fmt.Sprintf(`"id":"%s"`, did.DID()), string(resolvedDid)) +} diff --git a/client/ui/provider.go b/client/ui/provider.go index 981df5e..8f34061 100644 --- a/client/ui/provider.go +++ b/client/ui/provider.go @@ -194,11 +194,9 @@ func (p *Provider) Update(req *updateRequest) error { } } - // sync with the network in the background - go func() { - _ = p.Sync(req.Name, req.Passphrase) - }() - return nil + err = p.Sync(req.Name, req.Passphrase) + + return err } // ServerHandler returns an HTTP handler that can be used to exposed @@ -266,6 +264,7 @@ func (p *Provider) registerHandlerFunc(w http.ResponseWriter, r *http.Request) { } if err = p.Register(network, name, passphrase); err != nil { w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(err.Error())) return } _, _ = w.Write([]byte("ok")) diff --git a/go.mod b/go.mod index 89f4e9b..5f8eea5 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,7 @@ require ( github.com/pkg/errors v0.9.1 github.com/spf13/cobra v1.8.0 github.com/spf13/viper v1.18.2 + github.com/stretchr/testify v1.8.4 go.bryk.io/pkg v0.0.0-20240216184430-db271b89fadd golang.org/x/crypto v0.20.0 google.golang.org/genproto/googleapis/api v0.0.0-20240125205218-1f4bbc51befe @@ -27,6 +28,7 @@ require ( github.com/briandowns/spinner v1.23.0 // indirect github.com/charmbracelet/lipgloss v0.9.1 // indirect github.com/charmbracelet/log v0.3.1 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect github.com/fatih/color v1.15.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect @@ -49,6 +51,7 @@ require ( github.com/muesli/termenv v0.15.2 // indirect github.com/pelletier/go-toml/v2 v2.1.0 // indirect github.com/piprate/json-gold v0.5.0 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/pquerna/cachecontrol v0.1.0 // indirect github.com/rivo/uniseg v0.2.0 // indirect github.com/rogpeppe/go-internal v1.10.0 // indirect diff --git a/go.sum b/go.sum index bcaa74d..9595d91 100644 --- a/go.sum +++ b/go.sum @@ -24,6 +24,7 @@ github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46t github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/decred/dcrd/crypto/blake256 v1.0.1 h1:7PltbUIQB7u/FfZ39+DGa/ShuMyJ5ilcvdfma9wOH6Y= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= @@ -86,6 +87,7 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pquerna/cachecontrol v0.1.0 h1:yJMy84ti9h/+OEWa752kBTKv4XC30OtVVHYv/8cTqKc= github.com/pquerna/cachecontrol v0.1.0/go.mod h1:NrUG3Z7Rdu85UNR3vm7SOsl1nFIeSiQnrHV5K9mBcUI= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= @@ -188,8 +190,6 @@ google.golang.org/grpc v1.61.0 h1:TOvOcuXn30kRao+gfcvsebNEa5iZIiLkisYEkf7R7o0= google.golang.org/grpc v1.61.0/go.mod h1:VUbo7IFqmF1QtCAstipjG0GIoq49KvMe9+h1jFLBNJs= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I= -google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= From f2f5a9d22c933a2e1d78c9fd6409877359985413 Mon Sep 17 00:00:00 2001 From: Joe Polny Date: Wed, 8 May 2024 18:14:32 -0400 Subject: [PATCH 08/27] initial work on spec --- README.md | 879 +----------------------------------------------------- SPEC.md | 57 ++++ 2 files changed, 60 insertions(+), 876 deletions(-) create mode 100644 SPEC.md diff --git a/README.md b/README.md index 78d7027..cde3bb0 100644 --- a/README.md +++ b/README.md @@ -1,878 +1,5 @@ -# DID Method +# did:algo -[![Build Status](https://github.com/algorandfoundation/did-algo/workflows/ci/badge.svg?branch=main)](https://github.com/algorandfoundation/did-algo/actions) -[![Version](https://img.shields.io/github/tag/algorandfoundation/did-algo.svg)](https://github.com/algorandfoundation/did-algo/releases) -[![Software License](https://img.shields.io/badge/license-BSD3-red.svg)](LICENSE) -[![Go Report Card](https://goreportcard.com/badge/github.com/algorandfoundation/did-algo?style=flat)](https://goreportcard.com/report/github.com/algorandfoundation/did-algo) -[![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-v2.0-ff69b4.svg)](.github/CODE_OF_CONDUCT.md) +The repository contains the universal resolver driver for the `did:algo` method and all related software. -The present document describes the __"algo"__ DID Method specification. The -definitions, conventions and technical details included intend to provide a -solid base for further developments while maintaining compliance with the work, -on the [W3C Credentials Community Group](https://w3c-ccg.github.io/did-spec/). - -For more information about the origin and purpose of Decentralized Identifiers please -refer to the original [DID Primer.](https://github.com/WebOfTrustInfo/rwot5-boston/blob/master/topics-and-advance-readings/did-primer.md) - -To facilitate adoption and testing, and promote open discussions about the subjects -treated, this repository also includes an open source reference implementation for a -CLI client and network agent. You can directly download a precompiled binary from the -[published releases](https://github.com/algorandfoundation/did-algo/releases). - -## 1. Decentralized Identifiers - -__In order to access online, i.e. digital, services, we need to be electronically -identifiable.__ It means we need an electronic profile that, with a certain level -of assurance, the service provider (either another person or an entity) can trust -it corresponds to our real identity. - -__Conventional identity management systems are based on centralized authorities.__ -These authorities establish a process by which to entitle a user temporary access -to a given identifier element. Nevertheless, the true ownership of the identifier -remains on the assigner side and thus, can be removed, revoked and reassigned if -deemed adequate. This creates and intrinsically asymmetric power relationship between -the authority entity and the user. Some examples of this kind of identifiers include: - -- Domain names -- Email addresses -- Phone numbers -- IP addresses -- User names - -Additionally, from the standpoint of cryptographic trust verification, each of these -centralized authorities serves as its own -[Root of Trust](https://csrc.nist.gov/Projects/Hardware-Roots-of-Trust). - -__An alternative model to manage digital identifiers must be open and user-centric__. -It should be considered as such by satisfying at least the following considerations: - -- Anyone must have access to freely register, publish and update as many identifiers - as considered necessary. -- There should be no centralized authority required for the generation and assignment - of identifiers. -- The end user must have true ownership of the assigned identifiers, i.e. no one but - the user should be able to remove, revoke and/or reassign the user's identifiers. - -This model is commonly referred to as __Decentralized Identifiers__, and allows us to -build a new __(3P)__ digital identity: __Private, Permanent__ and -__Portable__. - -## 2. Access Considerations - -In order to be considered open, the system must be publicly available. Any user -should be able to freely register, publish and update as many identifiers as -desired without the express authorization of any third party. This characteristic -of the model permits us to classify it as __censorship resistant.__ - -At the same time, this level of openness makes the model vulnerable to malicious -intentions and abuse. In such a way that a bad actor may prevent legitimate access -to the system by consuming the available resources. This kind of cyber-attack is -known as a [DoS (Denial-of-Service) attack](https://en.wikipedia.org/wiki/Denial-of-service_attack). - -> In computing, a denial-of-service attack (DoS attack) is a cyber-attack in which - the perpetrator seeks to make a machine or network resource unavailable to its - intended users by temporarily or indefinitely disrupting services of a host - connected to the Internet. Denial of service is typically accomplished by - flooding the targeted machine or resource with superfluous requests in an attempt - to overload systems and prevent some or all legitimate requests from being - fulfilled. - -The `algo` DID Method specification includes a __"Request Ticket"__ security -mechanism designed to mitigate risks of abuse while ensuring open access and -censorship resistance. - -## 3. DID Method Specification - -The method specification provides all the technical considerations, guidelines and -recommendations produced for the design and deployment of the DID method -implementation. The document is organized in 3 main sections. - -1. __DID Schema.__ Definitions and conventions used to generate valid identifier - instances. -2. __DID Document.__ Considerations on how to generate and use the DID document - associated with a given identifier instance. -3. __Agent Protocol.__ Technical specifications detailing how to perform basic - network operations, and the risk mitigation mechanisms in place, for tasks such as: - - Publish a new identifier instance. - - Update an existing identifier instance. - - Resolve an existing identifier and retrieve the latest published version of - its DID Document. - -### 3.1 DID Schema - -A Decentralized Identifier is defined as a [RFC3986](https://tools.ietf.org/html/rfc3986) -Uniform Resource Identifier, with a format based on the generic DID schema. Fore more -information you can refer to the -[original documentation](https://w3c.github.io/did-core/#did-syntax). - -```abnf -did = "did:" method-name ":" method-specific-id -method-name = 1*method-char -method-char = %x61-7A / DIGIT -method-specific-id = *( *idchar ":" ) 1*idchar -idchar = ALPHA / DIGIT / "." / "-" / "_" / pct-encoded -pct-encoded = "%" HEXDIG HEXDIG -``` - -Example of a simple Decentralized Identifier (DID). - -``` -did:example:123456789abcdefghi -``` - -Expanding on the previous definitions the `algo` DID Method specification use the -following format. - -```abnf -did = "did:algo:" [tag ":"] specific-idstring -tag = 1*tagchar -specific-idstring = depends on the particular use case -tagchar = ALPHA / DIGIT / "." / "-" -``` - -The optional `tag` element provides a flexible namespace mechanism that can be used -to classify identifier instances into logical groups of arbitrary complexity. - -The `specific-idstring` field does not impose any format requirements to ensure the -maximum level of flexibility to end users and implementers. The official implementation -however, proposes and recommends two formal modes for id strings. - -A DID URL is a network location identifier for a specific resource. It can be used -to retrieve things like representations of DID subjects, verification methods, services, -specific parts of a DID document, or other resources. - -The following is the ABNF definition using the syntax in [RFC5234]. It builds on the -basic DID Syntax. The path-abempty, query, and fragment components are defined in -[RFC3986]. All DID URLs MUST conform to the DID URL Syntax ABNF Rules. - -```abnf -did-url = did path-abempty [ "?" query ] [ "#" fragment ] -``` - -#### 3.1.1 Mode UUID - -The id string should be a randomly generated lower-case UUID v4 instance as defined by -[RFC4122](https://tools.ietf.org/html/rfc4122). The formal schema for the -`specific-idstring` field on this mode is the following. - -```abnf -specific-idstring = time-low "-" time-mid "-" - time-high-and-version "-" - clock-seq-and-reserved - clock-seq-low "-" node -time-low = 4hexOctet -time-mid = 2hexOctet -time-high-and-version = 2hexOctet -clock-seq-and-reserved = hexOctet -clock-seq-low = hexOctet -node = 6hexOctet -hexOctet = hexDigit hexDigit -hexDigit = "0" / "1" / "2" / "3" / "4" / "5" / "6" / "7" / - "8" / "9" / "a" / "b" / "c" / "d" / "e" / "f" -``` - -Example of a DID instance of mode UUID with a `tag` value of `c137`. - -```abnf -did:algo:c137:02825c9d-6660-4f17-92db-2bd22c4ed902 -``` - -#### 3.1.2 Mode Hash - -The id string should be a randomly generated 32 bytes [SHA3-256](https://goo.gl/Wx8pTY) -hash value, encoded in hexadecimal format as a lower-case string of 64 characters. -The formal schema for the `specific-idstring` field on this mode is the following. - -```abnf -specific-idstring = 32hexOctet -hexOctet = hexDigit hexDigit -hexDigit = "0" / "1" / "2" / "3" / "4" / "5" / "6" / "7" / - "8" / "9" / "a" / "b" / "c" / "d" / "e" / "f" -``` - -Example of a DID instance of mode hash with a `tag` value of `c137`. - -``` -did:algo:c137:85d48aebe67da2fdd273d03071de663d4fdd470cff2f5f3b8b41839f8b07075c -``` - -### 3.2 DID Document - -A Decentralized Identifier, regardless of its particular method, can be resolved -to a standard resource describing the subject. This resource is called a -[DID Document](https://w3c.github.io/did-core/#dfn-did-documents), and typically -contains, among other relevant details, cryptographic material to support -authentication of the DID subject. - -> A DID Document set of data describing the DID subject, including mechanisms, such - as cryptographic public keys, that the DID subject or a DID delegate can use to - authenticate itself and prove its association with the DID. A DID document might - have one or more different representations. - -The document is a Linked Data structure that ensures a high degree of flexibility -while facilitating the process of acquiring, parsing and using the contained -information. For the moment, the suggested encoding format for the document is -[JSON-LD](https://www.w3.org/TR/json-ld/). Other formats could be used in the future. - -> The term Linked Data is used to describe a recommended best practice for exposing - sharing, and connecting information on the Web using standards, such as URLs, - to identify things and their properties. When information is presented as Linked - Data, other related information can be easily discovered and new information can be - easily linked to it. Linked Data is extensible in a decentralized way, greatly - reducing barriers to large scale integration. - -At the very least, the document must include the DID subject it's referring to under -the `id` key. - -```json -{ - "@context": "https://www.w3.org/ns/did/v1", - "id": "did:algo:c137:b616fca9-ad86-4be5-bc9c-0e3f8e27dc8d" -} -``` - -As it stands, this document is not very useful in itself. Other relevant details that -are often included in a DID Document are: - -- [Created:](https://w3c-ccg.github.io/did-spec/#created-optional) - Timestamp of the original creation. -- [Updated:](https://w3c-ccg.github.io/did-spec/#updated-optional) - Timestamp of the most recent change. -- [Public Keys:](https://w3c-ccg.github.io/did-spec/#public-keys) - Public keys are used for digital signatures, encryption and other cryptographic - operations, which in turn are the basis for purposes such as authentication, secure - communication, etc. -- [Authentication:](https://w3c-ccg.github.io/did-spec/#authentication) - List the enabled mechanisms by which the DID subject can cryptographically prove - that they are, in fact, associated with a DID Document. -- [Services:](https://w3c-ccg.github.io/did-spec/#service-endpoints) - In addition to publication of authentication and authorization mechanisms, the - other primary purpose of a DID Document is to enable discovery of service endpoints - for the subject. A service endpoint may represent any type of service the subject - wishes to advertise, including decentralized identity management services for - further discovery, authentication, authorization, or interaction. - -Additionally, the DID Document may include any other fields deemed relevant for the -particular use case or implementation. - -Example of a more complete, and useful, DID Document. -```json -{ - "@context": [ - "https://www.w3.org/ns/did/v1", - "https://w3id.org/security/v1" - ], - "id": "did:algo:85eda27f-eb7b-4a3b-9ea3-913188511802", - "created": "2021-10-15T02:13:56Z", - "updated": "2021-10-19T21:14:53Z", - "verificationMethod": [ - { - "id": "did:algo:85eda27f-eb7b-4a3b-9ea3-913188511802#master", - "type": "Ed25519VerificationKey2018", - "controller": "did:algo:85eda27f-eb7b-4a3b-9ea3-913188511802", - "publicKeyMultibase": "zCh9PDTZzeWxk2WdH4M1e8k2951D5D11jz7Uti9HRBGiK" - } - ], - "authentication": [ - "did:algo:85eda27f-eb7b-4a3b-9ea3-913188511802#master" - ], - "service": [ - { - "id": "did:algo:85eda27f-eb7b-4a3b-9ea3-913188511802#algo-connect", - "type": "did.algorand.foundation.ExternalService", - "serviceEndpoint": "https://did.algorand.foundation", - "extensions": [ - { - "id": "algo-address", - "version": "0.1.0", - "data": [ - { - "address": "Q4HSY6GM7AJGVSZGWPI5NZW2TJ4SIFHPSBXG4MCL72B5DAJL3PCCXIE3HI", - "asset": "ALGO", - "network": "testnet" - } - ] - } - ] - } - ] -} -``` - -Is important to note that the official specifications around service endpoints are -still in a very early stage at this point. Where appropriate or required the present -Method specification builds on it and introduces new considerations. - -#### 3.2.1 Method Requirements - -Building upon the base requirements and recommendations from the original -specification, the `algo` DID method introduces the following additional guidelines. - -- The fields `created` and `updated` are required for all generated DID Documents. -- All service endpoints included in the DID Document may include an additional `data` - field. Is recommended to include all extra parameters required for the particular - service under this field. -- Supported verification methods and signature formats - - [Ed25519](https://w3c-ccg.github.io/ld-cryptosuite-registry/#ed25519signature2018) - - [RSA](https://w3c-ccg.github.io/ld-cryptosuite-registry/#rsasignature2018) - (with a minimum length of 4096 bits). - - [secp256k1](https://w3c-ccg.github.io/ld-cryptosuite-registry/#eddsasasignaturesecp256k1) - -More information on the official keys and signatures formats is available at -[LD Cryptographic Suite Registry](https://w3c-ccg.github.io/ld-cryptosuite-registry/). - -#### 3.2.2 Proofs - -[proof:](https://www.w3.org/TR/vc-data-model/#proofs-signatures) Cryptographic proof -of the integrity of the DID Document according its subject. Recently it was removed -from the DID core document. This method still generates valid proofs for all mutations -performed on the DID documents and returns it under the `proof` element of all -resolved identifiers. - -```json -{ - "document": "...", - "proof": { - "@context": [ - "https://w3id.org/security/v1" - ], - "type": "Ed25519Signature2018", - "created": "2020-08-08T03:12:53Z", - "domain": "did.algorandfoundation.org", - "nonce": "3ec84acf8b301f3d7e0bba25a24b438a", - "proofPurpose": "authentication", - "verificationMethod": "did:algo:46389176-6109-4de7-bdb4-67e4fcf0230d#master", - "proofValue": "QvVkJxTWHf6BQO5A/RzgqDoz6neKaagHWspwSeWqztWnjnt7Rlc73KKiHRs9++C2tdV3pZQtPiKDk6C7Q7nFAQ==" - } -} -``` - -> More information about this change is [available here](https://github.com/w3c/did-core/issues/293). - -### 3.3 Agent Protocol - -The method implementation introduces the concept of a __network agent__. A network -agent is responsible for handling incoming client requests. It's very important to -note that the agent itself adheres to an operational protocol. The protocol is -independent of the data storage and message delivery mechanisms used. The method -protocol can be implemented using a __Distributed Ledger Platform__, as well as any -other infrastructure components suitable for the particular use case. - -There are two main groups of operations available, __read__ and __write__. Write -operations are required when a user wishes to publish a new identifier record to -the network, or update the available information for an existing one. Read -operations enable resolution and retrieval of DID Documents and other relevant -assets published in the network. - -#### 3.3.1 Request Ticket - -As described earlier, a security mechanism is required to prevent malicious and -abusive activities. For these purposes, we introduce a __ticket__ requirement for all -write network operations. The mechanism is based on the original -[HashCash](http://www.hashcash.org/hashcash.pdf) algorithm and aims to mitigate -the following problems. - -- __Discourage [DoS Attacks](https://en.wikipedia.org/wiki/Denial-of-service_attack)__. - By making the user cover the “costs” of submitting a request for processing. -- __Prevent [Replay Attacks](https://en.wikipedia.org/wiki/Replay_attack)__. - Validating the ticket was specifically generated for the request being processed. -- __Authentication__. - Ensuring the user submitting the ticket is the owner of the DID, by incorporating - a digital signature requirement that covers both the ticket details and the - DID instance. - -A request ticket has the following structure. - -```protobuf -message Ticket { - int64 timestamp = 1; - int64 nonce_value = 2; - string key_id = 3; - bytes document = 4; - bytes proof = 5; - bytes signature = 6; -} -``` - -The client generates a ticket for the request using the following algorithm. - -1. Let the __"bel"__ function be a method to produce a deterministic binary-encoded - representation of a given input value using little endian byte order. -2. Let the __"hex"__ function be a method to produce a deterministic hexadecimal - binary-encoded representation of a given input value. -3. __"timestamp"__ is set to the current UNIX time at the moment of creating the - ticket. -4. __"nonce"__ is a randomly generated integer of 64bit precision. -5. __"key_id"__ is set to the identifier from the cryptographic key used to - generate the ticket signature, MUST be enabled as an authentication key for the - DID instance. -6. __"document"__ is set to the JSON-encoded DID Document to process. -7. __"proof"__ is set to the JSON-encoded valid proof for the DID Document to process. -8. A HashCash round is initiated for the ticket. The hash mechanism used MUST be - SHA3-256 and the content submitted for each iteration of the round is a byte - concatenation of the form: - `"bel(timestamp) | bel(nonce) | hex(key_id) | document | proof"`. -9. The __"nonce"__ value of the ticket is atomically increased by one for each - iteration of the round. -10. The ticket's __"challenge"__ is implicitly set to the produced hash from the - HashCash round. -11. The __"signature"__ for the ticket is generated using the selected key of the DID - and the obtained challenge value: `did.keys["key_id"].sign(challenge)` - -Upon receiving a new write request the network agent validates the request ticket -using the following procedure. - -1. Verify the ticket's `challenge` is valid by performing a HashCash - verification. -2. Validate `document` are a properly encoded DID Document. -3. Validate `proof` is valid for the DID Document included in the ticket. -4. DID instance `method` value is properly set and supported by the agent. -5. Ensure `document` don’t include any private key. For security reasons no - private keys should ever be published on the network. -6. Verify `signature` is valid. - - For operations submitting a new entry, the key contents are obtained directly - from the ticket contents. This ensures the user submitting the new DID instance - is the one in control of the corresponding private key. - - For operations updating an existing entry, the key contents are obtained from - the previously stored record. This ensures the user submitting the request is - the one in control of the original private key. -7. If the request is valid, the entry will be created or updated accordingly. - -A sample implementation of the described __Request Ticket__ mechanism is available -[here](https://github.com/algorandfoundation/did-algo/blob/master/proto/v1/ticket.go). - -#### 3.3.2 DID Resolution - -The simplest mechanism to resolve a particular DID instance to the latest published -version of its corresponding DID Document is using the provided CLI client. - -```shell -algoid get did:algo:85eda27f-eb7b-4a3b-9ea3-913188511802 -``` - -The resolution and data retrieval is done via the agent's HTTP interface, performing -an HTTP __GET__ request of the form: - -`https://did.algorand.foundation/v1/retrieve/{{method}}/{{subject}}` - -For example: - -```bash -curl -v https://did.algorand.foundation/v1/retrieve/algo/85eda27f-eb7b-4a3b-9ea3-913188511802 -``` - -If the subject is valid, and information has been published to the network, the -response will include the latest version available of its corresponding DID Document -encoded in JSON-LD with a __200__ status code. If no information is available the -response will be a JSON encoded error message with a __404__ status code. - -```json -{ - "document": "...", - "proof": "..." -} -``` - -You can also retrieve an existing subject using the provided SDK and RPC interface. -For example, using the Go client. - -```go -// Error handling omitted for brevity -sub := "c137:eeb0c865-ce21-4ad6-baf8-5ba287ba8683" -response, _ := client.Retrieve(context.TODO(), proto.Request{Subject:sub}) -if response.Ok { - id := new(did.Identifier) - id.Decode(response.Contents) -} -``` - -#### 3.3.3 DID Publishing and Update - -To publish a new identifier instance or to update an existing one you can also use -the agent's HTTP interface or the provided SDK and clients. - -When using HTTP the operation should be a __POST__ request with a properly -constructed and JSON-encoded request as the request's data. Binary data should be -encoded in standard [Base64](https://en.wikipedia.org/wiki/Base64) when transmitted -using JSON. - -You can also publish and update a DID identifier instance using the provided SDK and -RPC interface. For example, using the Go client. - -```go -// Error handling omitted for brevity -res, _ := client.Process(context.TODO(), request) -if res.Ok { - // ... -} -``` - -## 4. Client Operations - -> To enable the full functionality of DIDs and DID Documents on a particular - distributed ledger or network (called the target system), a DID method - specification MUST specify how each of the following CRUD operations is performed - by a client. Each operation MUST be specified to the level of detail necessary to - build and test interoperable client implementations with the target system. - -The following sections provide detailed descriptions and examples of all required -CRUD base operations and some more advanced use cases. As described earlier, all -supported operations can be accessed using either the agent's HTTP interface or the -provided SDK and CLI client tool. - -For brevity the following examples use the provided CLI client tool. - -### 4.1 CRUD Operations - -Basic operations enabling the users to create, read, update and delete identifier -instances. - -#### 4.1.1 Create (Register) - -To locally create a new DID instance. - -```sh -algoid create [reference name] -``` - -The value provided for `reference name` is an easy-to-remember alias you choose for -the new identifier instance, __it won't have any use in the network context__. -The CLI also performs the following tasks for the newly generated identifier. - -- Create a new `master` Ed25519 private key for the identifier -- Set the `master` key as an authentication mechanism for the identifier -- Generates a cryptographic integrity proof for the identifier using the `master` key - -If required, the `master` key can be recovered using the selected `recovery-mode`, -for more information inspect the options available for the `create` command. - -``` -Creates a new DID locally - -Usage: - algoid register [flags] - -Aliases: - register, create, new - -Examples: -algoid register [DID reference name] - -Flags: - -h, --help help for register - -m, --method string method value for the identifier instance (default "algo") - -p, --passphrase set a passphrase as recovery method for the primary key - -s, --secret-sharing string number of shares and threshold value: shares,threshold (default "3,2") - -t, --tag string tag value for the identifier instance -``` - -#### 4.1.2 Read (Verify) - -You can retrieve a list of all your existing identifiers using the following command. - -```sh -algoid list -``` - -The output produced will be something like this. - -``` -Name DID -my-id did:algo:85eda27f-eb7b-4a3b-9ea3-913188511802 -sample did:algo:99dc4a30-7434-42e5-ac75-5f330be0ea0a -``` - -To inspect the DID Document of your local identifiers. - -``` -algoid info [reference name] -``` - -The generated document will be something similar for the following example. - -```json -{ - "@context": [ - "https://www.w3.org/ns/did/v1", - "https://w3id.org/security/v1" - ], - "id": "did:algo:85eda27f-eb7b-4a3b-9ea3-913188511802", - "created": "2021-10-15T02:13:56Z", - "updated": "2021-10-19T21:14:53Z", - "verificationMethod": [ - { - "id": "did:algo:85eda27f-eb7b-4a3b-9ea3-913188511802#master", - "type": "Ed25519VerificationKey2018", - "controller": "did:algo:85eda27f-eb7b-4a3b-9ea3-913188511802", - "publicKeyMultibase": "zCh9PDTZzeWxk2WdH4M1e8k2951D5D11jz7Uti9HRBGiK" - } - ], - "authentication": [ - "did:algo:85eda27f-eb7b-4a3b-9ea3-913188511802#master" - ], - "service": [ - { - "id": "did:algo:85eda27f-eb7b-4a3b-9ea3-913188511802#algo-connect", - "type": "did.algorand.foundation.ExternalService", - "serviceEndpoint": "https://did.algorand.foundation", - "extensions": [ - { - "id": "algo-address", - "version": "0.1.0", - "data": [ - { - "address": "Q4HSY6GM7AJGVSZGWPI5NZW2TJ4SIFHPSBXG4MCL72B5DAJL3PCCXIE3HI", - "asset": "ALGO", - "network": "testnet" - } - ] - } - ] - } - ] -} -``` - -#### 4.1.3 Update (Publish) - -Whenever you wish to make one of your identifiers, in its current state, accessible -to the world, you can publish it to the network. - -```sh -algoid sync sample -``` - -The CLI tool will generate the __Request Ticket__, submit the operation for -processing to the network and present the final result. - -``` -2021-10-19T17:12:39-05:00 DBG key selected for the operation: did:algo:85eda27f-eb7b-4a3b-9ea3-913188511802#master -2021-10-19T17:12:39-05:00 INF publishing: my-id -2021-10-19T17:12:39-05:00 INF generating request ticket pow=8 -2021-10-19T17:12:39-05:00 DBG ticket obtained: 0081d16b3603582c5f585eeaa432fe0aa1595b43b19ffaa2b829a92aac18148b -2021-10-19T17:12:39-05:00 DBG time: 1.673612ms (rounds completed 38) -2021-10-19T17:12:39-05:00 INF establishing connection to network agent: localhost:9090 -2021-10-19T17:12:39-05:00 INF submitting request to the network -2021-10-19T17:12:42-05:00 DBG request status: true -2021-10-19T17:12:42-05:00 INF identifier: /ipfs/bafkreicvif7gb62oofiyposafedjfc6pmwqjr7qkfix7yu426zolmn3rxq -``` - -Once an identifier is published any user can retrieve and validate your DID document. -If you make local changes to your identifier, like adding a new cryptographic key or -service endpoint, and you wish these adjustments to be accessible to the rest of the -users, you'll need to publish it again. - -### 4.2 DID Instance Management - -The CLI client also facilitates some tasks required to manage a DID instance. - -#### 4.2.1 Key Management - -A DID Document list all public keys in use for the referenced DID instance. Public -keys are used for digital signatures, encryption and other cryptographic operations, -which in turn are the basis for purposes such as authentication, secure communication, -etc. - -``` -Manage cryptographic keys associated with the DID - -Usage: - algoid edit key [command] - -Available Commands: - add Add a new cryptographic key for the DID - remove Remove an existing cryptographic key for the DID -``` - -To add a new cryptographic key to one of your identifiers you can use the `did key add` -command. - -``` -Add a new cryptographic key for the DID - -Usage: - algoid edit key add [flags] - -Examples: -algoid edit key add [DID reference name] --name my-new-key --type ed --authentication - -Flags: - -a, --authentication enable this key for authentication purposes - -h, --help help for add - -n, --name string name to be assigned to the newly added key (default "key-#") - -t, --type string type of cryptographic key: RSA (rsa), Ed25519 (ed) or secp256k1 (koblitz) (default "ed") -``` - -It will produce and properly add a public key entry. The cryptographic -integrity proof on the DID Document will also be updated accordingly. - -```json -{ - "id": "did:algo:4d81bd52-2edb-4703-b8fc-b26d514a9c56#code-sign", - "type": "Ed25519VerificationKey2018", - "controller": "did:algo:4d81bd52-2edb-4703-b8fc-b26d514a9c56", - "publicKeyMultibase": "zCh9PDTZzeWxk2WdH4M1e8k2951D5D11jz7Uti9HRBGiK" -} -``` - -You can also safely remove an existing key from your identifier using the -`edit key rm` command. - -``` -Remove an existing cryptographic key for the DID - -Usage: - algoid edit key remove [flags] - -Aliases: - remove, rm - -Examples: -algoid edit key remove [DID reference name] [key name] -``` - -#### 4.2.2 Linked Data Signatures - -The CLI client also facilitates the process of generating and validating [Linked Data -Signatures](https://w3c-dvcg.github.io/ld-signatures/). - -``` -Produce a linked digital proof document - -Usage: - algoid proof [flags] - -Aliases: - proof, sign - -Examples: -algoid proof [DID reference name] --input "contents to sign" - -Flags: - -d, --domain string domain value to use (default "did.algorand.foundation") - -h, --help help for proof - -i, --input string contents to sign - -k, --key string key to use to produce the proof (default "master") - -p, --purpose string specific intent for the proof (default "authentication") -``` - -For example, to create a new signature document from an existing file you can run the -following command. - -```sh -cat file_to_sign | algoid sign my-id -``` - -The output produced will be a valid JSON-LD document containing the signature details. - -```json -{ - "@context": [ - "https://w3id.org/security/v1" - ], - "type": "Ed25519Signature2018", - "creator": "did:algo:4d81bd52-2edb-4703-b8fc-b26d514a9c56#master", - "created": "2019-03-15T14:05:54Z", - "domain": "did.algorandfoundation.org", - "nonce": "f14d4619a39f7deb5a382bf32b220726", - "signatureValue": "khqsBcnCViYm/3QFjgAQX2iOGDbNpsD5rPWsokWNLsBxhtRf79A+qV1f+9sphjVCxNP02jesOOni3t9zMCZbBw==" -} -``` - -You can save and share the produced JSON output. Other users will be able to verify the -integrity and authenticity of the signature using the `verify` command. - -```sh -cat file_to_sign | algoid verify signature.json -``` - -The CLI will inspect the signature file, retrieve the DID Document for the creator -and use the public key to verify the integrity and authenticity of the signature. - -``` -2021-10-19T17:22:36-05:00 INF verifying proof document -2021-10-19T17:22:36-05:00 DBG load signature file -2021-10-19T17:22:36-05:00 DBG decoding contents -2021-10-19T17:22:36-05:00 DBG validating proof verification method -2021-10-19T17:22:38-05:00 INF proof is valid -``` - -#### 4.2.3 Service Management - -As mentioned in earlier sections, one of the more relevant aspects of a DID Document -is its capability to list interaction mechanisms available for a particular subject. -This is done by including information of __Service Endpoints__ in the document. Using -the CLI client you can manage the services enabled for any of your identifiers. - -``` -Manage services enabled for the identifier - -Usage: - algoid edit service [command] - -Available Commands: - add Register a new service entry for the DID - remove Remove an existing service entry for the DID -``` - -To add a new service you can use the `edit service add` command. - -``` -Register a new service entry for the DID - -Usage: - algoid edit service add [flags] - -Examples: -algoid edit service add [DID] --name my-service --endpoint https://www.agency.com/user_id - -Flags: - -e, --endpoint string main URL to access the service - -h, --help help for add - -n, --name string service's reference name (default "external-service-#") - -t, --type string type identifier for the service handler (default "did.algorand.foundation.ExternalService") -``` - -It will produce and properly add a service endpoint entry. The cryptographic -integrity proof on the DID Document will also be updated accordingly. - -```json -{ - "id": "did:algo:85eda27f-eb7b-4a3b-9ea3-913188511802#algo-connect", - "type": "did.algorand.foundation.ExternalService", - "serviceEndpoint": "https://did.algorand.foundation", -} -``` - -You can also safely remove a service from your identifier using the -`edit service remove` command. - -```sh -Remove an existing service entry for the DID - -Usage: - algoid edit service remove [flags] - -Aliases: - remove, rm - -Examples: -algoid edit service remove [DID reference name] [service name] -``` - -## 5. Cryptography Notice - -This distribution includes cryptographic software. The country in which you currently -reside may have restrictions on the import, possession, use, and/or re-export to another -country, of encryption software. BEFORE using any encryption software, please check your -country's laws, regulations and policies concerning the import, possession, or use, and -re-export of encryption software, to see if this is permitted. -See for more information. - -The U.S. Government Department of Commerce, Bureau of Industry and Security (BIS), has -classified this software as Export Commodity Control Number (ECCN) 5D002.C.1, which -includes information security software using or performing cryptographic functions with -asymmetric algorithms. The form and manner of this distribution makes it eligible for -export under the License Exception ENC Technology Software Unrestricted (TSU) exception -(see the BIS Export Administration Regulations, Section 740.13) for both object code and -source code. +The full spec for `did-algo` can be read in [SPEC.md](./SPEC.md) diff --git a/SPEC.md b/SPEC.md new file mode 100644 index 0000000..d630fcb --- /dev/null +++ b/SPEC.md @@ -0,0 +1,57 @@ +# Abstract + +Decentralized Identifiers (DIDs) are a new type of identifier for that enable decentralized dentification of various entities. DIDs are designed to enable individuals and organizations to generate their own identifiers using systems they trust. These new identifiers enable entities to prove control over them by authenticating using cryptographic proofs such as digital signatures. + +Most DIDs involve a registry which is often, but not always, a distributed ledger technology (DLT). This specification utilize Algorand, a blockchain DLT, as the registry for identities. The initial method defined in this spec utilizes a stateful application on an Algorand network but this spec may be expanded in the future to allow further ways of resolving identity using Algorand. + +# 1. Introduction + +## 1.1 Goals + +The primary goal of `did:algo` is to leverage the unique features of Algorand to provide a reliable decentralized resolution of DIDs. In paticular, `did:algo` is designed to inherit the permisionless nature of Algorand and allow anyone to publish an identifier in the immutable ledger. Publishers of the DID and its corresponding DID document **MAY** be the subject that is identified, but `did:algo` also enables 3rd party entites to publish DID documents on behalf of others in a verifiable manner. For example, one user, identified by their ed25519 public key, may have multiple DIDs via the `did:algo` method provided by various entities. + +# 2. Terminology + +## Algorand app + +A stateful smart contract that exists on the Algorand blockchain. Every app has a unique uint64 identifier. + +## non-archival algod node + +The Algorand node software that has the entirity of active state but not necessarily the full chain history + +## box storage + +A type of key-value storage available in Algorand apps + +## Algorand address + +The identifier for an account in the Algorand ledger. The address is derived from an ed25519 public key. + +## auth address + +The Algorand address that has authority to sign transactions for a given Algorand address + +## ARC4 ABI + +The Application Binary Interface (ABI) defined in ARC4 for Algorand smart contracts + +# 3. The did:algo Format + +The ABNF for the `did:algo` format is described below + +```abnf +did-algo-format = "did:algo" [":" algorand-network] ":" algorand-namespace ":" namespace-format + +algorand-network = "testnet' +algorand-namepspace = app-namespace + +app-namespace = algorand-app ":" hex-ed25519key + +algorand-app = 1*DIGIT ; The unsigned 64-bit integer for an application on the Algorand network +hex-ed25519key = 64HEXDIG ; The public ed25519 key of the subject encoded in base16 +``` + +# 4. Implementation + +TODO From a6f5eb66fdc8b649cf3393d29dd02d28580088c3 Mon Sep 17 00:00:00 2001 From: Joe Polny Date: Wed, 8 May 2024 18:15:39 -0400 Subject: [PATCH 09/27] update algorand-network --- SPEC.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SPEC.md b/SPEC.md index d630fcb..d947593 100644 --- a/SPEC.md +++ b/SPEC.md @@ -43,7 +43,7 @@ The ABNF for the `did:algo` format is described below ```abnf did-algo-format = "did:algo" [":" algorand-network] ":" algorand-namespace ":" namespace-format -algorand-network = "testnet' +algorand-network = "testnet" / "mainnet" ; If omitted, the algorand-network is implicity "mainnet" algorand-namepspace = app-namespace app-namespace = algorand-app ":" hex-ed25519key From 9b0ca75f1fa830956750daf3103d6a0e18ac0216 Mon Sep 17 00:00:00 2001 From: Joe Polny Date: Wed, 8 May 2024 18:25:35 -0400 Subject: [PATCH 10/27] add "custom" network --- SPEC.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SPEC.md b/SPEC.md index d947593..1e6a9ba 100644 --- a/SPEC.md +++ b/SPEC.md @@ -43,7 +43,7 @@ The ABNF for the `did:algo` format is described below ```abnf did-algo-format = "did:algo" [":" algorand-network] ":" algorand-namespace ":" namespace-format -algorand-network = "testnet" / "mainnet" ; If omitted, the algorand-network is implicity "mainnet" +algorand-network = "testnet" / "mainnet" / "custom" ; If omitted, the algorand-network is implicity "mainnet" algorand-namepspace = app-namespace app-namespace = algorand-app ":" hex-ed25519key From 55feb463db0a01ba3ea8584a421e3340b6b178ea Mon Sep 17 00:00:00 2001 From: Joe Polny Date: Wed, 8 May 2024 18:34:53 -0400 Subject: [PATCH 11/27] update ABNF --- SPEC.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/SPEC.md b/SPEC.md index 1e6a9ba..3bc44ce 100644 --- a/SPEC.md +++ b/SPEC.md @@ -41,12 +41,13 @@ The Application Binary Interface (ABI) defined in ARC4 for Algorand smart contra The ABNF for the `did:algo` format is described below ```abnf -did-algo-format = "did:algo" [":" algorand-network] ":" algorand-namespace ":" namespace-format +did-algo-format = "did:algo" [":" network] ":" namespace -algorand-network = "testnet" / "mainnet" / "custom" ; If omitted, the algorand-network is implicity "mainnet" -algorand-namepspace = app-namespace +network = "testnet" / "mainnet" / "custom" ; If omitted, the algorand-network is implicity "mainnet" -app-namespace = algorand-app ":" hex-ed25519key +namespace = app-namespace ; Currently only one namespace is supported, but there may be more in the future + +app-namespace = "app:" algorand-app ":" hex-ed25519key algorand-app = 1*DIGIT ; The unsigned 64-bit integer for an application on the Algorand network hex-ed25519key = 64HEXDIG ; The public ed25519 key of the subject encoded in base16 From 22936c75f2e0c1edeeae36aeebdcc532a40397ef Mon Sep 17 00:00:00 2001 From: Joe Polny Date: Wed, 8 May 2024 20:24:43 -0400 Subject: [PATCH 12/27] add Specification to spec --- SPEC.md | 54 +++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 47 insertions(+), 7 deletions(-) diff --git a/SPEC.md b/SPEC.md index 3bc44ce..506055f 100644 --- a/SPEC.md +++ b/SPEC.md @@ -1,6 +1,6 @@ # Abstract -Decentralized Identifiers (DIDs) are a new type of identifier for that enable decentralized dentification of various entities. DIDs are designed to enable individuals and organizations to generate their own identifiers using systems they trust. These new identifiers enable entities to prove control over them by authenticating using cryptographic proofs such as digital signatures. +Decentralized Identifiers (DIDs) are a new type of identifier for that enable decentralized identification of various entities. DIDs are designed to enable individuals and organizations to generate their own identifiers using systems they trust. These new identifiers enable entities to prove control over them by authenticating using cryptographic proofs such as digital signatures. Most DIDs involve a registry which is often, but not always, a distributed ledger technology (DLT). This specification utilize Algorand, a blockchain DLT, as the registry for identities. The initial method defined in this spec utilizes a stateful application on an Algorand network but this spec may be expanded in the future to allow further ways of resolving identity using Algorand. @@ -18,7 +18,7 @@ A stateful smart contract that exists on the Algorand blockchain. Every app has ## non-archival algod node -The Algorand node software that has the entirity of active state but not necessarily the full chain history +The Algorand node software that has the entirety of active state but not necessarily the full chain history ## box storage @@ -43,16 +43,56 @@ The ABNF for the `did:algo` format is described below ```abnf did-algo-format = "did:algo" [":" network] ":" namespace -network = "testnet" / "mainnet" / "custom" ; If omitted, the algorand-network is implicity "mainnet" +network = "testnet" / "mainnet" / "betanet" / "custom" ; If omitted, the algorand-network is implicitly "mainnet" namespace = app-namespace ; Currently only one namespace is supported, but there may be more in the future -app-namespace = "app:" algorand-app ":" hex-ed25519key +app-namespace = "app:" algorand-app ":" hex-ed25519-key algorand-app = 1*DIGIT ; The unsigned 64-bit integer for an application on the Algorand network -hex-ed25519key = 64HEXDIG ; The public ed25519 key of the subject encoded in base16 +hex-ed25519-key = 64HEXDIG ; The public ed25519 key of the subject encoded in base16 ``` -# 4. Implementation +# 4. Specification -TODO +## 4.1 Network + +The `network` in the `did:algo` method is used to specify which Algorand network must be used to resolve the DID. To resolve DID, only a non-archival algod node is needed. Resolvers SHOULD check the genesis hash of the node they are using to resolve the DID and verify it matches the network in the DID. It should be noted that this is NOT a security measure. A malicious node could serve incorrect genesis hash or box data through the API so it is important for resolvers to use a trusted node for resolution. + +The table of supported networks and their corresponding genesis hashes is below. + +| Network | Genesis Hash (base64) | +| ------- | -------------------------------------------- | +| mainnet | wGHE2Pwdvd7S12BL5FaOP20EGYesN73ktiC1qzkkit8= | +| testnet | SGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/cOUJOiI= | +| betanet | mFgazF+2uRS1tMiL9dsj01hJGySEmPN28B/TjjvpVW0= | + +If the network in the DID is `custom`, the genesis hash MUST NOT be verified by resolvers. The primary use-case for `custom` is development on a local network. + +## 4.2 App Namespace + +### 4.2.1 Controller + +DIDDocuments created for `did:algo:app` DIDs SHOULD NOT have a controller field. Consumers of the DIDDocument MUST NOT assume the subject is the controller. In the reference implementation the controller is an Algorand address, which may have a dynamic auth address. This means ed25519 verification methods are not sufficient for verification and MUST NOT be used to identify the address that has permission to modify the in-app data. + +In other app implementations there may be one or more entities that control the DIDDocument data. Since these entities do not have a DID, the only way to verify controllers for a `did:algo` DID is to inspect the TEAL code of the app. + +If a controller field is set, it should be assumed that there are additional Algorand entities without a DID that may have control over the document. + +### 4.2.2 Metadata Box + +In order to support `did:algo:app` resolution, an application must contain a box with a 32 byte key corresponding to the `hex-ed25519-key` of the subject. This box must contain the ARC4 ABI tuple: `(uint64,uint8,uint64,uint64)` this data structure shall be referred to as the metadata box. Additional data MAY be in the metadata box, but it MUST NOT alter the five values defined here. This means if following ARC4 ABI encoding, additional fields MUST be static types. + +`metadata[0]` is a `uint64` indicating the key of the data box that contains the start of the DIDDocument for the subject. + +`metadata[1]` is a `uint64` indicating the key of the data box that contains the end of the DIDdocument for the subject. Reading the contents of `metadata[0]` to `metadata[1]` (inclusive) will result in the full DIDDocument for the subject. If `metadata[0] == metadata[1]`, the entire DIDDocument is in a single box. + +`metadata[2]` is a `uint8` containing the status of the binary data for the subject's DID document within the app. A value of `0` indicates the data is currently being uploaded and is not currently resolvable. A value of `1` indicates the data is ready and can be resolved. A value of `2` indicates the data is being deleted and is not resolvable. + +`metadata[3]` is a `uint64` indicating the amount of bytes in the final box. + +`lastDeleted[4]` is a `uint64` indicating which data box was last deleted. This is used to ensure data is properly deleted. + +### 4.2.3 Data box + +The data for the DIDDocument may be split across multiple boxes since each box can only hold 4 kilobytes of data. Data boxes are referenced via their `uint64` keys. DIDDocuments MUST be read and written sequentially across data boxes. From 75077b7b9e596a37a16082a95bbdccadb67eede5 Mon Sep 17 00:00:00 2001 From: Joe Polny Date: Thu, 9 May 2024 07:16:20 -0400 Subject: [PATCH 13/27] update metadata --- SPEC.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/SPEC.md b/SPEC.md index 506055f..b927503 100644 --- a/SPEC.md +++ b/SPEC.md @@ -81,18 +81,16 @@ If a controller field is set, it should be assumed that there are additional Alg ### 4.2.2 Metadata Box -In order to support `did:algo:app` resolution, an application must contain a box with a 32 byte key corresponding to the `hex-ed25519-key` of the subject. This box must contain the ARC4 ABI tuple: `(uint64,uint8,uint64,uint64)` this data structure shall be referred to as the metadata box. Additional data MAY be in the metadata box, but it MUST NOT alter the five values defined here. This means if following ARC4 ABI encoding, additional fields MUST be static types. +In order to support `did:algo:app` resolution, an application must contain a box with a 32 byte key corresponding to the `hex-ed25519-key` of the subject. The value of this box box must start with the ARC4 ABI tuple: `(uint64,uint64,uint8,uint64)` this data structure shall be referred to as the metadata box. Additional data MAY be in the metadata box, but it MUST NOT alter the four initial values defined here. `metadata[0]` is a `uint64` indicating the key of the data box that contains the start of the DIDDocument for the subject. -`metadata[1]` is a `uint64` indicating the key of the data box that contains the end of the DIDdocument for the subject. Reading the contents of `metadata[0]` to `metadata[1]` (inclusive) will result in the full DIDDocument for the subject. If `metadata[0] == metadata[1]`, the entire DIDDocument is in a single box. +`metadata[1]` is a `uint64` indicating the key of the data box that contains the end of the DIDdocument for the subject. Reading the contents of `metadata[0]` to `metadata[1]` (inclusive) sequentially will result in the full DIDDocument for the subject. If `metadata[0] == metadata[1]`, the entire DIDDocument is in a single box. `metadata[2]` is a `uint8` containing the status of the binary data for the subject's DID document within the app. A value of `0` indicates the data is currently being uploaded and is not currently resolvable. A value of `1` indicates the data is ready and can be resolved. A value of `2` indicates the data is being deleted and is not resolvable. `metadata[3]` is a `uint64` indicating the amount of bytes in the final box. -`lastDeleted[4]` is a `uint64` indicating which data box was last deleted. This is used to ensure data is properly deleted. - ### 4.2.3 Data box The data for the DIDDocument may be split across multiple boxes since each box can only hold 4 kilobytes of data. Data boxes are referenced via their `uint64` keys. DIDDocuments MUST be read and written sequentially across data boxes. From e98fa96578d836e147c62e7a3de3828105fd19a5 Mon Sep 17 00:00:00 2001 From: Joe Polny Date: Thu, 9 May 2024 07:17:05 -0400 Subject: [PATCH 14/27] typos --- SPEC.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SPEC.md b/SPEC.md index b927503..8cf4cfe 100644 --- a/SPEC.md +++ b/SPEC.md @@ -8,7 +8,7 @@ Most DIDs involve a registry which is often, but not always, a distributed ledge ## 1.1 Goals -The primary goal of `did:algo` is to leverage the unique features of Algorand to provide a reliable decentralized resolution of DIDs. In paticular, `did:algo` is designed to inherit the permisionless nature of Algorand and allow anyone to publish an identifier in the immutable ledger. Publishers of the DID and its corresponding DID document **MAY** be the subject that is identified, but `did:algo` also enables 3rd party entites to publish DID documents on behalf of others in a verifiable manner. For example, one user, identified by their ed25519 public key, may have multiple DIDs via the `did:algo` method provided by various entities. +The primary goal of `did:algo` is to leverage the unique features of Algorand to provide a reliable decentralized resolution of DIDs. In particular, `did:algo` is designed to inherit the permission-less nature of Algorand and allow anyone to publish an identifier in the immutable ledger. Publishers of the DID and its corresponding DID document **MAY** be the subject that is identified, but `did:algo` also enables 3rd party entities to publish DID documents on behalf of others in a verifiable manner. For example, one user, identified by their ed25519 public key, may have multiple DIDs via the `did:algo` method provided by various entities. # 2. Terminology From 61a8c3c3634597c54828ea82b4c9a60cd8c54020 Mon Sep 17 00:00:00 2001 From: Joe Polny Date: Thu, 9 May 2024 07:31:01 -0400 Subject: [PATCH 15/27] links/formatting --- SPEC.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/SPEC.md b/SPEC.md index 8cf4cfe..6f42010 100644 --- a/SPEC.md +++ b/SPEC.md @@ -34,7 +34,7 @@ The Algorand address that has authority to sign transactions for a given Algoran ## ARC4 ABI -The Application Binary Interface (ABI) defined in ARC4 for Algorand smart contracts +The Application Binary Interface (ABI) defined in [ARC4](https://github.com/algorandfoundation/ARCs/blob/main/ARCs/arc-0004.md) for Algorand smart contracts # 3. The did:algo Format @@ -57,7 +57,7 @@ hex-ed25519-key = 64HEXDIG ; The public ed25519 key of the subject encoded in ba ## 4.1 Network -The `network` in the `did:algo` method is used to specify which Algorand network must be used to resolve the DID. To resolve DID, only a non-archival algod node is needed. Resolvers SHOULD check the genesis hash of the node they are using to resolve the DID and verify it matches the network in the DID. It should be noted that this is NOT a security measure. A malicious node could serve incorrect genesis hash or box data through the API so it is important for resolvers to use a trusted node for resolution. +The `network` in the `did:algo` method is used to specify which Algorand network must be used to resolve the DID. To resolve DID, only a non-archival algod node is needed. Resolvers **SHOULD** check the genesis hash of the node they are using to resolve the DID and verify it matches the network in the DID. It should be noted that this is NOT a security measure. A malicious node could serve incorrect genesis hash or box data through the API so it is important for resolvers to use a trusted node for resolution. The table of supported networks and their corresponding genesis hashes is below. @@ -67,13 +67,13 @@ The table of supported networks and their corresponding genesis hashes is below. | testnet | SGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/cOUJOiI= | | betanet | mFgazF+2uRS1tMiL9dsj01hJGySEmPN28B/TjjvpVW0= | -If the network in the DID is `custom`, the genesis hash MUST NOT be verified by resolvers. The primary use-case for `custom` is development on a local network. +If the network in the DID is `custom`, the genesis hash **MUST NOT** be verified by resolvers. The primary use-case for `custom` is development on a local network. ## 4.2 App Namespace ### 4.2.1 Controller -DIDDocuments created for `did:algo:app` DIDs SHOULD NOT have a controller field. Consumers of the DIDDocument MUST NOT assume the subject is the controller. In the reference implementation the controller is an Algorand address, which may have a dynamic auth address. This means ed25519 verification methods are not sufficient for verification and MUST NOT be used to identify the address that has permission to modify the in-app data. +DIDDocuments created for `did:algo:app` DIDs **SHOULD NOT** have a [controller property](https://www.w3.org/TR/did-core/#did-controller). Consumers of the DIDDocument **MUST NOT** assume the subject is the controller. In the reference implementation the controller is an Algorand address, which may have a dynamic auth address. This means ed25519 verification methods are not sufficient for verification and **MUST NOT** be used to identify the address that has permission to modify the in-app data. In other app implementations there may be one or more entities that control the DIDDocument data. Since these entities do not have a DID, the only way to verify controllers for a `did:algo` DID is to inspect the TEAL code of the app. @@ -81,7 +81,7 @@ If a controller field is set, it should be assumed that there are additional Alg ### 4.2.2 Metadata Box -In order to support `did:algo:app` resolution, an application must contain a box with a 32 byte key corresponding to the `hex-ed25519-key` of the subject. The value of this box box must start with the ARC4 ABI tuple: `(uint64,uint64,uint8,uint64)` this data structure shall be referred to as the metadata box. Additional data MAY be in the metadata box, but it MUST NOT alter the four initial values defined here. +In order to support `did:algo:app` resolution, an application must contain a box with a 32 byte key corresponding to the `hex-ed25519-key` of the subject. The value of this box box must start with the ARC4 ABI tuple: `(uint64,uint64,uint8,uint64)` this data structure shall be referred to as the metadata box. Additional data **MAY** be in the metadata box, but it **MUST NOT** alter the four initial values defined here. `metadata[0]` is a `uint64` indicating the key of the data box that contains the start of the DIDDocument for the subject. @@ -93,4 +93,4 @@ In order to support `did:algo:app` resolution, an application must contain a box ### 4.2.3 Data box -The data for the DIDDocument may be split across multiple boxes since each box can only hold 4 kilobytes of data. Data boxes are referenced via their `uint64` keys. DIDDocuments MUST be read and written sequentially across data boxes. +The data for the DIDDocument may be split across multiple boxes since each box can only hold 4 kilobytes of data. Data boxes are referenced via their `uint64` keys. DIDDocuments **MUST** be read and written sequentially across data boxes. From 4dc9a4d221da26075dfb0473ac7cbab3ab9c4cb1 Mon Sep 17 00:00:00 2001 From: Joe Polny Date: Mon, 13 May 2024 12:53:35 -0400 Subject: [PATCH 16/27] box -> app namespace --- TUTORIAL.md | 24 ++++++++++++------------ client/cli/cmd/register.go | 2 +- client/internal/main.go | 2 +- client/ui/api_test.go | 2 +- client/ui/provider.go | 2 +- 5 files changed, 16 insertions(+), 16 deletions(-) diff --git a/TUTORIAL.md b/TUTORIAL.md index 43acf06..8f6d79e 100644 --- a/TUTORIAL.md +++ b/TUTORIAL.md @@ -307,7 +307,7 @@ The list displays every DID instance along it's local alias for simpler usage. ```txt Name DID -sample-account did:algo:testnet:box:654583141:7d42e6e0f3c71d777de36849cf96e28b812453198cee2d2cc2b7b19438060007 +sample-account did:algo:testnet:app:654583141:7d42e6e0f3c71d777de36849cf96e28b812453198cee2d2cc2b7b19438060007 ``` ### 4.3 Inspect your local DID document @@ -336,17 +336,17 @@ document. "https://w3id.org/security/suites/ed25519-2020/v1", "https://w3id.org/security/suites/x25519-2020/v1" ], - "id": "did:algo:testnet:box:654583141:7d42e6e0f3c71d777de36849cf96e28b812453198cee2d2cc2b7b19438060007", + "id": "did:algo:testnet:app:654583141:7d42e6e0f3c71d777de36849cf96e28b812453198cee2d2cc2b7b19438060007", "verificationMethod": [ { - "id": "did:algo:testnet:box:654583141:7d42e6e0f3c71d777de36849cf96e28b812453198cee2d2cc2b7b19438060007#master", + "id": "did:algo:testnet:app:654583141:7d42e6e0f3c71d777de36849cf96e28b812453198cee2d2cc2b7b19438060007#master", "type": "Ed25519VerificationKey2020", - "controller": "did:algo:testnet:box:654583141:7d42e6e0f3c71d777de36849cf96e28b812453198cee2d2cc2b7b19438060007", + "controller": "did:algo:testnet:app:654583141:7d42e6e0f3c71d777de36849cf96e28b812453198cee2d2cc2b7b19438060007", "publicKeyMultibase": "z9Ry8aFPMLKapvtYkNNSFsoNhkc4192j4ai17EzMquAZc" } ], "authentication": [ - "did:algo:testnet:box:654583141:7d42e6e0f3c71d777de36849cf96e28b812453198cee2d2cc2b7b19438060007#master" + "did:algo:testnet:app:654583141:7d42e6e0f3c71d777de36849cf96e28b812453198cee2d2cc2b7b19438060007#master" ] } } @@ -422,7 +422,7 @@ the following. ```shell 2024-04-30T17:42:54-04:00 INF submitting request to the network 2024-04-30T17:42:54-04:00 INF publishing: sample-account -2024-04-30T17:42:54-04:00 INF publishing DID document did=did:algo:testnet:box:654583141:7d42e6e0f3c71d777de36849cf96e28b812453198cee2d2cc2b7b19438060007 +2024-04-30T17:42:54-04:00 INF publishing DID document did=did:algo:testnet:app:654583141:7d42e6e0f3c71d777de36849cf96e28b812453198cee2d2cc2b7b19438060007 2024-04-30T17:43:10-04:00 INF DID instance published ``` @@ -448,10 +448,10 @@ algoid retrieve [existing DID] For example, to resolve the DID created as part of this tutorial. ```shell -algoid resolve did:algo:testnet:box:654583141:7d42e6e0f3c71d777de36849cf96e28b812453198cee2d2cc2b7b19438060007 +algoid resolve did:algo:testnet:app:654583141:7d42e6e0f3c71d777de36849cf96e28b812453198cee2d2cc2b7b19438060007 2024-04-30T17:43:41-04:00 INF retrieving record -2024-04-30T17:43:41-04:00 INF retrieving DID document did=did:algo:testnet:box:654583141:7d42e6e0f3c71d777de36849cf96e28b812453198cee2d2cc2b7b19438060007 +2024-04-30T17:43:41-04:00 INF retrieving DID document did=did:algo:testnet:app:654583141:7d42e6e0f3c71d777de36849cf96e28b812453198cee2d2cc2b7b19438060007 2024-04-30T17:43:43-04:00 WRN skipping validation { "@context": [ @@ -459,17 +459,17 @@ algoid resolve did:algo:testnet:box:654583141:7d42e6e0f3c71d777de36849cf96e28b81 "https://w3id.org/security/suites/ed25519-2020/v1", "https://w3id.org/security/suites/x25519-2020/v1" ], - "id": "did:algo:testnet:box:654583141:7d42e6e0f3c71d777de36849cf96e28b812453198cee2d2cc2b7b19438060007", + "id": "did:algo:testnet:app:654583141:7d42e6e0f3c71d777de36849cf96e28b812453198cee2d2cc2b7b19438060007", "verificationMethod": [ { - "id": "did:algo:testnet:box:654583141:7d42e6e0f3c71d777de36849cf96e28b812453198cee2d2cc2b7b19438060007#master", + "id": "did:algo:testnet:app:654583141:7d42e6e0f3c71d777de36849cf96e28b812453198cee2d2cc2b7b19438060007#master", "type": "Ed25519VerificationKey2020", - "controller": "did:algo:testnet:box:654583141:7d42e6e0f3c71d777de36849cf96e28b812453198cee2d2cc2b7b19438060007", + "controller": "did:algo:testnet:app:654583141:7d42e6e0f3c71d777de36849cf96e28b812453198cee2d2cc2b7b19438060007", "publicKeyMultibase": "z9Ry8aFPMLKapvtYkNNSFsoNhkc4192j4ai17EzMquAZc" } ], "authentication": [ - "did:algo:testnet:box:654583141:7d42e6e0f3c71d777de36849cf96e28b812453198cee2d2cc2b7b19438060007#master" + "did:algo:testnet:app:654583141:7d42e6e0f3c71d777de36849cf96e28b812453198cee2d2cc2b7b19438060007#master" ] } ``` diff --git a/client/cli/cmd/register.go b/client/cli/cmd/register.go index 4140f04..aea545b 100644 --- a/client/cli/cmd/register.go +++ b/client/cli/cmd/register.go @@ -76,7 +76,7 @@ func runRegisterCmd(_ *cobra.Command, args []string) error { } // Generate base identifier instance - subject := fmt.Sprintf("%s:box:%d:%x", network, appID, account.PublicKey) + subject := fmt.Sprintf("%s:app:%d:%x", network, appID, account.PublicKey) method := "algo" log.WithFields(xlog.Fields{ "subject": subject, diff --git a/client/internal/main.go b/client/internal/main.go index 627554c..1a4b3f7 100644 --- a/client/internal/main.go +++ b/client/internal/main.go @@ -245,7 +245,7 @@ func parseSubjectString(subject string) (pub []byte, network string, appID uint6 } namespace := idSegments[1] - if namespace != "box" { + if namespace != "app" { err = fmt.Errorf("invalid namespace in subject identifier: %s", namespace) return pub, network, appID, err } diff --git a/client/ui/api_test.go b/client/ui/api_test.go index 6be6218..c4394c1 100644 --- a/client/ui/api_test.go +++ b/client/ui/api_test.go @@ -202,7 +202,7 @@ func TestListAfterRegister(t *testing.T) { body, err := io.ReadAll(res.Body) require.NoError(t, err) - require.Regexp(t, `{"name":"TestListAfterRegister","did":"did:algo:custom:box:`, string(body)) + require.Regexp(t, `{"name":"TestListAfterRegister","did":"did:algo:custom:app:`, string(body)) } func TestUpdate(t *testing.T) { diff --git a/client/ui/provider.go b/client/ui/provider.go index 8f34061..79eef3f 100644 --- a/client/ui/provider.go +++ b/client/ui/provider.go @@ -91,7 +91,7 @@ func (p *Provider) Register(network string, name string, passphrase string) erro } // Generate base identifier instance - subject := fmt.Sprintf("%s:box:%d:%x", network, p.client.Networks[network].StorageAppID(), account.PublicKey) + subject := fmt.Sprintf("%s:app:%d:%x", network, p.client.Networks[network].StorageAppID(), account.PublicKey) method := "algo" p.log.WithFields(xlog.Fields{ "subject": subject, From a29d9c0cc0c6b938fcd16b69ef5ec90160b6ad68 Mon Sep 17 00:00:00 2001 From: Joe Polny Date: Mon, 13 May 2024 13:44:07 -0400 Subject: [PATCH 17/27] support omitted network --- client/internal/main.go | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/client/internal/main.go b/client/internal/main.go index 1a4b3f7..94b7d84 100644 --- a/client/internal/main.go +++ b/client/internal/main.go @@ -226,12 +226,23 @@ func addressFromPub(pub []byte) (string, error) { // parseSubjectString extracts the network, public key and application ID from a DID (network:appID:pubkey). func parseSubjectString(subject string) (pub []byte, network string, appID uint64, err error) { idSegments := strings.Split(subject, ":") - if len(idSegments) != 4 { - err = fmt.Errorf("invalid subject identifier. Expected 3 segments, got %d", len(idSegments)) + if len(idSegments) != 4 && len(idSegments) != 3 { + err = fmt.Errorf("invalid subject identifier. Expected 3 or 4 segments, got %d", len(idSegments)) return pub, network, appID, err } - network = idSegments[0] + idxOffset := 0 + + if len(idSegments) == 3 { + idxOffset = 1 + } + + if len(idSegments) == 3 { + network = "mainnet" + } else { + network = idSegments[0] + } + matchFound := false for _, a := range []string{"mainnet", "testnet", "custom"} { if a == network { @@ -244,19 +255,19 @@ func parseSubjectString(subject string) (pub []byte, network string, appID uint6 return pub, network, appID, err } - namespace := idSegments[1] + namespace := idSegments[1-idxOffset] if namespace != "app" { err = fmt.Errorf("invalid namespace in subject identifier: %s", namespace) return pub, network, appID, err } - pub, err = hex.DecodeString(idSegments[3]) + pub, err = hex.DecodeString(idSegments[3-idxOffset]) if err != nil { err = fmt.Errorf("invalid public key in subject identifier") return pub, network, appID, err } - app, err := strconv.Atoi(idSegments[2]) + app, err := strconv.Atoi(idSegments[2-idxOffset]) if err != nil { err = fmt.Errorf("invalid storage app ID in subject identifier") return pub, network, appID, err From 27a0727340e64c254c76e3ec00c45cd057c1b516 Mon Sep 17 00:00:00 2001 From: Joe Polny Date: Mon, 13 May 2024 13:53:10 -0400 Subject: [PATCH 18/27] fix linting --- client/internal/contracts.go | 1 + client/ui/api_test.go | 46 +++++++++++++++++++++++------------- client/ui/provider.go | 2 +- 3 files changed, 32 insertions(+), 17 deletions(-) diff --git a/client/internal/contracts.go b/client/internal/contracts.go index 5baec5d..934e73b 100644 --- a/client/internal/contracts.go +++ b/client/internal/contracts.go @@ -50,6 +50,7 @@ func init() { _ = clearFile.Close() } +// LoadContract loads the AlgoDID smart contract ABI from JSON file. func LoadContract() *abi.Contract { abiFile, _ := StorageContracts.Open("AlgoDID.abi.json") abiContents, _ := io.ReadAll(abiFile) diff --git a/client/ui/api_test.go b/client/ui/api_test.go index c4394c1..6668c58 100644 --- a/client/ui/api_test.go +++ b/client/ui/api_test.go @@ -41,45 +41,45 @@ func getKmdClient() (kmd.Client, error) { func getSandboxAccounts() ([]crypto.Account, error) { client, err := getKmdClient() if err != nil { - return nil, fmt.Errorf("Failed to create kmd client: %+v", err) + return nil, fmt.Errorf("Failed to create kmd client: %w", err) } resp, err := client.ListWallets() if err != nil { - return nil, fmt.Errorf("Failed to list wallets: %+v", err) + return nil, fmt.Errorf("Failed to list wallets: %w", err) } - var walletId string + var walletID string for _, wallet := range resp.Wallets { if wallet.Name == "unencrypted-default-wallet" { - walletId = wallet.ID + walletID = wallet.ID } } - if walletId == "" { + if walletID == "" { return nil, fmt.Errorf("No wallet named %s", "unencrypted-default-wallet") } - whResp, err := client.InitWalletHandle(walletId, "") + whResp, err := client.InitWalletHandle(walletID, "") if err != nil { - return nil, fmt.Errorf("Failed to init wallet handle: %+v", err) + return nil, fmt.Errorf("Failed to init wallet handle: %w", err) } addrResp, err := client.ListKeys(whResp.WalletHandleToken) if err != nil { - return nil, fmt.Errorf("Failed to list keys: %+v", err) + return nil, fmt.Errorf("Failed to list keys: %w", err) } - var accts []crypto.Account + accts := make([]crypto.Account, len(addrResp.Addresses)) for _, addr := range addrResp.Addresses { expResp, err := client.ExportKey(whResp.WalletHandleToken, "", addr) if err != nil { - return nil, fmt.Errorf("Failed to export key: %+v", err) + return nil, fmt.Errorf("Failed to export key: %w", err) } acct, err := crypto.AccountFromPrivateKey(expResp.PrivateKey) if err != nil { - return nil, fmt.Errorf("Failed to create account from private key: %+v", err) + return nil, fmt.Errorf("Failed to create account from private key: %w", err) } accts = append(accts, acct) @@ -96,6 +96,7 @@ func req(t *testing.T, method string, endpoint string, reqBody string) *http.Res require.NoError(t, err) res, err := http.DefaultClient.Do(req) + require.NoError(t, err) return res } @@ -117,6 +118,9 @@ func TestMain(m *testing.M) { } status, err := algodClient.Status().Do(context.Background()) + if err != nil { + panic(err) + } // type NetworkProfile struct { // // Profile name. @@ -158,6 +162,9 @@ func TestMain(m *testing.M) { } srv, err := LocalAPIServer(localStore, cl, logger) + if err != nil { + panic(err) + } logger.Debug("starting local API server on localhost:9090") go func() { @@ -169,12 +176,14 @@ func TestMain(m *testing.M) { func TestReady(t *testing.T) { res := req(t, http.MethodGet, "ready", "") + defer res.Body.Close() require.Equal(t, http.StatusOK, res.StatusCode) } func TestList(t *testing.T) { res := req(t, http.MethodGet, "list", "") + defer res.Body.Close() require.Equal(t, http.StatusOK, res.StatusCode) @@ -186,16 +195,19 @@ func TestList(t *testing.T) { func TestRegister(t *testing.T) { res := req(t, http.MethodPost, "register", `{"name": "TestRegister", "recovery_key": "test", "network": "custom"}`) + defer res.Body.Close() require.Equal(t, http.StatusOK, res.StatusCode) } func TestListAfterRegister(t *testing.T) { res := req(t, http.MethodPost, "register", `{"name": "TestListAfterRegister", "recovery_key": "test", "network": "custom"}`) + defer res.Body.Close() require.Equal(t, http.StatusOK, res.StatusCode) res = req(t, http.MethodGet, "list", "") + defer res.Body.Close() require.Equal(t, http.StatusOK, res.StatusCode) @@ -207,6 +219,7 @@ func TestListAfterRegister(t *testing.T) { func TestUpdate(t *testing.T) { res := req(t, http.MethodPost, "register", `{"name": "TestUpdate", "recovery_key": "test", "network": "custom"}`) + defer res.Body.Close() require.Equal(t, http.StatusOK, res.StatusCode) @@ -249,12 +262,12 @@ func TestUpdate(t *testing.T) { // Create the app - createdAppId, err := internal.CreateApp(algodClient, internal.LoadContract(), acct.Address, signer) + createdAppID, err := internal.CreateApp(algodClient, internal.LoadContract(), acct.Address, signer) require.NoError(t, err) - require.Equal(t, uint64(profile.AppID), createdAppId) - fmt.Printf("Created app ID: %d\n", createdAppId) + require.Equal(t, uint64(profile.AppID), createdAppID) + fmt.Printf("Created app ID: %d\n", createdAppID) - appInfo, err := algodClient.GetApplicationByID(createdAppId).Do(context.Background()) + appInfo, err := algodClient.GetApplicationByID(createdAppID).Do(context.Background()) require.NoError(t, err) require.False(t, appInfo.Deleted) @@ -262,12 +275,13 @@ func TestUpdate(t *testing.T) { res = req(t, http.MethodPost, "update", `{"name": "TestUpdate", "passphrase": "test"}`) require.NoError(t, err) + defer res.Body.Close() require.Equal(t, http.StatusOK, res.StatusCode) did, err := localStore.Get("TestUpdate") require.NoError(t, err) - resolvedDid, err := internal.ResolveDID(createdAppId, acct.PublicKey, algodClient, "custom") + resolvedDid, err := internal.ResolveDID(createdAppID, acct.PublicKey, algodClient, "custom") require.NoError(t, err) fmt.Println(string(resolvedDid)) diff --git a/client/ui/provider.go b/client/ui/provider.go index 79eef3f..4eb4501 100644 --- a/client/ui/provider.go +++ b/client/ui/provider.go @@ -264,7 +264,7 @@ func (p *Provider) registerHandlerFunc(w http.ResponseWriter, r *http.Request) { } if err = p.Register(network, name, passphrase); err != nil { w.WriteHeader(http.StatusInternalServerError) - w.Write([]byte(err.Error())) + w.Write([]byte(err.Error())) //nolint:errcheck,gosec return } _, _ = w.Write([]byte("ok")) From aec398ab7368c75738e8ed0e945fad9e5a2e8acf Mon Sep 17 00:00:00 2001 From: Joe Polny Date: Mon, 13 May 2024 13:57:37 -0400 Subject: [PATCH 19/27] use MkdirAll --- client/store/local.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/store/local.go b/client/store/local.go index f063d3d..4574cba 100644 --- a/client/store/local.go +++ b/client/store/local.go @@ -34,7 +34,7 @@ type IdentifierRecord struct { func NewLocalStore(home string) (*LocalStore, error) { h := filepath.Clean(home) if !dirExist(h) { - if err := os.Mkdir(h, 0700); err != nil { + if err := os.MkdirAll(h, 0700); err != nil { return nil, fmt.Errorf("failed to create new home directory: %w", err) } } From 06405b7d0911f00daede6c775282ac7f73a4e92f Mon Sep 17 00:00:00 2001 From: Joe Polny Date: Mon, 13 May 2024 14:01:39 -0400 Subject: [PATCH 20/27] algokit localnet in workflow --- .github/workflows/ci.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b240ada..2377505 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -157,6 +157,10 @@ jobs: with: version: v1.54.2 + # Run algokit localnet for tests + - name: Algokit localnet + run: pipx install algokit && algokit localnet start + # Run unit tests - name: Test run: make test From 43533ca2e6b8fa5416793e470c77d3bdafa04085 Mon Sep 17 00:00:00 2001 From: Joe Polny Date: Tue, 14 May 2024 16:37:12 -0400 Subject: [PATCH 21/27] update maintainer --- .goreleaser.yml | 2 +- Makefile | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.goreleaser.yml b/.goreleaser.yml index 4511b9e..4c11f0f 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -117,7 +117,7 @@ nfpms: - did-algo vendor: Algorand Foundation homepage: https://github.com/algorandfoundation/did-algo - maintainer: Ben Cessa + maintainer: Joe Polny description: Decentralized Identifiers for the Algorand blockchain license: BSD-3-Clause formats: diff --git a/Makefile b/Makefile index 905be2b..dc66615 100644 --- a/Makefile +++ b/Makefile @@ -7,7 +7,7 @@ OWNER=algorandfoundation REPO=did-algo PROJECT_REPO=github.com/$(OWNER)/$(REPO) DOCKER_IMAGE=ghcr.io/$(OWNER)/$(BINARY_NAME) -MAINTAINERS='Ben Cessa ' +MAINTAINERS='Joe Polny ' # State values GIT_COMMIT_DATE=$(shell TZ=UTC git log -n1 --pretty=format:'%cd' --date='format-local:%Y-%m-%dT%H:%M:%SZ') From 5e55d49f87ea674a7df4c5af553bcff3a011019b Mon Sep 17 00:00:00 2001 From: Joe Polny Date: Tue, 14 May 2024 16:39:49 -0400 Subject: [PATCH 22/27] rm ipfs from TUTORIAL --- TUTORIAL.md | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/TUTORIAL.md b/TUTORIAL.md index 8f6d79e..4cf9007 100644 --- a/TUTORIAL.md +++ b/TUTORIAL.md @@ -390,11 +390,7 @@ Flags: ## 5. Publish your DID globally -Up to this point all the details about our DID are just available on our machine; -to be really useful we need to be able to publish this information on a decentralized -way so that others can retrieve it when required. To this end, the `algo` DID method -utilizes a robust integration with the [IPFS](https://ipfs.io/) decentralized storage -protocol. +Up to this point all the details about our DID are just available on our machine; to be really useful we need to be able to publish this information on the Algorand network so it can be accessed by anyone. To publish your local DID use the `publish` command. From 2fad71b888fc9c83b317138c58ca7a3c315dcb42 Mon Sep 17 00:00:00 2001 From: Joe Polny Date: Wed, 15 May 2024 09:10:36 -0400 Subject: [PATCH 23/27] add authors --- SPEC.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/SPEC.md b/SPEC.md index 6f42010..d5318c3 100644 --- a/SPEC.md +++ b/SPEC.md @@ -1,3 +1,9 @@ +# Authors + +- [Cosimo Bassi](mailto:cosimo.bassi@algorand.foundation?subject=did:algo) +- [Joe Polny](mailto:joe@algorand.foundation?subject=did:algo) +- [Bruno Martins](mailto:bruno.martins@algorand.foundation?subject=did:algo) + # Abstract Decentralized Identifiers (DIDs) are a new type of identifier for that enable decentralized identification of various entities. DIDs are designed to enable individuals and organizations to generate their own identifiers using systems they trust. These new identifiers enable entities to prove control over them by authenticating using cryptographic proofs such as digital signatures. From b908127cc64415041283cac45ddd5870cce72fe4 Mon Sep 17 00:00:00 2001 From: Joe Polny Date: Wed, 15 May 2024 09:14:20 -0400 Subject: [PATCH 24/27] update DRIVER.md --- DRIVER.md | 62 +++++++++++++++++++++---------------------------------- 1 file changed, 24 insertions(+), 38 deletions(-) diff --git a/DRIVER.md b/DRIVER.md index 7d0d3bc..9b89b8f 100644 --- a/DRIVER.md +++ b/DRIVER.md @@ -21,7 +21,7 @@ default behavior when no `Accept` header is provided. Request: ```shell -curl -X GET +curl -X GET ``` Response: @@ -29,34 +29,22 @@ Response: ```json { "@context": [ - "https://w3id.org/did-resolution/v1" + "https://www.w3.org/ns/did/v1", + "https://w3id.org/security/suites/ed25519-2020/v1", + "https://w3id.org/security/suites/x25519-2020/v1" ], - "didDocument": { - "@context": [ - "https://www.w3.org/ns/did/v1", - "https://w3id.org/security/suites/ed25519-2020/v1", - "https://w3id.org/security/suites/x25519-2020/v1" - ], - "id": "did:algo:426165491c77a6c95eeed8d0420b38b0afbe9057c4f33147eb90fafd32aaad22-591154170", - "verificationMethod": [ - { - "id": "did:algo:426165491c77a6c95eeed8d0420b38b0afbe9057c4f33147eb90fafd32aaad22-591154170#master", - "type": "Ed25519VerificationKey2020", - "controller": "did:algo:426165491c77a6c95eeed8d0420b38b0afbe9057c4f33147eb90fafd32aaad22-591154170", - "publicKeyMultibase": "z5U83dEzpKaRwaTxJ8iqF6dC58fNpsEfhugDYfVC9ouZ3" - } - ], - "authentication": [ - "did:algo:426165491c77a6c95eeed8d0420b38b0afbe9057c4f33147eb90fafd32aaad22-591154170#master" - ] - }, - "didDocumentMetadata": { - "deactivated": false - }, - "didResolutionMetadata": { - "contentType": "application/did+ld+json", - "retrieved": "2024-03-01T01:32:42Z" - } + "id": "did:algo:mainnet:app:1845671812:da490f2d15a625459bf970a3d55e1a646dfd3a956d011546e953e945d39fdada", + "verificationMethod": [ + { + "id": "did:algo:mainnet:app:1845671812:da490f2d15a625459bf970a3d55e1a646dfd3a956d011546e953e945d39fdada#master", + "type": "Ed25519VerificationKey2020", + "controller": "did:algo:mainnet:app:1845671812:da490f2d15a625459bf970a3d55e1a646dfd3a956d011546e953e945d39fdada", + "publicKeyMultibase": "zFh6VYTmcxGD2vTNMSTHhtMSa7TkGCded9ofBX5C6CxYq" + } + ], + "authentication": [ + "did:algo:mainnet:app:1845671812:da490f2d15a625459bf970a3d55e1a646dfd3a956d011546e953e945d39fdada#master" + ] } ``` @@ -72,7 +60,7 @@ Request: ```shell curl -X GET \ --header "Accept: application/did+ld+json" \ - + ``` Response: @@ -84,17 +72,17 @@ Response: "https://w3id.org/security/suites/ed25519-2020/v1", "https://w3id.org/security/suites/x25519-2020/v1" ], - "id": "did:algo:426165491c77a6c95eeed8d0420b38b0afbe9057c4f33147eb90fafd32aaad22-591154170", + "id": "did:algo:mainnet:app:1845671812:da490f2d15a625459bf970a3d55e1a646dfd3a956d011546e953e945d39fdada", "verificationMethod": [ { - "id": "did:algo:426165491c77a6c95eeed8d0420b38b0afbe9057c4f33147eb90fafd32aaad22-591154170#master", + "id": "did:algo:mainnet:app:1845671812:da490f2d15a625459bf970a3d55e1a646dfd3a956d011546e953e945d39fdada#master", "type": "Ed25519VerificationKey2020", - "controller": "did:algo:426165491c77a6c95eeed8d0420b38b0afbe9057c4f33147eb90fafd32aaad22-591154170", - "publicKeyMultibase": "z5U83dEzpKaRwaTxJ8iqF6dC58fNpsEfhugDYfVC9ouZ3" + "controller": "did:algo:mainnet:app:1845671812:da490f2d15a625459bf970a3d55e1a646dfd3a956d011546e953e945d39fdada", + "publicKeyMultibase": "zFh6VYTmcxGD2vTNMSTHhtMSa7TkGCded9ofBX5C6CxYq" } ], "authentication": [ - "did:algo:426165491c77a6c95eeed8d0420b38b0afbe9057c4f33147eb90fafd32aaad22-591154170#master" + "did:algo:mainnet:app:1845671812:da490f2d15a625459bf970a3d55e1a646dfd3a956d011546e953e945d39fdada#master" ] } ``` @@ -113,16 +101,14 @@ Request: ```shell curl -X GET \ --header "Accept: application/did+cbor" \ - + ``` Response: ```json { - "@context": [ - "https://w3id.org/did-resolution/v1" - ], + "@context": ["https://w3id.org/did-resolution/v1"], "didResolutionMetadata": { "contentType": "application/did+cbor", "retrieved": "2024-03-01T01:39:03Z", From 225f35229e67e096954f38eaa58469da6a956e4f Mon Sep 17 00:00:00 2001 From: Joe Polny Date: Wed, 15 May 2024 09:18:45 -0400 Subject: [PATCH 25/27] update configs --- resolver-config.yaml | 10 +++++++--- sample-config.yaml | 14 ++++++-------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/resolver-config.yaml b/resolver-config.yaml index f2cd141..8a2aea5 100644 --- a/resolver-config.yaml +++ b/resolver-config.yaml @@ -1,8 +1,12 @@ network: - active: testnet profiles: - # to deploy your own storage provider contract + - name: mainnet + node: https://mainnet-api.algonode.cloud + node_token: "" - name: testnet node: https://testnet-api.algonode.cloud node_token: "" - app_id: 602746084 + profiles: + - name: betanet + node: https://betanet-api.algonode.cloud + node_token: "" diff --git a/sample-config.yaml b/sample-config.yaml index a8cdd8c..8a2aea5 100644 --- a/sample-config.yaml +++ b/sample-config.yaml @@ -1,14 +1,12 @@ network: - active: testnet profiles: - # to deploy your own storage provider contract + - name: mainnet + node: https://mainnet-api.algonode.cloud + node_token: "" - name: testnet node: https://testnet-api.algonode.cloud node_token: "" - app_id: 602746084 - # to use a storage provider 3rd party - - name: using-provider - node: https://testnet-api.algonode.cloud + profiles: + - name: betanet + node: https://betanet-api.algonode.cloud node_token: "" - app_id: 591154170 - store_provider: http://localhost:3000 From 2842c2eb68626f5d5bf18ba71a229784bc644ac3 Mon Sep 17 00:00:00 2001 From: Joe Polny Date: Wed, 15 May 2024 09:21:23 -0400 Subject: [PATCH 26/27] fix workflow yaml --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2377505..4dbd19a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -159,7 +159,7 @@ jobs: # Run algokit localnet for tests - name: Algokit localnet - run: pipx install algokit && algokit localnet start + run: pipx install algokit && algokit localnet start # Run unit tests - name: Test From 5e1c096dac1c3159dc906f4e64cc30748dfc6c3e Mon Sep 17 00:00:00 2001 From: Joe Polny Date: Wed, 15 May 2024 09:49:49 -0400 Subject: [PATCH 27/27] undo make in getSandboxAccounts --- client/ui/api_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ui/api_test.go b/client/ui/api_test.go index 6668c58..f9b64fa 100644 --- a/client/ui/api_test.go +++ b/client/ui/api_test.go @@ -70,7 +70,7 @@ func getSandboxAccounts() ([]crypto.Account, error) { return nil, fmt.Errorf("Failed to list keys: %w", err) } - accts := make([]crypto.Account, len(addrResp.Addresses)) + var accts []crypto.Account //nolint:prealloc for _, addr := range addrResp.Addresses { expResp, err := client.ExportKey(whResp.WalletHandleToken, "", addr) if err != nil {