diff --git a/core/client/set.go b/core/client/set.go index 113909e90..cb490f541 100644 --- a/core/client/set.go +++ b/core/client/set.go @@ -6,10 +6,10 @@ import ( "errors" "fmt" "strings" - - "github.com/0chain/gosdk/core/conf" + "sync" "github.com/0chain/gosdk/constants" + "github.com/0chain/gosdk/core/conf" "github.com/0chain/gosdk/core/sys" "github.com/0chain/gosdk/core/zcncrypto" ) @@ -19,10 +19,13 @@ var ( sdkInitialized bool Sign SignFunc + // SignByMultiWallet SignByMultiWalletFunc sigC = make(chan struct{}, 1) ) -type SignFunc func(hash string, clients ...string) (string, error) +type SignFunc func(hash string, keys ...string) (string, error) + +// type SignByMultiWalletFunc func(hash string, pubkey string) (string, error) // maintains client's information type Client struct { @@ -34,6 +37,9 @@ type Client struct { nonce int64 txnFee uint64 sign SignFunc + wg map[string]*sync.WaitGroup + walletCount map[string]int // maintains count of wallets in the WaitGroup by pubkey + mu sync.RWMutex // allow concurrent readers } type InitSdkOptions struct { @@ -57,24 +63,53 @@ func init() { sys.Sign = signHash sys.SignWithAuth = signHashWithAuth + // prime the sign channel sigC <- struct{}{} - // initialize SignFunc as default implementation - Sign = func(hash string, clients ...string) (string, error) { + // default Sign implementation (uses client.wallet or wallets map) + // default Sign implementation (uses client.wallet or wallets map) + Sign = func(hash string, keys ...string) (string, error) { + client.mu.RLock() wallet := client.wallet - - if len(clients) > 0 && clients[0] != "" && client.wallets[clients[0]] != nil { - wallet = client.wallets[clients[0]] + if len(keys) > 0 && keys[0] != "" { + if wallet != nil && len(wallet.Keys) > 0 && wallet.Keys[0].PublicKey == keys[0] { + // use default wallet + } else if client.wallets != nil { + if w, ok := client.wallets[keys[0]]; ok && w != nil { + wallet = w + } else { + // Wallet not found by the provided key + client.mu.RUnlock() + return "", errors.New("no wallets available for signing by key: " + keys[0]) + } + } else { + client.mu.RUnlock() + return "", errors.New("no wallets available for signing by key: " + keys[0]) + } } - if !wallet.IsSplit { - return sys.Sign(hash, client.signatureScheme, GetClientSysKeys(clients...)) + if wallet == nil { + client.mu.RUnlock() + return "", errors.New("wallet not initialized") } - // get sign lock + // fmt.Print("wallet found: client_id: ", wallet.ClientID, "is_split: ", wallet.IsSplit, "pubkey: ", wallet.Keys[0].PublicKey) + + isSplit := wallet.IsSplit + sigScheme := client.signatureScheme + + // Release lock before calling external functions that might re-acquire it + client.mu.RUnlock() + + if !isSplit { + return sys.Sign(hash, sigScheme, GetClientSysKeys(keys...)) + } + fmt.Println("Signature: ", "It actually get's to this point right after signing") + // split-key signing via auth <-sigC - fmt.Println("Sign: with sys.SignWithAuth:", sys.SignWithAuth, "sysKeys:", GetClientSysKeys(clients...)) - sig, err := sys.SignWithAuth(hash, client.signatureScheme, GetClientSysKeys(clients...)) + fmt.Println("Sign: with sys.SignWithAuth:", sys.SignWithAuth, "sysKeys:", GetClientSysKeys(keys...)) + sig, err := sys.SignWithAuth(hash, client.signatureScheme, GetClientSysKeys(keys...), wallet.Keys[0].PublicKey) + fmt.Println("Signature: ", sig) sigC <- struct{}{} return sig, err } @@ -82,12 +117,30 @@ func init() { sys.Verify = verifySignature sys.VerifyWith = verifySignatureWith sys.VerifyEd25519With = verifyEd25519With + + client.wg = make(map[string]*sync.WaitGroup) + client.walletCount = make(map[string]int) +} + +func GetClient() *zcncrypto.Wallet { + return client.wallet } -var SignFn = func(hash string) (string, error) { +var SignFn = func(hash string, keys ...string) (string, error) { ss := zcncrypto.NewSignatureScheme(client.signatureScheme) - err := ss.SetPrivateKey(client.wallet.Keys[0].PrivateKey) + var err error + if len(keys) > 0 && keys[0] != "" { + wallet := GetWalletByKey(keys[0]) + if wallet == nil { + return "", errors.New("multi-wallet-settings err: " + keys[0]) + } + err = ss.SetPrivateKey(wallet.Keys[0].PrivateKey) + + } else { + err = ss.SetPrivateKey(client.wallet.Keys[0].PrivateKey) + } + if err != nil { return "", err } @@ -95,16 +148,113 @@ var SignFn = func(hash string) (string, error) { return ss.Sign(hash) } -func signHashWithAuth(hash, signatureScheme string, keys []sys.KeyPair) (string, error) { +// InitSDK Initialize the storage SDK +// +// - walletJSON: Client's wallet JSON +// - blockWorker: Block worker URL (block worker refers to 0DNS) +// - chainID: ID of the blokcchain network +// - signatureScheme: Signature scheme that will be used for signing transactions +// - preferredBlobbers: List of preferred blobbers to use when creating an allocation. This is usually configured by the client in the configuration files +// - nonce: Initial nonce value for the transactions +// - fee: Preferred value for the transaction fee, just the first value is taken +func InitSDK(walletJSON string, + blockWorker, chainID, signatureScheme string, + nonce int64, addWallet bool, + options ...int) error { + + if addWallet { + wallet := zcncrypto.Wallet{} + err := json.Unmarshal([]byte(walletJSON), &wallet) + if err != nil { + return err + } + + SetWallet(wallet) + SetSignatureScheme(signatureScheme) + SetNonce(nonce) + if len(options) > 0 { + SetTxnFee(uint64(options[0])) + } + } + + var minConfirmation, minSubmit, confirmationChainLength, sharderConsensous int + if len(options) > 1 { + minConfirmation = options[1] + } + if len(options) > 2 { + minSubmit = options[2] + } + if len(options) > 3 { + confirmationChainLength = options[3] + } + if len(options) > 4 { + sharderConsensous = options[4] + } + + err := Init(context.Background(), conf.Config{ + BlockWorker: blockWorker, + SignatureScheme: signatureScheme, + ChainID: chainID, + MinConfirmation: minConfirmation, + MinSubmit: minSubmit, + ConfirmationChainLength: confirmationChainLength, + SharderConsensous: sharderConsensous, + }) + if err != nil { + return err + } + SetSdkInitialized(true) + return nil +} + +func InitSDKWithWebApp(params InitSdkOptions) error { + if params.MinConfirmation != nil && params.MinSubmit != nil && params.ConfirmationChainLength != nil && params.SharderConsensous != nil { + err := InitSDK(params.WalletJSON, params.BlockWorker, params.ChainID, params.SignatureScheme, params.Nonce, params.AddWallet, *params.MinConfirmation, *params.MinSubmit, *params.ConfirmationChainLength, *params.SharderConsensous) + if err != nil { + return err + } + } else { + err := InitSDK(params.WalletJSON, params.BlockWorker, params.ChainID, params.SignatureScheme, params.Nonce, params.AddWallet) + if err != nil { + return err + } + } + conf.SetZboxAppConfigs(params.ZboxHost, params.ZboxAppType) + SetIsAppFlow(true) + return nil +} + + +func IsSDKInitialized() bool { + return sdkInitialized +} + +func SetSdkInitialized(val bool) { + sdkInitialized = val +} + +// Sign helpers that use sys package +func signHashWithAuth(hash, signatureScheme string, keys []sys.KeyPair, key ...string) (string, error) { sig, err := sys.Sign(hash, signatureScheme, keys) if err != nil { return "", fmt.Errorf("failed to sign with split key: %v", err) } + var clientID string + if len(key) > 0 && key[0] != "" { + wallet := GetWalletByKey(key[0]) + if wallet == nil { + return "", fmt.Errorf("wallet not found for pubkey: %s", key[0]) + } + clientID = wallet.ClientID + } else { + clientID = client.wallet.ClientID + } + data, err := json.Marshal(AuthMessage{ Hash: hash, Signature: sig, - ClientID: client.wallet.ClientID, + ClientID: clientID, }) if err != nil { return "", err @@ -114,7 +264,7 @@ func signHashWithAuth(hash, signatureScheme string, keys []sys.KeyPair) (string, return "", errors.New("authCommon is not set") } - rsp, err := sys.AuthCommon(string(data)) + rsp, err := sys.AuthCommon(string(data), key...) if err != nil { return "", err } @@ -123,8 +273,7 @@ func signHashWithAuth(hash, signatureScheme string, keys []sys.KeyPair) (string, Sig string `json:"sig"` } - err = json.Unmarshal([]byte(rsp), &sigpk) - if err != nil { + if err = json.Unmarshal([]byte(rsp), &sigpk); err != nil { return "", err } @@ -150,7 +299,6 @@ func signHash(hash string, signatureScheme string, keys []sys.KeyPair) (string, return "", err } } - return retSignature, nil } @@ -181,27 +329,32 @@ func verifyEd25519With(pubKey, signature, hash string) (bool, error) { return sch.Verify(signature, hash) } -func GetClientSysKeys(clients ...string) []sys.KeyPair { - var wallet *zcncrypto.Wallet - if len(clients) > 0 && clients[0] != "" && client.wallets[clients[0]] != nil { - wallet = client.wallets[clients[0]] - } else { - wallet = client.wallet +// GetClientSysKeys reads wallets -> use RLock +func GetClientSysKeys(keys ...string) []sys.KeyPair { + client.mu.RLock() + defer client.mu.RUnlock() + wallet := client.wallet + if len(keys) > 0 && keys[0] != "" && client.wallets != nil { + if w, ok := client.wallets[keys[0]]; ok && w != nil { + wallet = w + } } - var keys []sys.KeyPair + var sysKeys []sys.KeyPair for _, kv := range wallet.Keys { - keys = append(keys, sys.KeyPair{ + sysKeys = append(sysKeys, sys.KeyPair{ PrivateKey: kv.PrivateKey, PublicKey: kv.PublicKey, }) } - - return keys + return sysKeys } // SetWallet should be set before any transaction or client specific APIs func SetWallet(w zcncrypto.Wallet) { + client.mu.Lock() + defer client.mu.Unlock() + client.wallet = &w if client.wallets == nil { client.wallets = make(map[string]*zcncrypto.Wallet) @@ -209,6 +362,98 @@ func SetWallet(w zcncrypto.Wallet) { client.wallets[w.ClientID] = &w } +// GetWalletByKey gets a wallet by client pubkey. +func GetWalletByKey(key string) *zcncrypto.Wallet { + client.mu.RLock() + defer client.mu.RUnlock() + if client.wallets == nil { + return nil + } + return client.wallets[key] +} + +func GetWallet() *zcncrypto.Wallet { + return client.wallet +} + +// AddWallet adds a new wallet to the sdk. +// The wallet is indexed by BOTH ClientID and PublicKey to support +// lookups from different parts of the codebase (workers use ClientID, +// Sign function uses PublicKey). +func AddWallet(wallet zcncrypto.Wallet) { + if len(wallet.Keys) == 0 { + return + } + pubkey := wallet.Keys[0].PublicKey + clientID := wallet.ClientID + + client.mu.Lock() + defer client.mu.Unlock() + + if client.wallets == nil { + client.wallets = make(map[string]*zcncrypto.Wallet) + } + if client.wg == nil { + client.wg = make(map[string]*sync.WaitGroup) + } + if client.walletCount == nil { + client.walletCount = make(map[string]int) + } + + // Check if wallet already exists (by ClientID to avoid double-counting) + if _, exists := client.wallets[clientID]; exists { + client.walletCount[clientID]++ + // ensure wg exists + if client.wg[clientID] == nil { + client.wg[clientID] = &sync.WaitGroup{} + } + client.wg[clientID].Add(1) + return + } + + // Add new wallet - index by BOTH ClientID and PublicKey + client.wallets[clientID] = &wallet + client.wallets[pubkey] = &wallet + + // Use ClientID for reference counting and wait groups + if client.wg[clientID] == nil { + client.wg[clientID] = &sync.WaitGroup{} + } + client.wg[clientID].Add(1) + client.walletCount[clientID]++ +} + +// RemoveWallet removes a wallet from the sdk by ClientID. +// This also removes the PublicKey index to maintain dual-index consistency. +func RemoveWallet(clientID string) { + client.mu.Lock() + defer client.mu.Unlock() + + if clientID == "" || client.walletCount == nil { + return + } + + // only decrement if count > 0 + if client.walletCount[clientID] > 0 { + client.walletCount[clientID]-- + // call Done on wg only if it exists + if wg, ok := client.wg[clientID]; ok && wg != nil { + wg.Done() + } + } + + // if count reaches zero, clean up both indexes + if client.walletCount[clientID] == 0 { + // Remove both ClientID and PublicKey indexes + if w := client.wallets[clientID]; w != nil && len(w.Keys) > 0 { + delete(client.wallets, w.Keys[0].PublicKey) // Remove PublicKey index + } + delete(client.wallets, clientID) // Remove ClientID index + delete(client.wg, clientID) + delete(client.walletCount, clientID) + } +} + // SetWalletMode sets current wallet split key mode. func SetWalletMode(mode bool) { if client.wallet != nil { @@ -276,153 +521,142 @@ func TxnFee() uint64 { } func IsWalletSet() bool { - return client.wallet.ClientID != "" -} - -func PublicKey(clients ...string) string { - if len(clients) > 0 && clients[0] != "" && client.wallets[clients[0]] != nil { - if client.wallets[clients[0]] == nil { - fmt.Println("Public key is empty") - return "" + client.mu.RLock() + defer client.mu.RUnlock() + if client.wallet != nil && client.wallet.ClientID != "" { + return true + } + return len(client.wallets) > 0 +} + +// PublicKey lookup uses read lock +func PublicKey(keys ...string) string { + if len(keys) > 0 && keys[0] != "" { + client.mu.RLock() + if client.wallets != nil { + if w, ok := client.wallets[keys[0]]; ok && w != nil { + k := w.ClientKey + client.mu.RUnlock() + return k + } } - return client.wallets[clients[0]].ClientKey + client.mu.RUnlock() } - return client.wallet.ClientKey -} - -func Mnemonic() string { - return client.wallet.Mnemonic -} - -func PrivateKey() string { - for _, kv := range client.wallet.Keys { - return kv.PrivateKey + if client.wallet != nil { + return client.wallet.ClientKey } return "" } -func Id(clients ...string) string { - if len(clients) > 0 && clients[0] != "" && client.wallets[clients[0]] != nil { - if client.wallets[clients[0]] == nil { - fmt.Println("Id is empty : ", clients[0]) - return "" +// PublicKey lookup uses read lock +func SigningKey(keys ...string) (string, error) { + if len(keys) > 0 && keys[0] != "" { + client.mu.RLock() + if client.wallets != nil { + if w, ok := client.wallets[keys[0]]; ok && w != nil { + k := w.Keys[0].PublicKey + client.mu.RUnlock() + return k, nil + } else { + client.mu.RUnlock() + return "", errors.New("multi-wallet-settings err: " + keys[0]) + } } - return client.wallets[clients[0]].ClientID + client.mu.RUnlock() } - return client.wallet.ClientID -} - -func GetWallet() *zcncrypto.Wallet { - return client.wallet -} -func GetClient() *zcncrypto.Wallet { - return client.wallet + if client.wallet != nil { + return client.wallet.ClientID, nil + } + return "", errors.New("wallet not initialized") } -// InitSDK Initialize the storage SDK -// -// - walletJSON: Client's wallet JSON -// - blockWorker: Block worker URL (block worker refers to 0DNS) -// - chainID: ID of the blokcchain network -// - signatureScheme: Signature scheme that will be used for signing transactions -// - preferredBlobbers: List of preferred blobbers to use when creating an allocation. This is usually configured by the client in the configuration files -// - nonce: Initial nonce value for the transactions -// - fee: Preferred value for the transaction fee, just the first value is taken -func InitSDK(walletJSON string, - blockWorker, chainID, signatureScheme string, - nonce int64, addWallet bool, - options ...int) error { - if addWallet { - wallet := zcncrypto.Wallet{} - err := json.Unmarshal([]byte(walletJSON), &wallet) - if err != nil { - return err - } +func Mnemonic() string { + if client.wallet != nil { + return client.wallet.Mnemonic + } + return "" +} - SetWallet(wallet) - SetSignatureScheme(signatureScheme) - SetNonce(nonce) - if len(options) > 0 { - SetTxnFee(uint64(options[0])) +// GetWalletMnemonic returns the mnemonic for a wallet identified by pubkey. +// If the pubkey is empty or not found, it returns the SDK default wallet mnemonic. +func GetWalletMnemonic(pubkey string) string { + if pubkey != "" { + client.mu.RLock() + if client.wallets != nil { + if w, ok := client.wallets[pubkey]; ok && w != nil { + mn := w.Mnemonic + client.mu.RUnlock() + return mn + } } + client.mu.RUnlock() } - var minConfirmation, minSubmit, confirmationChainLength, sharderConsensous int - if len(options) > 1 { - minConfirmation = options[1] - } - if len(options) > 2 { - minSubmit = options[2] - } - if len(options) > 3 { - confirmationChainLength = options[3] - } - if len(options) > 4 { - sharderConsensous = options[4] + if client.wallet != nil { + return client.wallet.Mnemonic } + return "" +} - err := Init(context.Background(), conf.Config{ - BlockWorker: blockWorker, - SignatureScheme: signatureScheme, - ChainID: chainID, - MinConfirmation: minConfirmation, - MinSubmit: minSubmit, - ConfirmationChainLength: confirmationChainLength, - SharderConsensous: sharderConsensous, - }) - if err != nil { - return err +func PrivateKey() string { + if client.wallet == nil { + return "" } - SetSdkInitialized(true) - return nil + for _, kv := range client.wallet.Keys { + return kv.PrivateKey + } + return "" } -func InitSDKWithWebApp(params InitSdkOptions) error { - if params.MinConfirmation != nil && params.MinSubmit != nil && params.ConfirmationChainLength != nil && params.SharderConsensous != nil { - err := InitSDK(params.WalletJSON, params.BlockWorker, params.ChainID, params.SignatureScheme, params.Nonce, params.AddWallet, *params.MinConfirmation, *params.MinSubmit, *params.ConfirmationChainLength, *params.SharderConsensous) - if err != nil { - return err - } - } else { - err := InitSDK(params.WalletJSON, params.BlockWorker, params.ChainID, params.SignatureScheme, params.Nonce, params.AddWallet) - if err != nil { - return err +func Id(keys ...string) string { + if len(keys) > 0 && keys[0] != "" { + client.mu.RLock() + if client.wallets != nil { + if w, ok := client.wallets[keys[0]]; ok && w != nil { + id := w.ClientID + client.mu.RUnlock() + return id + } } + client.mu.RUnlock() } - conf.SetZboxAppConfigs(params.ZboxHost, params.ZboxAppType) - SetIsAppFlow(true) - return nil + if client.wallet != nil { + return client.wallet.ClientID + } + return "" } -func IsSDKInitialized() bool { - return sdkInitialized -} +// IsWalletSplit returns whether the wallet identified by keys[0] (pubkey or id) +// is a split-key wallet. If no key is provided the default SDK wallet's split +// flag is returned. +func IsWalletSplit(keys ...string) (bool, error) { + client.mu.RLock() + defer client.mu.RUnlock() -func SetSdkInitialized(val bool) { - sdkInitialized = val -} + if len(keys) > 0 && keys[0] != "" && client.wallets != nil { + if w, ok := client.wallets[keys[0]]; ok && w != nil { + return w.IsSplit, nil + } else { + return false, errors.New("multi-wallet-settings err: " + keys[0]) + } + } -func PopulateClient(walletJSON, signatureScheme string) (zcncrypto.Wallet, error) { - wallet := zcncrypto.Wallet{} - err := json.Unmarshal([]byte(walletJSON), &wallet) - if err != nil { - return wallet, err + if client.wallet != nil { + return client.wallet.IsSplit, nil } - SetWallet(wallet) - SetSignatureScheme(signatureScheme) - return wallet, nil + return false, nil } +// VerifySignature ... func VerifySignature(signature string, msg string) (bool, error) { ss := zcncrypto.NewSignatureScheme(client.signatureScheme) if err := ss.SetPublicKey(PublicKey()); err != nil { return false, err } - return ss.Verify(signature, msg) } diff --git a/core/client/zauth.go b/core/client/zauth.go index 9e31f3457..feac4e167 100644 --- a/core/client/zauth.go +++ b/core/client/zauth.go @@ -535,7 +535,7 @@ func CallZvaultRetrieveSharedWallets(serverAddr, token string) (string, error) { // ZauthSignTxn returns a function that sends a txn signing request to the zauth server func ZauthSignTxn(serverAddr string) sys.AuthorizeFunc { - return func(msg string) (string, error) { + return func(msg string, keys ...string) (string, error) { req, err := http.NewRequest("POST", serverAddr+"/sign/txn", bytes.NewBuffer([]byte(msg))) if err != nil { return "", errors.Wrap(err, "failed to create HTTP request") @@ -543,6 +543,14 @@ func ZauthSignTxn(serverAddr string) sys.AuthorizeFunc { req.Header.Set("Content-Type", "application/json") c := GetClient() pubkey := c.Keys[0].PublicKey + if len(keys) > 0 { + c = GetWalletByKey(keys[0]) + if c == nil { + return "", errors.Errorf("wallet not found for pubkey: %s", keys[0]) + } + pubkey = c.Keys[0].PublicKey + } + req.Header.Set("X-Peer-Public-Key", pubkey) client := &http.Client{} @@ -572,7 +580,7 @@ func ZauthSignTxn(serverAddr string) sys.AuthorizeFunc { } func ZauthAuthCommon(serverAddr string) sys.AuthorizeFunc { - return func(msg string) (string, error) { + return func(msg string, keys ...string) (string, error) { req, err := http.NewRequest("POST", serverAddr+"/sign/msg", bytes.NewBuffer([]byte(msg))) if err != nil { return "", errors.Wrap(err, "failed to create HTTP request") @@ -580,6 +588,14 @@ func ZauthAuthCommon(serverAddr string) sys.AuthorizeFunc { c := GetClient() pubkey := c.Keys[0].PublicKey + if len(keys) > 0 { + c = GetWalletByKey(keys[0]) + if c == nil { + return "", errors.Errorf("multi-wallet-settings err: %v", keys[0]) + } + pubkey = c.Keys[0].PublicKey + } + req.Header.Set("Content-Type", "application/json") req.Header.Set("X-Peer-Public-Key", pubkey) diff --git a/core/sys/sign.go b/core/sys/sign.go index 37ac18479..d66baa171 100644 --- a/core/sys/sign.go +++ b/core/sys/sign.go @@ -9,8 +9,11 @@ type KeyPair struct { // SignFunc sign method for request verification type SignFunc func(hash string, signatureScheme string, keys []KeyPair) (string, error) +// SignFunc sign method for request verification +type SignWithAuthFunc func(hash string, signatureScheme string, keys []KeyPair, pubkeys ...string) (string, error) + type VerifyFunc func(signature string, msg string) (bool, error) type VerifyWithFunc func(pk, signature string, msg string) (bool, error) -type AuthorizeFunc func(msg string) (string, error) +type AuthorizeFunc func(msg string, pubkeys... string) (string, error) diff --git a/core/sys/vars.go b/core/sys/vars.go index f1f0fde5c..866c3980d 100644 --- a/core/sys/vars.go +++ b/core/sys/vars.go @@ -15,7 +15,7 @@ var ( // Sign sign method. it should be initialized on different platform. Sign SignFunc - SignWithAuth SignFunc + SignWithAuth SignWithAuthFunc // Verify verify method. it should be initialized on different platform. Verify VerifyFunc diff --git a/core/transaction/entity.go b/core/transaction/entity.go index 995a45954..5a594e229 100644 --- a/core/transaction/entity.go +++ b/core/transaction/entity.go @@ -166,7 +166,7 @@ const ( FEES_TABLE = `/v1/fees_table` ) -type SignFunc = func(msg string) (string, error) +type SignFunc = func(msg string, keys ...string) (string, error) type VerifyFunc = func(publicKey, signature, msgHash string) (bool, error) type SignWithWallet = func(msg string, wallet interface{}) (string, error) @@ -211,6 +211,14 @@ func (t *Transaction) getAuthorize() (string, error) { return "", errors.New("not_initialized", "no authorize func is set, define it in native code and set in sys") } + if t.MultiWalletSupportKey != "" { + authorize, err := sys.Authorize(string(jsonByte), t.MultiWalletSupportKey) + if err != nil { + return "", err + } + return authorize, nil + } + authorize, err := sys.Authorize(string(jsonByte)) if err != nil { return "", err @@ -222,7 +230,7 @@ func (t *Transaction) getAuthorize() (string, error) { func (t *Transaction) ComputeHashAndSign(signHandler SignFunc) error { t.ComputeHashData() var err error - t.Signature, err = signHandler(t.Hash) + t.Signature, err = signHandler(t.Hash, t.MultiWalletSupportKey) if err != nil { return err } @@ -270,7 +278,7 @@ func (t *Transaction) VerifySigWith(pubkey string, verifyHandler VerifyFunc) (bo } func SendTransactionSync(txn *Transaction, miners []string) error { - const requestTimeout = 3 * time.Second // Timeout for each request + const requestTimeout = 12 * time.Second // Timeout for each request fails := make(chan error, len(miners)) var wg sync.WaitGroup @@ -282,7 +290,7 @@ func SendTransactionSync(txn *Transaction, miners []string) error { go func(url string) { defer wg.Done() - // Create a context with a 30-second timeout for each request + // Create a context with a 12-second timeout for each request ctx, cancel := context.WithTimeout(context.Background(), requestTimeout) defer cancel() @@ -320,6 +328,13 @@ func SendTransactionSync(txn *Transaction, miners []string) error { } } + // Reset stable miners list if any miner failed + if failureCount > 0 { + if nodeClient, err := client.GetNode(); err == nil { + nodeClient.ResetStableMiners() + } + } + if failureCount == len(miners) { return fmt.Errorf(dominantErr) } @@ -523,22 +538,21 @@ func SmartContractTxnValue(scAddress string, sn SmartContractTxnData, value uint } func SmartContractTxnValueFeeWithRetry(scAddress string, sn SmartContractTxnData, - value, fee uint64, verifyTxn bool, clients ...string) (hash, out string, nonce int64, t *Transaction, err error) { - hash, out, nonce, t, err = SmartContractTxnValueFee(scAddress, sn, value, fee, verifyTxn, clients...) + value, fee uint64, verifyTxn bool, keys ...string) (hash, out string, nonce int64, t *Transaction, err error) { + hash, out, nonce, t, err = SmartContractTxnValueFee(scAddress, sn, value, fee, verifyTxn, keys...) if err != nil && (strings.Contains(err.Error(), "invalid transaction nonce") || strings.Contains(err.Error(), "invalid future transaction")) { - return SmartContractTxnValueFee(scAddress, sn, value, fee, verifyTxn, clients...) + return SmartContractTxnValueFee(scAddress, sn, value, fee, verifyTxn, keys...) } return } func SmartContractTxnValueFee(scAddress string, sn SmartContractTxnData, - value, fee uint64, verifyTxn bool, clients ...string) (hash, out string, nonce int64, t *Transaction, err error) { + value, fee uint64, verifyTxn bool, keys ...string) (hash, out string, nonce int64, t *Transaction, err error) { - clientId := client.Id() - if len(clients) > 0 && clients[0] != "" { - clientId = clients[0] - } + // Determine client identifier/public key to use for signing. If an explicit key is + // provided in keys varargs, prefer it; otherwise default to SDK client id. + clientId := client.Id(keys...) var requestBytes []byte if requestBytes, err = json.Marshal(sn); err != nil { @@ -555,8 +569,8 @@ func SmartContractTxnValueFee(scAddress string, sn SmartContractTxnData, return } - txn := NewTransactionEntity(client.Id(clientId), - cfg.ChainID, client.PublicKey(clientId), nonce) + txn := NewTransactionEntity(clientId, + cfg.ChainID, client.PublicKey(keys...), nonce) txn.TransactionData = string(requestBytes) txn.ToClientID = scAddress @@ -565,8 +579,8 @@ func SmartContractTxnValueFee(scAddress string, sn SmartContractTxnData, txn.TransactionType = TxnTypeSmartContract txn.ClientID = clientId - if len(clients) > 1 { - txn.ToClientID = clients[1] + if len(keys) > 1 { + txn.ToClientID = keys[1] txn.TransactionType = TxnTypeSend } @@ -586,12 +600,20 @@ func SmartContractTxnValueFee(scAddress string, sn SmartContractTxnData, txn.TransactionNonce = client.Cache.GetNextNonce(txn.ClientID) } + if len(keys) > 0 && keys[0] != "" { + txn.MultiWalletSupportKey = keys[0] + } + err = txn.ComputeHashAndSign(client.SignFn) if err != nil { return } - - if client.GetClient().IsSplit { + + isSplit, err := client.IsWalletSplit(keys...) + if err != nil { + return + } + if isSplit { txn.Signature, err = txn.getAuthorize() if err != nil { return diff --git a/core/transaction/transaction.go b/core/transaction/transaction.go index 985e2f37c..cf6b74743 100644 --- a/core/transaction/transaction.go +++ b/core/transaction/transaction.go @@ -2,20 +2,25 @@ package transaction // Transaction entity that encapsulates the transaction related data and meta data type Transaction struct { - Hash string `json:"hash,omitempty"` - Version string `json:"version,omitempty"` - ClientID string `json:"client_id,omitempty"` - PublicKey string `json:"public_key,omitempty"` - ToClientID string `json:"to_client_id,omitempty"` - ChainID string `json:"chain_id,omitempty"` - TransactionData string `json:"transaction_data"` - Value uint64 `json:"transaction_value"` - Signature string `json:"signature,omitempty"` - CreationDate int64 `json:"creation_date,omitempty"` - TransactionType int `json:"transaction_type"` - TransactionOutput string `json:"transaction_output,omitempty"` - TransactionFee uint64 `json:"transaction_fee"` - TransactionNonce int64 `json:"transaction_nonce"` - OutputHash string `json:"txn_output_hash"` - Status int `json:"transaction_status"` + Hash string `json:"hash,omitempty"` + Version string `json:"version,omitempty"` + ClientID string `json:"client_id,omitempty"` + PublicKey string `json:"public_key,omitempty"` + // MultiWalletSupportKey is set when the transaction needs external + // authorization (split-key wallets). It is embedded in the marshaled + // transaction so the external authorizer (sys.Authorize) can know which + // public key the signature should be associated with. + MultiWalletSupportKey string `json:"authorizer_public_key,omitempty"` + ToClientID string `json:"to_client_id,omitempty"` + ChainID string `json:"chain_id,omitempty"` + TransactionData string `json:"transaction_data"` + Value uint64 `json:"transaction_value"` + Signature string `json:"signature,omitempty"` + CreationDate int64 `json:"creation_date,omitempty"` + TransactionType int `json:"transaction_type"` + TransactionOutput string `json:"transaction_output,omitempty"` + TransactionFee uint64 `json:"transaction_fee"` + TransactionNonce int64 `json:"transaction_nonce"` + OutputHash string `json:"txn_output_hash"` + Status int `json:"transaction_status"` } diff --git a/core/util/httpnet.go b/core/util/httpnet.go index bcc5f2b77..7a9e9703f 100644 --- a/core/util/httpnet.go +++ b/core/util/httpnet.go @@ -85,18 +85,27 @@ func init() { func httpDo(req *http.Request, ctx context.Context, cncl context.CancelFunc, f func(*http.Response, error) error) error { c := make(chan error, 1) + done := make(chan struct{}) - go func() { c <- f(Client.Do(req.WithContext(ctx))) }() + go func() { + select { + case c <- f(Client.Do(req.WithContext(ctx))): + // normal completion + case <-done: + // context cancelled, do not call f + } + }() select { - case <-ctx.Done(): - // Use the cancel function only after trying to get the result. - <-c // Wait for f to return. - return ctx.Err() - case err := <-c: - // Ensure that we call cncl after we are done with the response - defer cncl() // Move this here to ensure we cancel after processing - return err + case <-ctx.Done(): + // Use the cancel function only after trying to get the result. + close(done) + cncl() + return ctx.Err() + case err := <-c: + // Ensure that we call cncl after we are done with the response + defer cncl() // Move this here to ensure we cancel after processing + return err } } diff --git a/core/version/version.go b/core/version/version.go index fa0af25cc..4f54f23ab 100644 --- a/core/version/version.go +++ b/core/version/version.go @@ -1,5 +1,6 @@ + //====== THIS IS AUTOGENERATED FILE. DO NOT MODIFY ======== package version +const VERSIONSTR = "v1.20.6-1-gafade4fb" -const VERSIONSTR = "v1.17.11-269-g7fd90660" diff --git a/core/zcncrypto/bls0chain_wasm.go b/core/zcncrypto/bls0chain_wasm.go index a7592ed08..de9009d96 100644 --- a/core/zcncrypto/bls0chain_wasm.go +++ b/core/zcncrypto/bls0chain_wasm.go @@ -10,7 +10,7 @@ import ( ) var ( - Sign func(hash string) (string, error) + Sign func(hash string, keys ...string) (string, error) ) // WasmScheme - a signature scheme for BLS0Chain Signature diff --git a/mobilesdk/sdk/sdk.go b/mobilesdk/sdk/sdk.go index 8ce6558c9..6fb295493 100644 --- a/mobilesdk/sdk/sdk.go +++ b/mobilesdk/sdk/sdk.go @@ -508,5 +508,8 @@ func decodeTicket(ticket string) (string, string, uint64, error) { // } // } func RegisterAuthorizer(auth Autorizer) { - sys.Authorize = auth.Auth + sys.Authorize = func(msg string, clientIDs ...string) (string, error) { + // clientIDs are ignored as the interface only accepts msg + return auth.Auth(msg) + } } diff --git a/wasmsdk/allocation_mw.go b/wasmsdk/allocation_mw.go new file mode 100644 index 000000000..027a6bbf8 --- /dev/null +++ b/wasmsdk/allocation_mw.go @@ -0,0 +1,387 @@ +//go:build js && wasm +// +build js,wasm + +package main + +import ( + "errors" + "strings" + "sync" + "syscall/js" + "time" + + "github.com/0chain/gosdk/core/transaction" + "github.com/0chain/gosdk/wasmsdk/jsbridge" + "github.com/0chain/gosdk/zboxcore/sdk" +) + +// MW (multi-wallet) variants of allocation helpers. Each function accepts an +// additional `key string` argument (the multi-wallet support key). When the +// underlying SDK supports explicit key-based signing (via variadic keys) the +// key is forwarded. When selecting an allocation object the cached helper +// `getAllocation(allocationID, key)` is used to select the allocation for that +// key. + +// createfreeallocationMW creates a free allocation +func createfreeallocationMW(freeStorageMarker string, key string) (string, error) { + allocationID, _, err := sdk.CreateFreeAllocation(freeStorageMarker, 0, key) + if err != nil { + sdkLogger.Error("Error creating free allocation: ", err) + return "", err + } + return allocationID, err +} + +// getAllocationBlobbersMW retrieves allocation blobbers (key is ignored) +func getAllocationBlobbersMW(preferredBlobberURLs []string, + dataShards, parityShards int, size int64, + minReadPrice, maxReadPrice, minWritePrice, maxWritePrice int64, isRestricted int, force bool, key string) ([]string, error) { + + if len(preferredBlobberURLs) > 0 { + return sdk.GetBlobberIds(preferredBlobberURLs) + } + + return sdk.GetAllocationBlobbers(sdk.StorageV2, dataShards, parityShards, size, isRestricted, sdk.PriceRange{ + Min: uint64(minReadPrice), + Max: uint64(maxReadPrice), + }, sdk.PriceRange{ + Min: uint64(minWritePrice), + Max: uint64(maxWritePrice), + }, force) +} + +// getBlobberIdsMW retrieves blobber ids from the given blobber urls (key ignored) +func getBlobberIdsMW(blobberUrls []string, key string) ([]string, error) { + return sdk.GetBlobberIds(blobberUrls) +} + +// reloadAllocationMW reload allocation from blockchain and update cache (supports key) +func reloadAllocationMW(allocationID, key string) (*sdk.Allocation, error) { + a, err := sdk.GetAllocation(allocationID, key) + if err != nil { + return nil, err + } + + it := &cachedAllocation{ + Allocation: a, + Expiration: time.Now().Add(5 * time.Minute), + } + + cachedAllocations.Add(allocationID, it) + + return it.Allocation, nil +} + +// createAllocationMW creates an allocation given allocation creation parameters +func createAllocationMW(datashards, parityshards int, size, authRoundExpiry int64, + minReadPrice, maxReadPrice, minWritePrice, maxWritePrice int64, lock int64, blobberIds, blobberAuthTickets []string, setThirdPartyExtendable, IsEnterprise, force bool, key string) ( + *transaction.Transaction, error) { + + options := sdk.CreateAllocationOptions{ + DataShards: datashards, + ParityShards: parityshards, + Size: size, + ReadPrice: sdk.PriceRange{ + Min: uint64(minReadPrice), + Max: uint64(maxReadPrice), + }, + WritePrice: sdk.PriceRange{ + Min: uint64(minWritePrice), + Max: uint64(maxWritePrice), + }, + Lock: uint64(lock), + BlobberIds: blobberIds, + ThirdPartyExtendable: setThirdPartyExtendable, + IsEnterprise: IsEnterprise, + StorageVersion: sdk.StorageV2, + BlobberAuthTickets: blobberAuthTickets, + Force: force, + AuthRoundExpiry: authRoundExpiry, + } + + sdkLogger.Info(options) + _, _, txn, err := sdk.CreateAllocationWith(options, key) + return txn, err +} + +// listAllocationsMW retrieves the list of allocations owned by the client +func listAllocationsMW(key string) ([]*sdk.Allocation, error) { + return sdk.GetAllocations(key) +} + +// transferAllocationMW transfers the ownership of an allocation to a new owner +func transferAllocationMW(allocationID, newOwnerId, newOwnerPublicKey, key string) error { + if allocationID == "" { + return RequiredArg("allocationID") + } + if newOwnerId == "" { + return RequiredArg("newOwnerId") + } + if newOwnerPublicKey == "" { + return RequiredArg("newOwnerPublicKey") + } + + _, _, err := sdk.TransferAllocation(allocationID, newOwnerId, newOwnerPublicKey, key) + if err == nil { + clearAllocation(allocationID) + } + return err +} + +// UpdateForbidAllocationMW updates allocation file options with optional key +func UpdateForbidAllocationMW(allocationID string, forbidupload, forbiddelete, forbidupdate, forbidmove, forbidcopy, forbidrename bool, key string) (string, error) { + fop := &sdk.FileOptionsParameters{ + ForbidUpload: sdk.FileOptionParam{Changed: forbidupload, Value: forbidupload}, + ForbidDelete: sdk.FileOptionParam{Changed: forbiddelete, Value: forbiddelete}, + ForbidUpdate: sdk.FileOptionParam{Changed: forbidupdate, Value: forbidupdate}, + ForbidMove: sdk.FileOptionParam{Changed: forbidmove, Value: forbidmove}, + ForbidCopy: sdk.FileOptionParam{Changed: forbidcopy, Value: forbidcopy}, + ForbidRename: sdk.FileOptionParam{Changed: forbidrename, Value: forbidrename}, + } + + hash, _, err := sdk.UpdateAllocation(0, 0, false, allocationID, 0, "", "", "", "", "", false, fop, "", key) + return hash, err +} + +// freezeAllocationMW freezes one of the client's allocations +func freezeAllocationMW(allocationID, key string) (string, error) { + hash, _, err := sdk.UpdateAllocation(0, 0, false, allocationID, 0, "", "", "", "", "", false, &sdk.FileOptionsParameters{ + ForbidUpload: sdk.FileOptionParam{Changed: true, Value: true}, + ForbidDelete: sdk.FileOptionParam{Changed: true, Value: true}, + ForbidUpdate: sdk.FileOptionParam{Changed: true, Value: true}, + ForbidMove: sdk.FileOptionParam{Changed: true, Value: true}, + ForbidCopy: sdk.FileOptionParam{Changed: true, Value: true}, + ForbidRename: sdk.FileOptionParam{Changed: true, Value: true}, + }, "", key) + if err == nil { + clearAllocation(allocationID) + } + return hash, err +} + +// cancelAllocationMW cancels one of the client's allocations +func cancelAllocationMW(allocationID, key string) (string, error) { + hash, _, err := sdk.CancelAllocation(allocationID, key) + if err == nil { + clearAllocation(allocationID) + } + return hash, err +} + +// updateAllocationWithRepairMW updates the allocation settings and repairs if necessary +func updateAllocationWithRepairMW(allocationID string, + size, authRoundExpiry int64, + extend bool, + lock int64, + addBlobberId, addBlobberAuthTicket, removeBlobberId, ownerSigninPublicKey, updateAllocTicket, callbackFuncName, key string) (string, error) { + + sdk.SetWasm() + allocationObj, err := getAllocation(allocationID, key) + if err != nil { + return "", err + } + + wg := &sync.WaitGroup{} + statusBar := &StatusBar{wg: wg, isRepair: true, totalBytesMap: make(map[string]int)} + wg.Add(1) + if callbackFuncName != "" { + callback := js.Global().Get(callbackFuncName) + statusBar.callback = func(totalBytes, completedBytes int, filename, objURL, err string) { + callback.Invoke(totalBytes, completedBytes, filename, objURL, err) + } + } + + alloc, hash, isRepairRequired, err := allocationObj.UpdateWithStatus(size, authRoundExpiry, extend, uint64(lock), addBlobberId, addBlobberAuthTicket, removeBlobberId, ownerSigninPublicKey, false, &sdk.FileOptionsParameters{}, updateAllocTicket) + if err != nil { + return hash, err + } + clearAllocation(allocationID) + + if isRepairRequired { + addWebWorkers(alloc) + if removeBlobberId != "" { + jsbridge.RemoveWorker(removeBlobberId) + } + err := alloc.RepairAlloc(statusBar) + if err != nil { + return "", err + } + wg.Wait() + if statusBar.err != nil { + return "", statusBar.err + } + } + + return hash, err +} + +// updateAllocationMW updates the allocation settings +func updateAllocationMW(allocationID string, + size, authRoundExpiry int64, extend bool, + lock int64, + addBlobberId, addBlobberAuthTicket, removeBlobberId, ownerSigninPublicKey string, setThirdPartyExtendable bool, key string) (string, error) { + + hash, _, err := sdk.UpdateAllocation(size, authRoundExpiry, extend, allocationID, uint64(lock), addBlobberId, addBlobberAuthTicket, removeBlobberId, "", ownerSigninPublicKey, setThirdPartyExtendable, &sdk.FileOptionsParameters{}, "", key) + if err == nil { + clearAllocation(allocationID) + } + return hash, err +} + +// getUpdateAllocTicketMW forwards key to SDK's GetUpdateAllocTicket +func getUpdateAllocTicketMW(allocationID, userID, operationType string, roundExpiry int64, key string) (string, error) { + return sdk.GetUpdateAllocTicket(allocationID, userID, operationType, roundExpiry, key) +} + +// getAllocationMW returns allocation for given id using the provided key (if any) +func getAllocationMW(allocationID, key string) (*sdk.Allocation, error) { + return getAllocation(allocationID, key) +} + +// getAllocationMinLockMW retrieves the minimum lock value for allocation creation. +// The `key` parameter is accepted for API symmetry but not forwarded because the +// underlying SDK call does not require a signing key for this computation. +func getAllocationMinLockMW(datashards, parityshards int, + size int64, + maxwritePrice uint64, + key string, +) (int64, error) { + writePrice := sdk.PriceRange{Min: 0, Max: maxwritePrice} + + value, err := sdk.GetAllocationMinLock(datashards, parityshards, size, writePrice) + if err != nil { + sdkLogger.Error(err) + return 0, err + } + sdkLogger.Info("allocation Minlock value", value) + return value, nil +} + +// getUpdateAllocationMinLockMW retrieves the minimum lock value required for an allocation update. +// The `key` parameter is accepted for API symmetry but is not used because the SDK +// function does not require a signing key. +func getUpdateAllocationMinLockMW( + allocationID string, + size int64, + extend bool, + addBlobberId, removeBlobberId, key string) (int64, error) { + return sdk.GetUpdateAllocationMinLock(allocationID, size, extend, addBlobberId, removeBlobberId) +} + +// getRemoteFileMapMW list all files in an allocation from the blobbers. +func getRemoteFileMapMW(allocationID string, key string) ([]*fileResp, error) { + if len(allocationID) == 0 { + return nil, RequiredArg("allocationID") + } + allocationObj, err := getAllocation(allocationID, key) + if err != nil { + return nil, err + } + + ref, err := allocationObj.GetRemoteFileMap(nil, "/") + if err != nil { + sdkLogger.Error(err) + return nil, err + } + + fileResps := make([]*fileResp, 0) + for path, data := range ref { + paths := strings.SplitAfter(path, "/") + var resp = fileResp{ + Name: paths[len(paths)-1], + Path: path, + FileInfo: data, + } + fileResps = append(fileResps, &resp) + } + + return fileResps, nil +} + +// lockWritePoolMW locks given number of tokens for duration in write pool +func lockWritePoolMW(allocID string, tokens, fee uint64, key string) (string, error) { + hash, _, err := sdk.WritePoolLock(allocID, tokens, fee, key) + return hash, err +} + +// lockStakePoolMW stake number of tokens for a given provider +func lockStakePoolMW(providerType, tokens, fee uint64, providerID string, key string) (string, error) { + hash, _, err := sdk.StakePoolLock(sdk.ProviderType(providerType), providerID, tokens, fee, key) + return hash, err +} + +// unlockStakePoolMW unlocks stake pool +func unlockStakePoolMW(providerType, fee uint64, providerID, clientID, key string) (int64, error) { + unstake, _, err := sdk.StakePoolUnlock(sdk.ProviderType(providerType), providerID, clientID, fee, key) + return unstake, err +} + +// collectRewardsMW collects rewards +func collectRewardsMW(providerType int, providerID, key string) (string, error) { + hash, _, err := sdk.CollectRewards(providerID, sdk.ProviderType(providerType), key) + return hash, err +} + +// getSkatePoolInfoMW gets stake pool info +func getSkatePoolInfoMW(providerType int, providerID, key string) (*sdk.StakePoolInfo, error) { + info, err := sdk.GetStakePoolInfo(sdk.ProviderType(providerType), providerID, key) + if err != nil { + return nil, err + } + return info, err +} + +// getAllocationWithMW retrieves allocation with auth ticket and optional key +func getAllocationWithMW(authTicket, key string) (*sdk.Allocation, error) { + sdk.SetWasm() + sdkAllocation, err := sdk.GetAllocationFromAuthTicket(authTicket, key) + if err != nil { + return nil, err + } + return sdkAllocation, err +} + +// convertTokenToSASMW converts tokens in ZCN to SAS. +func convertTokenToSASMW(token float64, key string) uint64 { + return uint64(token * float64(TOKEN_UNIT)) +} + +// allocationRepairMW issue repair process for an allocation +func allocationRepairMW(allocationID, remotePath, key string) error { + if len(allocationID) == 0 { + return RequiredArg("allocationID") + } + allocationObj, err := getAllocation(allocationID, key) + if err != nil { + return err + } + sdk.SetWasm() + wg := &sync.WaitGroup{} + statusBar := &StatusBar{wg: wg, isRepair: true, totalBytesMap: make(map[string]int)} + wg.Add(1) + + err = allocationObj.StartRepair("/tmp", remotePath, statusBar) + if err != nil { + PrintError("Upload failed.", err) + return err + } + wg.Wait() + if !statusBar.success { + return errors.New("upload failed: unknown") + } + return nil +} + +// repairSizeMW retrieves the repair size for a specific path in an allocation +func repairSizeMW(allocationID, remotePath, key string) (sdk.RepairSize, error) { + alloc, err := getAllocation(allocationID, key) + if err != nil { + return sdk.RepairSize{}, err + } + return alloc.RepairSize(remotePath) +} + +// generateOwnerSigningKeyMW generates owner signing key (key is ignored) +func generateOwnerSigningKeyMW(ownerPublicKey, ownerID, key string) (string, error) { + return sdk.GenerateOwnerSigningPublicKey(key) +} diff --git a/wasmsdk/auth_txn.go b/wasmsdk/auth_txn.go index e917512e5..269ff88c8 100644 --- a/wasmsdk/auth_txn.go +++ b/wasmsdk/auth_txn.go @@ -29,7 +29,7 @@ func registerAuthorizer(this js.Value, args []js.Value) interface{} { authCallback = parseAuthorizerCallback(args[0]) authResponseC = make(chan string, 1) - sys.Authorize = func(msg string) (string, error) { + sys.Authorize = func(msg string, clientIDs ...string) (string, error) { authCallback(msg) return <-authResponseC, nil } @@ -93,7 +93,7 @@ func registerAuthCommon(this js.Value, args []js.Value) interface{} { authMsgCallback = parseAuthorizerCallback(args[0]) authMsgResponseC = make(chan string, 1) - sys.AuthCommon = func(msg string) (string, error) { + sys.AuthCommon = func(msg string, clientIDs ...string) (string, error) { authMsgLock <- struct{}{} defer func() { <-authMsgLock @@ -129,6 +129,25 @@ func callAuth(this js.Value, args []js.Value) interface{} { return nil } +// callAuth Call the authorization callback function and provide the message to pass to it. +// The message is passed as the first argument to the js calling. +func callAuthMW(this js.Value, args []js.Value) interface{} { + fmt.Println("callAuth is called") + if len(args) == 0 { + return nil + } + + if authCallback != nil { + msg := args[0].String() + key := args[1].String() + result, _ := sys.Authorize(msg, key) + fmt.Println("auth is called, result:", result) + return js.ValueOf(result) + } + + return nil +} + // Parse the JavaScript callback function into Go AuthorizerCallback type func parseAuthorizerCallback(jsCallback js.Value) AuthCallbackFunc { return func(msg string) string { diff --git a/wasmsdk/blobber_mw.go b/wasmsdk/blobber_mw.go new file mode 100644 index 000000000..057d78cac --- /dev/null +++ b/wasmsdk/blobber_mw.go @@ -0,0 +1,849 @@ +//go:build js && wasm +// +build js,wasm + +package main + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "path" + "strings" + "sync" + "syscall/js" + "time" + + "github.com/0chain/gosdk/constants" + "github.com/0chain/gosdk/core/encryption" + "github.com/0chain/gosdk/core/pathutil" + "github.com/0chain/gosdk/core/sys" + "github.com/0chain/gosdk/core/transaction" + "github.com/0chain/gosdk/wasmsdk/jsbridge" + "github.com/0chain/gosdk/zboxcore/fileref" + "github.com/0chain/gosdk/zboxcore/sdk" + "github.com/0chain/gosdk/zboxcore/zboxutil" +) + +// NOTE: This file provides *MW suffixed* wrappers for several wasm blobber functions +// that accept an additional `key string` parameter (the MultiWalletSupportKey). +// Where the underlying SDK exposes a way to use a per-operation key, the wrapper +// forwards it. Where the SDK does not currently accept a key for that operation, +// the wrapper will either call the existing function (and ignore the key) or +// return an error indicating the key cannot be applied. + +// listObjectsMW lists allocation objects but signs the list request using `key` when provided. +func listObjectsMW(allocationID string, remotePath string, offset, pageLimit int, key string) (*sdk.ListResult, error) { + alloc, err := getAllocation(allocationID, key) + if err != nil { + return nil, err + } + if key != "" { + return alloc.ListDir(remotePath, sdk.WithListRequestOffset(offset), sdk.WithListRequestPageLimit(pageLimit), sdk.WithListRequestPubKey(key)) + } + return alloc.ListDir(remotePath, sdk.WithListRequestOffset(offset), sdk.WithListRequestPageLimit(pageLimit)) +} + +// listObjectsFromAuthTicketMW lists allocation objects using an auth ticket and an optional signing key. +func listObjectsFromAuthTicketMW(allocationID, authTicket, lookupHash string, offset, pageLimit int, key string) (*sdk.ListResult, error) { + alloc, err := getAllocation(allocationID, key) + if err != nil { + return nil, err + } + if key != "" { + return alloc.ListDirFromAuthTicket(authTicket, lookupHash, sdk.WithListRequestOffset(offset), sdk.WithListRequestPageLimit(pageLimit), sdk.WithListRequestPubKey(key)) + } + return alloc.ListDirFromAuthTicket(authTicket, lookupHash, sdk.WithListRequestOffset(offset), sdk.WithListRequestPageLimit(pageLimit)) +} + +// createDirMW creates a directory and uses the provided key for signing the multi-operation. +func createDirMW(allocationID, remotePath, key string) error { + if allocationID == "" { + return errors.New("allocationID required") + } + if remotePath == "" { + return errors.New("remotePath required") + } + allocationObj, err := getAllocation(allocationID, key) + if err != nil { + return err + } + // Use DoMultiOperation and set the MultiWalletSupportKey on the MultiOperation + return allocationObj.DoMultiOperation([]sdk.OperationRequest{{ + OperationType: constants.FileOperationCreateDir, + RemotePath: remotePath, + }}, func(mo *sdk.MultiOperation) { mo.MultiWalletSupportKey = key }) +} + +// DeleteMW deletes a file using the provided MultiWalletSupportKey. +func DeleteMW(allocationID, remotePath, key string) (*FileCommandResponse, error) { + if allocationID == "" { + return nil, RequiredArg("allocationID") + } + if remotePath == "" { + return nil, RequiredArg("remotePath") + } + allocationObj, err := getAllocation(allocationID, key) + if err != nil { + return nil, err + } + + err = allocationObj.DoMultiOperation([]sdk.OperationRequest{{ + OperationType: constants.FileOperationDelete, + RemotePath: remotePath, + }}, func(mo *sdk.MultiOperation) { mo.MultiWalletSupportKey = key }) + if err != nil { + return nil, err + } + resp := &FileCommandResponse{CommandSuccess: true} + return resp, nil +} + +// RenameMW renames a file using the provided key for signing. +func RenameMW(allocationID, remotePath, destName, key string) (*FileCommandResponse, error) { + if allocationID == "" { + return nil, RequiredArg("allocationID") + } + if remotePath == "" { + return nil, RequiredArg("remotePath") + } + if destName == "" { + return nil, RequiredArg("destName") + } + allocationObj, err := getAllocation(allocationID, key) + if err != nil { + return nil, err + } + + err = allocationObj.DoMultiOperation([]sdk.OperationRequest{{ + OperationType: constants.FileOperationRename, + RemotePath: remotePath, + DestName: destName, + }}, func(mo *sdk.MultiOperation) { mo.MultiWalletSupportKey = key }) + if err != nil { + return nil, err + } + resp := &FileCommandResponse{CommandSuccess: true} + return resp, nil +} + +// CopyMW copies a file and signs using the provided key. +func CopyMW(allocationID, remotePath, destPath, key string) (*FileCommandResponse, error) { + if allocationID == "" { + return nil, RequiredArg("allocationID") + } + if remotePath == "" { + return nil, RequiredArg("remotePath") + } + if destPath == "" { + return nil, RequiredArg("destPath") + } + allocationObj, err := getAllocation(allocationID, key) + if err != nil { + return nil, err + } + + err = allocationObj.DoMultiOperation([]sdk.OperationRequest{{ + OperationType: constants.FileOperationCopy, + RemotePath: remotePath, + DestPath: destPath, + }}, func(mo *sdk.MultiOperation) { mo.MultiWalletSupportKey = key }) + if err != nil { + return nil, err + } + resp := &FileCommandResponse{CommandSuccess: true} + return resp, nil +} + +// MoveMW moves a file and signs using the provided key. +func MoveMW(allocationID, remotePath, destPath, key string) (*FileCommandResponse, error) { + if allocationID == "" { + return nil, RequiredArg("allocationID") + } + if remotePath == "" { + return nil, RequiredArg("remotePath") + } + if destPath == "" { + return nil, RequiredArg("destPath") + } + allocationObj, err := getAllocation(allocationID, key) + if err != nil { + return nil, err + } + + err = allocationObj.DoMultiOperation([]sdk.OperationRequest{{ + OperationType: constants.FileOperationMove, + RemotePath: remotePath, + DestPath: destPath, + }}, func(mo *sdk.MultiOperation) { mo.MultiWalletSupportKey = key }) + if err != nil { + return nil, err + } + resp := &FileCommandResponse{CommandSuccess: true} + return resp, nil +} + +// MultiOperationMW performs multiple operations and signs the whole multi-operation with `key`. +func MultiOperationMW(allocationID string, jsonMultiUploadOptions string, key string) error { + if allocationID == "" { + return errors.New("AllocationID is required") + } + if jsonMultiUploadOptions == "" { + return errors.New("operations are empty") + } + var options []MultiOperationOption + if err := json.Unmarshal([]byte(jsonMultiUploadOptions), &options); err != nil { + return err + } + + totalOp := len(options) + operations := make([]sdk.OperationRequest, totalOp) + for idx, op := range options { + operations[idx] = sdk.OperationRequest{ + OperationType: op.OperationType, + RemotePath: op.RemotePath, + DestName: op.DestName, + DestPath: op.DestPath, + } + } + allocationObj, err := getAllocation(allocationID, key) + if err != nil { + return err + } + return allocationObj.DoMultiOperation(operations, func(mo *sdk.MultiOperation) { mo.MultiWalletSupportKey = key }) +} + +// multiDownloadMW - start multi-download operation with per-op key support. +func multiDownloadMW(allocationID, jsonMultiDownloadOptions, authTicket, callbackFuncName, key string) (string, error) { + defer func() { + if r := recover(); r != nil { + PrintError("Recovered in multiDownload Error", r) + } + }() + wg := &sync.WaitGroup{} + useCallback := false + if callbackFuncName != "" { + useCallback = true + } + var options []*MultiDownloadOption + err := json.Unmarshal([]byte(jsonMultiDownloadOptions), &options) + if err != nil { + return "", err + } + var alloc *sdk.Allocation + if authTicket == "" { + alloc, err = getAllocation(allocationID, key) + } else { + alloc, err = sdk.GetAllocationFromAuthTicket(authTicket, key) + } + if err != nil { + return "", err + } + allStatusBar := make([]*StatusBar, len(options)) + wg.Add(len(options)) + for ind, option := range options { + fileName := strings.Replace(path.Base(option.RemotePath), "/", "-", -1) + localPath := allocationID + "_" + fileName + option.LocalPath = localPath + statusBar := &StatusBar{wg: wg, totalBytesMap: make(map[string]int)} + allStatusBar[ind] = statusBar + if useCallback { + callback := js.Global().Get(callbackFuncName) + statusBar.callback = func(totalBytes, completedBytes int, filename, objURL, err string) { + callback.Invoke(totalBytes, completedBytes, filename, objURL, err) + } + } + var mf sys.File + if option.DownloadToDisk { + if option.SuggestedName != "" { + fileName = option.SuggestedName + } + mf, err = jsbridge.NewFileWriter(fileName) + if err != nil { + PrintError(err.Error()) + return "", err + } + } else { + statusBar.localPath = localPath + fs, _ := sys.Files.Open(localPath) + mf, _ = fs.(*sys.MemFile) + } + + var downloader sdk.Downloader + if option.DownloadOp == 1 { + downloader, err = sdk.CreateDownloader(allocationID, localPath, option.RemotePath, + sdk.WithAllocation(alloc), + sdk.WithAuthticket(authTicket, option.RemoteLookupHash), + sdk.WithOnlyThumbnail(false), + sdk.WithBlocks(0, 0, option.NumBlocks), + sdk.WithFileHandler(mf), + sdk.WithDownloadPubKey(key), + ) + } else { + downloader, err = sdk.CreateDownloader(allocationID, localPath, option.RemotePath, + sdk.WithAllocation(alloc), + sdk.WithAuthticket(authTicket, option.RemoteLookupHash), + sdk.WithOnlyThumbnail(true), + sdk.WithBlocks(0, 0, option.NumBlocks), + sdk.WithFileHandler(mf), + sdk.WithDownloadPubKey(key), + ) + } + if err != nil { + PrintError(err.Error()) + return "", err + } + defer sys.Files.Remove(option.LocalPath) //nolint + downloader.Start(statusBar, ind == len(options)-1) + } + wg.Wait() + resp := make([]DownloadCommandResponse, len(options)) + for ind, statusBar := range allStatusBar { + statusResponse := DownloadCommandResponse{} + if !statusBar.success { + statusResponse.CommandSuccess = false + statusResponse.Error = "Download failed: " + statusBar.err.Error() + } else { + statusResponse.CommandSuccess = true + statusResponse.FileName = options[ind].RemoteFileName + statusResponse.Url = statusBar.objURL + } + resp[ind] = statusResponse + } + respBytes, err := json.Marshal(resp) + if err != nil { + return "", err + } + return string(respBytes), nil +} + +// uploadMW uploads a single file using the provided per-op key when fetching the allocation. +func uploadMW(allocationID, remotePath string, fileBytes, thumbnailBytes []byte, webStreaming, encrypt, isUpdate, isRepair bool, numBlocks int, key string) (*FileCommandResponse, error) { + if len(allocationID) == 0 { + return nil, RequiredArg("allocationID") + } + if len(remotePath) == 0 { + return nil, RequiredArg("remotePath") + } + allocationObj, err := getAllocation(allocationID, key) + if err != nil { + PrintError("Error fetching the allocation", err) + return nil, err + } + wg := &sync.WaitGroup{} + statusBar := &StatusBar{wg: wg, totalBytesMap: make(map[string]int)} + wg.Add(1) + fileReader := bytes.NewReader(fileBytes) + localPath := remotePath + remotePath = zboxutil.RemoteClean(remotePath) + isabs := zboxutil.IsRemoteAbs(remotePath) + if !isabs { + err = errors.New("invalid_path: Path should be valid and absolute") + return nil, err + } + remotePath = zboxutil.GetFullRemotePath(localPath, remotePath) + _, fileName := pathutil.Split(remotePath) + mimeType, err := zboxutil.GetFileContentType(path.Ext(fileName), fileReader) + if err != nil { + return nil, err + } + fileMeta := sdk.FileMeta{ + Path: localPath, + ActualSize: int64(len(fileBytes)), + MimeType: mimeType, + RemoteName: fileName, + RemotePath: remotePath, + } + if numBlocks < 1 { + numBlocks = 100 + } + if allocationObj.DataShards > 7 { + numBlocks = 50 + } + ChunkedUpload, err := sdk.CreateChunkedUpload(context.TODO(), "/", allocationObj, fileMeta, fileReader, isUpdate, isRepair, webStreaming, + zboxutil.NewConnectionId(), + sdk.WithThumbnail(thumbnailBytes), + sdk.WithEncrypt(encrypt), + sdk.WithStatusCallback(statusBar), + sdk.WithChunkNumber(numBlocks), + sdk.WithUploadPubKey(key), + ) + if err != nil { + return nil, err + } + err = ChunkedUpload.Start() + if err != nil { + PrintError("Upload failed.", err) + return nil, err + } + wg.Wait() + if !statusBar.success { + return nil, errors.New("upload failed: unknown") + } + resp := &FileCommandResponse{CommandSuccess: true} + return resp, nil +} + + +// multiUploadMW uploads multiple files in parallel using per-op key when selecting the allocation. +func multiUploadMW(jsonBulkUploadOptions string, key string) ([]BulkUploadResult, error) { + var options []BulkUploadOption + err := json.Unmarshal([]byte(jsonBulkUploadOptions), &options) + if err != nil { + return nil, err + } + n := len(options) + if n == 0 { + return nil, errors.New("No files to upload") + } + allocationID := options[0].AllocationID + allocationObj, err := getAllocation(allocationID, key) + if err != nil { + return nil, err + } + err = addWebWorkers(allocationObj) + if err != nil { + return nil, err + } + wait := make(chan BulkUploadResult, 1) + for _, option := range options { + go func(o BulkUploadOption) { + result := BulkUploadResult{RemotePath: o.RemotePath} + defer func() { wait <- result }() + + ok, err := uploadWithJsFuncsWithKey(o.AllocationID, o.RemotePath, o.ReadChunkFuncName, o.FileSize, o.ThumbnailBytes.Buffer, o.IsWebstreaming, o.Encrypt, o.IsUpdate, o.IsRepair, o.NumBlocks, o.CallbackFuncName, key) + + result.Success = ok + if err != nil { + result.Error = err.Error() + result.Success = false + } + }(option) + } + results := make([]BulkUploadResult, 0, n) + for i := 0; i < n; i++ { + result := <-wait + results = append(results, result) + } + return results, nil +} + +// uploadWithJsFuncsWithKey mirrors uploadWithJsFuncs but allows passing per-op key when fetching allocation. +func uploadWithJsFuncsWithKey(allocationID, remotePath string, readChunkFuncName string, fileSize int64, thumbnailBytes []byte, webStreaming, encrypt, isUpdate, isRepair bool, numBlocks int, callbackFuncName string, key string) (bool, error) { + if len(allocationID) == 0 { + return false, RequiredArg("allocationID") + } + if len(remotePath) == 0 { + return false, RequiredArg("remotePath") + } + allocationObj, err := getAllocation(allocationID, key) + if err != nil { + PrintError("Error fetching the allocation", err) + return false, err + } + wg := &sync.WaitGroup{} + statusBar := &StatusBar{wg: wg, totalBytesMap: make(map[string]int)} + if callbackFuncName != "" { + callback := js.Global().Get(callbackFuncName) + statusBar.callback = func(totalBytes, completedBytes int, filename, objURL, err string) { + callback.Invoke(totalBytes, completedBytes, filename, objURL, err) + } + } + wg.Add(1) + + fileReader, err := jsbridge.NewFileReader(readChunkFuncName, fileSize, allocationObj.GetChunkReadSize(encrypt)) + if err != nil { + return false, err + } + + localPath := remotePath + remotePath = zboxutil.RemoteClean(remotePath) + isabs := zboxutil.IsRemoteAbs(remotePath) + if !isabs { + err = errors.New("invalid_path: Path should be valid and absolute") + return false, err + } + remotePath = zboxutil.GetFullRemotePath(localPath, remotePath) + + _, fileName := pathutil.Split(remotePath) + + mimeType, err := zboxutil.GetFileContentType(path.Ext(fileName), fileReader) + if err != nil { + return false, err + } + + fileMeta := sdk.FileMeta{ + Path: localPath, + ActualSize: fileSize, + MimeType: mimeType, + RemoteName: fileName, + RemotePath: remotePath, + } + + if numBlocks < 1 { + numBlocks = 100 + } + if allocationObj.DataShards > 7 { + numBlocks = 50 + } + + ChunkedUpload, err := sdk.CreateChunkedUpload(context.TODO(), "/", allocationObj, fileMeta, fileReader, isUpdate, isRepair, webStreaming, zboxutil.NewConnectionId(), + sdk.WithThumbnail(thumbnailBytes), + sdk.WithEncrypt(encrypt), + sdk.WithStatusCallback(statusBar), + sdk.WithChunkNumber(numBlocks), + sdk.WithUploadPubKey(key), + ) + if err != nil { + return false, err + } + + err = ChunkedUpload.Start() + if err != nil { + PrintError("Upload failed.", err) + return false, err + } + + wg.Wait() + if !statusBar.success { + return false, errors.New("upload failed: unknown") + } + + return true, nil +} + +// cancelUploadMW cancels an upload using the provided key when fetching the allocation. +func cancelUploadMW(allocationID, remotePath, key string) error { + if allocationID == "" { + return errors.New("allocationID required") + } + allocationObj, err := getAllocation(allocationID, key) + if err != nil { + return err + } + return allocationObj.CancelUpload(remotePath) +} + +// pauseUploadMW pauses an upload using the provided key when fetching the allocation. +func pauseUploadMW(allocationID, remotePath, key string) error { + if allocationID == "" { + return errors.New("allocationID required") + } + allocationObj, err := getAllocation(allocationID, key) + if err != nil { + return err + } + return allocationObj.PauseUpload(remotePath) +} + +// getFileStatsMW returns file stats and allows selecting the wallet via key. +func getFileStatsMW(allocationID, remotePath, key string) ([]*sdk.FileStats, error) { + if allocationID == "" { + return nil, RequiredArg("allocationID") + } + if remotePath == "" { + return nil, RequiredArg("remotePath") + } + allocationObj, err := getAllocation(allocationID, key) + if err != nil { + return nil, err + } + m, err := allocationObj.GetFileStats(remotePath) + if err != nil { + return nil, err + } + var stats []*sdk.FileStats + for _, v := range m { + stats = append(stats, v) + } + return stats, nil +} + +// updateBlobberSettingsMW updates blobber settings. The underlying SDK call does not accept a per-op key +// so the key parameter is ignored here (kept for API symmetry). +func updateBlobberSettingsMW(blobberSettingsJson string, key string) (*transaction.Transaction, error) { + // Forward to existing implementation (no per-op key supported in SDK wrapper) + return updateBlobberSettings(blobberSettingsJson) +} + +// ShareMW generates an auth ticket and uses the provided key when acquiring the allocation. +func ShareMW(allocationID, remotePath, clientID, encryptionPublicKey string, expiration int, revoke bool, availableAfter string, key string) (string, error) { + if allocationID == "" { + return "", RequiredArg("allocationID") + } + if remotePath == "" { + return "", RequiredArg("remotePath") + } + allocationObj, err := getAllocation(allocationID, key) + if err != nil { + return "", err + } + if revoke { + if err := allocationObj.RevokeShare(remotePath, clientID); err != nil { + return "", err + } + return "", nil + } + availableAt := time.Now() + ref, err := allocationObj.GetAuthTicket(remotePath, path.Base(remotePath), fileref.DIRECTORY, clientID, encryptionPublicKey, int64(expiration), &availableAt) + if err != nil { + return "", err + } + return ref, nil +} + +// getFileMetaByNameMW wraps getFileMetaByName and supports selecting the allocation via key. +func getFileMetaByNameMW(allocationID, fileNameQuery, key string) ([]*sdk.ConsolidatedFileMetaByName, error) { + allocationObj, err := getAllocation(allocationID, key) + if err != nil { + return nil, err + } + return allocationObj.GetFileMetaByName(fileNameQuery) +} + +// getFileMetaByAuthTicketMW wraps getFileMetaByAuthTicket and supports selecting the allocation via key. +func getFileMetaByAuthTicketMW(allocationID, authTicket, lookupHash, key string) (*sdk.ConsolidatedFileMeta, error) { + allocationObj, err := getAllocation(allocationID, key) + if err != nil { + return nil, err + } + return allocationObj.GetFileMetaFromAuthTicket(authTicket, lookupHash) +} + +// downloadBlocksMW downloads blocks and supports selecting allocation via key. +func downloadBlocksMW(allocId, remotePath, authTicket, lookupHash, writeChunkFuncName string, startBlock, endBlock int64, key string) ([]byte, error) { + return downloadBlocksWithKey(allocId, remotePath, authTicket, lookupHash, writeChunkFuncName, startBlock, endBlock, key) +} + +// repairAllocationMW repairs allocation using provided key for allocation selection. +func repairAllocationMW(allocationID, callbackFuncName, key string) error { + return repairAllocationWithKey(allocationID, callbackFuncName, key) +} + +// checkAllocStatusMW checks allocation status and supports key for allocation selection. +func checkAllocStatusMW(allocationID, key string) (string, error) { + alloc, err := getAllocation(allocationID, key) + if err != nil { + return "", err + } + status, _, err := alloc.CheckAllocStatus(key) + var statusStr string + switch status { + case sdk.Repair: + statusStr = "repair" + case sdk.Broken: + statusStr = "broken" + default: + statusStr = "ok" + } + return statusStr, err +} + +// skipStatusCheckMW sets the check status flag on the allocation, using the provided key. +func skipStatusCheckMW(allocationID string, checkStatus bool, key string) error { + alloc, err := getAllocation(allocationID, key) + if err != nil { + return err + } + alloc.SetCheckStatus(checkStatus) + return nil +} + +// terminateWorkersMW cancels workers for an allocation using provided key. +func terminateWorkersMW(allocationID, key string) { + alloc, err := getAllocation(allocationID, key) + if err != nil { + return + } + terminateWorkersWithAllocation(alloc) +} + +// createWorkersMW creates workers for allocation using provided key. +func createWorkersMW(allocationID, key string) error { + alloc, err := getAllocation(allocationID, key) + if err != nil { + return err + } + return addWebWorkers(alloc) +} + +// downloadDirectoryMW downloads a directory; uses provided key to select allocation. +func downloadDirectoryMW(allocationID, remotePath, authticket, callbackFuncName, key string) error { + return downloadDirectoryWithKey(allocationID, remotePath, authticket, callbackFuncName, key) +} + +// cancelDownloadDirectoryMW cancels a directory download; key ignored. +func cancelDownloadDirectoryMW(remotePath, key string) { + cancelDownloadDirectory(remotePath) +} + +// cancelDownloadBlocksMW cancels download blocks for allocation using provided key. +func cancelDownloadBlocksMW(allocationID, remotePath string, start, end int64, key string) error { + alloc, err := getAllocation(allocationID, key) + if err != nil { + return err + } + return alloc.CancelDownloadBlocks(remotePath, start, end) +} + +// setConsensusThresholdMW sets consensus threshold using provided key to find allocation. +func setConsensusThresholdMW(allocationID string, threshold int, key string) error { + alloc, err := getAllocation(allocationID, key) + if err != nil { + return err + } + alloc.SetConsensusThreshold(threshold) + return nil +} + +// downloadBlocks downloads file blocks using an optional per-op key when fetching the allocation. +func downloadBlocksWithKey(allocId, remotePath, authTicket, lookupHash, writeChunkFuncName string, startBlock, endBlock int64, key string) ([]byte, error) { + + if len(remotePath) == 0 && len(authTicket) == 0 { + return nil, RequiredArg("remotePath/authTicket") + } + + alloc, err := getAllocation(allocId, key) + if err != nil { + PrintError("Error fetching the allocation", err) + return nil, err + } + + var ( + wg = &sync.WaitGroup{} + statusBar = &StatusBar{wg: wg, totalBytesMap: make(map[string]int)} + ) + + if lookupHash == "" { + lookupHash = getLookupHash(allocId, remotePath) + } + + var fh sys.File + if writeChunkFuncName == "" { + pathHash := encryption.FastHash(fmt.Sprintf("%s:%d:%d", lookupHash, startBlock, endBlock)) + fs, err := sys.Files.Open(pathHash) + if err != nil { + return nil, fmt.Errorf("could not open local file: %v", err) + } + + mf, _ := fs.(*sys.MemFile) + if mf == nil { + return nil, fmt.Errorf("invalid memfile") + } + fh = mf + defer sys.Files.Remove(pathHash) //nolint + } else { + fh = jsbridge.NewFileCallbackWriter(writeChunkFuncName, lookupHash) + if fh == nil { + return nil, fmt.Errorf("could not create file writer, callback function not found") + } + } + + wg.Add(1) + if authTicket != "" { + err = alloc.DownloadByBlocksToFileHandlerFromAuthTicket(fh, authTicket, lookupHash, startBlock, endBlock, 100, remotePath, false, statusBar, true, sdk.WithFileCallback( + func() { + fh.Close() //nolint:errcheck + }, + )) + } else { + err = alloc.DownloadByBlocksToFileHandler( + fh, + remotePath, + startBlock, + endBlock, + 100, + false, + statusBar, true, sdk.WithFileCallback( + func() { + fh.Close() //nolint:errcheck + }, + )) + } + if err != nil { + return nil, err + } + wg.Wait() + var buf []byte + if mf, ok := fh.(*sys.MemFile); ok { + buf = mf.Buffer + } + return buf, nil +} + +// repairAllocation repairs the allocation using an optional per-op key for allocation lookup. +func repairAllocationWithKey(allocationID, callbackFuncName, key string) error { + alloc, err := getAllocation(allocationID, key) + if err != nil { + return err + } + err = addWebWorkers(alloc) + if err != nil { + return err + } + wg := &sync.WaitGroup{} + statusBar := &StatusBar{wg: wg, isRepair: true, totalBytesMap: make(map[string]int)} + wg.Add(1) + if callbackFuncName != "" { + callback := js.Global().Get(callbackFuncName) + statusBar.callback = func(totalBytes, completedBytes int, filename, objURL, err string) { + callback.Invoke(totalBytes, completedBytes, filename, objURL, err) + } + } + err = alloc.RepairAlloc(statusBar) + if err != nil { + return err + } + wg.Wait() + if statusBar.err != nil { + fmt.Println("Error in repair allocation: ", statusBar.err) + return statusBar.err + } + status, _, err := alloc.CheckAllocStatus() + if err != nil { + return err + } + if status == sdk.Repair || status == sdk.Broken { + fmt.Println("allocation repair failed") + return errors.New("allocation repair failed") + } + return nil +} + +// downloadDirectory downloads directory to local file system using fs api, will only work in browsers where fs api is available +// It uses the provided per-op key when selecting the allocation. +func downloadDirectoryWithKey(allocationID, remotePath, authticket, callbackFuncName, key string) error { + alloc, err := getAllocation(allocationID, key) + if err != nil { + return err + } + wg := &sync.WaitGroup{} + wg.Add(1) + statusBar := &StatusBar{wg: wg, totalBytesMap: make(map[string]int)} + if callbackFuncName != "" { + callback := js.Global().Get(callbackFuncName) + statusBar.callback = func(totalBytes, completedBytes int, filename, objURL, err string) { + callback.Invoke(totalBytes, completedBytes, filename, objURL, err) + } + } + ctx, cancel := context.WithCancelCause(context.Background()) + defer cancel(nil) + errChan := make(chan error, 1) + go func() { + errChan <- alloc.DownloadDirectory(ctx, remotePath, "", authticket, statusBar) + }() + downloadDirLock.Lock() + downloadDirContextMap[remotePath] = cancel + downloadDirLock.Unlock() + select { + case err = <-errChan: + if err != nil { + PrintError("Error in download directory: ", err) + } + return err + case <-ctx.Done(): + return context.Cause(ctx) + } +} + diff --git a/wasmsdk/bridge_mw.go b/wasmsdk/bridge_mw.go new file mode 100644 index 000000000..6ab6bcd6e --- /dev/null +++ b/wasmsdk/bridge_mw.go @@ -0,0 +1,199 @@ +package main + +import ( + "context" + "encoding/base64" + "encoding/json" + "github.com/0chain/gosdk/zcnbridge/errors" + "github.com/0chain/gosdk/zcnbridge/log" + "github.com/0chain/gosdk/zcncore" + "strconv" +) + + +// burnZCN Burns ZCN tokens and returns a hash of the burn transaction +// - amount: amount of ZCN tokens to burn +// - txnfee: transaction fee +func burnZCNMW(amount uint64, key string) string { //nolint + if bridge == nil { + return errors.New("burnZCN", "bridge is not initialized").Error() + } + + hash, _, err := bridge.BurnZCN(amount, key) + if err != nil { + return errors.Wrap("burnZCN", "failed to burn ZCN tokens", err).Error() + } + + return hash +} + +// mintZCN Mints ZCN tokens and returns a hash of the mint transaction +// - burnTrxHash: hash of the burn transaction +// - timeout: timeout in seconds +func mintZCNMW(burnTrxHash string, timeout int, key string) string { //nolint + mintPayload, + + err := bridge.QueryZChainMintPayload(burnTrxHash, key) + if err != nil { + return errors.Wrap("mintZCN", "failed to QueryZChainMintPayload", err).Error() + } + + hash, err := bridge.MintZCN(mintPayload, key) + if err != nil { + return errors.Wrap("mintZCN", "failed to MintZCN for txn "+hash, err).Error() + } + + return hash +} + +// getMintWZCNPayload returns the mint payload for the given burn transaction hash +// - burnTrxHash: hash of the burn transaction +func getMintWZCNPayloadMW(burnTrxHash string, key string) string { //nolint:unused + mintPayload, err := bridge.QueryEthereumMintPayload(burnTrxHash) + if err != nil { + return errors.Wrap("getMintWZCNPayload", "failed to query ethereum mint payload", err).Error() + } + var result []byte + result, err = json.Marshal(mintPayload) + if err != nil { + return errors.Wrap("getMintWZCNPayload", "failed to query ethereum mint payload", err).Error() + } + + return string(result) +} + +// getNotProcessedWZCNBurnEvents returns all not processed WZCN burn events from the Ethereum network +func getNotProcessedWZCNBurnEventsMW(key string) string { //nolint:unused + var ( + mintNonce int64 + res []byte + err error + ) + if res, err = zcncore.GetMintNonce(); err != nil { + return errors.Wrap("getNotProcessedWZCNBurnEvents", "failed to retreive last ZCN processed mint nonce", err).Error() + } + + if err = json.Unmarshal(res, &mintNonce); err != nil { + return errors.Wrap("getNotProcessedWZCNBurnEvents", "failed to unmarshal last ZCN processed mint nonce", err).Error() + } + + log.Logger.Debug("MintNonce = " + strconv.Itoa(int(mintNonce))) + burnEvents, err := bridge.QueryEthereumBurnEvents(strconv.Itoa(int(mintNonce)), key) + if err != nil { + return errors.Wrap("getNotProcessedWZCNBurnEvents", "failed to retrieve WZCN burn events", err).Error() + } + + var result []byte + result, err = json.Marshal(burnEvents) + if err != nil { + return errors.Wrap("getNotProcessedWZCNBurnEvents", "failed to marshal WZCN burn events", err).Error() + } + + return string(result) +} + +// getNotProcessedZCNBurnTickets Returns all not processed ZCN burn tickets burned for a certain ethereum address +func getNotProcessedZCNBurnTicketsMW(key string) string { //nolint:unused + userNonce, err := bridge.GetUserNonceMinted(context.Background(), bridge.EthereumAddress) + if err != nil { + return errors.Wrap("getNotProcessedZCNBurnTickets", "failed to retreive user nonce", err).Error() + } + + var ( + res []byte + burnTickets []zcncore.BurnTicket + ) + + res, err = zcncore.GetNotProcessedZCNBurnTickets(bridge.EthereumAddress, userNonce.String()) + if err != nil { + return errors.Wrap("getNotProcessedZCNBurnTickets", "failed to retreive ZCN burn tickets", err).Error() + } + + if err = json.Unmarshal(res, &burnTickets); err != nil { + return errors.Wrap("getNotProcessedZCNBurnTickets", "failed to unmarshal ZCN burn tickets", err).Error() + } + + var result []byte + result, err = json.Marshal(burnTickets) + if err != nil { + return errors.Wrap("getNotProcessedZCNBurnTickets", "failed to marshal ZCN burn tickets", err).Error() + } + + return string(result) +} + +// estimateBurnWZCNGasAmount performs gas amount estimation for the given burn wzcn transaction. +// - from: address of the sender +// - to: address of the receiver +// - amountTokens: amount of tokens to burn (as a string) +func estimateBurnWZCNGasAmountMW(from, to, amountTokens, key string) string { // nolint:golint,unused + estimateBurnWZCNGasAmountResponse, err := bridge.EstimateBurnWZCNGasAmount( + context.Background(), from, to, amountTokens, key) + if err != nil { + return errors.Wrap("estimateBurnWZCNGasAmount", "failed to estimate gas amount", err).Error() + } + + var result []byte + result, err = json.Marshal(estimateBurnWZCNGasAmountResponse) + if err != nil { + return errors.Wrap("estimateBurnWZCNGasAmount", "failed to marshal gas amount estimation result", err).Error() + } + + return string(result) +} + +// estimateMintWZCNGasAmount performs gas amount estimation for the given mint wzcn transaction. +// - from: address of the sender +// - to: address of the receiver +// - zcnTransaction: hash of the ZCN transaction +// - amountToken: amount of tokens to mint (as a string) +// - nonce: nonce of the transaction +// - signaturesRaw: encoded format (base-64) of the burn signatures received from the authorizers. +func estimateMintWZCNGasAmountMW(from, to, zcnTransaction, amountToken string, nonce int64, signaturesRaw []string, key string) string { // nolint:golint,unused + var signaturesBytes [][]byte + + var ( + signatureBytes []byte + err error + ) + + for _, signature := range signaturesRaw { + signatureBytes, err = base64.StdEncoding.DecodeString(signature) + if err != nil { + return errors.Wrap("estimateMintWZCNGasAmount", "failed to convert raw signature into bytes", err).Error() + } + + signaturesBytes = append(signaturesBytes, signatureBytes) + } + + estimateMintWZCNGasAmountResponse, err := bridge.EstimateMintWZCNGasAmount( + context.Background(), from, to, zcnTransaction, amountToken, nonce, signaturesBytes) + if err != nil { + return errors.Wrap("estimateMintWZCNGasAmount", "failed to estimate gas amount", err).Error() + } + + var result []byte + result, err = json.Marshal(estimateMintWZCNGasAmountResponse) + if err != nil { + return errors.Wrap("estimateMintWZCNGasAmount", "failed to marshal gas amount estimation result", err).Error() + } + + return string(result) +} + +// estimateGasPrice performs gas estimation for the given transaction using Alchemy enhanced API returning +// approximate final gas fee. +func estimateGasPriceMW(key string) string { // nolint:golint,unused + estimateGasPriceResponse, err := bridge.EstimateGasPrice(context.Background()) + if err != nil { + return errors.Wrap("estimateGasPrice", "failed to estimate gas price", err).Error() + } + + var result []byte + result, err = json.Marshal(estimateGasPriceResponse) + if err != nil { + return errors.Wrap("estimateGasPrice", "failed to marshal gas price estimation result", err).Error() + } + + return string(result) +} diff --git a/wasmsdk/cache.go b/wasmsdk/cache.go index 9089015b3..f22fa20e5 100644 --- a/wasmsdk/cache.go +++ b/wasmsdk/cache.go @@ -27,7 +27,7 @@ var ( // if not found in cache, fetch from blockchain // and store in cache // - allocationId is the allocation id -func getAllocation(allocationId string) (*sdk.Allocation, error) { +func getAllocation(allocationId string, keys ...string) (*sdk.Allocation, error) { it, ok := cachedAllocations.Get(allocationId) @@ -37,7 +37,7 @@ func getAllocation(allocationId string) (*sdk.Allocation, error) { } } sdk.SetWasm() - a, err := sdk.GetAllocation(allocationId) + a, err := sdk.GetAllocation(allocationId, keys...) if err != nil { return nil, err } diff --git a/wasmsdk/player_file.go b/wasmsdk/player_file.go index 82de0d4c8..bbe0aae04 100644 --- a/wasmsdk/player_file.go +++ b/wasmsdk/player_file.go @@ -23,6 +23,9 @@ type FilePlayer struct { authTicketObj *marker.AuthTicket playlistFile *sdk.PlaylistFile + // mwKey is the multi-wallet key used for per-op signing/selection. + mwKey string + downloadedChunks chan []byte downloadedLen int ctx context.Context @@ -66,7 +69,7 @@ func (p *FilePlayer) download(startBlock int64) { } fmt.Println("start:", startBlock, "end:", endBlock, "numBlocks:", p.numBlocks, "total:", p.playlistFile.NumBlocks) - data, err := downloadBlocks(p.allocationObj.ID, p.remotePath, p.authTicket, p.lookupHash, "", startBlock, endBlock) + data, err := downloadBlocksWithKey(p.allocationObj.ID, p.remotePath, p.authTicket, p.lookupHash, "", startBlock, endBlock, p.mwKey) // data, err := downloadBlocks2(int(startBlock), int(endBlock), p.allocationObj, p.remotePath) if err != nil { PrintError(err.Error()) @@ -159,7 +162,8 @@ func (p *FilePlayer) GetNext() []byte { } // createFilePalyer create player for remotePath -func createFilePalyer(allocationID, remotePath, authTicket, lookupHash string) (*FilePlayer, error) { +// accepts optional keys varargs to support multi-wallet selection when fetching allocations +func createFilePalyer(allocationID, remotePath, authTicket, lookupHash string, keys ...string) (*FilePlayer, error) { player := &FilePlayer{} player.prefetchQty = 3 player.remotePath = remotePath @@ -167,6 +171,9 @@ func createFilePalyer(allocationID, remotePath, authTicket, lookupHash string) ( player.lookupHash = lookupHash player.numBlocks = 10 player.allocationID = allocationID + if len(keys) > 0 { + player.mwKey = keys[0] + } //player is viewer if len(authTicket) > 0 { @@ -178,7 +185,7 @@ func createFilePalyer(allocationID, remotePath, authTicket, lookupHash string) ( return nil, err } - allocationObj, err := sdk.GetAllocationFromAuthTicket(authTicket) + allocationObj, err := sdk.GetAllocationFromAuthTicket(authTicket, keys...) if err != nil { PrintError("Error fetching the allocation", err) return nil, err @@ -197,7 +204,7 @@ func createFilePalyer(allocationID, remotePath, authTicket, lookupHash string) ( return nil, RequiredArg("allocationID") } - allocationObj, err := sdk.GetAllocation(allocationID) + allocationObj, err := getAllocation(allocationID, keys...) if err != nil { PrintError("Error fetching the allocation", err) return nil, err diff --git a/wasmsdk/player_mw.go b/wasmsdk/player_mw.go new file mode 100644 index 000000000..e25321fb8 --- /dev/null +++ b/wasmsdk/player_mw.go @@ -0,0 +1,61 @@ +//go:build js && wasm +// +build js,wasm + +package main + +import "errors" + +var currentPlayerMW Player + +// play starts playing a playable file or stream +// - allocationID is the allocation id +// - remotePath is the remote path of the file or stream +// - authTicket is the auth ticket, in case of accessing as a shared file +// - lookupHash is the lookup hash for the file +// - isLive is the flag to indicate if the file is live or not +// +// playMW starts playing a playable file or stream and accepts a key for multi-wallet signing. +func playMW(allocationID, remotePath, authTicket, lookupHash string, isLive bool, key string) error { + var err error + + if currentPlayerMW != nil { + currentPlayerMW.Stop() + currentPlayerMW = nil + } + + if isLive { + currentPlayerMW, err = createStreamPalyer(allocationID, remotePath, authTicket, lookupHash, key) + if err != nil { + return err + } + + } else { + currentPlayerMW, err = createFilePalyer(allocationID, remotePath, authTicket, lookupHash, key) + if err != nil { + return err + } + } + + return currentPlayerMW.Start() + +} + +// stop stops the current player +func stopMW() error { + if currentPlayerMW != nil { + currentPlayerMW.Stop() + } + + currentPlayerMW = nil + + return nil +} + +// getNextSegment gets the next segment of the current player +func getNextSegmentMW() ([]byte, error) { + if currentPlayerMW == nil { + return nil, errors.New("No player is available") + } + + return currentPlayerMW.GetNext(), nil +} diff --git a/wasmsdk/player_stream.go b/wasmsdk/player_stream.go index 51c844e20..2dc80c1ec 100644 --- a/wasmsdk/player_stream.go +++ b/wasmsdk/player_stream.go @@ -209,7 +209,8 @@ func (p *StreamPlayer) GetNext() []byte { } // createStreamPalyer create player for remotePath -func createStreamPalyer(allocationID, remotePath, authTicket, lookupHash string) (*StreamPlayer, error) { +// accepts optional keys varargs to support multi-wallet selection when fetching allocations +func createStreamPalyer(allocationID, remotePath, authTicket, lookupHash string, keys ...string) (*StreamPlayer, error) { player := &StreamPlayer{} player.prefetchQty = 3 @@ -227,7 +228,7 @@ func createStreamPalyer(allocationID, remotePath, authTicket, lookupHash string) return nil, err } - allocationObj, err := sdk.GetAllocationFromAuthTicket(authTicket) + allocationObj, err := sdk.GetAllocationFromAuthTicket(authTicket, keys...) if err != nil { PrintError("Error fetching the allocation", err) return nil, err @@ -246,7 +247,7 @@ func createStreamPalyer(allocationID, remotePath, authTicket, lookupHash string) return nil, RequiredArg("allocationID") } - allocationObj, err := sdk.GetAllocation(allocationID) + allocationObj, err := getAllocation(allocationID, keys...) if err != nil { PrintError("Error fetching the allocation", err) return nil, err diff --git a/wasmsdk/proxy.go b/wasmsdk/proxy.go index f70e7e471..51c988c12 100644 --- a/wasmsdk/proxy.go +++ b/wasmsdk/proxy.go @@ -54,13 +54,21 @@ func main() { jsSign := jsProxy.Get("sign") if !(jsSign.IsNull() || jsSign.IsUndefined()) { - signFunc := func(hash string) (string, error) { + signFunc := func(hash string, keys ...string) (string, error) { c := client.GetClient() if c == nil || len(c.Keys) == 0 { return "", errors.New("no keys found") } pk := c.Keys[0].PrivateKey + if len(keys) > 0 && keys[0] != "" { + wallet := client.GetWalletByKey(keys[0]) + if wallet == nil { + return "", errors.New("wallet not found") + } + pk = wallet.Keys[0].PrivateKey + } + result, err := jsbridge.Await(jsSign.Invoke(hash, pk)) if len(err) > 0 && !err[0].IsNull() { @@ -78,16 +86,24 @@ func main() { return signFunc(hash) } - sys.SignWithAuth = func(hash, signatureScheme string, keys []sys.KeyPair) (string, error) { + sys.SignWithAuth = func(hash, signatureScheme string, keys []sys.KeyPair, clientIds ...string) (string, error) { sig, err := sys.Sign(hash, signatureScheme, keys) if err != nil { return "", fmt.Errorf("failed to sign with split key: %v", err) } + // Get the first clientID from variadic arguments, or use default wallet clientID + var clientID string + if len(clientIds) > 0 && clientIds[0] != "" { + clientID = clientIds[0] + } else { + clientID = client.Wallet().ClientID + } + data, err := json.Marshal(client.AuthMessage{ Hash: hash, Signature: sig, - ClientID: client.Wallet().ClientID, + ClientID: clientID, }) if err != nil { return "", err @@ -217,6 +233,8 @@ func main() { "createThumbnail": createThumbnail, "makeSCRestAPICall": makeSCRestAPICall, "wasmType": getWasmType, + "addWallet": addWallet, + "removeWallet": removeWallet, //blobber "delete": Delete, @@ -252,11 +270,49 @@ func main() { "cancelDownloadDirectory": cancelDownloadDirectory, "cancelDownloadBlocks": cancelDownloadBlocks, "setConsensusThreshold": setConsensusThreshold, + //blobber mw (multi-wallet support) + "deleteMW": DeleteMW, + "shareMW": ShareMW, + "multiDownloadMW": multiDownloadMW, + "uploadMW": uploadMW, + // "setUploadModeMW": setUploadModeMW, + "multiUploadMW": multiUploadMW, + "multiOperationMW": MultiOperationMW, + "listObjectsMW": listObjectsMW, + "listObjectsFromAuthTicketMW": listObjectsFromAuthTicketMW, + "createDirMW": createDirMW, + "downloadBlocksMW": downloadBlocksMW, + "getFileStatsMW": getFileStatsMW, + "updateBlobberSettingsMW": updateBlobberSettingsMW, + "getRemoteFileMapMW": getRemoteFileMapMW, + // "getBlobbersMW": getBlobbersMW, + // "getcontainersMW": GetContainersMW, + // "updatecontainerMW": UpdateContainerMW, + // "searchcontainerMW": SearchContainerMW, + "updateForbidAllocationMW": UpdateForbidAllocationMW, + "sendMW": sendMW, + "cancelUploadMW": cancelUploadMW, + "pauseUploadMW": pauseUploadMW, + "repairAllocationMW": repairAllocationMW, + "checkAllocStatusMW": checkAllocStatusMW, + "skipStatusCheckMW": skipStatusCheckMW, + "terminateWorkersMW": terminateWorkersMW, + "createWorkersMW": createWorkersMW, + "getFileMetaByNameMW": getFileMetaByNameMW, + "getFileMetaByAuthTicketMW": getFileMetaByAuthTicketMW, + "downloadDirectoryMW": downloadDirectoryMW, + "cancelDownloadDirectoryMW": cancelDownloadDirectoryMW, + "cancelDownloadBlocksMW": cancelDownloadBlocksMW, + "setConsensusThresholdMW": setConsensusThresholdMW, // player "play": play, "stop": stop, "getNextSegment": getNextSegment, + // player mw + "playMW": playMW, + "stopMW": stopMW, + "getNextSegmentMW": getNextSegmentMW, //allocation "createAllocation": createAllocation, @@ -275,23 +331,55 @@ func main() { "getAllocationWith": getAllocationWith, "createfreeallocation": createfreeallocation, "getUpdateAllocTicket": getUpdateAllocTicket, + //allocation mw (multi-wallet support) + "createAllocationMW": createAllocationMW, + "getAllocationBlobbersMW": getAllocationBlobbersMW, + "getBlobberIdsMW": getBlobberIdsMW, + "listAllocationsMW": listAllocationsMW, + "getAllocationMW": getAllocationMW, + "reloadAllocationMW": reloadAllocationMW, + "transferAllocationMW": transferAllocationMW, + "freezeAllocationMW": freezeAllocationMW, + "cancelAllocationMW": cancelAllocationMW, + "updateAllocationMW": updateAllocationMW, + "updateAllocationWithRepairMW": updateAllocationWithRepairMW, + "getAllocationMinLockMW": getAllocationMinLockMW, + "getUpdateAllocationMinLockMW": getUpdateAllocationMinLockMW, + "getAllocationWithMW": getAllocationWithMW, + "createfreeallocationMW": createfreeallocationMW, + "getUpdateAllocTicketMW": getUpdateAllocTicketMW, // claim rewards "collectRewards": collectRewards, + //claim rewards mw + "collectRewardsMW": collectRewardsMW, // stakepool "getSkatePoolInfo": getSkatePoolInfo, "lockStakePool": lockStakePool, "unlockStakePool": unlockStakePool, + // stakepool mw + "getSkatePoolInfoMW": getSkatePoolInfoMW, + "lockStakePoolMW": lockStakePoolMW, + "unlockStakePoolMW": unlockStakePoolMW, // writepool "lockWritePool": lockWritePool, + // writepool mw + "lockWritePoolMW": lockWritePoolMW, + + "decodeAuthTicket": decodeAuthTicket, "allocationRepair": allocationRepair, "repairSize": repairSize, + // "decodeAuthTicketMW": decodeAuthTicketMW, + "allocationRepairMW": allocationRepairMW, + "repairSizeMW": repairSizeMW, "generateOwnerSigningKey": generateOwnerSigningKey, + "generateOwnerSigningKeyMW": generateOwnerSigningKeyMW, + // bridge "initBridge": initBridge, @@ -303,6 +391,15 @@ func main() { "estimateBurnWZCNGasAmount": estimateBurnWZCNGasAmount, "estimateMintWZCNGasAmount": estimateMintWZCNGasAmount, "estimateGasPrice": estimateGasPrice, + // bridge mw + "burnZCNMW": burnZCNMW, + "mintZCNMW": mintZCNMW, + "getMintWZCNPayloadMW": getMintWZCNPayloadMW, + "getNotProcessedWZCNBurnEventsMW": getNotProcessedWZCNBurnEventsMW, + "getNotProcessedZCNBurnTicketsMW": getNotProcessedZCNBurnTicketsMW, + "estimateBurnWZCNGasAmountMW": estimateBurnWZCNGasAmountMW, + "estimateMintWZCNGasAmountMW": estimateMintWZCNGasAmountMW, + "estimateGasPriceMW": estimateGasPriceMW, //zcn "getWalletBalance": getWalletBalance, @@ -320,6 +417,7 @@ func main() { "registerAuthorizer": js.FuncOf(registerAuthorizer), "registerAuthCommon": js.FuncOf(registerAuthCommon), "callAuth": js.FuncOf(callAuth), + "callAuthMW": js.FuncOf(callAuthMW), "authResponse": authResponse, // zauth @@ -356,13 +454,20 @@ func main() { if !(jsProxy.IsNull() || jsProxy.IsUndefined()) { jsSign := jsProxy.Get("sign") if !(jsSign.IsNull() || jsSign.IsUndefined()) { - signFunc := func(hash string) (string, error) { + signFunc := func(hash string, keys ...string) (string, error) { c := client.GetClient() if c == nil || len(c.Keys) == 0 { return "", errors.New("no keys found") } pk := c.Keys[0].PrivateKey + if len(keys) > 0 && keys[0] != "" { + wallet := client.GetWalletByKey(keys[0]) + if wallet == nil { + return "", errors.New("wallet not found") + } + pk = wallet.Keys[0].PrivateKey + } result, err := jsbridge.Await(jsSign.Invoke(hash, pk)) if len(err) > 0 && !err[0].IsNull() { @@ -378,17 +483,25 @@ func main() { return signFunc(hash) } - sys.SignWithAuth = func(hash, signatureScheme string, keys []sys.KeyPair) (string, error) { + sys.SignWithAuth = func(hash, signatureScheme string, keys []sys.KeyPair, clientIds ...string) (string, error) { fmt.Println("[worker] SignWithAuth pubkey:", keys[0]) sig, err := sys.Sign(hash, signatureScheme, keys) if err != nil { return "", fmt.Errorf("failed to sign with split key: %v", err) } + // Get the first clientID from variadic arguments, or use default wallet clientID + var clientID string + if len(clientIds) > 0 && clientIds[0] != "" { + clientID = clientIds[0] + } else { + clientID = client.Wallet().ClientID + } + data, err := json.Marshal(client.AuthMessage{ Hash: hash, Signature: sig, - ClientID: client.GetClient().ClientID, + ClientID: clientID, }) if err != nil { return "", err @@ -462,7 +575,7 @@ func main() { gInitProxyKeys(publicKey, privateKey) if isSplit { - sys.AuthCommon = func(msg string) (string, error) { + sys.AuthCommon = func(msg string, clientIDs ...string) (string, error) { // send message to main thread sendMessageToMainThread(msg) // wait for response from main thread diff --git a/wasmsdk/sdk.go b/wasmsdk/sdk.go index 07f9974ad..e3e71c3bc 100644 --- a/wasmsdk/sdk.go +++ b/wasmsdk/sdk.go @@ -15,6 +15,7 @@ import ( "github.com/0chain/gosdk/core/screstapi" "github.com/0chain/gosdk/zboxcore/sdk" "github.com/0chain/gosdk/zcncore" + "github.com/0chain/gosdk/core/zcncrypto" "io" "os" @@ -178,3 +179,37 @@ func send(toClientID string, tokens uint64, fee uint64, desc string) (string, er } return txn.TransactionOutput, nil } + +// send Send tokens to a client +// - toClientID is the client id to send tokens to +// - tokens is the number of tokens to send +// - fee is the transaction fee +// - desc is the description of the transaction +func sendMW(toClientID string, tokens uint64, fee uint64, desc string, key string) (string, error) { + _, _, _, txn, err := zcncore.Send(toClientID, tokens, desc, key) + if err != nil { + return "", err + } + return txn.TransactionOutput, nil +} + +// addWallet adds a new wallet to the SDK (used by wasm bindings). +func addWallet(clientID, clientKey, peerPublicKey, publicKey, privateKey, mnemonic string, isSplit bool) error { + keys := []zcncrypto.KeyPair{{PrivateKey: privateKey, PublicKey: publicKey}} + w := zcncrypto.Wallet{ + ClientID: clientID, + ClientKey: clientKey, + PeerPublicKey: peerPublicKey, + Mnemonic: mnemonic, + Keys: keys, + IsSplit: isSplit, + } + client.AddWallet(w) + return nil +} + +// removeWallet removes a wallet from the SDK by public key (used by wasm bindings). +func removeWallet(pubKey string) error { + client.RemoveWallet(pubKey) + return nil +} diff --git a/zboxcore/marker/authticket.go b/zboxcore/marker/authticket.go index fd2bc9819..8ef4df08e 100644 --- a/zboxcore/marker/authticket.go +++ b/zboxcore/marker/authticket.go @@ -25,6 +25,7 @@ type AuthTicket struct { Encrypted bool `json:"encrypted"` Signature string `json:"signature"` EncryptionPublicKey string `json:"encryption_public_key"` + MultiWalletSupportKey string `json:"-"` } // NewAuthTicket returns the MPT hash of the AuthTicket @@ -49,6 +50,6 @@ func (at *AuthTicket) GetHashData() string { func (at *AuthTicket) Sign() error { var err error hash := encryption.Hash(at.GetHashData()) - at.Signature, err = client.Sign(hash) + at.Signature, err = client.Sign(hash, at.MultiWalletSupportKey) return err } diff --git a/zboxcore/marker/readmarker.go b/zboxcore/marker/readmarker.go index 53db55ccd..c56ef0e04 100644 --- a/zboxcore/marker/readmarker.go +++ b/zboxcore/marker/readmarker.go @@ -4,22 +4,23 @@ import ( "fmt" "github.com/0chain/errors" + "github.com/0chain/gosdk/core/client" "github.com/0chain/gosdk/core/common" "github.com/0chain/gosdk/core/encryption" "github.com/0chain/gosdk/core/sys" - "github.com/0chain/gosdk/core/client" ) type ReadMarker struct { - ClientID string `json:"client_id"` - ClientPublicKey string `json:"client_public_key"` - BlobberID string `json:"blobber_id"` - AllocationID string `json:"allocation_id"` - OwnerID string `json:"owner_id"` - Timestamp common.Timestamp `json:"timestamp"` - ReadCounter int64 `json:"counter"` - Signature string `json:"signature"` - SessionRC int64 `json:"session_rc"` + ClientID string `json:"client_id"` + ClientPublicKey string `json:"client_public_key"` + BlobberID string `json:"blobber_id"` + AllocationID string `json:"allocation_id"` + OwnerID string `json:"owner_id"` + Timestamp common.Timestamp `json:"timestamp"` + ReadCounter int64 `json:"counter"` + Signature string `json:"signature"` + SessionRC int64 `json:"session_rc"` + IsSignUnderMultiWallet bool `json:"is_sign_under_multi_wallet"` } func (rm *ReadMarker) GetHash() string { @@ -30,7 +31,12 @@ func (rm *ReadMarker) GetHash() string { } func (rm *ReadMarker) Sign() error { + fmt.Print("ReadMarker sign: ", rm.ClientID, rm.ClientPublicKey, rm.IsSignUnderMultiWallet) var err error + if rm.IsSignUnderMultiWallet { + rm.Signature, err = client.Sign(rm.GetHash(), rm.ClientPublicKey) + return err + } rm.Signature, err = client.Sign(rm.GetHash()) return err } diff --git a/zboxcore/marker/writemarker.go b/zboxcore/marker/writemarker.go index 55a4ad3b4..b50da4462 100644 --- a/zboxcore/marker/writemarker.go +++ b/zboxcore/marker/writemarker.go @@ -21,6 +21,7 @@ type WriteMarker struct { BlobberID string `json:"blobber_id"` Timestamp int64 `json:"timestamp"` ClientID string `json:"client_id"` + MultiWalletSupportKey string `json:"pub_key"` Signature string `json:"signature"` } @@ -48,7 +49,16 @@ func (wm *WriteMarker) GetHash() string { func (wm *WriteMarker) Sign() error { var err error - wm.Signature, err = client.Sign(wm.GetHash(), wm.ClientID) + // If Pubkey is set, use that wallet for signing and set ClientID accordingly. + if wm.MultiWalletSupportKey != "" { + // Use the provided pubkey to sign, but do not overwrite ClientID here. + wm.Signature, err = client.Sign(wm.GetHash(), wm.MultiWalletSupportKey) + return err + } + + // Default: sign with the current SDK wallet. Do not overwrite ClientID + // here either; callers should set ClientID to the allocation owner. + wm.Signature, err = client.Sign(wm.GetHash()) return err } diff --git a/zboxcore/sdk/allocation.go b/zboxcore/sdk/allocation.go index 2bb806bb5..89244cd9b 100644 --- a/zboxcore/sdk/allocation.go +++ b/zboxcore/sdk/allocation.go @@ -299,6 +299,8 @@ type Allocation struct { // Owner ecdsa public key OwnerSigningPublicKey string `json:"owner_signing_public_key"` + // MultiWalletSupportKey holds an allocation-level default signing pubkey used + MultiWalletSupportKey string `json:"multi_wallet_support_key,omitempty"` // FileOptions to define file restrictions on an allocation for third-parties // default 00000000 for all crud operations suggesting only owner has the below listed abilities. @@ -413,13 +415,20 @@ func (a *Allocation) GetStats() *AllocationStats { } // GetBlobberStats returns the statistics of the blobbers in the allocation. -func (a *Allocation) GetBlobberStats() map[string]*BlobberAllocationStats { +func (a *Allocation) GetBlobberStats(keys ...string) map[string]*BlobberAllocationStats { + // Allow caller to provide an explicit client key (e.g., signing pubkey) to use + // when requesting allocation info from blobbers. If provided, use that as the + // client id passed to blobbers; otherwise default to the allocation owner id. + clientKey := a.Owner + if len(keys) > 0 && keys[0] != "" { + clientKey = keys[0] + } numList := len(a.Blobbers) wg := &sync.WaitGroup{} wg.Add(numList) rspCh := make(chan *BlobberAllocationStats, numList) for _, blobber := range a.Blobbers { - go getAllocationDataFromBlobber(blobber, a.ID, a.Tx, rspCh, wg, a.Owner) + go getAllocationDataFromBlobber(blobber, a.ID, a.Tx, rspCh, wg, clientKey) } wg.Wait() result := make(map[string]*BlobberAllocationStats, len(a.Blobbers)) @@ -437,7 +446,7 @@ func SetDownloadWorkerCount(count int) { } // InitAllocation initializes the allocation. -func (a *Allocation) InitAllocation() { +func (a *Allocation) InitAllocation(keys ...string) { a.downloadChan = make(chan *DownloadRequest, 400) a.repairChan = make(chan *RepairRequest, 1) a.ctx, a.ctxCancelF = context.WithCancel(context.Background()) @@ -458,30 +467,46 @@ func (a *Allocation) InitAllocation() { for _, blobber := range a.Blobbers { addLogChan(blobber.Baseurl) } - a.generateAndSetOwnerSigningPublicKey() + if len(keys) > 0 && keys[0] != "" { + a.MultiWalletSupportKey = keys[0] + } + a.generateAndSetOwnerSigningPublicKey(keys...) a.startWorker(a.ctx) InitCommitWorker(a.Blobbers) InitBlockDownloader(a.Blobbers, downloadWorkerCount) - if a.StorageVersion == StorageV2 && a.OwnerPublicKey == client.PublicKey() { - a.CheckAllocStatus() //nolint:errcheck + if a.StorageVersion == StorageV2 && a.OwnerPublicKey == client.PublicKey(a.Owner) { + a.CheckAllocStatus(keys...) //nolint:errcheck } a.initialized = true } -func (a *Allocation) generateAndSetOwnerSigningPublicKey() { +func (a *Allocation) generateAndSetOwnerSigningPublicKey(keys ...string) { + l.Logger.Info("a.OwnerPublicKey:", a.OwnerPublicKey, " client.PublicKey(keys...):", client.PublicKey(keys...)) //create ecdsa public key from signature - if a.OwnerPublicKey != client.PublicKey() { + if a.OwnerPublicKey != client.PublicKey(keys...) { return } - privateSigningKey, err := GenerateOwnerSigningKey(a.OwnerPublicKey, a.Owner) + + wallet := client.Wallet() + if len(keys) > 0 && keys[0] != "" { + wallet = client.GetWalletByKey(keys[0]) + if wallet == nil { + l.Logger.Error("multi-wallet-settings err ", keys[0]) + return + } + } + + + privateSigningKey, err := GenerateOwnerSigningKey(a.OwnerPublicKey, a.Owner, keys...) if err != nil { l.Logger.Error("Failed to generate owner signing key", zap.Error(err)) return } - if a.OwnerSigningPublicKey == "" && !a.Finalized && !a.Canceled && client.Wallet().IsSplit { + l.Logger.Info("privateSigningKey: ", privateSigningKey) + if a.OwnerSigningPublicKey == "" && !a.Finalized && !a.Canceled && wallet.IsSplit { pubKey := privateSigningKey.Public().(ed25519.PublicKey) a.OwnerSigningPublicKey = hex.EncodeToString(pubKey) - hash, _, err := UpdateAllocation(0, 0, false, a.ID, 0, "", "", "", "", a.OwnerSigningPublicKey, false, nil, "") + hash, _, err := UpdateAllocation(0, 0, false, a.ID, 0, "", "", "", "", a.OwnerSigningPublicKey, false, nil, "", keys...) if err != nil { l.Logger.Error("Failed to update owner signing public key ", err, " allocationID: ", a.ID, " hash: ", hash) return @@ -495,6 +520,7 @@ func (a *Allocation) generateAndSetOwnerSigningPublicKey() { return } a.privateSigningKey = privateSigningKey + l.Logger.Info("a.privateSigningKey ", a.privateSigningKey) } func (a *Allocation) isInitialized() bool { @@ -574,7 +600,7 @@ func (a *Allocation) UploadFile(workdir, localpath string, remotepath string, // - statusCallback: a callback function to get the status of the repair. // - mask: the mask of the repair descriping the blobbers to repair. // - ref: the file reference, a representation of the file in the database. -func (a *Allocation) RepairFile(file sys.File, remotepath string, statusCallback StatusCallback, mask zboxutil.Uint128, ref *fileref.FileRef) *OperationRequest { +func (a *Allocation) RepairFile(file sys.File, remotepath string, statusCallback StatusCallback, mask zboxutil.Uint128, ref *fileref.FileRef, uploadOpts ...ChunkedUploadOption) *OperationRequest { idr, _ := homedir.Dir() if Workdir != "" { idr = Workdir @@ -605,6 +631,11 @@ func (a *Allocation) RepairFile(file sys.File, remotepath string, statusCallback WithChunkNumber(RepairBlocks), } } + // append any additional upload options provided by caller + if len(uploadOpts) > 0 { + opts = append(opts, uploadOpts...) + } + op := &OperationRequest{ OperationType: constants.FileOperationInsert, IsRepair: true, @@ -699,7 +730,7 @@ func (a *Allocation) EncryptAndUploadFileWithThumbnail( // - status: the status callback function. Will be used to gather the status of the upload operations. // // Returns any error encountered during any of the upload operations, or during preparation of the upload operations. -func (a *Allocation) StartMultiUpload(workdir string, localPaths []string, fileNames []string, thumbnailPaths []string, encrypts []bool, chunkNumbers []int, remotePaths []string, isUpdate []bool, isWebstreaming []bool, status StatusCallback) error { +func (a *Allocation) StartMultiUpload(workdir string, localPaths []string, fileNames []string, thumbnailPaths []string, encrypts []bool, chunkNumbers []int, remotePaths []string, isUpdate []bool, isWebstreaming []bool, status StatusCallback, keys ...string) error { if len(localPaths) != len(thumbnailPaths) { return errors.New("invalid_value", "length of localpaths and thumbnailpaths must be equal") } @@ -795,6 +826,23 @@ func (a *Allocation) StartMultiUpload(workdir string, localPaths []string, fileN } } + + // Determine effective multi-wallet key: prefer allocation default, but + // allow caller-provided key to override when present. + effectiveKey := a.MultiWalletSupportKey + if len(keys) > 0 && keys[0] != "" { + effectiveKey = keys[0] + } + + if effectiveKey != "" { + err := a.DoMultiOperation(operationRequests, func(mo *MultiOperation) { mo.MultiWalletSupportKey = effectiveKey }) + if err != nil { + logger.Logger.Error("Error in multi upload ", err.Error()) + return err + } + return nil + } + err := a.DoMultiOperation(operationRequests) if err != nil { logger.Logger.Error("Error in multi upload ", err.Error()) @@ -917,7 +965,14 @@ func (a *Allocation) GetCurrentVersion() (bool, error) { go func(blobber *blockchain.StorageNode) { defer wg.Done() - wr, err := GetWritemarker(a.ID, a.Tx, a.sig, blobber.ID, blobber.Baseurl, a.Owner) + // Prefer allocation-level MultiWalletSupportKey for getting writemarkers + // so that the writemarker returned corresponds to the wallet used for + // signing in multi-wallet setups. Fall back to the allocation owner id. + key := a.Owner + if a.MultiWalletSupportKey != "" { + key = a.MultiWalletSupportKey + } + wr, err := GetWritemarker(a.ID, a.Tx, a.sig, blobber.ID, blobber.Baseurl, key) if err != nil { atomic.AddInt32(&errCnt, 1) logger.Logger.Error("error during getWritemarke", zap.Error(err)) @@ -926,10 +981,11 @@ func (a *Allocation) GetCurrentVersion() (bool, error) { markerChan <- nil } else { markerChan <- &RollbackBlobber{ - ClientId: a.Owner, - blobber: blobber, - lpm: wr, - commitResult: &CommitResult{}, + ClientId: a.Owner, + blobber: blobber, + lpm: wr, + commitResult: &CommitResult{}, + MultiWalletSupportKey: a.MultiWalletSupportKey, } } }(blobber) @@ -1031,6 +1087,7 @@ func (a *Allocation) RepairRequired(remotepath string) (zboxutil.Uint128, zboxut listReq.consensusThresh = a.DataShards listReq.ctx = a.ctx listReq.remotefilepath = remotepath + listReq.MultiWalletSupportKey = a.MultiWalletSupportKey found, deleteMask, fileRef, _ := listReq.getFileConsensusFromBlobbers() if fileRef == nil { var repairErr error @@ -1071,6 +1128,7 @@ func (a *Allocation) DoMultiOperation(operations []OperationRequest, opts ...Mul consensusThresh: a.consensusThreshold, fullconsensus: a.fullconsensus, } + mo.MultiWalletSupportKey = a.MultiWalletSupportKey for _, opt := range opts { opt(&mo) } @@ -1139,35 +1197,51 @@ func (a *Allocation) DoMultiOperation(operations []OperationRequest, opts ...Mul switch op.OperationType { case constants.FileOperationRename: - operation = NewRenameOperation(op.RemotePath, op.DestName, mo.operationMask, mo.maskMU, mo.consensusThresh, mo.fullconsensus, mo.ctx) + operation = NewRenameOperation(op.RemotePath, op.DestName, mo.operationMask, mo.maskMU, mo.consensusThresh, mo.fullconsensus, mo.ctx, mo.allocationObj.Owner, mo.MultiWalletSupportKey) case constants.FileOperationCopy: - operation = NewCopyOperation(mo.ctx, op.RemotePath, op.DestPath, mo.operationMask, mo.maskMU, mo.consensusThresh, mo.fullconsensus, op.CopyDirOnly) + operation = NewCopyOperation(mo.ctx, op.RemotePath, op.DestPath, mo.operationMask, mo.maskMU, mo.consensusThresh, mo.fullconsensus, op.CopyDirOnly, mo.allocationObj.Owner, mo.MultiWalletSupportKey) case constants.FileOperationMove: - operation = NewMoveOperation(op.RemotePath, op.DestPath, mo.operationMask, mo.maskMU, mo.consensusThresh, mo.fullconsensus, mo.ctx) + operation = NewMoveOperation(op.RemotePath, op.DestPath, mo.operationMask, mo.maskMU, mo.consensusThresh, mo.fullconsensus, mo.ctx, mo.allocationObj.Owner, mo.MultiWalletSupportKey) case constants.FileOperationInsert: cancelLock.Lock() CancelOpCtx[op.FileMeta.RemotePath] = mo.ctxCncl cancelLock.Unlock() - operation, newConnectionID, err = NewUploadOperation(mo.ctx, op.Workdir, mo.allocationObj, mo.connectionID, op.FileMeta, op.FileReader, false, op.IsWebstreaming, op.IsRepair, op.DownloadFile, op.StreamUpload, op.Opts...) + // Ensure upload uses operation-level signing key when provided. + uploadOpts := op.Opts + if mo.MultiWalletSupportKey != "" { + uploadOpts = append(uploadOpts, WithWallet(mo.MultiWalletSupportKey)) + } + operation, newConnectionID, err = NewUploadOperation(mo.ctx, op.Workdir, mo.allocationObj, mo.connectionID, op.FileMeta, op.FileReader, false, op.IsWebstreaming, op.IsRepair, op.DownloadFile, op.StreamUpload, uploadOpts...) case constants.FileOperationDelete: + var key string + if mo.MultiWalletSupportKey != "" { + key = mo.MultiWalletSupportKey + } else { + key = mo.allocationObj.Owner + } + if op.Mask != nil { - operation = NewDeleteOperation(mo.ctx, op.RemotePath, *op.Mask, mo.maskMU, mo.consensusThresh, mo.fullconsensus) + operation = NewDeleteOperation(mo.ctx, op.RemotePath, *op.Mask, mo.maskMU, mo.consensusThresh, mo.fullconsensus, key) } else { - operation = NewDeleteOperation(mo.ctx, op.RemotePath, mo.operationMask, mo.maskMU, mo.consensusThresh, mo.fullconsensus) + operation = NewDeleteOperation(mo.ctx, op.RemotePath, mo.operationMask, mo.maskMU, mo.consensusThresh, mo.fullconsensus, key) } case constants.FileOperationUpdate: cancelLock.Lock() CancelOpCtx[op.FileMeta.RemotePath] = mo.ctxCncl cancelLock.Unlock() - operation, newConnectionID, err = NewUploadOperation(mo.ctx, op.Workdir, mo.allocationObj, mo.connectionID, op.FileMeta, op.FileReader, true, op.IsWebstreaming, op.IsRepair, op.DownloadFile, op.StreamUpload, op.Opts...) + updOpts := op.Opts + if mo.MultiWalletSupportKey != "" { + updOpts = append(updOpts, WithWallet(mo.MultiWalletSupportKey)) + } + operation, newConnectionID, err = NewUploadOperation(mo.ctx, op.Workdir, mo.allocationObj, mo.connectionID, op.FileMeta, op.FileReader, true, op.IsWebstreaming, op.IsRepair, op.DownloadFile, op.StreamUpload, updOpts...) case constants.FileOperationCreateDir: - operation = NewDirOperation(op.RemotePath, op.FileMeta.CustomMeta, mo.operationMask, mo.maskMU, mo.consensusThresh, mo.fullconsensus, mo.ctx) + operation = NewDirOperation(op.RemotePath, op.FileMeta.CustomMeta, mo.operationMask, mo.maskMU, mo.consensusThresh, mo.fullconsensus, mo.ctx, mo.MultiWalletSupportKey) default: return errors.New("invalid_operation", "Operation is not valid") @@ -1305,6 +1379,7 @@ func (a *Allocation) DownloadFile(localPath string, remotePath string, verifyDow })) err = a.addAndGenerateDownloadRequest(f, remotePath, DOWNLOAD_CONTENT_FULL, 1, 0, numBlockDownloads, verifyDownload, status, isFinal, localFilePath, downloadReqOpts...) + fmt.Print("err in addAndGenerateDownloadRequest: ", err) if err != nil { if !toKeep { os.Remove(localFilePath) //nolint: errcheck @@ -1359,16 +1434,18 @@ func (a *Allocation) DownloadFileByBlock( // - verifyDownload: a flag to verify the download. If true, the download should be verified against the client keys. // - status: the status callback function. Will be used to gather the status of the download operation. // - isFinal: a flag to indicate if the download is the final download, meaning no more downloads are expected. It triggers the finalization of the download operation. -func (a *Allocation) DownloadThumbnail(localPath string, remotePath string, verifyDownload bool, status StatusCallback, isFinal bool) error { +func (a *Allocation) DownloadThumbnail(localPath string, remotePath string, verifyDownload bool, status StatusCallback, isFinal bool, downloadReqOpts ...DownloadRequestOption) error { f, localFilePath, toKeep, err := a.prepareAndOpenLocalFile(localPath, remotePath) if err != nil { return err } + // append the file callback into the provided options, then pass all options through + downloadReqOpts = append(downloadReqOpts, WithFileCallback(func() { + f.Close() //nolint: errcheck + })) err = a.addAndGenerateDownloadRequest(f, remotePath, DOWNLOAD_CONTENT_THUMB, 1, 0, - numBlockDownloads, verifyDownload, status, isFinal, localFilePath, WithFileCallback(func() { - f.Close() //nolint: errcheck - })) + numBlockDownloads, verifyDownload, status, isFinal, localFilePath, downloadReqOpts...) if err != nil { if !toKeep { os.Remove(localFilePath) //nolint: errcheck @@ -1403,11 +1480,27 @@ func (a *Allocation) generateDownloadRequest( downloadReq.allocOwnerPubKey = a.OwnerPublicKey downloadReq.allocOwnerSigningPubKey = a.OwnerSigningPublicKey if len(a.privateSigningKey) == 0 { - sk, err := GenerateOwnerSigningKey(client.PublicKey(), client.Id()) - if err != nil { - return nil, err + // If allocation has a MultiWalletSupportKey configured, prefer the + // corresponding wallet for generating the owner signing key. This + // provides allocation-level default signing behavior for multi-wallet + // setups. Otherwise fallback to the client/owner key. + if a.MultiWalletSupportKey != "" { + if w := client.GetWalletByKey(a.MultiWalletSupportKey); w != nil && len(w.Keys) > 0 { + sk, err := GenerateOwnerSigningKey(w.ClientKey, w.ClientID, a.MultiWalletSupportKey) + if err != nil { + return nil, err + } + downloadReq.allocOwnerSigningPrivateKey = sk + } else { + return nil, errors.New("multi-wallet-settings err: ", "wallet not found : "+a.MultiWalletSupportKey) + } + } else { + sk, err := GenerateOwnerSigningKey(client.PublicKey(), client.Id()) + if err != nil { + return nil, err + } + downloadReq.allocOwnerSigningPrivateKey = sk } - downloadReq.allocOwnerSigningPrivateKey = sk } else { downloadReq.allocOwnerSigningPrivateKey = a.privateSigningKey } @@ -1444,6 +1537,10 @@ func (a *Allocation) generateDownloadRequest( } downloadReq.isEnterprise = a.IsEnterprise + if a.MultiWalletSupportKey != "" { + downloadReq.MultiWalletSupportKey = a.MultiWalletSupportKey + } + return downloadReq, nil } @@ -1656,6 +1753,9 @@ func (a *Allocation) ListDirFromAuthTicket(authTicket string, lookupHash string, listReq.ctx = a.ctx listReq.remotefilepathhash = lookupHash listReq.authToken = at + if a.MultiWalletSupportKey != "" { + listReq.MultiWalletSupportKey = a.MultiWalletSupportKey + } for _, opt := range opts { opt(listReq) } @@ -1671,6 +1771,15 @@ func (a *Allocation) ListDirFromAuthTicket(authTicket string, lookupHash string, return nil, errors.New("list_request_failed", "Failed to get list response from the blobbers") } +// WithListRequestPubKey sets the public key to be used for list request signing and +// header selection in multi-wallet scenarios. When set, list requests will use +// this pubkey to select the wallet used for signing and to populate client headers. +func WithListRequestPubKey(pubkey string) ListRequestOptions { + return func(lr *ListRequest) { + lr.MultiWalletSupportKey = pubkey + } +} + // ListDir lists the allocation directory. // - path: the path of the directory to list. // - opts: the options of the list request as operation functions that customize the list request. @@ -1696,6 +1805,9 @@ func (a *Allocation) ListDir(path string, opts ...ListRequestOptions) (*ListResu listReq.consensusThresh = a.DataShards listReq.ctx = a.ctx listReq.remotefilepath = path + if a.MultiWalletSupportKey != "" { + listReq.MultiWalletSupportKey = a.MultiWalletSupportKey + } for _, opt := range opts { opt(listReq) } @@ -1716,23 +1828,24 @@ func (a *Allocation) getRefs(path, pathHash, authToken, offsetPath, updatedDate, } oTreeReq := &ObjectTreeRequest{ - ClientId: a.Owner, - allocationID: a.ID, - allocationTx: a.Tx, - sig: a.sig, - blobbers: a.Blobbers, - authToken: authToken, - pathHash: pathHash, - remotefilepath: path, - pageLimit: pageLimit, - level: level, - offsetPath: offsetPath, - updatedDate: updatedDate, - offsetDate: offsetDate, - fileType: fileType, - refType: refType, - ctx: a.ctx, - reqMask: zboxutil.NewUint128(1).Lsh(uint64(len(a.Blobbers))).Sub64(1), + ClientId: a.Owner, + allocationID: a.ID, + allocationTx: a.Tx, + sig: a.sig, + blobbers: a.Blobbers, + authToken: authToken, + pathHash: pathHash, + remotefilepath: path, + pageLimit: pageLimit, + level: level, + offsetPath: offsetPath, + updatedDate: updatedDate, + offsetDate: offsetDate, + fileType: fileType, + refType: refType, + ctx: a.ctx, + reqMask: zboxutil.NewUint128(1).Lsh(uint64(len(a.Blobbers))).Sub64(1), + MultiWalletSupportKey: a.MultiWalletSupportKey, } oTreeReq.fullconsensus = a.fullconsensus oTreeReq.consensusThresh = a.DataShards @@ -1959,6 +2072,7 @@ func (a *Allocation) GetRecentlyAddedRefs(page int, fromDate int64, pageLimit in fullconsensus: a.fullconsensus, consensusThresh: a.consensusThreshold, }, + MultiWalletSupportKey: a.MultiWalletSupportKey, } return req.GetRecentlyAddedRefs() } @@ -1982,6 +2096,7 @@ func (a *Allocation) GetFileMeta(path string) (*ConsolidatedFileMeta, error) { listReq.consensusThresh = a.consensusThreshold listReq.ctx = a.ctx listReq.remotefilepath = path + listReq.MultiWalletSupportKey = a.MultiWalletSupportKey _, _, ref, _ := listReq.getFileConsensusFromBlobbers() if ref != nil { result.Type = ref.Type @@ -2021,6 +2136,7 @@ func (a *Allocation) GetFileMetaByName(fileName string) ([]*ConsolidatedFileMeta listReq.consensusThresh = a.consensusThreshold listReq.ctx = a.ctx listReq.filename = fileName + listReq.MultiWalletSupportKey = a.MultiWalletSupportKey _, _, refs, _ := listReq.getMultipleFileConsensusFromBlobbers() if len(refs) != 0 { for _, ref := range refs { @@ -2103,6 +2219,7 @@ func (a *Allocation) GetFileMetaFromAuthTicket(authTicket string, lookupHash str listReq.ctx = a.ctx listReq.remotefilepathhash = lookupHash listReq.authToken = at + listReq.MultiWalletSupportKey = a.MultiWalletSupportKey _, _, ref, _ := listReq.getFileConsensusFromBlobbers() if ref != nil { result.Type = ref.Type @@ -2148,6 +2265,7 @@ func (a *Allocation) GetFileStats(path string) (map[string]*FileStats, error) { listReq.consensusThresh = a.consensusThreshold listReq.ctx = a.ctx listReq.remotefilepath = path + listReq.MultiWalletSupportKey = a.MultiWalletSupportKey ref := listReq.getFileStatsFromBlobbers() if ref != nil { return ref, nil @@ -2193,6 +2311,7 @@ func (a *Allocation) deleteFile(path string, threshConsensus, fullConsensus int, req.deleteMask = mask req.maskMu = &sync.Mutex{} req.timestamp = int64(common.Now()) + req.MultiWalletSupportKey = a.MultiWalletSupportKey err := req.ProcessDelete() return err } @@ -2229,7 +2348,8 @@ func (a *Allocation) createDir(remotePath string, threshConsensus, fullConsensus consensusThresh: threshConsensus, fullconsensus: fullConsensus, }, - alreadyExists: make(map[uint64]bool), + alreadyExists: make(map[uint64]bool), + multiWalletSupportKey: a.MultiWalletSupportKey, } req.ctx, req.ctxCncl = context.WithCancel(a.ctx) @@ -2276,7 +2396,11 @@ func (a *Allocation) RevokeShare(path string, refereeClientID string) error { query.Add("path", path) query.Add("refereeClientID", refereeClientID) - httpreq, err := zboxutil.NewRevokeShareRequest(baseUrl, a.ID, a.Tx, a.sig, query, a.Owner) + key := a.Owner + if a.MultiWalletSupportKey != "" { + key = a.MultiWalletSupportKey + } + httpreq, err := zboxutil.NewRevokeShareRequest(baseUrl, a.ID, a.Tx, a.sig, query, key) if err != nil { return err } @@ -2357,16 +2481,17 @@ func (a *Allocation) GetAuthTicket(path, filename string, } shareReq := &ShareRequest{ - ClientId: a.Owner, - expirationSeconds: expiration, - allocationID: a.ID, - allocationTx: a.Tx, - sig: a.sig, - blobbers: a.Blobbers, - ctx: a.ctx, - remotefilepath: path, - remotefilename: filename, - signingPrivateKey: a.privateSigningKey, + ClientId: a.Owner, + expirationSeconds: expiration, + allocationID: a.ID, + allocationTx: a.Tx, + sig: a.sig, + blobbers: a.Blobbers, + ctx: a.ctx, + remotefilepath: path, + remotefilename: filename, + signingPrivateKey: a.privateSigningKey, + MultiWalletSupportKey: a.MultiWalletSupportKey, } if referenceType == fileref.DIRECTORY { @@ -2429,7 +2554,11 @@ func (a *Allocation) UploadAuthTicketToBlobber(authTicket string, clientEncPubKe if err := formWriter.Close(); err != nil { return err } - httpreq, err := zboxutil.NewShareRequest(url, a.ID, a.Tx, a.sig, body, a.Owner) + key := a.Owner + if a.MultiWalletSupportKey != "" { + key = a.MultiWalletSupportKey + } + httpreq, err := zboxutil.NewShareRequest(url, a.ID, a.Tx, a.sig, body, key) if err != nil { return err } @@ -2846,12 +2975,32 @@ func (a *Allocation) downloadFromAuthTicket(fileHandler sys.File, authTicket str downloadReq.allocOwnerID = a.Owner downloadReq.allocOwnerPubKey = a.OwnerPublicKey downloadReq.allocOwnerSigningPubKey = a.OwnerSigningPublicKey - //for auth ticket set your own signing key - sk, err := GenerateOwnerSigningKey(client.PublicKey(), client.Id()) - if err != nil { - return err + if a.MultiWalletSupportKey != "" { + if w := client.GetWalletByKey(a.MultiWalletSupportKey); w != nil { + if len(w.Keys) > 0 { + sk, err := GenerateOwnerSigningKey(w.ClientKey, w.ClientID, a.MultiWalletSupportKey) + if err != nil { + return err + } + downloadReq.allocOwnerSigningPrivateKey = sk + } else { + return errors.New("multi-wallet-settings err: ", "wallet not found for signing public key "+downloadReq.MultiWalletSupportKey) + } + } else { + // wallet not found for pubkey; fallback to current client wallet + sk, err := GenerateOwnerSigningKey(client.PublicKey(), client.Id()) + if err != nil { + return err + } + downloadReq.allocOwnerSigningPrivateKey = sk + } + } else { + sk, err := GenerateOwnerSigningKey(client.PublicKey(), client.Id()) + if err != nil { + return err + } + downloadReq.allocOwnerSigningPrivateKey = sk } - downloadReq.allocOwnerSigningPrivateKey = sk downloadReq.ctx, downloadReq.ctxCncl = context.WithCancel(a.ctx) downloadReq.fileHandler = fileHandler downloadReq.localFilePath = localFilePath @@ -3242,7 +3391,7 @@ func (a *Allocation) UpdateWithStatus( } l.Logger.Info("Updating allocation") - hash, _, err := UpdateAllocation(size, authRoundExpiry, extend, a.ID, lock, addBlobberId, addBlobberAuthTicket, removeBlobberId, "", ownerSigninPublicKey, setThirdPartyExtendable, fileOptionsParams, updateAllocTicket) + hash, _, err := UpdateAllocation(size, authRoundExpiry, extend, a.ID, lock, addBlobberId, addBlobberAuthTicket, removeBlobberId, "", ownerSigninPublicKey, setThirdPartyExtendable, fileOptionsParams, updateAllocTicket, a.MultiWalletSupportKey) if err != nil { return alloc, "", isRepairRequired, err } @@ -3253,7 +3402,7 @@ func (a *Allocation) UpdateWithStatus( deadline := time.Now().Add(1 * time.Minute) for time.Now().Before(deadline) { - alloc, err = GetAllocation(a.ID) + alloc, err = GetAllocation(a.ID, a.MultiWalletSupportKey) if err != nil { l.Logger.Error("failed to get allocation") return alloc, hash, isRepairRequired, err @@ -3301,7 +3450,8 @@ func (a *Allocation) DownloadDirectory(ctx context.Context, remotePath, localPat } defer sys.Files.RemoveAllDirectories() - oRefChan := a.ListObjects(ctx, remotePath, "", "", "", fileref.FILE, fileref.REGULAR, 0, getRefPageLimit, WithAuthToken(authTicket)) + objOpts := []ObjectTreeRequestOption{WithAuthToken(authTicket)} + oRefChan := a.ListObjects(ctx, remotePath, "", "", "", fileref.FILE, fileref.REGULAR, 0, getRefPageLimit, objOpts...) refSlice := make([]ORef, BatchSize) refIndex := 0 wg := &sync.WaitGroup{} @@ -3344,13 +3494,17 @@ func (a *Allocation) DownloadDirectory(ctx context.Context, remotePath, localPat return err } if authTicket == "" { - _ = a.DownloadFileToFileHandler(fh, ref.Path, false, downloadStatusBar, ind == BatchSize-1, WithFileCallback(func() { - fh.Close() //nolint: errcheck - })) //nolint: errcheck + opts := []DownloadRequestOption{ + WithFileCallback(func() { fh.Close() }), + } + + _ = a.DownloadFileToFileHandler(fh, ref.Path, false, downloadStatusBar, ind == BatchSize-1, opts...) //nolint: errcheck } else { - _ = a.DownloadFileToFileHandlerFromAuthTicket(fh, authTicket, ref.LookupHash, ref.Path, false, downloadStatusBar, ind == BatchSize-1, WithFileCallback(func() { - fh.Close() //nolint: errcheck - })) //nolint: errcheck + opts := []DownloadRequestOption{ + WithFileCallback(func() { fh.Close() }), + } + + _ = a.DownloadFileToFileHandlerFromAuthTicket(fh, authTicket, ref.LookupHash, ref.Path, false, downloadStatusBar, ind == BatchSize-1, opts...) //nolint: errcheck } totalSize += int(ref.ActualFileSize) } @@ -3383,13 +3537,17 @@ func (a *Allocation) DownloadDirectory(ctx context.Context, remotePath, localPat return err } if authTicket == "" { - _ = a.DownloadFileToFileHandler(fh, ref.Path, false, downloadStatusBar, ind == refIndex-1, WithFileCallback(func() { - fh.Close() //nolint: errcheck - })) //nolint: errcheck + opts := []DownloadRequestOption{ + WithFileCallback(func() { fh.Close() }), + } + + _ = a.DownloadFileToFileHandler(fh, ref.Path, false, downloadStatusBar, ind == refIndex-1, opts...) //nolint: errcheck } else { - _ = a.DownloadFileToFileHandlerFromAuthTicket(fh, authTicket, ref.LookupHash, ref.Path, false, downloadStatusBar, ind == refIndex-1, WithFileCallback(func() { - fh.Close() //nolint: errcheck - })) //nolint: errcheck + opts := []DownloadRequestOption{ + WithFileCallback(func() { fh.Close() }), + } + + _ = a.DownloadFileToFileHandlerFromAuthTicket(fh, authTicket, ref.LookupHash, ref.Path, false, downloadStatusBar, ind == refIndex-1, opts...) //nolint: errcheck } totalSize += int(ref.ActualFileSize) } @@ -3453,7 +3611,10 @@ func (a *Allocation) DownloadObject(ctx context.Context, remotePath string, rang downloadStatusBar := &StatusBar{ wg: wg, } - err = a.DownloadByBlocksToFileHandler(pipeFile, remotePath, startBlock, endBlock, numBlockDownloads, false, downloadStatusBar, true) + // Prepare optional download request options (e.g., signing pubkey) + var dlOpts []DownloadRequestOption + + err = a.DownloadByBlocksToFileHandler(pipeFile, remotePath, startBlock, endBlock, numBlockDownloads, false, downloadStatusBar, true, dlOpts...) if err != nil { return nil, err } diff --git a/zboxcore/sdk/blobber_operations.go b/zboxcore/sdk/blobber_operations.go index cdcd3b9c2..8b98e506d 100644 --- a/zboxcore/sdk/blobber_operations.go +++ b/zboxcore/sdk/blobber_operations.go @@ -35,6 +35,7 @@ func CreateAllocationForOwner( dataShards, parityShards int, size int64, readPrice, writePrice PriceRange, lock uint64, preferredBlobberIds, blobberAuthTickets []string, thirdPartyExtendable, IsEnterprise, force bool, fileOptionsParams *FileOptionsParameters, authRoundExpiry int64, + keys ...string, ) (hash string, nonce int64, txn *transaction.Transaction, err error) { if lock > math.MaxInt64 { @@ -55,8 +56,8 @@ func CreateAllocationForOwner( return "", 0, nil, sdkNotInitialized } - if client.PublicKey() == ownerPublicKey { - privateSigningKey, err := GenerateOwnerSigningKey(ownerPublicKey, owner) + if client.PublicKey(keys...) == ownerPublicKey { + privateSigningKey, err := GenerateOwnerSigningKey(ownerPublicKey, owner, keys...) if err != nil { return "", 0, nil, errors.New("failed_generate_owner_signing_key", "failed to generate owner signing key: "+err.Error()) } @@ -79,7 +80,7 @@ func CreateAllocationForOwner( Name: transaction.NEW_ALLOCATION_REQUEST, InputArgs: allocationRequest, } - hash, _, nonce, txn, err = storageSmartContractTxnValue(sn, lock) + hash, _, nonce, txn, err = storageSmartContractTxnValue(sn, lock, keys...) return } @@ -88,12 +89,16 @@ func CreateAllocationForOwner( // - value is the value of the free allocation. // // returns the hash of the transaction, the nonce of the transaction and an error if any. -func CreateFreeAllocation(marker string, value uint64) (string, int64, error) { +func CreateFreeAllocation(marker string, value uint64, keys ...string) (string, int64, error) { if !client.IsSDKInitialized() { return "", 0, sdkNotInitialized } + // allow overriding the signing/public key via keys varargs recipientPublicKey := client.PublicKey() + if len(keys) > 0 && keys[0] != "" { + recipientPublicKey = client.PublicKey(keys[0]) + } var input = map[string]interface{}{ "recipient_public_key": recipientPublicKey, @@ -111,7 +116,7 @@ func CreateFreeAllocation(marker string, value uint64) (string, int64, error) { Name: transaction.NEW_FREE_ALLOCATION, InputArgs: input, } - hash, _, n, _, err := storageSmartContractTxnValue(sn, value) + hash, _, n, _, err := storageSmartContractTxnValue(sn, value, keys...) return hash, n, err } @@ -134,10 +139,16 @@ func UpdateAllocation( allocationID string, lock uint64, addBlobberId, addBlobberAuthTicket, removeBlobberId, ownerID, ownerSigninPublicKey string, - setThirdPartyExtendable bool, fileOptionsParams *FileOptionsParameters, ticket string, + setThirdPartyExtendable bool, fileOptionsParams *FileOptionsParameters, ticket string, keys ...string, ) (hash string, nonce int64, err error) { + + // prefer explicit owner id derived from provided pubkey when available + var key string + if len(keys) > 0 && keys[0] != "" { + key = keys[0] + } if ownerID == "" { - ownerID = client.Id() + ownerID = client.Id(key) } if lock > math.MaxInt64 { @@ -155,7 +166,8 @@ func UpdateAllocation( updateAllocationRequest := make(map[string]interface{}) updateAllocationRequest["owner_id"] = ownerID - updateAllocationRequest["owner_public_key"] = "" + // set owner_public_key based on provided pubkey (if any) to ensure downstream signing/verification + updateAllocationRequest["owner_public_key"] = client.PublicKey(key) updateAllocationRequest["id"] = allocationID updateAllocationRequest["size"] = size updateAllocationRequest["extend"] = extend @@ -189,14 +201,20 @@ func UpdateAllocation( Name: transaction.STORAGESC_UPDATE_ALLOCATION, InputArgs: updateAllocationRequest, } - hash, _, nonce, _, err = storageSmartContractTxnValue(sn, lock) + // Use explicit pubkey/client identifier for signing if provided + if key != "" { + hash, _, nonce, _, err = storageSmartContractTxnValue(sn, lock, key) + } else { + hash, _, nonce, _, err = storageSmartContractTxnValue(sn, lock) + } return } -func GetUpdateAllocTicket(allocationID, userID, operationType string, roundExpiry int64) (string, error) { +func GetUpdateAllocTicket(allocationID, userID, operationType string, roundExpiry int64, keys ...string) (string, error) { payload := fmt.Sprintf("%s:%d:%s:%s", allocationID, roundExpiry, userID, operationType) - signature, err := client.Sign(hex.EncodeToString([]byte(payload))) + // forward keys to client.Sign so multi-wallet or explicit-key signing is used when provided + signature, err := client.Sign(hex.EncodeToString([]byte(payload)), keys...) if err != nil { return "", err } @@ -212,7 +230,7 @@ func GetUpdateAllocTicket(allocationID, userID, operationType string, roundExpir // - providerID: provider ID // - value: value to lock // - fee: transaction fee -func StakePoolLock(providerType ProviderType, providerID string, value, fee uint64) (hash string, nonce int64, err error) { +func StakePoolLock(providerType ProviderType, providerID string, value, fee uint64, keys ...string) (hash string, nonce int64, err error) { if !client.IsSDKInitialized() { return "", 0, sdkNotInitialized } @@ -249,7 +267,7 @@ func StakePoolLock(providerType ProviderType, providerID string, value, fee uint return "", 0, errors.Newf("stake_pool_lock", "unsupported provider type: %v", providerType) } - hash, _, nonce, _, err = transaction.SmartContractTxnValueFeeWithRetry(scAddress, sn, value, fee, true) + hash, _, nonce, _, err = transaction.SmartContractTxnValueFeeWithRetry(scAddress, sn, value, fee, true, keys...) return } @@ -262,7 +280,7 @@ func StakePoolLock(providerType ProviderType, providerID string, value, fee uint // - providerType: provider type // - providerID: provider ID // - fee: transaction fee -func StakePoolUnlock(providerType ProviderType, providerID, clientID string, fee uint64) (unstake int64, nonce int64, err error) { +func StakePoolUnlock(providerType ProviderType, providerID, clientID string, fee uint64, keys ...string) (unstake int64, nonce int64, err error) { if !client.IsSDKInitialized() { return 0, 0, sdkNotInitialized } @@ -301,7 +319,7 @@ func StakePoolUnlock(providerType ProviderType, providerID, clientID string, fee } var out string - if _, out, nonce, _, err = transaction.SmartContractTxnValueFeeWithRetry(scAddress, sn, 0, fee, true); err != nil { + if _, out, nonce, _, err = transaction.SmartContractTxnValueFeeWithRetry(scAddress, sn, 0, fee, true, keys...); err != nil { return // an error } @@ -317,7 +335,7 @@ func StakePoolUnlock(providerType ProviderType, providerID, clientID string, fee // - allocID: allocation ID // - tokens: number of tokens to lock // - fee: transaction fee -func WritePoolLock(allocID string, tokens, fee uint64) (hash string, nonce int64, err error) { +func WritePoolLock(allocID string, tokens, fee uint64, keys ...string) (hash string, nonce int64, err error) { if !client.IsSDKInitialized() { return "", 0, sdkNotInitialized } @@ -334,14 +352,14 @@ func WritePoolLock(allocID string, tokens, fee uint64) (hash string, nonce int64 InputArgs: &req, } - hash, _, nonce, _, err = transaction.SmartContractTxnValueFeeWithRetry(STORAGE_SCADDRESS, sn, tokens, fee, true) + hash, _, nonce, _, err = transaction.SmartContractTxnValueFeeWithRetry(STORAGE_SCADDRESS, sn, tokens, fee, true, keys...) return } // WritePoolUnlock unlocks ALL tokens of a write pool. Needs to be cancelled first. // - allocID: allocation ID // - fee: transaction fee -func WritePoolUnlock(allocID string, fee uint64) (hash string, nonce int64, err error) { +func WritePoolUnlock(allocID string, fee uint64, keys ...string) (hash string, nonce int64, err error) { if !client.IsSDKInitialized() { return "", 0, sdkNotInitialized } @@ -357,16 +375,22 @@ func WritePoolUnlock(allocID string, fee uint64) (hash string, nonce int64, err Name: transaction.STORAGESC_WRITE_POOL_UNLOCK, InputArgs: &req, } - hash, _, nonce, _, err = transaction.SmartContractTxnValueFeeWithRetry(STORAGE_SCADDRESS, sn, 0, fee, true) + hash, _, nonce, _, err = transaction.SmartContractTxnValueFeeWithRetry(STORAGE_SCADDRESS, sn, 0, fee, true, keys...) return } -func GenerateOwnerSigningKey(ownerPublicKey, ownerID string) (ed25519.PrivateKey, error) { +func GenerateOwnerSigningKey(ownerPublicKey, ownerID string, signingPubKey ...string) (ed25519.PrivateKey, error) { if ownerPublicKey == "" { return nil, errors.New("owner_public_key_required", "owner public key is required") } hashData := fmt.Sprintf("%s:%s", ownerPublicKey, "owner_signing_public_key") - sig, err := client.Sign(encryption.Hash(hashData), ownerID) + // prefer explicit signing pubkey when provided (for split-wallet scenarios) + signingKey, err := client.SigningKey(signingPubKey...) + if err != nil { + return nil, err + } + fmt.Printf("signingKey: %s\n", signingKey) + sig, err := client.Sign(encryption.Hash(hashData), signingKey) if err != nil { logger.Logger.Error("error during sign", zap.Error(err)) return nil, err @@ -377,7 +401,16 @@ func GenerateOwnerSigningKey(ownerPublicKey, ownerID string) (ed25519.PrivateKey return privateSigningKey, nil } -func GenerateOwnerSigningPublicKey() (string, error) { +func GenerateOwnerSigningPublicKey(keys ...string) (string, error) { + if len(keys) > 0 && keys[0] != "" { + privateSigningKey, err := GenerateOwnerSigningKey(client.PublicKey(keys...), client.Id(keys...), keys...) + if err != nil { + return "", err + } + + pubKey := privateSigningKey.Public().(ed25519.PublicKey) + return hex.EncodeToString(pubKey), nil + } privateSigningKey, err := GenerateOwnerSigningKey(client.PublicKey(), client.Id()) if err != nil { return "", err diff --git a/zboxcore/sdk/blockdownloadworker.go b/zboxcore/sdk/blockdownloadworker.go index d05f90feb..56e705062 100644 --- a/zboxcore/sdk/blockdownloadworker.go +++ b/zboxcore/sdk/blockdownloadworker.go @@ -48,6 +48,7 @@ type BlockDownloadRequest struct { shouldVerify bool connectionID string respBuf []byte + Pubkey string } type downloadResponse struct { @@ -128,7 +129,13 @@ func (req *BlockDownloadRequest) downloadBlobberBlock(fastClient *fasthttp.Clien req.remotefilepathhash = fileref.GetReferenceLookup(req.allocationID, req.remotefilepath) } - httpreq, err := zboxutil.NewFastDownloadRequest(req.blobber.Baseurl, req.allocationID, req.allocationTx) + var httpreq *fasthttp.Request + var err error + if len(req.Pubkey) > 0 { + httpreq, err = zboxutil.NewFastDownloadRequest(req.blobber.Baseurl, req.allocationID, req.allocationTx, req.Pubkey) + } else { + httpreq, err = zboxutil.NewFastDownloadRequest(req.blobber.Baseurl, req.allocationID, req.allocationTx) + } if err != nil { req.result <- &downloadBlock{Success: false, idx: req.blobberIdx, err: errors.Wrap(err, "Error creating download request")} return diff --git a/zboxcore/sdk/chunked_upload.go b/zboxcore/sdk/chunked_upload.go index 1408fa539..c08db0836 100644 --- a/zboxcore/sdk/chunked_upload.go +++ b/zboxcore/sdk/chunked_upload.go @@ -257,7 +257,7 @@ func CreateChunkedUpload( } - su.writeMarkerMutex, err = CreateWriteMarkerMutex(su.allocationObj) + su.writeMarkerMutex, err = CreateWriteMarkerMutex(su.allocationObj, su.multiWalletSupportKey) if err != nil { return nil, err } @@ -293,6 +293,7 @@ func CreateChunkedUpload( su.chunkReader = cReader + logger.Logger.Info("CreateChunkedUploadFormBuilder storageVersion", su.allocationObj.StorageVersion, " and encryptedVersion ", su.encryptionVersion, "and privateSigningKey ", su.allocationObj.privateSigningKey) su.formBuilder = CreateChunkedUploadFormBuilder(su.allocationObj.StorageVersion, su.encryptionVersion, su.allocationObj.privateSigningKey) su.isRepair = isRepair @@ -430,10 +431,10 @@ func (su *ChunkedUpload) process() error { defer su.chunkReader.Release() defer su.chunkReader.Close() defer su.ctxCncl(nil) + for { chunks, err := su.readChunks(su.chunkNumber) - // chunk, err := su.chunkReader.Next() if err != nil { if su.statusCallback != nil { @@ -706,7 +707,6 @@ func (su *ChunkedUpload) uploadToBlobbers(uploadData UploadData) error { go func(pos uint64) { defer wg.Done() err := su.blobbers[pos].sendUploadRequest(ctx, su, uploadData.isFinal, su.encryptedKey, uploadData.uploadBody[pos].dataBuffers, uploadData.uploadBody[pos].formData, uploadData.uploadBody[pos].contentSlice, pos, &consensus) - if err != nil { if strings.Contains(err.Error(), "duplicate") { su.consensus.Done() diff --git a/zboxcore/sdk/chunked_upload_blobber.go b/zboxcore/sdk/chunked_upload_blobber.go index 2bbe2fe7a..8fa06323e 100644 --- a/zboxcore/sdk/chunked_upload_blobber.go +++ b/zboxcore/sdk/chunked_upload_blobber.go @@ -15,7 +15,6 @@ import ( "github.com/0chain/errors" thrown "github.com/0chain/errors" "github.com/0chain/gosdk/constants" - "github.com/0chain/gosdk/core/client" "github.com/0chain/gosdk/zboxcore/allocationchange" "github.com/0chain/gosdk/zboxcore/blockchain" "github.com/0chain/gosdk/zboxcore/fileref" @@ -71,6 +70,10 @@ func (sb *ChunkedUploadBlobber) sendUploadRequest( eg, _ := errgroup.WithContext(ctx) + key := su.allocationObj.Owner + if su.multiWalletSupportKey != "" { + key = su.multiWalletSupportKey + } for dataInd := 0; dataInd < len(dataBuffers); dataInd++ { ind := dataInd eg.Go(func() error { @@ -80,7 +83,7 @@ func (sb *ChunkedUploadBlobber) sendUploadRequest( var req *fasthttp.Request for i := 0; i < 6; i++ { req, err = zboxutil.NewFastUploadRequest( - sb.blobber.Baseurl, su.allocationObj.ID, su.allocationObj.Tx, dataBuffers[ind].Bytes(), su.httpMethod, su.allocationObj.Owner) + sb.blobber.Baseurl, su.allocationObj.ID, su.allocationObj.Tx, dataBuffers[ind].Bytes(), su.httpMethod, key) if err != nil { return err } @@ -219,7 +222,14 @@ func (sb *ChunkedUploadBlobber) processCommit(ctx context.Context, su *ChunkedUp wm.BlobberID = sb.blobber.ID wm.Timestamp = timestamp - wm.ClientID = client.Id(su.allocationObj.Owner) + // ClientID should always be the allocation owner. If an operation-level + // pubkey is provided use it only for signing (set Pubkey) but do not + // overwrite ClientID — the blobber expects the write marker ClientID to + // match the allocation owner (the uploader identity). + wm.ClientID = su.allocationObj.Owner + if su.multiWalletSupportKey != "" { + wm.MultiWalletSupportKey = su.multiWalletSupportKey + } err = wm.Sign() if err != nil { logger.Logger.Error("Signing writemarker failed: ", err) @@ -256,7 +266,12 @@ func (sb *ChunkedUploadBlobber) processCommit(ctx context.Context, su *ChunkedUp formWriter.Close() - req, err := zboxutil.NewCommitRequest(sb.blobber.Baseurl, su.allocationObj.ID, su.allocationObj.Tx, body, 0, su.allocationObj.Owner) + // choose signing key: prefer per-operation pubkey if set, otherwise allocation owner + key := su.allocationObj.Owner + if su.multiWalletSupportKey != "" { + key = su.multiWalletSupportKey + } + req, err := zboxutil.NewCommitRequest(sb.blobber.Baseurl, su.allocationObj.ID, su.allocationObj.Tx, body, 0, key) if err != nil { logger.Logger.Error("Error creating commit req: ", err) return err @@ -345,7 +360,12 @@ func (sb *ChunkedUploadBlobber) processWriteMarker( } var lR ReferencePathResult - req, err := zboxutil.NewReferencePathRequest(sb.blobber.Baseurl, su.allocationObj.ID, su.allocationObj.Tx, su.allocationObj.sig, paths, su.allocationObj.Owner) + // choose signing key for reference request: prefer per-operation pubkey if set + refKey := su.allocationObj.Owner + if su.multiWalletSupportKey != "" { + refKey = su.multiWalletSupportKey + } + req, err := zboxutil.NewReferencePathRequest(sb.blobber.Baseurl, su.allocationObj.ID, su.allocationObj.Tx, su.allocationObj.sig, paths, refKey) if err != nil || len(paths) == 0 { logger.Logger.Error("Creating ref path req", err) return nil, nil, 0, nil, err diff --git a/zboxcore/sdk/chunked_upload_form_builder.go b/zboxcore/sdk/chunked_upload_form_builder.go index 6781d311b..3702bed10 100644 --- a/zboxcore/sdk/chunked_upload_form_builder.go +++ b/zboxcore/sdk/chunked_upload_form_builder.go @@ -13,6 +13,7 @@ import ( "github.com/0chain/gosdk/core/client" "github.com/0chain/gosdk/core/encryption" + l "github.com/0chain/gosdk/zboxcore/logger" "golang.org/x/crypto/sha3" ) @@ -24,7 +25,7 @@ type ChunkedUploadFormBuilder interface { fileMeta *FileMeta, hasher Hasher, connectionID, blobberID string, chunkSize int64, chunkStartIndex, chunkEndIndex int, isFinal bool, encryptedKey, encryptedKeyPoint string, fileChunksData [][]byte, - thumbnailChunkData []byte, shardSize int64, + thumbnailChunkData []byte, shardSize int64, keys ...string, ) (blobberData, error) } @@ -59,7 +60,7 @@ func (b *chunkedUploadFormBuilder) Build( fileMeta *FileMeta, hasher Hasher, connectionID, blobberID string, chunkSize int64, chunkStartIndex, chunkEndIndex int, isFinal bool, encryptedKey, encryptedKeyPoint string, fileChunksData [][]byte, - thumbnailChunkData []byte, shardSize int64, + thumbnailChunkData []byte, shardSize int64, keys ...string, ) (blobberData, error) { metadata := ChunkedUploadFormMetadata{ @@ -187,7 +188,12 @@ func (b *chunkedUploadFormBuilder) Build( } formData.ActualFileHashSignature = hex.EncodeToString(sig) } else { - sig, err := client.Sign(fileMeta.ActualHash) + key := client.Wallet().ClientID + if len(keys) > 0 && keys[0] != "" { + key = keys[0] + } + + sig, err := client.Sign(fileMeta.ActualHash, key) if err != nil { return res, err } @@ -206,7 +212,11 @@ func (b *chunkedUploadFormBuilder) Build( } formData.ValidationRootSignature = hex.EncodeToString(sig) } else { - rootSig, err := client.Sign(hash) + var key string + if len(keys) > 0 && keys[0] != "" { + key = keys[0] + } + rootSig, err := client.Sign(hash, key) if err != nil { return res, err } @@ -258,6 +268,13 @@ func (b *chunkedUploadFormBuilder) Build( return res, err } + // Log upload metadata for debugging signature/validation issues + var keyUsed string + if len(keys) > 0 { + keyUsed = keys[0] + } + l.Logger.Info("uploadMeta prepared", "blobber", blobberID, "connection_id", connectionID, "key", keyUsed, "uploadMeta", string(uploadMeta)) + err = formWriter.WriteField("uploadMeta", string(uploadMeta)) if err != nil { return res, err diff --git a/zboxcore/sdk/chunked_upload_model.go b/zboxcore/sdk/chunked_upload_model.go index d5fb3d42f..ce2649ce2 100644 --- a/zboxcore/sdk/chunked_upload_model.go +++ b/zboxcore/sdk/chunked_upload_model.go @@ -96,7 +96,8 @@ type ChunkedUpload struct { //used in wasm check chunked_upload_process_js.go processMap map[int]zboxutil.Uint128 //nolint:unused //used in wasm check chunked_upload_process_js.go - processMapLock sync.Mutex //nolint:unused + processMapLock sync.Mutex //nolint:unused + multiWalletSupportKey string } // FileMeta metadata of stream input/local diff --git a/zboxcore/sdk/chunked_upload_option.go b/zboxcore/sdk/chunked_upload_option.go index 09bc26e56..046af6e65 100644 --- a/zboxcore/sdk/chunked_upload_option.go +++ b/zboxcore/sdk/chunked_upload_option.go @@ -37,6 +37,22 @@ func WithThumbnail(buf []byte) ChunkedUploadOption { } } +func WithWallet(w string) ChunkedUploadOption { + return func(su *ChunkedUpload) { + su.multiWalletSupportKey = w + } +} + +// WithUploadPubKey sets the per-operation public key used for owner-signing +// and request-level signing during chunked uploads. It's an alias for +// WithWallet but provides a clearer name for callers that want to set the +// upload-specific public key. +func WithUploadPubKey(pubKey string) ChunkedUploadOption { + return func(su *ChunkedUpload) { + su.multiWalletSupportKey = pubKey + } +} + // WithThumbnailFile add thumbnail from file. stream mode is unnecessary for thumbnail. // - fileName: file name of the thumbnail, which will be read and uploaded func WithThumbnailFile(fileName string) ChunkedUploadOption { diff --git a/zboxcore/sdk/chunked_upload_process.go b/zboxcore/sdk/chunked_upload_process.go index 72a7560b7..7de989a23 100644 --- a/zboxcore/sdk/chunked_upload_process.go +++ b/zboxcore/sdk/chunked_upload_process.go @@ -105,10 +105,19 @@ func (su *ChunkedUpload) processUpload(chunkStartIndex, chunkEndIndex int, wg.Add(1) go func(b *ChunkedUploadBlobber, thumbnailChunkData []byte, pos uint64) { defer wg.Done() - uploadData, err := su.formBuilder.Build( - &su.fileMeta, blobber.progress.Hasher, su.progress.ConnectionID, blobber.blobber.ID, - su.chunkSize, chunkStartIndex, chunkEndIndex, isFinal, su.encryptedKey, su.progress.EncryptedKeyPoint, - fileShards[pos], thumbnailChunkData, su.shardSize) + var uploadData blobberData + var err error + if su.multiWalletSupportKey != "" { + uploadData, err = su.formBuilder.Build( + &su.fileMeta, blobber.progress.Hasher, su.progress.ConnectionID, blobber.blobber.ID, + su.chunkSize, chunkStartIndex, chunkEndIndex, isFinal, su.encryptedKey, su.progress.EncryptedKeyPoint, + fileShards[pos], thumbnailChunkData, su.shardSize, su.multiWalletSupportKey) + } else { + uploadData, err = su.formBuilder.Build( + &su.fileMeta, blobber.progress.Hasher, su.progress.ConnectionID, blobber.blobber.ID, + su.chunkSize, chunkStartIndex, chunkEndIndex, isFinal, su.encryptedKey, su.progress.EncryptedKeyPoint, + fileShards[pos], thumbnailChunkData, su.shardSize) + } if err != nil { errC := atomic.AddInt32(&errCount, 1) if errC > int32(su.allocationObj.ParityShards-1) { // If atleast data shards + 1 number of blobbers can process the upload, it can be repaired later diff --git a/zboxcore/sdk/commitworker.go b/zboxcore/sdk/commitworker.go index aa4b864c0..8ab0f531b 100644 --- a/zboxcore/sdk/commitworker.go +++ b/zboxcore/sdk/commitworker.go @@ -60,17 +60,18 @@ func SuccessCommitResult() *CommitResult { const MARKER_VERSION = "v2" type CommitRequest struct { - ClientId string - changes []allocationchange.AllocationChange - blobber *blockchain.StorageNode - allocationID string - allocationTx string - connectionID string - sig string - wg *sync.WaitGroup - result *CommitResult - timestamp int64 - blobberInd uint64 + ClientId string + changes []allocationchange.AllocationChange + blobber *blockchain.StorageNode + allocationID string + allocationTx string + connectionID string + sig string + wg *sync.WaitGroup + result *CommitResult + timestamp int64 + blobberInd uint64 + multiWalletSupportKey string } type CommitRequestInterface interface { @@ -79,17 +80,18 @@ type CommitRequestInterface interface { } type CommitRequestV2 struct { - changes []allocationchange.AllocationChangeV2 - allocationObj *Allocation - connectionID string - sig string - wg *sync.WaitGroup - result *CommitResult - timestamp int64 - consensusThresh int - commitMask zboxutil.Uint128 - changeIndex uint64 - isRepair bool + changes []allocationchange.AllocationChangeV2 + allocationObj *Allocation + connectionID string + sig string + wg *sync.WaitGroup + result *CommitResult + timestamp int64 + consensusThresh int + commitMask zboxutil.Uint128 + changeIndex uint64 + isRepair bool + multiWalletSupportKey string } var ( @@ -147,7 +149,11 @@ func (commitreq *CommitRequest) processCommit() { } var req *http.Request var lR ReferencePathResult - req, err := zboxutil.NewReferencePathRequest(commitreq.blobber.Baseurl, commitreq.allocationID, commitreq.allocationTx, commitreq.sig, paths, commitreq.ClientId) + key := client.Id() + if commitreq.multiWalletSupportKey != "" { + key = commitreq.multiWalletSupportKey + } + req, err := zboxutil.NewReferencePathRequest(commitreq.blobber.Baseurl, commitreq.allocationID, commitreq.allocationTx, commitreq.sig, paths, key) if err != nil { l.Logger.Error("Creating ref path req", err) return @@ -193,7 +199,18 @@ func (commitreq *CommitRequest) processCommit() { } hasher := sha256.New() if lR.LatestWM != nil { - err = lR.LatestWM.VerifySignature(client.PublicKey()) + // Prefer verification using the MultiWalletSupportKey from the write + // marker (if present). Otherwise fall back to the client's public key. + if lR.LatestWM.MultiWalletSupportKey != "" { + if w := client.GetWalletByKey(lR.LatestWM.MultiWalletSupportKey); w != nil { + err = lR.LatestWM.VerifySignature(w.ClientKey) + } else { + commitreq.result = ErrorCommitResult("multi-wallet-settings err: wallet not found for signing public key " + lR.LatestWM.MultiWalletSupportKey) + return + } + } else { + err = lR.LatestWM.VerifySignature(client.PublicKey()) + } if err != nil { e := errors.New("signature_verification_failed", err.Error()) commitreq.result = ErrorCommitResult(e.Error()) @@ -283,7 +300,10 @@ func (req *CommitRequest) commitBlobber( wm.Size = size wm.BlobberID = req.blobber.ID wm.Timestamp = req.timestamp - wm.ClientID = client.Id(req.ClientId) + wm.ClientID = req.ClientId + if req.multiWalletSupportKey != "" { + wm.MultiWalletSupportKey = req.multiWalletSupportKey + } err = wm.Sign() if err != nil { l.Logger.Error("Signing writemarker failed: ", err) @@ -308,7 +328,12 @@ func (req *CommitRequest) commitBlobber( l.Logger.Error("Creating form writer failed: ", err) return } - httpreq, err := zboxutil.NewCommitRequest(req.blobber.Baseurl, req.allocationID, req.allocationTx, body, 0) + var httpreq *http.Request + if req.multiWalletSupportKey != "" { + httpreq, err = zboxutil.NewCommitRequest(req.blobber.Baseurl, req.allocationID, req.allocationTx, body, 0, req.multiWalletSupportKey) + } else { + httpreq, err = zboxutil.NewCommitRequest(req.blobber.Baseurl, req.allocationID, req.allocationTx, body, 0) + } if err != nil { l.Logger.Error("Error creating commit req: ", err) return @@ -433,7 +458,18 @@ func (commitReq *CommitRequestV2) processCommit() { pos = uint64(i.TrailingZeros()) go func(ind uint64) { blobber := commitReq.allocationObj.Blobbers[ind] - trie, err := getReferencePathV2(blobber, commitReq.allocationObj.ID, commitReq.allocationObj.Tx, commitReq.sig, paths, &success, mu) + // trie, err := getReferencePathV2(blobber, commitReq.allocationObj.ID, commitReq.allocationObj.Tx, commitReq.sig, paths, &success, mu) + + var ( + trie *wmpt.WeightedMerkleTrie + err error + ) + if commitReq.multiWalletSupportKey != "" { + trie, err = getReferencePathV2(blobber, commitReq.allocationObj.ID, commitReq.allocationObj.Tx, commitReq.sig, paths, &success, mu, commitReq.multiWalletSupportKey) + } else { + trie, err = getReferencePathV2(blobber, commitReq.allocationObj.ID, commitReq.allocationObj.Tx, commitReq.sig, paths, &success, mu) + } + resp := refPathResp{ trie: trie, err: err, @@ -576,7 +612,10 @@ func (req *CommitRequestV2) commitBlobber(rootHash []byte, rootWeight, prevWeigh wm.BlobberID = blobber.ID wm.AllocationID = req.allocationObj.ID wm.FileMetaRoot = fileMetaRoot - wm.ClientID = client.Id() + wm.ClientID = req.allocationObj.Owner + if req.multiWalletSupportKey != "" { + wm.MultiWalletSupportKey = req.multiWalletSupportKey + } err = wm.Sign() if err != nil { l.Logger.Error("Error signing writemarker", err) @@ -588,7 +627,11 @@ func (req *CommitRequestV2) commitBlobber(rootHash []byte, rootWeight, prevWeigh return err } - err = submitWriteMarker(wmData, nil, blobber, req.connectionID, req.allocationObj.ID, req.allocationObj.Tx, req.allocationObj.StorageVersion) + if req.multiWalletSupportKey != "" { + err = submitWriteMarker(wmData, nil, blobber, req.connectionID, req.allocationObj.ID, req.allocationObj.Tx, req.allocationObj.StorageVersion, req.multiWalletSupportKey) + } else { + err = submitWriteMarker(wmData, nil, blobber, req.connectionID, req.allocationObj.ID, req.allocationObj.Tx, req.allocationObj.StorageVersion) + } if err != nil { l.Logger.Error("Error submitting writemarker ", err) return err @@ -619,7 +662,7 @@ func getFormWritter(connectionID string, wmData, fileIDMetaData []byte, body *by return formWriter, nil } -func getReferencePathV2(blobber *blockchain.StorageNode, allocationID, allocationTx, sig string, paths []string, success *bool, mu *sync.Mutex) (*wmpt.WeightedMerkleTrie, error) { +func getReferencePathV2(blobber *blockchain.StorageNode, allocationID, allocationTx, sig string, paths []string, success *bool, mu *sync.Mutex, keys ...string) (*wmpt.WeightedMerkleTrie, error) { if len(paths) == 0 || blobber.LatestWM == nil || blobber.LatestWM.ChainSize == 0 { var node wmpt.Node if blobber.LatestWM != nil && len(blobber.LatestWM.FileMetaRoot) > 0 && blobber.LatestWM.ChainSize > 0 { @@ -638,7 +681,7 @@ func getReferencePathV2(blobber *blockchain.StorageNode, allocationID, allocatio for retries := 0; retries < 3; retries++ { err, shouldContinue = func() (err error, shouldContinue bool) { var req *http.Request - req, err = zboxutil.NewReferencePathRequestV2(blobber.Baseurl, allocationID, allocationTx, sig, paths, false) + req, err = zboxutil.NewReferencePathRequestV2(blobber.Baseurl, allocationID, allocationTx, sig, paths, false, keys...) if err != nil { l.Logger.Error("Creating ref path req", err) return @@ -710,7 +753,17 @@ func getReferencePathV2(blobber *blockchain.StorageNode, allocationID, allocatio } trie := wmpt.New(nil, nil) if lR.LatestWM != nil { - err = lR.LatestWM.VerifySignature(client.PublicKey()) + var useClientID string + if len(keys) > 0 && keys[0] != "" { + useClientID = keys[0] + } else { + useClientID = client.Id() + } + wallet := client.GetWalletByKey(useClientID) + if wallet == nil { + return nil, errors.New("wallet not found", useClientID) + } + err = lR.LatestWM.VerifySignature(wallet.ClientKey) if err != nil { return nil, errors.New("signature_verification_failed", err.Error()) } @@ -732,7 +785,7 @@ func getReferencePathV2(blobber *blockchain.StorageNode, allocationID, allocatio return trie, nil } -func submitWriteMarker(wmData, metaData []byte, blobber *blockchain.StorageNode, connectionID, allocationID, allocationTx string, apiVersion int) (err error) { +func submitWriteMarker(wmData, metaData []byte, blobber *blockchain.StorageNode, connectionID, allocationID, allocationTx string, apiVersion int, keys ...string) (err error) { var ( resp *http.Response shouldContinue bool @@ -745,7 +798,7 @@ func submitWriteMarker(wmData, metaData []byte, blobber *blockchain.StorageNode, l.Logger.Error("Creating form writer failed: ", err) return } - httpreq, err := zboxutil.NewCommitRequest(blobber.Baseurl, allocationID, allocationTx, body, apiVersion) + httpreq, err := zboxutil.NewCommitRequest(blobber.Baseurl, allocationID, allocationTx, body, apiVersion, keys...) if err != nil { l.Logger.Error("Error creating commit req: ", err) return diff --git a/zboxcore/sdk/common.go b/zboxcore/sdk/common.go index 3f9b698b6..9bf94f561 100644 --- a/zboxcore/sdk/common.go +++ b/zboxcore/sdk/common.go @@ -20,8 +20,8 @@ import ( "github.com/0chain/gosdk/zboxcore/zboxutil" ) -func getObjectTreeFromBlobber(ctx context.Context, allocationID, allocationTx, sig string, remoteFilePath string, blobber *blockchain.StorageNode, clientId ...string) (fileref.RefEntity, error) { - httpreq, err := zboxutil.NewObjectTreeRequest(blobber.Baseurl, allocationID, allocationTx, sig, remoteFilePath) +func getObjectTreeFromBlobber(ctx context.Context, allocationID, allocationTx, sig string, remoteFilePath string, blobber *blockchain.StorageNode, keys ...string) (fileref.RefEntity, error) { + httpreq, err := zboxutil.NewObjectTreeRequest(blobber.Baseurl, allocationID, allocationTx, sig, remoteFilePath, keys...) if err != nil { l.Logger.Error(blobber.Baseurl, "Error creating object tree request", err) return nil, err @@ -63,9 +63,9 @@ func getObjectTreeFromBlobber(ctx context.Context, allocationID, allocationTx, s return lR.GetRefFromObjectTree(allocationID) } -func getAllocationDataFromBlobber(blobber *blockchain.StorageNode, allocationId string, allocationTx string, respCh chan<- *BlobberAllocationStats, wg *sync.WaitGroup, clientId ...string) { +func getAllocationDataFromBlobber(blobber *blockchain.StorageNode, allocationId string, allocationTx string, respCh chan<- *BlobberAllocationStats, wg *sync.WaitGroup, keys ...string) { defer wg.Done() - httpreq, err := zboxutil.NewAllocationRequest(blobber.Baseurl, allocationId, allocationTx, clientId...) + httpreq, err := zboxutil.NewAllocationRequest(blobber.Baseurl, allocationId, allocationTx, keys...) if err != nil { l.Logger.Error(blobber.Baseurl, "Error creating allocation request", err) return @@ -123,14 +123,15 @@ func ValidateRemoteFileName(remotePath string) error { } type subDirRequest struct { - opType string - subOpType string - remotefilepath string - destPath string - allocationObj *Allocation - ctx context.Context - consensusThresh int - mask zboxutil.Uint128 + opType string + subOpType string + remotefilepath string + destPath string + allocationObj *Allocation + ctx context.Context + consensusThresh int + mask zboxutil.Uint128 + MultiWalletSupportKey string } func (req *subDirRequest) processSubDirectories() error { @@ -140,7 +141,13 @@ func (req *subDirRequest) processSubDirectories() error { ) for { - oResult, err := req.allocationObj.GetRefs(req.remotefilepath, offsetPath, "", "", fileref.FILE, fileref.REGULAR, 0, getRefPageLimit, WithObjectContext(req.ctx), WithObjectConsensusThresh(req.consensusThresh), WithSingleBlobber(true), WithObjectMask(req.mask)) + // Build options for GetRefs; include operation-level client key when provided so + // the underlying object tree / refs requests are signed with the correct wallet. + objOpts := []ObjectTreeRequestOption{WithObjectContext(req.ctx), WithObjectConsensusThresh(req.consensusThresh), WithSingleBlobber(true), WithObjectMask(req.mask)} + if req.MultiWalletSupportKey != "" { + objOpts = append(objOpts, WithObjectClientKey(req.MultiWalletSupportKey)) + } + oResult, err := req.allocationObj.GetRefs(req.remotefilepath, offsetPath, "", "", fileref.FILE, fileref.REGULAR, 0, getRefPageLimit, objOpts...) if err != nil { return err } @@ -171,7 +178,11 @@ func (req *subDirRequest) processSubDirectories() error { } ops = append(ops, op) } - err = req.allocationObj.DoMultiOperation(ops) + if req.MultiWalletSupportKey != "" { + err = req.allocationObj.DoMultiOperation(ops, func(mo *MultiOperation) { mo.MultiWalletSupportKey = req.MultiWalletSupportKey }) + } else { + err = req.allocationObj.DoMultiOperation(ops) + } if err != nil { return err } @@ -188,7 +199,11 @@ func (req *subDirRequest) processSubDirectories() error { } for pathLevel > level { - oResult, err := req.allocationObj.GetRefs(req.remotefilepath, offsetPath, "", "", fileref.DIRECTORY, fileref.REGULAR, pathLevel, getRefPageLimit, WithObjectContext(req.ctx), WithObjectMask(req.mask), WithObjectConsensusThresh(req.consensusThresh), WithSingleBlobber(true)) + objOpts := []ObjectTreeRequestOption{WithObjectContext(req.ctx), WithObjectMask(req.mask), WithObjectConsensusThresh(req.consensusThresh), WithSingleBlobber(true)} + if req.MultiWalletSupportKey != "" { + objOpts = append(objOpts, WithObjectClientKey(req.MultiWalletSupportKey)) + } + oResult, err := req.allocationObj.GetRefs(req.remotefilepath, offsetPath, "", "", fileref.DIRECTORY, fileref.REGULAR, pathLevel, getRefPageLimit, objOpts...) if err != nil { return err } @@ -216,7 +231,11 @@ func (req *subDirRequest) processSubDirectories() error { } ops = append(ops, op) } - err = req.allocationObj.DoMultiOperation(ops) + if req.MultiWalletSupportKey != "" { + err = req.allocationObj.DoMultiOperation(ops, func(mo *MultiOperation) { mo.MultiWalletSupportKey = req.MultiWalletSupportKey }) + } else { + err = req.allocationObj.DoMultiOperation(ops) + } if err != nil { return err } diff --git a/zboxcore/sdk/copyworker.go b/zboxcore/sdk/copyworker.go index 9d77f2425..674c8eb5e 100644 --- a/zboxcore/sdk/copyworker.go +++ b/zboxcore/sdk/copyworker.go @@ -31,21 +31,23 @@ import ( ) type CopyRequest struct { - allocationObj *Allocation - allocationID string - allocationTx string - sig string - blobbers []*blockchain.StorageNode - remotefilepath string - destPath string - ctx context.Context - ctxCncl context.CancelFunc - copyMask zboxutil.Uint128 - maskMU *sync.Mutex - connectionID string - timestamp int64 - dirOnly bool - destLookupHash string + allocationObj *Allocation + allocationID string + allocationTx string + sig string + blobbers []*blockchain.StorageNode + remotefilepath string + destPath string + ctx context.Context + ctxCncl context.CancelFunc + copyMask zboxutil.Uint128 + maskMU *sync.Mutex + connectionID string + timestamp int64 + dirOnly bool + destLookupHash string + clientId string + MultiWalletSupportKey string Consensus } @@ -54,16 +56,24 @@ var errNoChange = errors.New("no_change", "No change in the operation") const objAlreadyExists = "Object Already exists" func (req *CopyRequest) getObjectTreeFromBlobber(blobber *blockchain.StorageNode) (fileref.RefEntity, error) { - return getObjectTreeFromBlobber(req.ctx, req.allocationID, req.allocationTx, req.sig, req.remotefilepath, blobber, req.allocationObj.Owner) + key := req.clientId + if key == "" { + key = req.allocationObj.Owner + } + if req.MultiWalletSupportKey != "" { + key = req.MultiWalletSupportKey + } + return getObjectTreeFromBlobber(req.ctx, req.allocationID, req.allocationTx, req.sig, req.remotefilepath, blobber, key) } func (req *CopyRequest) getFileMetaFromBlobber(pos int) (fileRef *fileref.FileRef, err error) { listReq := &ListRequest{ - allocationID: req.allocationID, - allocationTx: req.allocationTx, - blobbers: req.blobbers, - remotefilepath: req.remotefilepath, - ctx: req.ctx, + allocationID: req.allocationID, + allocationTx: req.allocationTx, + blobbers: req.blobbers, + remotefilepath: req.remotefilepath, + ctx: req.ctx, + MultiWalletSupportKey: req.MultiWalletSupportKey, } respChan := make(chan *fileMetaResponse) go listReq.getFileMetaInfoFromBlobber(req.blobbers[pos], int(pos), respChan) @@ -130,7 +140,14 @@ func (req *CopyRequest) copyBlobberObject( cncl context.CancelFunc ) - httpreq, err = zboxutil.NewCopyRequest(blobber.Baseurl, req.allocationID, req.allocationTx, req.sig, body, req.allocationObj.Owner) + key := req.clientId + if key == "" { + key = req.allocationObj.Owner + } + if req.MultiWalletSupportKey != "" { + key = req.MultiWalletSupportKey + } + httpreq, err = zboxutil.NewCopyRequest(blobber.Baseurl, req.allocationID, req.allocationTx, req.sig, body, key) if err != nil { l.Logger.Error(blobber.Baseurl, "Error creating rename request", err) return @@ -319,7 +336,7 @@ func (req *CopyRequest) ProcessCopy() error { req.Consensus.consensusThresh, req.Consensus.consensus)) } - writeMarkerMutex, err := CreateWriteMarkerMutex(req.allocationObj) + writeMarkerMutex, err := CreateWriteMarkerMutex(req.allocationObj, req.MultiWalletSupportKey) if err != nil { return fmt.Errorf("Copy failed: %s", err.Error()) } @@ -369,14 +386,15 @@ func (req *CopyRequest) ProcessCopy() error { newChange.Operation = constants.FileOperationCopy newChange.Size = 0 commitReq := &CommitRequest{ - ClientId: req.allocationObj.Owner, - allocationID: req.allocationID, - allocationTx: req.allocationTx, - sig: req.sig, - blobber: req.blobbers[pos], - connectionID: req.connectionID, - wg: wg, - timestamp: req.timestamp, + ClientId: req.allocationObj.Owner, + allocationID: req.allocationID, + allocationTx: req.allocationTx, + sig: req.sig, + blobber: req.blobbers[pos], + connectionID: req.connectionID, + wg: wg, + timestamp: req.timestamp, + multiWalletSupportKey: req.MultiWalletSupportKey, } commitReq.changes = append(commitReq.changes, newChange) @@ -408,15 +426,17 @@ func (req *CopyRequest) ProcessCopy() error { } type CopyOperation struct { - remotefilepath string - destPath string - destLookupHash string - dirOnly bool - ctx context.Context - ctxCncl context.CancelFunc - copyMask zboxutil.Uint128 - maskMU *sync.Mutex - objectTreeRefs []fileref.RefEntity + remotefilepath string + destPath string + destLookupHash string + dirOnly bool + ctx context.Context + ctxCncl context.CancelFunc + copyMask zboxutil.Uint128 + maskMU *sync.Mutex + objectTreeRefs []fileref.RefEntity + clientId string + MultiWalletSupportKey string Consensus } @@ -424,20 +444,22 @@ type CopyOperation struct { func (co *CopyOperation) Process(allocObj *Allocation, connectionID string) ([]fileref.RefEntity, zboxutil.Uint128, error) { // make copyRequest object cR := &CopyRequest{ - allocationObj: allocObj, - allocationID: allocObj.ID, - allocationTx: allocObj.Tx, - sig: allocObj.sig, - connectionID: connectionID, - blobbers: allocObj.Blobbers, - remotefilepath: co.remotefilepath, - destPath: co.destPath, - ctx: co.ctx, - ctxCncl: co.ctxCncl, - copyMask: co.copyMask, - maskMU: co.maskMU, - dirOnly: co.dirOnly, - Consensus: Consensus{RWMutex: &sync.RWMutex{}}, + allocationObj: allocObj, + allocationID: allocObj.ID, + allocationTx: allocObj.Tx, + sig: allocObj.sig, + connectionID: connectionID, + blobbers: allocObj.Blobbers, + remotefilepath: co.remotefilepath, + destPath: co.destPath, + ctx: co.ctx, + ctxCncl: co.ctxCncl, + copyMask: co.copyMask, + maskMU: co.maskMU, + dirOnly: co.dirOnly, + Consensus: Consensus{RWMutex: &sync.RWMutex{}}, + clientId: co.clientId, + MultiWalletSupportKey: co.MultiWalletSupportKey, } cR.consensusThresh = co.consensusThresh @@ -517,7 +539,7 @@ func (co *CopyOperation) Error(allocObj *Allocation, consensus int, err error) { } -func NewCopyOperation(ctx context.Context, remotePath string, destPath string, copyMask zboxutil.Uint128, maskMU *sync.Mutex, consensusTh, fullConsensus int, copyDirOnly bool) *CopyOperation { +func NewCopyOperation(ctx context.Context, remotePath string, destPath string, copyMask zboxutil.Uint128, maskMU *sync.Mutex, consensusTh, fullConsensus int, copyDirOnly bool, clientId string, keys ...string) *CopyOperation { co := &CopyOperation{} co.remotefilepath = zboxutil.RemoteClean(remotePath) co.copyMask = copyMask @@ -530,6 +552,11 @@ func NewCopyOperation(ctx context.Context, remotePath string, destPath string, c co.destPath = destPath co.ctx, co.ctxCncl = context.WithCancel(ctx) co.dirOnly = copyDirOnly + co.clientId = clientId + co.MultiWalletSupportKey = "" + if len(keys) > 0 { + co.MultiWalletSupportKey = keys[0] + } return co } @@ -565,7 +592,11 @@ func (req *CopyRequest) copySubDirectoriees(dirOnly bool) error { for { if !dirOnly { - oResult, err := req.allocationObj.GetRefs(req.remotefilepath, offsetPath, "", "", fileref.FILE, fileref.REGULAR, 0, getRefPageLimit, WithObjectContext(req.ctx), WithObjectConsensusThresh(req.consensusThresh), WithSingleBlobber(true)) + objOpts := []ObjectTreeRequestOption{WithObjectContext(req.ctx), WithObjectConsensusThresh(req.consensusThresh), WithSingleBlobber(true)} + if req.MultiWalletSupportKey != "" { + objOpts = append(objOpts, WithObjectClientKey(req.MultiWalletSupportKey)) + } + oResult, err := req.allocationObj.GetRefs(req.remotefilepath, offsetPath, "", "", fileref.FILE, fileref.REGULAR, 0, getRefPageLimit, objOpts...) if err != nil { return err } @@ -591,7 +622,11 @@ func (req *CopyRequest) copySubDirectoriees(dirOnly bool) error { } ops = append(ops, op) } - err = req.allocationObj.DoMultiOperation(ops) + if req.MultiWalletSupportKey != "" { + err = req.allocationObj.DoMultiOperation(ops, func(mo *MultiOperation) { mo.MultiWalletSupportKey = req.MultiWalletSupportKey }) + } else { + err = req.allocationObj.DoMultiOperation(ops) + } if err != nil { return err } @@ -609,7 +644,11 @@ func (req *CopyRequest) copySubDirectoriees(dirOnly bool) error { } for pathLevel > level { - oResult, err := req.allocationObj.GetRefs(req.remotefilepath, offsetPath, "", "", fileref.DIRECTORY, fileref.REGULAR, pathLevel, getRefPageLimit, WithObjectContext(req.ctx), WithObjectMask(req.copyMask), WithObjectConsensusThresh(req.consensusThresh), WithSingleBlobber(true)) + objOpts := []ObjectTreeRequestOption{WithObjectContext(req.ctx), WithObjectMask(req.copyMask), WithObjectConsensusThresh(req.consensusThresh), WithSingleBlobber(true)} + if req.MultiWalletSupportKey != "" { + objOpts = append(objOpts, WithObjectClientKey(req.MultiWalletSupportKey)) + } + oResult, err := req.allocationObj.GetRefs(req.remotefilepath, offsetPath, "", "", fileref.DIRECTORY, fileref.REGULAR, pathLevel, getRefPageLimit, objOpts...) if err != nil { return err } @@ -633,7 +672,11 @@ func (req *CopyRequest) copySubDirectoriees(dirOnly bool) error { } ops = append(ops, op) } - err = req.allocationObj.DoMultiOperation(ops) + if req.MultiWalletSupportKey != "" { + err = req.allocationObj.DoMultiOperation(ops, func(mo *MultiOperation) { mo.MultiWalletSupportKey = req.MultiWalletSupportKey }) + } else { + err = req.allocationObj.DoMultiOperation(ops) + } if err != nil { return err } diff --git a/zboxcore/sdk/copyworker_test.go b/zboxcore/sdk/copyworker_test.go index f795ce51b..a9d32437a 100644 --- a/zboxcore/sdk/copyworker_test.go +++ b/zboxcore/sdk/copyworker_test.go @@ -4,7 +4,6 @@ import ( "bytes" "context" "encoding/json" - "github.com/0chain/gosdk/zboxcore/mocks" "io" "mime" "mime/multipart" @@ -14,6 +13,8 @@ import ( "sync" "testing" + "github.com/0chain/gosdk/zboxcore/mocks" + "github.com/0chain/errors" "github.com/0chain/gosdk/core/client" "github.com/0chain/gosdk/core/zcncrypto" @@ -495,7 +496,8 @@ func TestCopyRequest_ProcessCopy(t *testing.T) { maskMU: &sync.Mutex{}, connectionID: mockConnectionId, } - sig, err := client.Sign(mockAllocationTxId) + key := client.Id() + sig, err := client.Sign(mockAllocationTxId, key) require.NoError(err) req.sig = sig req.ctx, req.ctxCncl = context.WithCancel(context.TODO()) diff --git a/zboxcore/sdk/deleteworker.go b/zboxcore/sdk/deleteworker.go index 243e88c5b..bf76bb47f 100644 --- a/zboxcore/sdk/deleteworker.go +++ b/zboxcore/sdk/deleteworker.go @@ -19,6 +19,7 @@ import ( "github.com/google/uuid" "github.com/0chain/gosdk/constants" + "github.com/0chain/gosdk/core/client" "github.com/0chain/gosdk/core/common" "github.com/0chain/gosdk/zboxcore/allocationchange" "github.com/0chain/gosdk/zboxcore/blockchain" @@ -29,20 +30,21 @@ import ( ) type DeleteRequest struct { - allocationObj *Allocation - allocationID string - allocationTx string - sig string - blobbers []*blockchain.StorageNode - remotefilepath string - ctx context.Context - ctxCncl context.CancelFunc - wg *sync.WaitGroup - deleteMask zboxutil.Uint128 - maskMu *sync.Mutex - connectionID string - consensus Consensus - timestamp int64 + allocationObj *Allocation + allocationID string + allocationTx string + sig string + blobbers []*blockchain.StorageNode + remotefilepath string + ctx context.Context + ctxCncl context.CancelFunc + wg *sync.WaitGroup + deleteMask zboxutil.Uint128 + maskMu *sync.Mutex + connectionID string + consensus Consensus + timestamp int64 + MultiWalletSupportKey string } var errFileDeleted = errors.New("file_deleted", "file is already deleted") @@ -66,7 +68,7 @@ func (req *DeleteRequest) deleteBlobberFile( query.Add("connection_id", req.connectionID) query.Add("path", req.remotefilepath) - httpreq, err := zboxutil.NewDeleteRequest(blobber.Baseurl, req.allocationID, req.allocationTx, req.sig, query, req.allocationObj.Owner) + httpreq, err := zboxutil.NewDeleteRequest(blobber.Baseurl, req.allocationID, req.allocationTx, req.sig, query, req.MultiWalletSupportKey) if err != nil { l.Logger.Error(blobber.Baseurl, "Error creating delete request", err) return err @@ -173,10 +175,14 @@ func (req *DeleteRequest) getObjectTreeFromBlobber(pos uint64) ( req.maskMu.Unlock() } }() - + + key := req.allocationObj.Owner + if req.MultiWalletSupportKey != "" { + key = req.MultiWalletSupportKey + } fRefEntity, err = getObjectTreeFromBlobber( req.ctx, req.allocationID, req.allocationTx, req.sig, - req.remotefilepath, req.blobbers[pos], req.allocationObj.Owner) + req.remotefilepath, req.blobbers[pos], key) return } @@ -195,6 +201,7 @@ func (req *DeleteRequest) getFileMetaFromBlobber(pos uint64) (fileRef *fileref.F blobbers: req.blobbers, remotefilepath: req.remotefilepath, ctx: req.ctx, + MultiWalletSupportKey: req.MultiWalletSupportKey, } respChan := make(chan *fileMetaResponse) go listReq.getFileMetaInfoFromBlobber(req.blobbers[pos], int(pos), respChan) @@ -281,7 +288,7 @@ func (req *DeleteRequest) ProcessDelete() (err error) { req.consensus.consensusThresh, req.consensus.getConsensus())) } - writeMarkerMutex, err := CreateWriteMarkerMutex(req.allocationObj) + writeMarkerMutex, err := CreateWriteMarkerMutex(req.allocationObj, req.MultiWalletSupportKey) if err != nil { return fmt.Errorf("Delete failed: %s", err.Error()) } @@ -317,6 +324,7 @@ func (req *DeleteRequest) ProcessDelete() (err error) { connectionID: req.connectionID, wg: wg, timestamp: req.timestamp, + multiWalletSupportKey: req.MultiWalletSupportKey, } commitReq.changes = append(commitReq.changes, newChange) @@ -348,32 +356,34 @@ func (req *DeleteRequest) ProcessDelete() (err error) { } type DeleteOperation struct { - remotefilepath string - ctx context.Context - ctxCncl context.CancelFunc - deleteMask zboxutil.Uint128 - maskMu *sync.Mutex - consensus Consensus - lookupHash string - refs []fileref.RefEntity + remotefilepath string + ctx context.Context + ctxCncl context.CancelFunc + deleteMask zboxutil.Uint128 + maskMu *sync.Mutex + consensus Consensus + lookupHash string + refs []fileref.RefEntity + MultiWalletSupportKey string } func (dop *DeleteOperation) Process(allocObj *Allocation, connectionID string) ([]fileref.RefEntity, zboxutil.Uint128, error) { l.Logger.Info("Started Delete Process with Connection Id", connectionID) deleteReq := &DeleteRequest{ - allocationObj: allocObj, - allocationID: allocObj.ID, - allocationTx: allocObj.Tx, - sig: allocObj.sig, - connectionID: connectionID, - blobbers: allocObj.Blobbers, - remotefilepath: dop.remotefilepath, - ctx: dop.ctx, - ctxCncl: dop.ctxCncl, - deleteMask: dop.deleteMask, - maskMu: dop.maskMu, - wg: &sync.WaitGroup{}, - consensus: Consensus{RWMutex: &sync.RWMutex{}}, + allocationObj: allocObj, + allocationID: allocObj.ID, + allocationTx: allocObj.Tx, + sig: allocObj.sig, + connectionID: connectionID, + blobbers: allocObj.Blobbers, + remotefilepath: dop.remotefilepath, + ctx: dop.ctx, + ctxCncl: dop.ctxCncl, + deleteMask: dop.deleteMask, + maskMu: dop.maskMu, + wg: &sync.WaitGroup{}, + consensus: Consensus{RWMutex: &sync.RWMutex{}}, + MultiWalletSupportKey: dop.MultiWalletSupportKey, } deleteReq.consensus.fullconsensus = dop.consensus.fullconsensus deleteReq.consensus.consensusThresh = dop.consensus.consensusThresh @@ -582,7 +592,7 @@ func (dop *DeleteOperation) Error(allocObj *Allocation, consensus int, err error } -func NewDeleteOperation(ctx context.Context, remotePath string, deleteMask zboxutil.Uint128, maskMu *sync.Mutex, consensusTh, fullConsensus int) *DeleteOperation { +func NewDeleteOperation(ctx context.Context, remotePath string, deleteMask zboxutil.Uint128, maskMu *sync.Mutex, consensusTh, fullConsensus int, keys ...string) *DeleteOperation { dop := &DeleteOperation{} dop.remotefilepath = zboxutil.RemoteClean(remotePath) dop.deleteMask = deleteMask @@ -590,6 +600,9 @@ func NewDeleteOperation(ctx context.Context, remotePath string, deleteMask zboxu dop.consensus.consensusThresh = consensusTh dop.consensus.fullconsensus = fullConsensus dop.ctx, dop.ctxCncl = context.WithCancel(ctx) + if len(keys) > 0 { + dop.MultiWalletSupportKey = keys[0] + } return dop } @@ -600,7 +613,11 @@ func (req *DeleteRequest) deleteSubDirectories() error { pathLevel int ) for { - oResult, err := req.allocationObj.GetRefs(req.remotefilepath, offsetPath, "", "", fileref.FILE, fileref.REGULAR, 0, getRefPageLimit, WithObjectContext(req.ctx), WithObjectMask(req.deleteMask), WithObjectConsensusThresh(req.consensus.consensusThresh), WithSingleBlobber(true)) + objOpts := []ObjectTreeRequestOption{WithObjectContext(req.ctx), WithObjectMask(req.deleteMask), WithObjectConsensusThresh(req.consensus.consensusThresh), WithSingleBlobber(true)} + if req.MultiWalletSupportKey != "" { + objOpts = append(objOpts, WithObjectClientKey(req.MultiWalletSupportKey)) + } + oResult, err := req.allocationObj.GetRefs(req.remotefilepath, offsetPath, "", "", fileref.FILE, fileref.REGULAR, 0, getRefPageLimit, objOpts...) if err != nil { return err } @@ -623,7 +640,17 @@ func (req *DeleteRequest) deleteSubDirectories() error { } ops = append(ops, op) } - err = req.allocationObj.DoMultiOperation(ops) + if req.MultiWalletSupportKey != "" { + wallet := client.GetWalletByKey(req.MultiWalletSupportKey) + if wallet == nil { + return errors.New("multi-wallet-settings err: ", req.MultiWalletSupportKey) + } + err = req.allocationObj.DoMultiOperation(ops, func(mo *MultiOperation) { + mo.MultiWalletSupportKey = req.MultiWalletSupportKey + }) + } else { + err = req.allocationObj.DoMultiOperation(ops) + } if err != nil { return err } @@ -640,7 +667,11 @@ func (req *DeleteRequest) deleteSubDirectories() error { } // list all directories by descending order of path level for pathLevel > level { - oResult, err := req.allocationObj.GetRefs(req.remotefilepath, offsetPath, "", "", fileref.DIRECTORY, fileref.REGULAR, pathLevel, getRefPageLimit, WithObjectContext(req.ctx), WithObjectMask(req.deleteMask), WithObjectConsensusThresh(req.consensus.consensusThresh), WithSingleBlobber(true)) + objOpts := []ObjectTreeRequestOption{WithObjectContext(req.ctx), WithObjectMask(req.deleteMask), WithObjectConsensusThresh(req.consensus.consensusThresh), WithSingleBlobber(true)} + if req.MultiWalletSupportKey != "" { + objOpts = append(objOpts, WithObjectClientKey(req.MultiWalletSupportKey)) + } + oResult, err := req.allocationObj.GetRefs(req.remotefilepath, offsetPath, "", "", fileref.DIRECTORY, fileref.REGULAR, pathLevel, getRefPageLimit, objOpts...) if err != nil { return err } @@ -657,7 +688,17 @@ func (req *DeleteRequest) deleteSubDirectories() error { } ops = append(ops, op) } - err = req.allocationObj.DoMultiOperation(ops) + if req.MultiWalletSupportKey != "" { + wallet := client.GetWalletByKey(req.MultiWalletSupportKey) + if wallet == nil { + return errors.New("multi-wallet-settings err: ", req.MultiWalletSupportKey) + } + err = req.allocationObj.DoMultiOperation(ops, func(mo *MultiOperation) { + mo.MultiWalletSupportKey = req.MultiWalletSupportKey + }) + } else { + err = req.allocationObj.DoMultiOperation(ops) + } if err != nil { return err } diff --git a/zboxcore/sdk/dirworker.go b/zboxcore/sdk/dirworker.go index a1b4ef26f..61ba33b57 100644 --- a/zboxcore/sdk/dirworker.go +++ b/zboxcore/sdk/dirworker.go @@ -45,6 +45,7 @@ type DirRequest struct { timestamp int64 alreadyExists map[uint64]bool customMeta string + multiWalletSupportKey string Consensus } @@ -86,7 +87,7 @@ func (req *DirRequest) ProcessDir(a *Allocation) error { return errors.New("consensus_not_met", "directory creation failed due to consensus not met") } - writeMarkerMU, err := CreateWriteMarkerMutex(a) + writeMarkerMU, err := CreateWriteMarkerMutex(a, req.multiWalletSupportKey) if err != nil { return fmt.Errorf("directory creation failed. Err: %s", err.Error()) } @@ -133,6 +134,7 @@ func (req *DirRequest) commitRequest(existingDirCount int) error { commitReq.connectionID = req.connectionID commitReq.wg = wg commitReq.timestamp = req.timestamp + commitReq.multiWalletSupportKey = req.multiWalletSupportKey commitReqs[c] = commitReq c++ go AddCommitRequest(commitReq) @@ -187,7 +189,14 @@ func (req *DirRequest) createDirInBlobber(blobber *blockchain.StorageNode, pos u } formWriter.Close() - httpreq, err := zboxutil.NewCreateDirRequest(blobber.Baseurl, req.allocationID, req.allocationTx, req.sig, body, req.allocationObj.Owner) + var ( + httpreq *http.Request + ) + if req.multiWalletSupportKey != "" { + httpreq, err = zboxutil.NewCreateDirRequest(blobber.Baseurl, req.allocationID, req.allocationTx, req.sig, body, req.multiWalletSupportKey) + } else { + httpreq, err = zboxutil.NewCreateDirRequest(blobber.Baseurl, req.allocationID, req.allocationTx, req.sig, body, req.allocationObj.Owner) + } if err != nil { l.Logger.Error(blobber.Baseurl, "Error creating dir request", err) return err, false @@ -275,13 +284,14 @@ func (req *DirRequest) createDirInBlobber(blobber *blockchain.StorageNode, pos u } type DirOperation struct { - remotePath string - ctx context.Context - ctxCncl context.CancelFunc - dirMask zboxutil.Uint128 - maskMU *sync.Mutex - customMeta string - alreadyExists map[uint64]bool + remotePath string + ctx context.Context + ctxCncl context.CancelFunc + dirMask zboxutil.Uint128 + maskMU *sync.Mutex + customMeta string + alreadyExists map[uint64]bool + multiWalletSupportKey string Consensus } @@ -303,6 +313,7 @@ func (dirOp *DirOperation) Process(allocObj *Allocation, connectionID string) ([ wg: &sync.WaitGroup{}, alreadyExists: make(map[uint64]bool), customMeta: dirOp.customMeta, + multiWalletSupportKey: dirOp.multiWalletSupportKey, } dR.Consensus = Consensus{ RWMutex: &sync.RWMutex{}, @@ -364,7 +375,7 @@ func (dirOp *DirOperation) Error(allocObj *Allocation, consensus int, err error) } -func NewDirOperation(remotePath, customMeta string, dirMask zboxutil.Uint128, maskMU *sync.Mutex, consensusTh int, fullConsensus int, ctx context.Context) *DirOperation { +func NewDirOperation(remotePath, customMeta string, dirMask zboxutil.Uint128, maskMU *sync.Mutex, consensusTh int, fullConsensus int, ctx context.Context, key string) *DirOperation { dirOp := &DirOperation{} dirOp.remotePath = zboxutil.RemoteClean(remotePath) dirOp.dirMask = dirMask @@ -372,6 +383,7 @@ func NewDirOperation(remotePath, customMeta string, dirMask zboxutil.Uint128, ma dirOp.consensusThresh = consensusTh dirOp.fullconsensus = fullConsensus dirOp.customMeta = customMeta + dirOp.multiWalletSupportKey = key dirOp.ctx, dirOp.ctxCncl = context.WithCancel(ctx) dirOp.alreadyExists = make(map[uint64]bool) return dirOp diff --git a/zboxcore/sdk/downloader.go b/zboxcore/sdk/downloader.go index b2aa4062f..cef47bb7f 100644 --- a/zboxcore/sdk/downloader.go +++ b/zboxcore/sdk/downloader.go @@ -23,6 +23,7 @@ type DownloadOptions struct { localPath string remotePath string + MultiWalletSupportKey string isViewer bool authTicket string @@ -63,12 +64,16 @@ func CreateDownloader(allocationID, localPath, remotePath string, opts ...Downlo var err error if do.allocationObj == nil { if do.isViewer { - do.allocationObj, err = GetAllocationFromAuthTicket(do.authTicket) + do.allocationObj, err = GetAllocationFromAuthTicket(do.authTicket, do.MultiWalletSupportKey) if err != nil { return nil, err } } else { - do.allocationObj, err = GetAllocation(allocationID) + if do.MultiWalletSupportKey != "" { + do.allocationObj, err = GetAllocation(allocationID, do.MultiWalletSupportKey) + } else { + do.allocationObj, err = GetAllocation(allocationID) + } if err != nil { return nil, err } diff --git a/zboxcore/sdk/downloader_option.go b/zboxcore/sdk/downloader_option.go index 0dca50557..8faf23d5a 100644 --- a/zboxcore/sdk/downloader_option.go +++ b/zboxcore/sdk/downloader_option.go @@ -74,3 +74,11 @@ func WithFileHandler(fileHandler sys.File) DownloadOption { do.isFileHandlerDownload = true } } + +// WithDownloadPubKey sets the per-operation public key that should be used +// for owner-signing and request-level signing during downloads. +func WithDownloadPubKey(pubKey string) DownloadOption { + return func(do *DownloadOptions) { + do.MultiWalletSupportKey = pubKey + } +} diff --git a/zboxcore/sdk/downloadworker.go b/zboxcore/sdk/downloadworker.go index a28479f9e..e5ceb4a58 100644 --- a/zboxcore/sdk/downloadworker.go +++ b/zboxcore/sdk/downloadworker.go @@ -67,6 +67,16 @@ func WithFileCallback(cb func()) DownloadRequestOption { } } +// WithPubKey sets the public key to be used for download request signing and +// header selection in multi-wallet scenarios. When set, read markers and +// fast download requests will use this pubkey to select the wallet used for +// signing and to populate client headers. +func WithPubKey(pubkey string) DownloadRequestOption { + return func(dr *DownloadRequest) { + dr.MultiWalletSupportKey = pubkey + } +} + type DownloadRequest struct { ClientId string allocationID string @@ -120,6 +130,7 @@ type DownloadRequest struct { allocOwnerSigningPubKey string // in case of auth ticket, this key will be of the shared user rather than the owner of the allocation allocOwnerSigningPrivateKey ed25519.PrivateKey + MultiWalletSupportKey string // in case of multi-wallet settings, this will be the public key of the wallet used for downloading } type downloadPriority struct { @@ -270,6 +281,9 @@ func (req *DownloadRequest) downloadBlock( connectionID: req.connectionID, } + // propagate the public key (if multi-wallet / split-wallet scenario) + blockDownloadReq.Pubkey = req.MultiWalletSupportKey + if blockDownloadReq.blobber.IsSkip() { rspCh <- &downloadBlock{ Success: false, @@ -436,6 +450,7 @@ func (req *DownloadRequest) getDecryptedDataForAuthTicket(result *downloadBlock, // start block, end block and number of blocks to download in single request. // This will also write data to the file handler and will verify content by calculating content hash. func (req *DownloadRequest) processDownload() { + fmt.Print("inside process download: pubkey", req.MultiWalletSupportKey, "\n") ctx := req.ctx if req.completedCallback != nil { defer req.completedCallback(req.remotefilepath, req.remotefilepathhash) @@ -840,17 +855,31 @@ func (req *DownloadRequest) submitReadMarker(blobber *blockchain.StorageNode, re } func (req *DownloadRequest) attemptSubmitReadMarker(blobber *blockchain.StorageNode, readCount int64) error { + l.Logger.Info("attemptSubmitReadMarker: pubKey:", req.MultiWalletSupportKey, "\n") lockBlobberReadCtr(req.allocationID, blobber.ID) defer unlockBlobberReadCtr(req.allocationID, blobber.ID) + + clientID := client.Id(req.ClientId) + clientPublicKey := client.PublicKey() + if req.MultiWalletSupportKey != "" { + wallet := client.GetWalletByKey(req.MultiWalletSupportKey) + if wallet == nil { + return fmt.Errorf("wallet not found for public key: %s", req.MultiWalletSupportKey) + } + clientID = wallet.ClientID + clientPublicKey = wallet.ClientKey + } + l.Logger.Info("DownloadRequest: clientID:", clientID, " clientPublicKey:", clientPublicKey, "\n") rm := &marker.ReadMarker{ - ClientID: client.Id(req.ClientId), - ClientPublicKey: client.PublicKey(), - BlobberID: blobber.ID, - AllocationID: req.allocationID, - OwnerID: req.allocOwnerID, - Timestamp: common.Now(), - ReadCounter: getBlobberReadCtr(req.allocationID, blobber.ID) + readCount, - SessionRC: readCount, + ClientID: clientID, + ClientPublicKey: clientPublicKey, + BlobberID: blobber.ID, + AllocationID: req.allocationID, + OwnerID: req.allocOwnerID, + Timestamp: common.Now(), + ReadCounter: getBlobberReadCtr(req.allocationID, blobber.ID) + readCount, + SessionRC: readCount, + IsSignUnderMultiWallet: req.MultiWalletSupportKey != "", } err := rm.Sign() if err != nil { @@ -861,7 +890,7 @@ func (req *DownloadRequest) attemptSubmitReadMarker(blobber *blockchain.StorageN if err != nil { return fmt.Errorf("error marshaling read marker: %w", err) } - httpreq, err := zboxutil.NewRedeemRequest(blobber.Baseurl, req.allocationID, req.allocationTx, req.allocOwnerID) + httpreq, err := zboxutil.NewRedeemRequest(blobber.Baseurl, req.allocationID, req.allocationTx, req.allocOwnerID, req.MultiWalletSupportKey) if err != nil { return fmt.Errorf("error creating download request: %w", err) } @@ -1172,6 +1201,7 @@ func GetFileRefFromBlobber(allocationID, blobberId, remotePath string) (fRef *fi ctx := context.Background() listReq := &ListRequest{} + listReq.ClientId = a.Owner listReq.allocationID = a.ID listReq.allocationTx = a.Tx listReq.sig = a.sig @@ -1182,6 +1212,9 @@ func GetFileRefFromBlobber(allocationID, blobberId, remotePath string) (fRef *fi listReq.consensusThresh = 1 listReq.ctx = ctx listReq.remotefilepath = remotePath + if a.MultiWalletSupportKey != "" { + listReq.MultiWalletSupportKey = a.MultiWalletSupportKey + } rspCh := make(chan *fileMetaResponse, 1) go listReq.getFileMetaInfoFromBlobber(listReq.blobbers[0], 0, rspCh) @@ -1204,10 +1237,12 @@ func (req *DownloadRequest) getFileRef() (fRef *fileref.FileRef, err error) { fullconsensus: req.fullconsensus, consensusThresh: req.consensusThresh, }, - ctx: req.ctx, + ctx: req.ctx, + MultiWalletSupportKey: req.MultiWalletSupportKey, } fMetaResp := listReq.getFileMetaFromBlobbers() + l.Logger.Info("fMetaResp length: ", len(fMetaResp), "\n") fRef, err = req.getFileMetaConsensus(fMetaResp) if err != nil { @@ -1235,6 +1270,7 @@ func (req *DownloadRequest) getFileMetaConsensus(fMetaResp []*fileMetaResponse) } actualHash := fmr.fileref.ActualFileHash actualFileHashSignature := fmr.fileref.ActualFileHashSignature + var ( isValid bool err error @@ -1245,12 +1281,22 @@ func (req *DownloadRequest) getFileMetaConsensus(fMetaResp []*fileMetaResponse) actualFileHashSignature, actualHash, ) + l.Logger.Info("allocOwnerSigningPubKey: ", req.allocOwnerSigningPubKey, "\n") + l.Logger.Info("actualFileHashSignature: ", actualFileHashSignature, "\n") + l.Logger.Info("actualHash: ", actualHash, "\n") + l.Logger.Info("err: ", err, "\n") + l.Logger.Info("isValid: ", isValid, "\n") } else { isValid, err = sys.VerifyWith( req.allocOwnerPubKey, actualFileHashSignature, actualHash, ) + l.Logger.Info("allocOwnerPubKey: ", req.allocOwnerPubKey, "\n") + l.Logger.Info("actualFileHashSignature: ", actualFileHashSignature, "\n") + l.Logger.Info("actualHash: ", actualHash, "\n") + l.Logger.Info("err: ", err, "\n") + l.Logger.Info("isValid: ", isValid, "\n") } if err != nil { l.Logger.Error(err) @@ -1301,6 +1347,7 @@ func (req *DownloadRequest) getFileMetaConsensus(fMetaResp []*fileMetaResponse) hashData := fmt.Sprintf("%s:%s:%s:%s", fRef.ActualFileHash, fRef.ValidationRoot, fRef.FixedMerkleRoot, req.blobbers[i].ID) hash = encrypt.Hash(hashData) } + var ( isValid bool err error diff --git a/zboxcore/sdk/filemetaworker.go b/zboxcore/sdk/filemetaworker.go index 1d614f7f8..fcfaa8d0a 100644 --- a/zboxcore/sdk/filemetaworker.go +++ b/zboxcore/sdk/filemetaworker.go @@ -12,6 +12,7 @@ import ( "github.com/0chain/errors" "github.com/0chain/gosdk/constants" + "github.com/0chain/gosdk/core/client" "github.com/0chain/gosdk/zboxcore/blockchain" "github.com/0chain/gosdk/zboxcore/fileref" l "github.com/0chain/gosdk/zboxcore/logger" @@ -76,7 +77,11 @@ func (req *ListRequest) getFileMetaInfoFromBlobber(blobber *blockchain.StorageNo } formWriter.Close() - httpreq, err := zboxutil.NewFileMetaRequest(blobber.Baseurl, req.allocationID, req.allocationTx, req.sig, body, req.ClientId) + key := client.Wallet().ClientID + if req.MultiWalletSupportKey != "" { + key = req.MultiWalletSupportKey + } + httpreq, err := zboxutil.NewFileMetaRequest(blobber.Baseurl, req.allocationID, req.allocationTx, req.sig, body, key) if err != nil { l.Logger.Error("File meta info request error: ", err.Error()) return @@ -141,7 +146,11 @@ func (req *ListRequest) getFileMetaByNameInfoFromBlobber(blobber *blockchain.Sto } } formWriter.Close() - httpreq, err := zboxutil.NewFileMetaRequest(blobber.Baseurl, req.allocationID, req.allocationTx, req.sig, body, req.ClientId) + key := client.Wallet().ClientID + if req.MultiWalletSupportKey != "" { + key = req.MultiWalletSupportKey + } + httpreq, err := zboxutil.NewFileMetaRequest(blobber.Baseurl, req.allocationID, req.allocationTx, req.sig, body, key) if err != nil { l.Logger.Error("File meta info request error: ", err.Error()) return diff --git a/zboxcore/sdk/filerefsworker.go b/zboxcore/sdk/filerefsworker.go index c2e5a1e6d..0c6bbcdec 100644 --- a/zboxcore/sdk/filerefsworker.go +++ b/zboxcore/sdk/filerefsworker.go @@ -33,6 +33,7 @@ const INVALID_PATH = "invalid_path" type ObjectTreeRequest struct { ClientId string + MultiWalletSupportKey string allocationID string allocationTx string sig string @@ -94,6 +95,17 @@ func WithAuthToken(token string) ObjectTreeRequestOption { } } +// WithObjectClientKey sets the client pubkey (or client id) to be used for signing +// the object tree / refs HTTP request. When set, the request helper will use this +// key to resolve the wallet and sign the request headers. Note: ClientId should +// remain the allocation owner; this option sets the signing pubkey to use for +// the HTTP request when performing refs/list operations. +func WithObjectClientKey(key string) ObjectTreeRequestOption { + return func(o *ObjectTreeRequest) { + o.MultiWalletSupportKey = key + } +} + // Paginated tree should not be collected as this will stall the client // It should rather be handled by application that uses gosdk func (o *ObjectTreeRequest) GetRefs() (*ObjectTreeResult, error) { @@ -233,6 +245,14 @@ func (o *ObjectTreeRequest) getFileRefs(bUrl string, respChan chan *oTreeRespons oResult := ObjectTreeResult{} for i := 0; i < 3; i++ { + // Determine the client key to use for signing the refs request. Prefer + // the explicit PubKey when provided by the caller; otherwise fall back + // to ClientId (historical behavior). + key := o.ClientId + if o.MultiWalletSupportKey != "" { + key = o.MultiWalletSupportKey + } + oReq, err := zboxutil.NewRefsRequest( bUrl, o.allocationID, @@ -248,7 +268,7 @@ func (o *ObjectTreeRequest) getFileRefs(bUrl string, respChan chan *oTreeRespons o.refType, o.level, o.pageLimit, - o.ClientId, + key, ) if err != nil { oTR.err = err @@ -345,6 +365,7 @@ type SimilarField struct { type RecentlyAddedRefRequest struct { ctx context.Context ClientId string + MultiWalletSupportKey string allocationID string allocationTx string sig string @@ -424,7 +445,12 @@ func (r *RecentlyAddedRefRequest) GetRecentlyAddedRefs() (*RecentlyAddedRefResul func (r *RecentlyAddedRefRequest) getRecentlyAddedRefs(resp *RecentlyAddedRefResponse, bUrl string) { defer r.wg.Done() - req, err := zboxutil.NewRecentlyAddedRefsRequest(bUrl, r.allocationID, r.allocationTx, r.sig, r.fromDate, r.offset, r.pageLimit, r.ClientId) + // Choose key used to sign the request: prefer MultiWalletSupportKey when set + key := r.ClientId + if r.MultiWalletSupportKey != "" { + key = r.MultiWalletSupportKey + } + req, err := zboxutil.NewRecentlyAddedRefsRequest(bUrl, r.allocationID, r.allocationTx, r.sig, r.fromDate, r.offset, r.pageLimit, key) if err != nil { resp.err = err return diff --git a/zboxcore/sdk/filestatsworker_test.go b/zboxcore/sdk/filestatsworker_test.go index d9c7e6ba4..75e590998 100644 --- a/zboxcore/sdk/filestatsworker_test.go +++ b/zboxcore/sdk/filestatsworker_test.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "encoding/json" + "github.com/0chain/gosdk/zboxcore/mocks" "io" @@ -126,7 +127,8 @@ func TestListRequest_getFileStatsInfoFromBlobber(t *testing.T) { require.NoError(t, err) require.EqualValues(t, expected, string(actual)) - sign, _ := client.Sign(encryption.Hash(mockAllocationTxId)) + key := client.Id() + sign, _ := client.Sign(encryption.Hash(mockAllocationTxId), key) return req.URL.Path == "Test_Success"+zboxutil.FILE_STATS_ENDPOINT+mockAllocationTxId && req.Method == "POST" && req.Header.Get("X-App-Client-ID") == mockClientId && diff --git a/zboxcore/sdk/listworker.go b/zboxcore/sdk/listworker.go index 41b802aba..c89c3163e 100644 --- a/zboxcore/sdk/listworker.go +++ b/zboxcore/sdk/listworker.go @@ -23,22 +23,23 @@ import ( const CHUNK_SIZE = 64 * 1024 type ListRequest struct { - ClientId string - allocationID string - allocationTx string - sig string - blobbers []*blockchain.StorageNode - remotefilepathhash string - remotefilepath string - filename string - authToken *marker.AuthTicket - ctx context.Context - forRepair bool - listOnly bool - offset int - pageLimit int - storageVersion int - dataShards int + ClientId string + allocationID string + allocationTx string + sig string + blobbers []*blockchain.StorageNode + remotefilepathhash string + remotefilepath string + filename string + authToken *marker.AuthTicket + ctx context.Context + forRepair bool + listOnly bool + offset int + pageLimit int + storageVersion int + dataShards int + MultiWalletSupportKey string Consensus } @@ -131,7 +132,15 @@ func (req *ListRequest) getListInfoFromBlobber(blobber *blockchain.StorageNode, if req.forRepair { req.listOnly = true } - httpreq, err := zboxutil.NewListRequest(blobber.Baseurl, req.allocationID, req.allocationTx, req.remotefilepath, req.remotefilepathhash, string(authTokenBytes), req.listOnly, req.offset, req.pageLimit, req.ClientId) + // Choose signing key for the HTTP request: prefer operation-level + // MultiWalletSupportKey if present, otherwise fall back to the request ClientId. + var clientKey string + if req.MultiWalletSupportKey != "" { + clientKey = req.MultiWalletSupportKey + } else { + clientKey = req.ClientId + } + httpreq, err := zboxutil.NewListRequest(blobber.Baseurl, req.allocationID, req.allocationTx, req.remotefilepath, req.remotefilepathhash, string(authTokenBytes), req.listOnly, req.offset, req.pageLimit, clientKey) if err != nil { l.Logger.Error("List info request error: ", err.Error()) return diff --git a/zboxcore/sdk/moveworker.go b/zboxcore/sdk/moveworker.go index 2bceac689..b96499319 100644 --- a/zboxcore/sdk/moveworker.go +++ b/zboxcore/sdk/moveworker.go @@ -31,34 +31,44 @@ import ( ) type MoveRequest struct { - allocationObj *Allocation - allocationID string - allocationTx string - sig string - blobbers []*blockchain.StorageNode - remotefilepath string - destPath string - ctx context.Context - ctxCncl context.CancelFunc - moveMask zboxutil.Uint128 - maskMU *sync.Mutex - connectionID string - timestamp int64 - destLookupHash string + allocationObj *Allocation + allocationID string + allocationTx string + sig string + blobbers []*blockchain.StorageNode + remotefilepath string + destPath string + ctx context.Context + ctxCncl context.CancelFunc + moveMask zboxutil.Uint128 + maskMU *sync.Mutex + connectionID string + timestamp int64 + destLookupHash string + clientId string + MultiWalletSupportKey string Consensus } func (req *MoveRequest) getObjectTreeFromBlobber(blobber *blockchain.StorageNode) (fileref.RefEntity, error) { - return getObjectTreeFromBlobber(req.ctx, req.allocationID, req.allocationTx, req.sig, req.remotefilepath, blobber, req.allocationObj.Owner) + key := req.clientId + if key == "" { + key = req.allocationObj.Owner + } + if req.MultiWalletSupportKey != "" { + key = req.MultiWalletSupportKey + } + return getObjectTreeFromBlobber(req.ctx, req.allocationID, req.allocationTx, req.sig, req.remotefilepath, blobber, key) } func (req *MoveRequest) getFileMetaFromBlobber(pos int) (fileRef *fileref.FileRef, err error) { listReq := &ListRequest{ - allocationID: req.allocationID, - allocationTx: req.allocationTx, - blobbers: req.blobbers, - remotefilepath: req.remotefilepath, - ctx: req.ctx, + allocationID: req.allocationID, + allocationTx: req.allocationTx, + blobbers: req.blobbers, + remotefilepath: req.remotefilepath, + ctx: req.ctx, + MultiWalletSupportKey: req.MultiWalletSupportKey, } respChan := make(chan *fileMetaResponse) go listReq.getFileMetaInfoFromBlobber(req.blobbers[pos], int(pos), respChan) @@ -122,7 +132,14 @@ func (req *MoveRequest) moveBlobberObject( cncl context.CancelFunc ) - httpreq, err = zboxutil.NewMoveRequest(blobber.Baseurl, req.allocationID, req.allocationTx, req.sig, body, req.allocationObj.Owner) + key := req.clientId + if key == "" { + key = req.allocationObj.Owner + } + if req.MultiWalletSupportKey != "" { + key = req.MultiWalletSupportKey + } + httpreq, err = zboxutil.NewMoveRequest(blobber.Baseurl, req.allocationID, req.allocationTx, req.sig, body, key) if err != nil { l.Logger.Error(blobber.Baseurl, "Error creating rename request", err) return @@ -258,14 +275,15 @@ func (req *MoveRequest) ProcessWithBlobbersV2() ([]fileref.RefEntity, error) { } } subRequest := &subDirRequest{ - allocationObj: req.allocationObj, - remotefilepath: req.remotefilepath, - destPath: req.destPath, - ctx: req.ctx, - consensusThresh: req.consensusThresh, - opType: constants.FileOperationMove, - subOpType: constants.FileOperationMove, - mask: req.moveMask, + allocationObj: req.allocationObj, + remotefilepath: req.remotefilepath, + destPath: req.destPath, + ctx: req.ctx, + consensusThresh: req.consensusThresh, + opType: constants.FileOperationMove, + subOpType: constants.FileOperationMove, + mask: req.moveMask, + MultiWalletSupportKey: req.MultiWalletSupportKey, } err := subRequest.processSubDirectories() if err != nil { @@ -275,7 +293,11 @@ func (req *MoveRequest) ProcessWithBlobbersV2() ([]fileref.RefEntity, error) { OperationType: constants.FileOperationDelete, RemotePath: req.remotefilepath, } - err = req.allocationObj.DoMultiOperation([]OperationRequest{op}) + if req.MultiWalletSupportKey != "" { + err = req.allocationObj.DoMultiOperation([]OperationRequest{op}, func(mo *MultiOperation) { mo.MultiWalletSupportKey = req.MultiWalletSupportKey }) + } else { + err = req.allocationObj.DoMultiOperation([]OperationRequest{op}) + } if err != nil { return nil, err } @@ -326,7 +348,7 @@ func (req *MoveRequest) ProcessMove() error { req.Consensus.consensusThresh, req.Consensus.consensus)) } - writeMarkerMutex, err := CreateWriteMarkerMutex(req.allocationObj) + writeMarkerMutex, err := CreateWriteMarkerMutex(req.allocationObj, req.MultiWalletSupportKey) if err != nil { return fmt.Errorf("Move failed: %s", err.Error()) } @@ -373,14 +395,15 @@ func (req *MoveRequest) ProcessMove() error { moveChange.Operation = constants.FileOperationMove moveChange.Size = 0 commitReq := &CommitRequest{ - ClientId: req.allocationObj.Owner, - allocationID: req.allocationID, - allocationTx: req.allocationTx, - sig: req.sig, - blobber: req.blobbers[pos], - connectionID: req.connectionID, - wg: wg, - timestamp: req.timestamp, + ClientId: req.allocationObj.Owner, + allocationID: req.allocationID, + allocationTx: req.allocationTx, + sig: req.sig, + blobber: req.blobbers[pos], + connectionID: req.connectionID, + wg: wg, + timestamp: req.timestamp, + multiWalletSupportKey: req.MultiWalletSupportKey, } // commitReq.change = moveChange commitReq.changes = append(commitReq.changes, moveChange) @@ -412,33 +435,37 @@ func (req *MoveRequest) ProcessMove() error { } type MoveOperation struct { - remotefilepath string - destPath string - srcLookupHash string - destLookupHash string - ctx context.Context - ctxCncl context.CancelFunc - moveMask zboxutil.Uint128 - maskMU *sync.Mutex - consensus Consensus - objectTreeRefs []fileref.RefEntity + remotefilepath string + destPath string + srcLookupHash string + destLookupHash string + ctx context.Context + ctxCncl context.CancelFunc + moveMask zboxutil.Uint128 + maskMU *sync.Mutex + consensus Consensus + objectTreeRefs []fileref.RefEntity + clientId string + MultiWalletSupportKey string } func (mo *MoveOperation) Process(allocObj *Allocation, connectionID string) ([]fileref.RefEntity, zboxutil.Uint128, error) { mR := &MoveRequest{ - allocationObj: allocObj, - allocationID: allocObj.ID, - allocationTx: allocObj.Tx, - sig: allocObj.sig, - connectionID: connectionID, - blobbers: allocObj.Blobbers, - remotefilepath: mo.remotefilepath, - ctx: mo.ctx, - ctxCncl: mo.ctxCncl, - moveMask: mo.moveMask, - maskMU: mo.maskMU, - destPath: mo.destPath, - Consensus: Consensus{RWMutex: &sync.RWMutex{}}, + allocationObj: allocObj, + allocationID: allocObj.ID, + allocationTx: allocObj.Tx, + sig: allocObj.sig, + connectionID: connectionID, + blobbers: allocObj.Blobbers, + remotefilepath: mo.remotefilepath, + ctx: mo.ctx, + ctxCncl: mo.ctxCncl, + moveMask: mo.moveMask, + maskMU: mo.maskMU, + destPath: mo.destPath, + Consensus: Consensus{RWMutex: &sync.RWMutex{}}, + clientId: mo.clientId, + MultiWalletSupportKey: mo.MultiWalletSupportKey, } mR.Consensus.fullconsensus = mo.consensus.fullconsensus mR.Consensus.consensusThresh = mo.consensus.consensusThresh @@ -518,7 +545,7 @@ func (mo *MoveOperation) Error(allocObj *Allocation, consensus int, err error) { } -func NewMoveOperation(remotePath string, destPath string, moveMask zboxutil.Uint128, maskMU *sync.Mutex, consensusTh int, fullConsensus int, ctx context.Context) *MoveOperation { +func NewMoveOperation(remotePath string, destPath string, moveMask zboxutil.Uint128, maskMU *sync.Mutex, consensusTh int, fullConsensus int, ctx context.Context, clientId string, keys ...string) *MoveOperation { mo := &MoveOperation{} mo.remotefilepath = zboxutil.RemoteClean(remotePath) if destPath != "/" { @@ -530,6 +557,10 @@ func NewMoveOperation(remotePath string, destPath string, moveMask zboxutil.Uint mo.consensus.consensusThresh = consensusTh mo.consensus.fullconsensus = fullConsensus mo.ctx, mo.ctxCncl = context.WithCancel(ctx) + mo.clientId = clientId + if len(keys) > 0 && keys[0] != "" { + mo.MultiWalletSupportKey = keys[0] + } return mo } diff --git a/zboxcore/sdk/multi_operation_worker.go b/zboxcore/sdk/multi_operation_worker.go index 38a6a6f07..d583bcfdf 100644 --- a/zboxcore/sdk/multi_operation_worker.go +++ b/zboxcore/sdk/multi_operation_worker.go @@ -60,13 +60,13 @@ type MultiOperation struct { operationMask zboxutil.Uint128 maskMU *sync.Mutex Consensus - changes [][]allocationchange.AllocationChange - changesV2 []allocationchange.AllocationChangeV2 - isRepair bool + changes [][]allocationchange.AllocationChange + changesV2 []allocationchange.AllocationChangeV2 + isRepair bool + MultiWalletSupportKey string } func (mo *MultiOperation) createConnectionObj(blobberIdx int) (err error) { - defer func() { if err == nil { mo.maskMU.Lock() @@ -96,10 +96,18 @@ func (mo *MultiOperation) createConnectionObj(blobberIdx int) (err error) { formWriter.Close() var httpreq *http.Request - httpreq, err = zboxutil.NewConnectionRequest(blobber.Baseurl, mo.allocationObj.ID, mo.allocationObj.Tx, mo.allocationObj.sig, body, mo.allocationObj.Owner) - if err != nil { - l.Logger.Error(blobber.Baseurl, "Error creating new connection request", err) - return + if mo.MultiWalletSupportKey != "" { + httpreq, err = zboxutil.NewConnectionRequest(blobber.Baseurl, mo.allocationObj.ID, mo.allocationObj.Tx, mo.allocationObj.sig, body, mo.MultiWalletSupportKey) + if err != nil { + l.Logger.Error(blobber.Baseurl, "Error creating new connection request by wallet", err) + return err, false + } + } else { + httpreq, err = zboxutil.NewConnectionRequest(blobber.Baseurl, mo.allocationObj.ID, mo.allocationObj.Tx, mo.allocationObj.sig, body, mo.allocationObj.Owner) + if err != nil { + l.Logger.Error(blobber.Baseurl, "Error creating new connection request", err) + return + } } httpreq.Header.Add("Content-Type", formWriter.FormDataContentType()) @@ -147,7 +155,6 @@ func (mo *MultiOperation) createConnectionObj(blobberIdx int) (err error) { err = errors.New("response_error", string(respBody)) return }() - if err != nil { return } @@ -248,7 +255,7 @@ func (mo *MultiOperation) Process() error { mo.changes = zboxutil.Transpose(mo.changes) } - writeMarkerMutex, err := CreateWriteMarkerMutex(mo.allocationObj) + writeMarkerMutex, err := CreateWriteMarkerMutex(mo.allocationObj, mo.MultiWalletSupportKey) if err != nil { for _, op := range mo.operations { op.Error(mo.allocationObj, 0, err) @@ -270,7 +277,11 @@ func (mo *MultiOperation) Process() error { start = time.Now() status := Commit if !mo.isRepair && !mo.allocationObj.checkStatus { - status, _, err = mo.allocationObj.CheckAllocStatus() + if mo.MultiWalletSupportKey != "" { + status, _, err = mo.allocationObj.CheckAllocStatus(mo.MultiWalletSupportKey) + } else { + status, _, err = mo.allocationObj.CheckAllocStatus() + } if err != nil { logger.Logger.Error("Error checking allocation status", err) if singleClientMode { @@ -333,16 +344,19 @@ func (mo *MultiOperation) Process() error { timestamp := int64(common.Now()) for i := mo.operationMask; !i.Equals64(0); i = i.And(zboxutil.NewUint128(1).Lsh(pos).Not()) { pos = uint64(i.TrailingZeros()) + // ClientId must always be the allocation owner. Use Pubkey only for signing + // (stored in the commit request's pubkey field). commitReq := &CommitRequest{ - ClientId: mo.allocationObj.Owner, - allocationID: mo.allocationObj.ID, - allocationTx: mo.allocationObj.Tx, - sig: mo.allocationObj.sig, - blobber: mo.allocationObj.Blobbers[pos], - connectionID: mo.connectionID, - wg: wg, - timestamp: timestamp, - blobberInd: pos, + ClientId: mo.allocationObj.Owner, + allocationID: mo.allocationObj.ID, + allocationTx: mo.allocationObj.Tx, + sig: mo.allocationObj.sig, + blobber: mo.allocationObj.Blobbers[pos], + connectionID: mo.connectionID, + wg: wg, + timestamp: timestamp, + blobberInd: pos, + multiWalletSupportKey: mo.MultiWalletSupportKey, } commitReq.changes = append(commitReq.changes, mo.changes[pos]...) @@ -393,7 +407,6 @@ func (mo *MultiOperation) Process() error { } func (mo *MultiOperation) commitV2() error { - rootMap := make(map[string]zboxutil.Uint128) var pos uint64 for i := mo.operationMask; !i.Equals64(0); i = i.And(zboxutil.NewUint128(1).Lsh(pos).Not()) { @@ -418,15 +431,16 @@ func (mo *MultiOperation) commitV2() error { threshold = mask.CountOnes() } commitReq := &CommitRequestV2{ - allocationObj: mo.allocationObj, - connectionID: mo.connectionID, - sig: mo.allocationObj.sig, - wg: wg, - timestamp: timestamp, - commitMask: mask, - consensusThresh: threshold, - changes: changes, - isRepair: mo.isRepair, + allocationObj: mo.allocationObj, + connectionID: mo.connectionID, + sig: mo.allocationObj.sig, + wg: wg, + timestamp: timestamp, + commitMask: mask, + consensusThresh: threshold, + changes: changes, + isRepair: mo.isRepair, + multiWalletSupportKey: mo.MultiWalletSupportKey, } commitReqs[counter] = commitReq counter++ diff --git a/zboxcore/sdk/reader.go b/zboxcore/sdk/reader.go index ae005b369..f6a8efa1b 100644 --- a/zboxcore/sdk/reader.go +++ b/zboxcore/sdk/reader.go @@ -190,6 +190,7 @@ func GetDStorageFileReader(alloc *Allocation, ref *ORef, sdo *StreamDownloadOpti chunkSize: BlockSize, maskMu: &sync.Mutex{}, connectionID: zboxutil.NewConnectionId(), + MultiWalletSupportKey: alloc.MultiWalletSupportKey, }, open: true, } diff --git a/zboxcore/sdk/renameworker.go b/zboxcore/sdk/renameworker.go index 644bfcc75..aa923e70c 100644 --- a/zboxcore/sdk/renameworker.go +++ b/zboxcore/sdk/renameworker.go @@ -30,34 +30,44 @@ import ( ) type RenameRequest struct { - allocationObj *Allocation - allocationID string - allocationTx string - sig string - blobbers []*blockchain.StorageNode - remotefilepath string - newName string - ctx context.Context - ctxCncl context.CancelFunc - wg *sync.WaitGroup - renameMask zboxutil.Uint128 - maskMU *sync.Mutex - connectionID string - consensus Consensus - timestamp int64 + allocationObj *Allocation + allocationID string + allocationTx string + sig string + blobbers []*blockchain.StorageNode + remotefilepath string + newName string + ctx context.Context + ctxCncl context.CancelFunc + wg *sync.WaitGroup + renameMask zboxutil.Uint128 + maskMU *sync.Mutex + connectionID string + consensus Consensus + timestamp int64 + clientId string + MultiWalletSupportKey string } func (req *RenameRequest) getObjectTreeFromBlobber(blobber *blockchain.StorageNode) (fileref.RefEntity, error) { - return getObjectTreeFromBlobber(req.ctx, req.allocationID, req.allocationTx, req.sig, req.remotefilepath, blobber, req.allocationObj.Owner) + key := req.clientId + if key == "" { + key = req.allocationObj.Owner + } + if req.MultiWalletSupportKey != "" { + key = req.MultiWalletSupportKey + } + return getObjectTreeFromBlobber(req.ctx, req.allocationID, req.allocationTx, req.sig, req.remotefilepath, blobber, key) } func (req *RenameRequest) getFileMetaFromBlobber(pos int) (fileRef *fileref.FileRef, err error) { listReq := &ListRequest{ - allocationID: req.allocationID, - allocationTx: req.allocationTx, - blobbers: req.blobbers, - remotefilepath: req.remotefilepath, - ctx: req.ctx, + allocationID: req.allocationID, + allocationTx: req.allocationTx, + blobbers: req.blobbers, + remotefilepath: req.remotefilepath, + ctx: req.ctx, + MultiWalletSupportKey: req.MultiWalletSupportKey, } respChan := make(chan *fileMetaResponse) go listReq.getFileMetaInfoFromBlobber(req.blobbers[pos], int(pos), respChan) @@ -117,7 +127,11 @@ func (req *RenameRequest) renameBlobberObject( formWriter.Close() var httpreq *http.Request - httpreq, err = zboxutil.NewRenameRequest(blobber.Baseurl, req.allocationID, req.allocationTx, req.sig, body, req.allocationObj.Owner) + if req.MultiWalletSupportKey != "" { + httpreq, err = zboxutil.NewRenameRequest(blobber.Baseurl, req.allocationID, req.allocationTx, req.sig, body, req.MultiWalletSupportKey) + } else { + httpreq, err = zboxutil.NewRenameRequest(blobber.Baseurl, req.allocationID, req.allocationTx, req.sig, body, req.allocationObj.Owner) + } if err != nil { l.Logger.Error(blobber.Baseurl, "Error creating rename request", err) return @@ -253,14 +267,15 @@ func (req *RenameRequest) ProcessWithBlobbersV2() ([]fileref.RefEntity, error) { } } subRequest := &subDirRequest{ - allocationObj: req.allocationObj, - remotefilepath: req.remotefilepath, - destPath: path.Join(path.Dir(req.remotefilepath), req.newName), - ctx: req.ctx, - consensusThresh: req.consensus.consensusThresh, - opType: constants.FileOperationMove, - subOpType: constants.FileOperationRename, - mask: req.renameMask, + allocationObj: req.allocationObj, + remotefilepath: req.remotefilepath, + destPath: path.Join(path.Dir(req.remotefilepath), req.newName), + ctx: req.ctx, + consensusThresh: req.consensus.consensusThresh, + opType: constants.FileOperationMove, + subOpType: constants.FileOperationRename, + mask: req.renameMask, + MultiWalletSupportKey: req.MultiWalletSupportKey, } err := subRequest.processSubDirectories() if err != nil { @@ -271,7 +286,11 @@ func (req *RenameRequest) ProcessWithBlobbersV2() ([]fileref.RefEntity, error) { RemotePath: req.remotefilepath, Mask: &req.renameMask, } - err = req.allocationObj.DoMultiOperation([]OperationRequest{op}) + if req.MultiWalletSupportKey != "" { + err = req.allocationObj.DoMultiOperation([]OperationRequest{op}, func(mo *MultiOperation) { mo.MultiWalletSupportKey = req.MultiWalletSupportKey }) + } else { + err = req.allocationObj.DoMultiOperation([]OperationRequest{op}) + } if err != nil { return nil, err } @@ -316,7 +335,7 @@ func (req *RenameRequest) ProcessRename() error { req.consensus.consensusThresh, req.consensus.getConsensus())) } - writeMarkerMutex, err := CreateWriteMarkerMutex(req.allocationObj) + writeMarkerMutex, err := CreateWriteMarkerMutex(req.allocationObj, req.MultiWalletSupportKey) if err != nil { return fmt.Errorf("rename failed: %s", err.Error()) } @@ -329,7 +348,7 @@ func (req *RenameRequest) ProcessRename() error { defer writeMarkerMutex.Unlock(req.ctx, req.renameMask, req.blobbers, time.Minute, req.connectionID) //nolint: errcheck //Check if the allocation is to be repaired or rolled back - status, _, err := req.allocationObj.CheckAllocStatus() + status, _, err := req.allocationObj.CheckAllocStatus(req.MultiWalletSupportKey) if err != nil { logger.Logger.Error("Error checking allocation status: ", err) return fmt.Errorf("rename failed: %s", err.Error()) @@ -366,14 +385,15 @@ func (req *RenameRequest) ProcessRename() error { newChange.Size = 0 commitReq := &CommitRequest{ - ClientId: req.allocationObj.Owner, - allocationID: req.allocationID, - allocationTx: req.allocationTx, - sig: req.sig, - blobber: req.blobbers[pos], - connectionID: req.connectionID, - wg: wg, - timestamp: req.timestamp, + ClientId: req.allocationObj.Owner, + allocationID: req.allocationID, + allocationTx: req.allocationTx, + sig: req.sig, + blobber: req.blobbers[pos], + connectionID: req.connectionID, + wg: wg, + timestamp: req.timestamp, + multiWalletSupportKey: req.MultiWalletSupportKey, } commitReq.changes = append(commitReq.changes, newChange) commitReqs[counter] = commitReq @@ -409,15 +429,17 @@ func (req *RenameRequest) ProcessRename() error { } type RenameOperation struct { - remotefilepath string - srcLookupHash string - destLookupHash string - ctx context.Context - ctxCncl context.CancelFunc - renameMask zboxutil.Uint128 - newName string - maskMU *sync.Mutex - objectTreeRefs []fileref.RefEntity + remotefilepath string + srcLookupHash string + destLookupHash string + ctx context.Context + ctxCncl context.CancelFunc + renameMask zboxutil.Uint128 + newName string + maskMU *sync.Mutex + objectTreeRefs []fileref.RefEntity + clientId string + MultiWalletSupportKey string consensus Consensus } @@ -425,20 +447,22 @@ type RenameOperation struct { func (ro *RenameOperation) Process(allocObj *Allocation, connectionID string) ([]fileref.RefEntity, zboxutil.Uint128, error) { // make renameRequest object rR := &RenameRequest{ - allocationObj: allocObj, - allocationID: allocObj.ID, - allocationTx: allocObj.Tx, - sig: allocObj.sig, - connectionID: connectionID, - blobbers: allocObj.Blobbers, - remotefilepath: ro.remotefilepath, - newName: ro.newName, - ctx: ro.ctx, - ctxCncl: ro.ctxCncl, - renameMask: ro.renameMask, - maskMU: ro.maskMU, - wg: &sync.WaitGroup{}, - consensus: Consensus{RWMutex: &sync.RWMutex{}}, + allocationObj: allocObj, + allocationID: allocObj.ID, + allocationTx: allocObj.Tx, + sig: allocObj.sig, + connectionID: connectionID, + blobbers: allocObj.Blobbers, + remotefilepath: ro.remotefilepath, + newName: ro.newName, + ctx: ro.ctx, + ctxCncl: ro.ctxCncl, + renameMask: ro.renameMask, + maskMU: ro.maskMU, + wg: &sync.WaitGroup{}, + consensus: Consensus{RWMutex: &sync.RWMutex{}}, + clientId: ro.clientId, + MultiWalletSupportKey: ro.MultiWalletSupportKey, } if filepath.Base(ro.remotefilepath) == ro.newName { return nil, ro.renameMask, errors.New("invalid_operation", "Cannot rename to same name") @@ -526,7 +550,7 @@ func (ro *RenameOperation) Error(allocObj *Allocation, consensus int, err error) } -func NewRenameOperation(remotePath string, destName string, renameMask zboxutil.Uint128, maskMU *sync.Mutex, consensusTh int, fullConsensus int, ctx context.Context) *RenameOperation { +func NewRenameOperation(remotePath string, destName string, renameMask zboxutil.Uint128, maskMU *sync.Mutex, consensusTh int, fullConsensus int, ctx context.Context, clientId string, multiWalletKey string) *RenameOperation { ro := &RenameOperation{} ro.remotefilepath = zboxutil.RemoteClean(remotePath) ro.newName = path.Base(destName) @@ -535,6 +559,8 @@ func NewRenameOperation(remotePath string, destName string, renameMask zboxutil. ro.consensus.consensusThresh = consensusTh ro.consensus.fullconsensus = fullConsensus ro.ctx, ro.ctxCncl = context.WithCancel(ctx) + ro.clientId = clientId + ro.MultiWalletSupportKey = multiWalletKey return ro } diff --git a/zboxcore/sdk/repairworker.go b/zboxcore/sdk/repairworker.go index 319e23411..bd1cc3306 100644 --- a/zboxcore/sdk/repairworker.go +++ b/zboxcore/sdk/repairworker.go @@ -286,7 +286,12 @@ func (r *RepairRequest) repairFile(a *Allocation, file *ListResult) []OperationR } func (r *RepairRequest) repairOperation(a *Allocation, ops []OperationRequest) { - err := a.DoMultiOperation(ops, WithRepair()) + opts := []MultiOperationOption{WithRepair()} + if a.MultiWalletSupportKey != "" { + opts = append(opts, func(mo *MultiOperation) { mo.MultiWalletSupportKey = a.MultiWalletSupportKey }) + } + + err := a.DoMultiOperation(ops, opts...) if err != nil { l.Logger.Error("repair_file_failed", zap.Error(err)) status := r.statusCB != nil diff --git a/zboxcore/sdk/rollback.go b/zboxcore/sdk/rollback.go index 6d1607c44..005c723a3 100644 --- a/zboxcore/sdk/rollback.go +++ b/zboxcore/sdk/rollback.go @@ -20,6 +20,7 @@ import ( "github.com/0chain/common/core/common" thrown "github.com/0chain/errors" "github.com/0chain/gosdk/core/client" + "github.com/0chain/gosdk/core/zcncrypto" "github.com/0chain/gosdk/zboxcore/blockchain" l "github.com/0chain/gosdk/zboxcore/logger" "github.com/0chain/gosdk/zboxcore/marker" @@ -50,11 +51,12 @@ var ( ) type RollbackBlobber struct { - ClientId string - blobber *blockchain.StorageNode - commitResult *CommitResult - lpm *LatestPrevWriteMarker - blobIndex int + ClientId string + MultiWalletSupportKey string + blobber *blockchain.StorageNode + commitResult *CommitResult + lpm *LatestPrevWriteMarker + blobIndex int } type BlobberStatus struct { @@ -62,11 +64,11 @@ type BlobberStatus struct { Status string } -func GetWritemarker(allocID, allocTx, sig, id, baseUrl string, clientId ...string) (*LatestPrevWriteMarker, error) { +func GetWritemarker(allocID, allocTx, sig, id, baseUrl string, keys ...string) (*LatestPrevWriteMarker, error) { var lpm LatestPrevWriteMarker - req, err := zboxutil.NewWritemarkerRequest(baseUrl, allocID, allocTx, sig, clientId...) + req, err := zboxutil.NewWritemarkerRequest(baseUrl, allocID, allocTx, sig, keys...) if err != nil { return nil, err } @@ -103,12 +105,22 @@ func GetWritemarker(allocID, allocTx, sig, id, baseUrl string, clientId ...strin return nil, err } if lpm.LatestWM != nil { - err = lpm.LatestWM.VerifySignature(client.PublicKey()) + // pick wallet for verification + var walletForVerify *zcncrypto.Wallet + if len(keys) > 0 && keys[0] != "" { + walletForVerify = client.GetWalletByKey(keys[0]) + if walletForVerify == nil { + return nil, fmt.Errorf("multi-wallet-settings err: wallet not found : %s", keys[0]) + } + } else { + walletForVerify = client.GetWallet() + } + err = lpm.LatestWM.VerifySignature(walletForVerify.ClientKey) if err != nil { return nil, fmt.Errorf("signature verification failed for latest writemarker: %s", err.Error()) } if lpm.PrevWM != nil { - err = lpm.PrevWM.VerifySignature(client.PublicKey()) + err = lpm.PrevWM.VerifySignature(walletForVerify.ClientKey) if err != nil { return nil, fmt.Errorf("signature verification failed for latest writemarker: %s", err.Error()) } @@ -126,7 +138,10 @@ func (rb *RollbackBlobber) processRollback(ctx context.Context, tx string) error wm.AllocationID = rb.lpm.LatestWM.AllocationID wm.Timestamp = rb.lpm.LatestWM.Timestamp wm.BlobberID = rb.lpm.LatestWM.BlobberID - wm.ClientID = client.Id() + if rb.MultiWalletSupportKey != "" { + wm.MultiWalletSupportKey = rb.MultiWalletSupportKey + } + wm.ClientID = rb.ClientId wm.Size = -rb.lpm.LatestWM.Size wm.ChainSize = wm.Size + rb.lpm.LatestWM.ChainSize @@ -178,7 +193,12 @@ func (rb *RollbackBlobber) processRollback(ctx context.Context, tx string) error return err } - req, err := zboxutil.NewRollbackRequest(rb.blobber.Baseurl, wm.AllocationID, tx, body, wm.ClientID) + var req *http.Request + if rb.MultiWalletSupportKey != "" { + req, err = zboxutil.NewRollbackRequest(rb.blobber.Baseurl, wm.AllocationID, tx, body, rb.MultiWalletSupportKey) + } else { + req, err = zboxutil.NewRollbackRequest(rb.blobber.Baseurl, wm.AllocationID, tx, body) + } if err != nil { l.Logger.Error("Creating rollback request failed: ", err) return err @@ -269,7 +289,14 @@ func (rb *RollbackBlobber) processRollback(ctx context.Context, tx string) error // CheckAllocStatus checks the status of the allocation // and returns the status of the allocation and its blobbers. -func (a *Allocation) CheckAllocStatus() (AllocStatus, []BlobberStatus, error) { +func (a *Allocation) CheckAllocStatus(keys ...string) (AllocStatus, []BlobberStatus, error) { + + var key string + if len(keys) > 0 && keys[0] != "" { + key = keys[0] + } else { + key = a.Owner + } wg := &sync.WaitGroup{} markerChan := make(chan *RollbackBlobber, len(a.Blobbers)) @@ -286,7 +313,7 @@ func (a *Allocation) CheckAllocStatus() (AllocStatus, []BlobberStatus, error) { ID: blobber.ID, Status: "available", } - wr, err := GetWritemarker(a.ID, a.Tx, a.sig, blobber.ID, blobber.Baseurl, a.Owner) + wr, err := GetWritemarker(a.ID, a.Tx, a.sig, blobber.ID, blobber.Baseurl, key) if err != nil { atomic.AddInt32(&errCnt, 1) markerError = err diff --git a/zboxcore/sdk/sdk.go b/zboxcore/sdk/sdk.go index 91b8ee639..111031431 100644 --- a/zboxcore/sdk/sdk.go +++ b/zboxcore/sdk/sdk.go @@ -7,12 +7,21 @@ import ( "io" "math" "net/http" + "os" + "path" "strconv" + "strings" "github.com/0chain/common/core/currency" "github.com/0chain/errors" + thrown "github.com/0chain/errors" + "github.com/0chain/gosdk/constants" "github.com/0chain/gosdk/core/logger" + "github.com/0chain/gosdk/core/pathutil" "github.com/0chain/gosdk/core/screstapi" + "github.com/0chain/gosdk/core/sys" + + // "github.com/0chain/gosdk/zcncore" "gopkg.in/natefinch/lumberjack.v2" "github.com/0chain/gosdk/core/client" @@ -163,7 +172,7 @@ type StakePoolInfo struct { // GetStakePoolInfo retrieve stake pool info for the current client configured to the sdk, given provider type and provider ID. // - providerType: provider type // - providerID: provider ID -func GetStakePoolInfo(providerType ProviderType, providerID string) (info *StakePoolInfo, err error) { +func GetStakePoolInfo(providerType ProviderType, providerID string, keys ...string) (info *StakePoolInfo, err error) { if !client.IsSDKInitialized() { return nil, sdkNotInitialized } @@ -601,7 +610,7 @@ func GetClientEncryptedPublicKey() (string, error) { // - authTicket: the auth ticket hash // // returns the allocation instance and error if any -func GetAllocationFromAuthTicket(authTicket string) (*Allocation, error) { +func GetAllocationFromAuthTicket(authTicket string, keys ...string) (*Allocation, error) { if !client.IsSDKInitialized() { return nil, sdkNotInitialized } @@ -614,7 +623,7 @@ func GetAllocationFromAuthTicket(authTicket string) (*Allocation, error) { if err != nil { return nil, errors.New("auth_ticket_decode_error", "Error unmarshaling the auth ticket."+err.Error()) } - return GetAllocation(at.AllocationID) + return GetAllocation(at.AllocationID, keys...) } // GetAllocation - get allocation from given allocation id @@ -622,7 +631,7 @@ func GetAllocationFromAuthTicket(authTicket string) (*Allocation, error) { // - allocationID: the allocation id // // returns the allocation instance and error if any -func GetAllocation(allocationID string) (*Allocation, error) { +func GetAllocation(allocationID string, keys ...string) (*Allocation, error) { if !client.IsSDKInitialized() { return nil, sdkNotInitialized } @@ -640,7 +649,7 @@ func GetAllocation(allocationID string) (*Allocation, error) { } allocationObj.numBlockDownloads = numBlockDownloads - allocationObj.InitAllocation() + allocationObj.InitAllocation(keys...) return allocationObj, nil } @@ -715,7 +724,10 @@ func SetNumBlockDownloads(num int) { // GetAllocations - get all allocations for the current client // // returns the list of allocations and error if any -func GetAllocations() ([]*Allocation, error) { +func GetAllocations(keys ...string) ([]*Allocation, error) { + if len(keys) > 0 && keys[0] != "" { + return GetAllocationsForClient(client.Id(keys...)) + } return GetAllocationsForClient(client.Id()) } @@ -811,13 +823,20 @@ type CreateAllocationOptions struct { // - options is the options struct instance for creating the allocation. // // returns the hash of the new_allocation_request transaction, the nonce of the transaction, the transaction object and an error if any. -func CreateAllocationWith(options CreateAllocationOptions) ( +func CreateAllocationWith(options CreateAllocationOptions, keys ...string) ( string, int64, *transaction.Transaction, error) { + if len(keys) > 0 && keys[0] != "" { + return CreateAllocationForOwner( + client.Id(keys...), client.PublicKey(keys...), "", options.DataShards, options.ParityShards, + options.Size, options.ReadPrice, options.WritePrice, options.Lock, + options.BlobberIds, options.BlobberAuthTickets, options.ThirdPartyExtendable, options.IsEnterprise, options.Force, options.FileOptionsParams, options.AuthRoundExpiry, keys...) + } + return CreateAllocationForOwner(client.Id(), client.PublicKey(), "", options.DataShards, options.ParityShards, options.Size, options.ReadPrice, options.WritePrice, options.Lock, - options.BlobberIds, options.BlobberAuthTickets, options.ThirdPartyExtendable, options.IsEnterprise, options.Force, options.FileOptionsParams, options.AuthRoundExpiry) + options.BlobberIds, options.BlobberAuthTickets, options.ThirdPartyExtendable, options.IsEnterprise, options.Force, options.FileOptionsParams, options.AuthRoundExpiry, keys...) } // GetAllocationBlobbers returns a list of blobber ids that can be used for a new allocation. @@ -1033,7 +1052,7 @@ func FinalizeAllocation(allocID string) (hash string, nonce int64, err error) { // - allocID is the id of the allocation. // // returns the hash of the transaction, the nonce of the transaction and an error if any. -func CancelAllocation(allocID string) (hash string, nonce int64, err error) { +func CancelAllocation(allocID string, keys ...string) (hash string, nonce int64, err error) { if !client.IsSDKInitialized() { return "", 0, sdkNotInitialized } @@ -1041,7 +1060,7 @@ func CancelAllocation(allocID string) (hash string, nonce int64, err error) { Name: transaction.STORAGESC_CANCEL_ALLOCATION, InputArgs: map[string]interface{}{"allocation_id": allocID}, } - hash, _, nonce, _, err = storageSmartContractTxn(sn) + hash, _, nonce, _, err = storageSmartContractTxn(sn, keys...) return } @@ -1112,7 +1131,7 @@ func ShutdownProvider(providerType ProviderType, providerID string) (string, int // CollectRewards collects the rewards for a provider (txn: `storagesc.collect_reward`) // - providerId is the id of the provider. // - providerType is the type of the provider. -func CollectRewards(providerId string, providerType ProviderType) (string, int64, error) { +func CollectRewards(providerId string, providerType ProviderType, keys ...string) (string, int64, error) { if !client.IsSDKInitialized() { return "", 0, sdkNotInitialized } @@ -1141,7 +1160,7 @@ func CollectRewards(providerId string, providerType ProviderType) (string, int64 return "", 0, fmt.Errorf("collect rewards provider type %v not implimented", providerType) } - hash, _, n, _, err := transaction.SmartContractTxn(scAddress, sn, true) + hash, _, n, _, err := transaction.SmartContractTxn(scAddress, sn, true, keys...) return hash, n, err } @@ -1152,12 +1171,12 @@ func CollectRewards(providerId string, providerType ProviderType) (string, int64 // - newOwnerPublicKey is the public key of the new owner. // // returns the hash of the transaction, the nonce of the transaction and an error if any. -func TransferAllocation(allocationId, newOwner, newOwnerPublicKey string) (string, int64, error) { +func TransferAllocation(allocationId, newOwner, newOwnerPublicKey string, keys ...string) (string, int64, error) { if !client.IsSDKInitialized() { return "", 0, sdkNotInitialized } - alloc, err := GetAllocation(allocationId) + alloc, err := GetAllocation(allocationId, keys...) if err != nil { return "", 0, allocationNotFound } @@ -1179,7 +1198,7 @@ func TransferAllocation(allocationId, newOwner, newOwnerPublicKey string) (strin Name: transaction.STORAGESC_UPDATE_ALLOCATION, InputArgs: allocationRequest, } - hash, _, n, _, err := storageSmartContractTxn(sn) + hash, _, n, _, err := storageSmartContractTxn(sn, keys...) return hash, n, err } @@ -1276,17 +1295,17 @@ func StorageSmartContractTxn(sn transaction.SmartContractTxnData) ( return storageSmartContractTxnValue(sn, 0) } -func storageSmartContractTxn(sn transaction.SmartContractTxnData) ( +func storageSmartContractTxn(sn transaction.SmartContractTxnData, keys ...string) ( hash, out string, nonce int64, txn *transaction.Transaction, err error) { - return storageSmartContractTxnValue(sn, 0) + return storageSmartContractTxnValue(sn, 0, keys...) } -func storageSmartContractTxnValue(sn transaction.SmartContractTxnData, value uint64) ( +func storageSmartContractTxnValue(sn transaction.SmartContractTxnData, value uint64, keys ...string) ( hash, out string, nonce int64, txn *transaction.Transaction, err error) { // Fee is set during sdk initialization. - return transaction.SmartContractTxnValueFeeWithRetry(STORAGE_SCADDRESS, sn, value, client.TxnFee(), true) + return transaction.SmartContractTxnValueFeeWithRetry(STORAGE_SCADDRESS, sn, value, client.TxnFee(), true, keys...) } func CommitToFabric(metaTxnData, fabricConfigJSON string) (string, error) { @@ -1473,3 +1492,109 @@ func updateMaskBit(mask uint16, index uint8, value bool) uint16 { return mask & ^uint16(1<0 { return keys[0] } ; return "" }(), }, nil } @@ -99,8 +101,12 @@ func (wmMu *WriteMarkerMutex) UnlockBlobber( }() var req *http.Request + key := wmMu.allocationObj.Owner + if wmMu.Pubkey != "" { + key = wmMu.Pubkey + } req, err = zboxutil.NewWriteMarkerUnLockRequest( - b.Baseurl, wmMu.allocationObj.ID, wmMu.allocationObj.Tx, wmMu.allocationObj.sig, connID, "", wmMu.allocationObj.Owner) + b.Baseurl, wmMu.allocationObj.ID, wmMu.allocationObj.Tx, wmMu.allocationObj.sig, connID, "", key) if err != nil { return } @@ -280,8 +286,12 @@ func (wmMu *WriteMarkerMutex) lockBlobber( }() var req *http.Request + key := wmMu.allocationObj.Owner + if wmMu.Pubkey != "" { + key = wmMu.Pubkey + } req, err = zboxutil.NewWriteMarkerLockRequest( - b.Baseurl, wmMu.allocationObj.ID, wmMu.allocationObj.Tx, wmMu.allocationObj.sig, connID, wmMu.allocationObj.Owner) + b.Baseurl, wmMu.allocationObj.ID, wmMu.allocationObj.Tx, wmMu.allocationObj.sig, connID, key) if err != nil { return } diff --git a/zboxcore/zboxutil/http.go b/zboxcore/zboxutil/http.go index 65052aad8..599f6175f 100644 --- a/zboxcore/zboxutil/http.go +++ b/zboxcore/zboxutil/http.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "encoding/json" + "fmt" "io" "net" "net/http" @@ -19,6 +20,7 @@ import ( "github.com/0chain/gosdk/core/client" "github.com/0chain/gosdk/core/encryption" "github.com/0chain/gosdk/core/logger" + l "github.com/0chain/gosdk/zboxcore/logger" lru "github.com/hashicorp/golang-lru/v2" "github.com/hitenjain14/fasthttp" ) @@ -196,36 +198,52 @@ func NewHTTPRequest(method string, url string, data []byte) (*http.Request, cont return req, ctx, cncl, err } -func setClientInfo(req *http.Request) { +func setClientInfo(req *http.Request, keys ...string) error { + if len(keys) > 0 && keys[0] != "" { + wallet := client.GetWalletByKey(keys[0]) + if wallet == nil { + return errors.New("multi-wallet-settings err: ", "wallet not found : "+keys[0]) + } + req.Header.Set("X-App-Client-ID", wallet.ClientID) + req.Header.Set("X-App-Client-Key", wallet.ClientKey) + return nil + } req.Header.Set("X-App-Client-ID", client.Id()) req.Header.Set("X-App-Client-Key", client.PublicKey()) + return nil } -func setClientInfoWithSign(req *http.Request, sig, allocation, baseURL string, clients ...string) error { - var clientID string - if len(clients) > 0 && clients[0] != "" { - clientID = clients[0] +func setClientInfoWithSign(req *http.Request, sig, allocation, baseURL string, keys ...string) error { + var key string + if len(keys) > 0 && keys[0] != "" { + key = keys[0] } else { - clientID = client.Id() + key = client.Id() } - setClientInfo(req) + wallet := client.GetWalletByKey(key) + if wallet == nil { + return errors.New("multi-wallet-settings err: ", "wallet not found : "+key) + } + l.Logger.Info(fmt.Sprintf("setClientInfoWithSign: wallet details: %+v", *wallet)) + req.Header.Set("X-App-Client-ID", wallet.ClientID) + req.Header.Set("X-App-Client-Key", wallet.ClientKey) req.Header.Set(CLIENT_SIGNATURE_HEADER, sig) hashData := allocation + baseURL - sig2, ok := SignCache.Get(hashData + ":" + clientID) + sig2, ok := SignCache.Get(hashData + ":" + key) if !ok { var err error - sig2, err = client.Sign(encryption.Hash(hashData), clientID) + sig2, err = client.Sign(encryption.Hash(hashData), key) if err != nil { return err } - SignCache.Add(hashData+":"+clientID, sig2) + SignCache.Add(hashData+":"+key, sig2) } req.Header.Set(CLIENT_SIGNATURE_HEADER_V2, sig2) return nil } -func NewCommitRequest(baseUrl, allocationID string, allocationTx string, body io.Reader, apiVersion int, clients ...string) (*http.Request, error) { +func NewCommitRequest(baseUrl, allocationID string, allocationTx string, body io.Reader, apiVersion int, keys ...string) (*http.Request, error) { var ( u *url.URL err error @@ -243,14 +261,14 @@ func NewCommitRequest(baseUrl, allocationID string, allocationTx string, body io if err != nil { return nil, err } - setClientInfo(req) + setClientInfo(req, keys...) req.Header.Set(ALLOCATION_ID_HEADER, allocationID) return req, nil } -func NewReferencePathRequest(baseUrl, allocationID string, allocationTx string, sig string, paths []string, clients ...string) (*http.Request, error) { +func NewReferencePathRequest(baseUrl, allocationID string, allocationTx string, sig string, paths []string, keys ...string) (*http.Request, error) { nurl, err := joinUrl(baseUrl, REFERENCE_ENDPOINT, allocationTx) if err != nil { return nil, err @@ -270,7 +288,7 @@ func NewReferencePathRequest(baseUrl, allocationID string, allocationTx string, return nil, err } - if err := setClientInfoWithSign(req, sig, allocationTx, baseUrl, clients...); err != nil { + if err := setClientInfoWithSign(req, sig, allocationTx, baseUrl, keys...); err != nil { return nil, err } @@ -279,7 +297,7 @@ func NewReferencePathRequest(baseUrl, allocationID string, allocationTx string, return req, nil } -func NewReferencePathRequestV2(baseUrl, allocationID, allocationTx, sig string, paths []string, loadOnly bool, clients ...string) (*http.Request, error) { +func NewReferencePathRequestV2(baseUrl, allocationID, allocationTx, sig string, paths []string, loadOnly bool, keys ...string) (*http.Request, error) { nurl, err := joinUrl(baseUrl, REFERENCE_ENDPOINT_V2, allocationTx) if err != nil { return nil, err @@ -301,7 +319,7 @@ func NewReferencePathRequestV2(baseUrl, allocationID, allocationTx, sig string, return nil, err } - if err := setClientInfoWithSign(req, sig, allocationTx, baseUrl, clients...); err != nil { + if err := setClientInfoWithSign(req, sig, allocationTx, baseUrl, keys...); err != nil { return nil, err } @@ -310,7 +328,7 @@ func NewReferencePathRequestV2(baseUrl, allocationID, allocationTx, sig string, return req, nil } -func NewCalculateHashRequest(baseUrl, allocationID string, allocationTx string, paths []string, clients ...string) (*http.Request, error) { +func NewCalculateHashRequest(baseUrl, allocationID string, allocationTx string, paths []string, keys ...string) (*http.Request, error) { nurl, err := joinUrl(baseUrl, CALCULATE_HASH_ENDPOINT, allocationTx) if err != nil { return nil, err @@ -326,14 +344,14 @@ func NewCalculateHashRequest(baseUrl, allocationID string, allocationTx string, if err != nil { return nil, err } - setClientInfo(req) + setClientInfo(req, keys...) req.Header.Set(ALLOCATION_ID_HEADER, allocationID) return req, nil } -func NewObjectTreeRequest(baseUrl, allocationID string, allocationTx string, sig string, path string, clients ...string) (*http.Request, error) { +func NewObjectTreeRequest(baseUrl, allocationID string, allocationTx string, sig string, path string, keys ...string) (*http.Request, error) { nurl, err := joinUrl(baseUrl, OBJECT_TREE_ENDPOINT, allocationTx) if err != nil { return nil, err @@ -347,7 +365,7 @@ func NewObjectTreeRequest(baseUrl, allocationID string, allocationTx string, sig return nil, err } - if err := setClientInfoWithSign(req, sig, allocationTx, baseUrl, clients...); err != nil { + if err := setClientInfoWithSign(req, sig, allocationTx, baseUrl, keys...); err != nil { return nil, err } @@ -356,7 +374,7 @@ func NewObjectTreeRequest(baseUrl, allocationID string, allocationTx string, sig return req, nil } -func NewRefsRequest(baseUrl, allocationID, sig, allocationTx, path, pathHash, authToken, offsetPath, updatedDate, offsetDate, fileType, refType string, level, pageLimit int, clients ...string) (*http.Request, error) { +func NewRefsRequest(baseUrl, allocationID, sig, allocationTx, path, pathHash, authToken, offsetPath, updatedDate, offsetDate, fileType, refType string, level, pageLimit int, keys ...string) (*http.Request, error) { nUrl, err := joinUrl(baseUrl, REFS_ENDPOINT, allocationTx) if err != nil { return nil, err @@ -380,14 +398,14 @@ func NewRefsRequest(baseUrl, allocationID, sig, allocationTx, path, pathHash, au req.Header.Set(ALLOCATION_ID_HEADER, allocationID) - if err := setClientInfoWithSign(req, sig, allocationTx, baseUrl, clients...); err != nil { + if err := setClientInfoWithSign(req, sig, allocationTx, baseUrl, keys...); err != nil { return nil, err } return req, nil } -func NewRecentlyAddedRefsRequest(bUrl, allocID, allocTx, sig string, fromDate, offset int64, pageLimit int, clients ...string) (*http.Request, error) { +func NewRecentlyAddedRefsRequest(bUrl, allocID, allocTx, sig string, fromDate, offset int64, pageLimit int, keys ...string) (*http.Request, error) { nUrl, err := joinUrl(bUrl, RECENT_REFS_ENDPOINT, allocID) if err != nil { @@ -407,7 +425,7 @@ func NewRecentlyAddedRefsRequest(bUrl, allocID, allocTx, sig string, fromDate, o req.Header.Set(ALLOCATION_ID_HEADER, allocID) - if err = setClientInfoWithSign(req, sig, allocTx, bUrl, clients...); err != nil { + if err = setClientInfoWithSign(req, sig, allocTx, bUrl, keys...); err != nil { return nil, err } @@ -492,7 +510,7 @@ func DeleteCollaboratorRequest(baseUrl, allocationID, allocationTx, sig string, return req, nil } -func NewFileMetaRequest(baseUrl, allocationID, allocationTx, sig string, body io.Reader, clients ...string) (*http.Request, error) { +func NewFileMetaRequest(baseUrl, allocationID, allocationTx, sig string, body io.Reader, keys ...string) (*http.Request, error) { u, err := joinUrl(baseUrl, FILE_META_ENDPOINT, allocationTx) if err != nil { return nil, err @@ -502,7 +520,7 @@ func NewFileMetaRequest(baseUrl, allocationID, allocationTx, sig string, body io return nil, err } - if err := setClientInfoWithSign(req, sig, allocationTx, baseUrl, clients...); err != nil { + if err := setClientInfoWithSign(req, sig, allocationTx, baseUrl, keys...); err != nil { return nil, err } @@ -582,7 +600,7 @@ func NewUploadRequestWithMethod(baseURL, allocationID, allocationTx, sig string, } func NewWriteMarkerLockRequest( - baseURL, allocationID, allocationTx, sig, connID string, clients ...string) (*http.Request, error) { + baseURL, allocationID, allocationTx, sig, connID string, keys ...string) (*http.Request, error) { u, err := joinUrl(baseURL, WM_LOCK_ENDPOINT, allocationTx) if err != nil { @@ -598,7 +616,7 @@ func NewWriteMarkerLockRequest( return nil, err } - if err := setClientInfoWithSign(req, sig, allocationTx, baseURL, clients...); err != nil { + if err := setClientInfoWithSign(req, sig, allocationTx, baseURL, keys...); err != nil { return nil, err } @@ -608,7 +626,7 @@ func NewWriteMarkerLockRequest( } func NewWriteMarkerUnLockRequest( - baseURL, allocationID, allocationTx, sig, connID, requestTime string, clients ...string) (*http.Request, error) { + baseURL, allocationID, allocationTx, sig, connID, requestTime string, keys ...string) (*http.Request, error) { u, err := joinUrl(baseURL, WM_LOCK_ENDPOINT, allocationTx, connID) if err != nil { @@ -620,7 +638,7 @@ func NewWriteMarkerUnLockRequest( return nil, err } - if err := setClientInfoWithSign(req, sig, allocationTx, baseURL, clients...); err != nil { + if err := setClientInfoWithSign(req, sig, allocationTx, baseURL, keys...); err != nil { return nil, err } @@ -629,7 +647,7 @@ func NewWriteMarkerUnLockRequest( return req, nil } -func NewFastUploadRequest(baseURL, allocationID string, allocationTx string, body []byte, method string, clients ...string) (*fasthttp.Request, error) { +func NewFastUploadRequest(baseURL, allocationID string, allocationTx string, body []byte, method string, keys ...string) (*fasthttp.Request, error) { u, err := joinUrl(baseURL, UPLOAD_ENDPOINT, allocationTx) if err != nil { return nil, err @@ -642,7 +660,7 @@ func NewFastUploadRequest(baseURL, allocationID string, allocationTx string, bod req.SetBodyRaw(body) // set header: X-App-Client-Signature - if err := setFastClientInfoWithSign(req, allocationTx, baseURL, clients...); err != nil { + if err := setFastClientInfoWithSign(req, allocationTx, baseURL, keys...); err != nil { return nil, err } @@ -650,20 +668,33 @@ func NewFastUploadRequest(baseURL, allocationID string, allocationTx string, bod return req, nil } -func setFastClientInfoWithSign(req *fasthttp.Request, allocation, baseURL string, clients ...string) error { - req.Header.Set("X-App-Client-ID", client.Id()) - req.Header.Set("X-App-Client-Key", client.PublicKey()) +func setFastClientInfoWithSign(req *fasthttp.Request, allocation, baseURL string, keys ...string) error { + var key string + if len(keys) > 0 && keys[0] != "" { + key = keys[0] + wallet := client.GetWalletByKey(key) + if wallet == nil { + return errors.New("multi-wallet-settings err: ", "wallet not found : " + key) + } + req.Header.Set("X-App-Client-ID", wallet.ClientID) + req.Header.Set("X-App-Client-Key", wallet.ClientKey) + } else { + key = client.Id() + req.Header.Set("X-App-Client-ID", client.Id()) + req.Header.Set("X-App-Client-Key", client.PublicKey()) + } + hashData := allocation + baseURL - clientID := client.Id() - sig2, ok := SignCache.Get(hashData + ":" + clientID) + // clientID := client.Id() + sig2, ok := SignCache.Get(hashData + ":" + key) if !ok { var err error - sig2, err = client.Sign(encryption.Hash(hashData), clientID) + sig2, err = client.Sign(encryption.Hash(hashData), key) if err != nil { return err } - SignCache.Add(hashData+":"+clientID, sig2) + SignCache.Add(hashData+":"+key, sig2) } req.Header.Set(CLIENT_SIGNATURE_HEADER_V2, sig2) return nil @@ -694,7 +725,33 @@ func NewUploadRequest(baseUrl, allocationID, allocationTx, sig string, body io.R return req, nil } -func NewConnectionRequest(baseUrl, allocationID, allocationTx, sig string, body io.Reader, clients ...string) (*http.Request, error) { +// NewConnectionRequestByWallet creates a new connection request using the given wallet. +// func NewConnectionRequestByWallet(baseUrl, allocationID, allocationTx, sig string, body io.Reader, wallet *zcncrypto.Wallet) (*http.Request, error) { +// l.Logger.Info(fmt.Sprintf("NewConnectionRequestByWallet: baseUrl: %s, allocationID: %s, allocationTx: %s, sig: %s", baseUrl, allocationID, allocationTx, sig)) +// u, err := joinUrl(baseUrl, CREATE_CONNECTION_ENDPOINT, allocationTx) +// if err != nil { +// return nil, err +// } +// req, err := http.NewRequest(http.MethodPost, u.String(), body) +// if err != nil { +// return nil, err +// } +// req.Header.Set("X-App-Client-ID", wallet.ClientID) +// req.Header.Set("X-App-Client-Key", wallet.ClientKey) +// req.Header.Set(CLIENT_SIGNATURE_HEADER, sig) +// hashData := allocationTx + baseUrl +// sig2, err := wallet.Sign(encryption.Hash(hashData), constants.BLS0CHAIN.String()) +// if err != nil { +// return nil, err +// } +// req.Header.Set(CLIENT_SIGNATURE_HEADER, sig) +// req.Header.Set(CLIENT_SIGNATURE_HEADER_V2, sig2) +// req.Header.Set(ALLOCATION_ID_HEADER, allocationID) +// return req, nil +// } + +func NewConnectionRequest(baseUrl, allocationID, allocationTx, sig string, body io.Reader, keys ...string) (*http.Request, error) { + l.Logger.Info(fmt.Sprintf("NewConnectionRequest: baseUrl: %s, allocationID: %s, allocationTx: %s, sig: %s", baseUrl, allocationID, allocationTx, sig)) u, err := joinUrl(baseUrl, CREATE_CONNECTION_ENDPOINT, allocationTx) if err != nil { return nil, err @@ -704,7 +761,7 @@ func NewConnectionRequest(baseUrl, allocationID, allocationTx, sig string, body return nil, err } - if err := setClientInfoWithSign(req, sig, allocationTx, baseUrl, clients...); err != nil { + if err := setClientInfoWithSign(req, sig, allocationTx, baseUrl, keys...); err != nil { return nil, err } @@ -800,7 +857,7 @@ func NewDownloadRequest(baseUrl, allocationID, allocationTx string, clients ...s return req, nil } -func NewFastDownloadRequest(baseUrl, allocationID, allocationTx string, clients ...string) (*fasthttp.Request, error) { +func NewFastDownloadRequest(baseUrl, allocationID, allocationTx string, keys ...string) (*fasthttp.Request, error) { u, err := joinUrl(baseUrl, DOWNLOAD_ENDPOINT, allocationTx) if err != nil { return nil, err @@ -808,15 +865,24 @@ func NewFastDownloadRequest(baseUrl, allocationID, allocationTx string, clients req := fasthttp.AcquireRequest() req.SetRequestURI(u.String()) - req.Header.Set("X-App-Client-ID", client.Id()) - req.Header.Set("X-App-Client-Key", client.PublicKey()) + if len(keys) > 0 && keys[0] != "" { + wallet := client.GetWalletByKey(keys[0]) + if wallet == nil { + return nil, errors.New("multi-wallet-settings err: ", "wallet not found : " + keys[0]) + } + req.Header.Set("X-App-Client-ID", wallet.ClientID) + req.Header.Set("X-App-Client-Key", wallet.ClientKey) + } else { + req.Header.Set("X-App-Client-ID", client.Id()) + req.Header.Set("X-App-Client-Key", client.PublicKey()) + } req.Header.Set(ALLOCATION_ID_HEADER, allocationID) return req, nil } -func NewRedeemRequest(baseUrl, allocationID, allocationTx string, clients ...string) (*http.Request, error) { +func NewRedeemRequest(baseUrl, allocationID, allocationTx string, keys ...string) (*http.Request, error) { u, err := joinUrl(baseUrl, REDEEM_ENDPOINT, allocationTx) if err != nil { return nil, err @@ -826,12 +892,12 @@ func NewRedeemRequest(baseUrl, allocationID, allocationTx string, clients ...str if err != nil { return nil, err } - setClientInfo(req) + setClientInfo(req, keys...) req.Header.Set(ALLOCATION_ID_HEADER, allocationID) return req, nil } -func NewDeleteRequest(baseUrl, allocationID, allocationTx, sig string, query *url.Values, clients ...string) (*http.Request, error) { +func NewDeleteRequest(baseUrl, allocationID, allocationTx, sig string, query *url.Values, keys ...string) (*http.Request, error) { u, err := joinUrl(baseUrl, UPLOAD_ENDPOINT, allocationTx) if err != nil { return nil, err @@ -843,7 +909,7 @@ func NewDeleteRequest(baseUrl, allocationID, allocationTx, sig string, query *ur return nil, err } - if err := setClientInfoWithSign(req, sig, allocationTx, baseUrl, clients...); err != nil { + if err := setClientInfoWithSign(req, sig, allocationTx, baseUrl, keys...); err != nil { return nil, err } @@ -852,7 +918,7 @@ func NewDeleteRequest(baseUrl, allocationID, allocationTx, sig string, query *ur return req, nil } -func NewCreateDirRequest(baseUrl, allocationID, allocationTx, sig string, body io.Reader, clients ...string) (*http.Request, error) { +func NewCreateDirRequest(baseUrl, allocationID, allocationTx, sig string, body io.Reader, keys ...string) (*http.Request, error) { u, err := joinUrl(baseUrl, DIR_ENDPOINT, allocationTx) if err != nil { return nil, err @@ -863,7 +929,7 @@ func NewCreateDirRequest(baseUrl, allocationID, allocationTx, sig string, body i return nil, err } - if err := setClientInfoWithSign(req, sig, allocationTx, baseUrl, clients...); err != nil { + if err := setClientInfoWithSign(req, sig, allocationTx, baseUrl, keys...); err != nil { return nil, err } @@ -912,7 +978,7 @@ func NewRevokeShareRequest(baseUrl, allocationID, allocationTx, sig string, quer return req, nil } -func NewWritemarkerRequest(baseUrl, allocationID, allocationTx, sig string, clients ...string) (*http.Request, error) { +func NewWritemarkerRequest(baseUrl, allocationID, allocationTx, sig string, keys ...string) (*http.Request, error) { nurl, err := joinUrl(baseUrl, LATEST_WRITE_MARKER_ENDPOINT, allocationTx) if err != nil { @@ -924,7 +990,7 @@ func NewWritemarkerRequest(baseUrl, allocationID, allocationTx, sig string, clie return nil, err } - if err := setClientInfoWithSign(req, sig, allocationTx, baseUrl, clients...); err != nil { + if err := setClientInfoWithSign(req, sig, allocationTx, baseUrl, keys...); err != nil { return nil, err } @@ -933,7 +999,7 @@ func NewWritemarkerRequest(baseUrl, allocationID, allocationTx, sig string, clie return req, nil } -func NewRollbackRequest(baseUrl, allocationID string, allocationTx string, body io.Reader, clients ...string) (*http.Request, error) { +func NewRollbackRequest(baseUrl, allocationID string, allocationTx string, body io.Reader, keys ...string) (*http.Request, error) { u, err := joinUrl(baseUrl, ROLLBACK_ENDPOINT, allocationTx) if err != nil { return nil, err @@ -943,7 +1009,10 @@ func NewRollbackRequest(baseUrl, allocationID string, allocationTx string, body if err != nil { return nil, err } - setClientInfo(req) + err = setClientInfo(req, keys...) + if err != nil { + return nil, err + } req.Header.Set(ALLOCATION_ID_HEADER, allocationID) diff --git a/zcnbridge/authorizers_query.go b/zcnbridge/authorizers_query.go index 067e146fd..7cb2d0b4f 100644 --- a/zcnbridge/authorizers_query.go +++ b/zcnbridge/authorizers_query.go @@ -119,7 +119,7 @@ func (b *BridgeClient) QueryEthereumMintPayload(zchainBurnHash string) (*ethereu } // QueryEthereumBurnEvents gets ethereum burn events -func (b *BridgeClient) QueryEthereumBurnEvents(startNonce string) ([]*ethereum.BurnEvent, error) { +func (b *BridgeClient) QueryEthereumBurnEvents(startNonce string, keys ...string) ([]*ethereum.BurnEvent, error) { client = h.CleanClient() authorizers, err := getAuthorizers(true) @@ -130,7 +130,7 @@ func (b *BridgeClient) QueryEthereumBurnEvents(startNonce string) ([]*ethereum.B var ( totalWorkers = len(authorizers) values = map[string]string{ - "clientid": coreClient.Id(), + "clientid": coreClient.Id(keys...), "ethereumaddress": b.EthereumAddress, "startnonce": startNonce, } @@ -176,7 +176,7 @@ func (b *BridgeClient) QueryEthereumBurnEvents(startNonce string) ([]*ethereum.B // QueryZChainMintPayload gets burn ticket and creates mint payload to be minted in the ZChain // ethBurnHash - Ethereum burn transaction hash -func (b *BridgeClient) QueryZChainMintPayload(ethBurnHash string) (*zcnsc.MintPayload, error) { +func (b *BridgeClient) QueryZChainMintPayload(ethBurnHash string, keys ...string) (*zcnsc.MintPayload, error) { const maxRetries = 3 var lastErr error @@ -191,7 +191,7 @@ func (b *BridgeClient) QueryZChainMintPayload(ethBurnHash string) (*zcnsc.MintPa time.Sleep(delay) } - payload, err := b.queryZChainMintPayloadOnce(ethBurnHash) + payload, err := b.queryZChainMintPayloadOnce(ethBurnHash, keys...) if err == nil { return payload, nil } @@ -203,7 +203,7 @@ func (b *BridgeClient) QueryZChainMintPayload(ethBurnHash string) (*zcnsc.MintPa } // queryZChainMintPayloadOnce performs a single attempt to query mint payload -func (b *BridgeClient) queryZChainMintPayloadOnce(ethBurnHash string) (*zcnsc.MintPayload, error) { +func (b *BridgeClient) queryZChainMintPayloadOnce(ethBurnHash string, keys ...string) (*zcnsc.MintPayload, error) { client = h.CleanClient() authorizers, err := getAuthorizers(true) log.Logger.Info("Got authorizers", zap.Int("amount", len(authorizers))) @@ -216,7 +216,7 @@ func (b *BridgeClient) queryZChainMintPayloadOnce(ethBurnHash string) (*zcnsc.Mi totalWorkers = len(authorizers) values = map[string]string{ "hash": ethBurnHash, - "clientid": coreClient.Id(), + "clientid": coreClient.Id(keys...), } ) diff --git a/zcnbridge/bridge.go b/zcnbridge/bridge.go index f222a4fbd..164452a63 100644 --- a/zcnbridge/bridge.go +++ b/zcnbridge/bridge.go @@ -622,7 +622,7 @@ func (b *BridgeClient) BurnWZCN(ctx context.Context, amountTokens uint64) (*type // MintZCN mints ZCN tokens after receiving proof-of-burn of WZCN tokens // - ctx go context instance to run the transaction // - payload received from authorizers -func (b *BridgeClient) MintZCN(payload *zcnsc.MintPayload) (string, error) { +func (b *BridgeClient) MintZCN(payload *zcnsc.MintPayload, keys ...string) (string, error) { Logger.Info( "Starting MINT smart contract", zap.String("sc address", wallet.ZCNSCSmartContractAddress), @@ -632,7 +632,7 @@ func (b *BridgeClient) MintZCN(payload *zcnsc.MintPayload) (string, error) { hash, _, _, _, err := coreTransaction.SmartContractTxn(wallet.ZCNSCSmartContractAddress, coreTransaction.SmartContractTxnData{ Name: wallet.MintFunc, InputArgs: payload, - }, true) + }, true, keys...) if err != nil { return "", errors.Wrap(err, fmt.Sprintf("failed to execute smart contract, hash = %s", hash)) @@ -650,7 +650,7 @@ func (b *BridgeClient) MintZCN(payload *zcnsc.MintPayload) (string, error) { // - ctx go context instance to run the transaction // - amount amount of tokens to burn // - txnfee transaction fee -func (b *BridgeClient) BurnZCN(amount uint64) (string, string, error) { +func (b *BridgeClient) BurnZCN(amount uint64, keys ...string) (string, string, error) { payload := zcnsc.BurnPayload{ EthereumAddress: b.EthereumAddress, } @@ -664,7 +664,7 @@ func (b *BridgeClient) BurnZCN(amount uint64) (string, string, error) { hash, out, _, _, err := coreTransaction.SmartContractTxnValue(wallet.ZCNSCSmartContractAddress, coreTransaction.SmartContractTxnData{ Name: wallet.BurnFunc, InputArgs: payload, - }, amount, true) + }, amount, true, keys...) if err != nil { Logger.Error("Burn ZCN transaction FAILED", zap.Error(err)) return hash, out, errors.Wrap(err, fmt.Sprintf("failed to execute smart contract, hash = %s", hash)) @@ -1057,7 +1057,7 @@ func (b *BridgeClient) estimateAlchemyGasAmount(ctx context.Context, to, data st // - from source address // - to target address // - amountTokens amount of tokens to burn -func (b *BridgeClient) EstimateBurnWZCNGasAmount(ctx context.Context, from, to, amountTokens string) (float64, error) { +func (b *BridgeClient) EstimateBurnWZCNGasAmount(ctx context.Context, from, to, amountTokens string, keys ...string) (float64, error) { switch b.getProviderType() { case AlchemyProvider: abi, err := bridge.BridgeMetaData.GetAbi() @@ -1065,7 +1065,7 @@ func (b *BridgeClient) EstimateBurnWZCNGasAmount(ctx context.Context, from, to, return 0, errors.Wrap(err, "failed to get ABI") } - clientID := DefaultClientIDEncoder(coreClient.Id()) + clientID := DefaultClientIDEncoder(coreClient.Id(keys...)) amount := new(big.Int) amount.SetString(amountTokens, 10) diff --git a/zcncore/execute_transactions.go b/zcncore/execute_transactions.go index 5a75ddde8..0b31e23e1 100644 --- a/zcncore/execute_transactions.go +++ b/zcncore/execute_transactions.go @@ -202,17 +202,17 @@ type SendTxnData struct { Note string `json:"note"` } -func Send(toClientID string, tokens uint64, desc string, client ...string) (hash, out string, nonce int64, txn *transaction.Transaction, err error) { - if len(client) == 0 { - client = append(client, "") - client = append(client, toClientID) +func Send(toClientID string, tokens uint64, desc string, keys ...string) (hash, out string, nonce int64, txn *transaction.Transaction, err error) { + if len(keys) == 0 { + keys = append(keys, "") + keys = append(keys, toClientID) } else { - client = append(client, toClientID) + keys = append(keys, toClientID) } return transaction.SmartContractTxnValue(MinerSmartContractAddress, transaction.SmartContractTxnData{ Name: "transfer", InputArgs: SendTxnData{Note: desc}, - }, tokens, true, client...) + }, tokens, true, keys...) } func Faucet(tokens uint64, input string, client ...string) (hash, out string, nonce int64, txn *transaction.Transaction, err error) {