diff --git a/integration/token/interop/support.go b/integration/token/interop/support.go index 9f54231398..0122aba3a3 100644 --- a/integration/token/interop/support.go +++ b/integration/token/interop/support.go @@ -8,6 +8,7 @@ package interop import ( "crypto" + "crypto/rand" "fmt" "slices" "strconv" @@ -22,6 +23,7 @@ import ( common2 "github.com/hyperledger-labs/fabric-token-sdk/integration/token/common" "github.com/hyperledger-labs/fabric-token-sdk/integration/token/fungible/views" views2 "github.com/hyperledger-labs/fabric-token-sdk/integration/token/interop/views" + hashescrowviews "github.com/hyperledger-labs/fabric-token-sdk/integration/token/interop/views/hashescrow" "github.com/hyperledger-labs/fabric-token-sdk/integration/token/interop/views/htlc" "github.com/hyperledger-labs/fabric-token-sdk/token" token2 "github.com/hyperledger-labs/fabric-token-sdk/token/token" @@ -314,6 +316,76 @@ func HTLCLock(network *integration.Infrastructure, tmsID token.TMSID, id *token3 } } +func HashEscrowLock(network *integration.Infrastructure, tmsID token.TMSID, id *token3.NodeReference, wallet string, typ token2.Type, amount uint64, receiver *token3.NodeReference, auditor *token3.NodeReference, recipientHash []byte, senderHash []byte, hashFunc crypto.Hash, errorMsgs ...string) (string, []byte, []byte, []byte, []byte) { + if hashFunc == 0 { + hashFunc = crypto.SHA256 + } + + var recipientPreImage []byte + var senderPreImage []byte + if len(recipientHash) == 0 { + recipientPreImage = make([]byte, 24) + _, err := rand.Read(recipientPreImage) + gomega.Expect(err).NotTo(gomega.HaveOccurred()) + h := hashFunc.New() + _, err = h.Write(recipientPreImage) + gomega.Expect(err).NotTo(gomega.HaveOccurred()) + recipientHash = h.Sum(nil) + } + if len(senderHash) == 0 { + senderPreImage = make([]byte, 24) + _, err := rand.Read(senderPreImage) + gomega.Expect(err).NotTo(gomega.HaveOccurred()) + h := hashFunc.New() + _, err = h.Write(senderPreImage) + gomega.Expect(err).NotTo(gomega.HaveOccurred()) + senderHash = h.Sum(nil) + } + + result, err := network.Client(id.ReplicaName()).CallView("hashescrow.lock", common.JSONMarshall(&hashescrowviews.Lock{ + TMSID: tmsID, + Wallet: wallet, + Type: typ, + Amount: amount, + Recipient: network.Identity(receiver.Id()), + RecipientHash: recipientHash, + SenderHash: senderHash, + HashFunc: hashFunc, + })) + if len(errorMsgs) == 0 { + gomega.Expect(err).NotTo(gomega.HaveOccurred()) + lockResult := &hashescrowviews.LockInfo{} + common.JSONUnmarshal(result.([]byte), lockResult) + + common2.CheckFinality(network, receiver, lockResult.TxID, &tmsID, false) + common2.CheckFinality(network, auditor, lockResult.TxID, &tmsID, false) + + gomega.Expect(lockResult.RecipientHash).NotTo(gomega.BeNil()) + gomega.Expect(lockResult.SenderHash).NotTo(gomega.BeNil()) + gomega.Expect(lockResult.RecipientHash).To(gomega.BeEquivalentTo(recipientHash)) + gomega.Expect(lockResult.SenderHash).To(gomega.BeEquivalentTo(senderHash)) + + return lockResult.TxID, recipientPreImage, senderPreImage, lockResult.RecipientHash, lockResult.SenderHash + } + + gomega.Expect(err).To(gomega.HaveOccurred()) + for _, msg := range errorMsgs { + gomega.Expect(err.Error()).To(gomega.ContainSubstring(msg)) + } + time.Sleep(5 * time.Second) + + errMsg := err.Error() + fmt.Printf("Got error message [%s]\n", errMsg) + txID := "" + index := strings.Index(err.Error(), "<<<[") + if index != -1 { + txID = errMsg[index+4 : index+strings.Index(err.Error()[index:], "]>>>")] + } + fmt.Printf("Got error message, extracted tx id [%s]\n", txID) + + return txID, nil, nil, nil, nil +} + func HTLCReclaimAll(network *integration.Infrastructure, id *token3.NodeReference, wallet string, errorMsgs ...string) { txID, err := network.Client(id.ReplicaName()).CallView("htlc.reclaimAll", common.JSONMarshall(&htlc.ReclaimAll{ Wallet: wallet, @@ -398,6 +470,39 @@ func htlcClaim(network *integration.Infrastructure, tmsID token.TMSID, id *token } } +func hashEscrowClaim(network *integration.Infrastructure, tmsID token.TMSID, id *token3.NodeReference, wallet string, preImage []byte, auditor *token3.NodeReference, errorMsgs ...string) string { + txIDBoxed, err := network.Client(id.ReplicaName()).CallView("hashescrow.claim", common.JSONMarshall(&hashescrowviews.Claim{ + TMSID: tmsID, + Wallet: wallet, + PreImage: preImage, + })) + if len(errorMsgs) == 0 { + gomega.Expect(err).NotTo(gomega.HaveOccurred()) + txID := common.JSONUnmarshalString(txIDBoxed) + common2.CheckFinality(network, id, txID, &tmsID, false) + common2.CheckFinality(network, auditor, txID, &tmsID, false) + + return txID + } + + gomega.Expect(err).To(gomega.HaveOccurred()) + for _, msg := range errorMsgs { + gomega.Expect(err.Error()).To(gomega.ContainSubstring(msg)) + } + time.Sleep(5 * time.Second) + + errMsg := err.Error() + fmt.Printf("Got error message [%s]\n", errMsg) + txID := "" + index := strings.Index(err.Error(), "<<<[") + if index != -1 { + txID = errMsg[index+4 : index+strings.Index(err.Error()[index:], "]>>>")] + } + fmt.Printf("Got error message, extracted tx id [%s]\n", txID) + + return txID +} + func fastExchange(network *integration.Infrastructure, id *token3.NodeReference, recipient *token3.NodeReference, tmsID1 token.TMSID, typ1 token2.Type, amount1 uint64, tmsID2 token.TMSID, typ2 token2.Type, amount2 uint64, deadline time.Duration) { _, err := network.Client(id.ReplicaName()).CallView("htlc.fastExchange", common.JSONMarshall(&htlc.FastExchange{ Recipient: network.Identity(recipient.Id()), diff --git a/integration/token/interop/tests.go b/integration/token/interop/tests.go index 366383fa2d..5bfc94c5e7 100644 --- a/integration/token/interop/tests.go +++ b/integration/token/interop/tests.go @@ -25,6 +25,7 @@ import ( const ( EUR = token3.Type("EUR") USD = token3.Type("USD") + GBP = token3.Type("GBP") ) func TestHTLCSingleNetwork(network *integration.Infrastructure, sel *token2.ReplicaSelector) { @@ -143,6 +144,42 @@ func TestHTLCSingleNetwork(network *integration.Infrastructure, sel *token2.Repl IDs := ListVaultUnspentTokens(network, defaultTMSID, name) CheckIfExistsInVault(network, defaultTMSID, auditor, IDs) } + + TestHashEscrowSingleNetwork(network, sel) +} + +func TestHashEscrowSingleNetwork(network *integration.Infrastructure, sel *token2.ReplicaSelector) { + alice := sel.Get("alice") + bob := sel.Get("bob") + auditor := sel.Get("auditor") + + defaultTMSID := token.TMSID{} + + IssueCash(network, "", GBP, 20, alice, auditor) + CheckBalance(network, alice, "", GBP, 20) + CheckBalance(network, bob, "", GBP, 0) + + _, recipientPreImage, _, _, _ := HashEscrowLock(network, defaultTMSID, alice, "", GBP, 10, bob, auditor, nil, nil, crypto.SHA3_256) + CheckBalance(network, alice, "", GBP, 10) + CheckBalance(network, bob, "", GBP, 0) + + hashEscrowClaim(network, defaultTMSID, bob, "", recipientPreImage, auditor) + CheckBalance(network, alice, "", GBP, 10) + CheckBalance(network, bob, "", GBP, 10) + + _, _, senderPreImage, _, _ := HashEscrowLock(network, defaultTMSID, alice, "", GBP, 7, bob, auditor, nil, nil, crypto.SHA3_256) + CheckBalance(network, alice, "", GBP, 3) + CheckBalance(network, bob, "", GBP, 10) + + hashEscrowClaim(network, defaultTMSID, alice, "", senderPreImage, auditor) + gomega.Eventually(CheckBalanceReturnError).WithArguments(network, alice, "", GBP, uint64(10)).WithTimeout(2 * time.Minute).WithPolling(5 * time.Second).Should(gomega.Succeed()) + CheckBalance(network, bob, "", GBP, 10) + + <-time.After(30 * time.Second) + CheckOwnerStore(network, defaultTMSID, nil, alice, bob) + CheckAuditorStore(network, defaultTMSID, auditor, "", nil) + + hashEscrowClaim(network, defaultTMSID, bob, "", recipientPreImage, auditor, "expected only one hash escrow script to match") } func TestHTLCTwoNetworks(network *integration.Infrastructure, sel *token2.ReplicaSelector) { diff --git a/integration/token/interop/topology.go b/integration/token/interop/topology.go index 53391686ef..bdfadcf53a 100644 --- a/integration/token/interop/topology.go +++ b/integration/token/interop/topology.go @@ -19,6 +19,7 @@ import ( views3 "github.com/hyperledger-labs/fabric-token-sdk/integration/token/common/views" "github.com/hyperledger-labs/fabric-token-sdk/integration/token/fungible/views" views2 "github.com/hyperledger-labs/fabric-token-sdk/integration/token/interop/views" + "github.com/hyperledger-labs/fabric-token-sdk/integration/token/interop/views/hashescrow" "github.com/hyperledger-labs/fabric-token-sdk/integration/token/interop/views/htlc" ) @@ -245,6 +246,11 @@ func addAlice(fscTopology *fsc.Topology) *node.Node { RegisterViewFactory("htlc.lock", &htlc.LockViewFactory{}). RegisterViewFactory("htlc.reclaimAll", &htlc.ReclaimAllViewFactory{}). RegisterViewFactory("htlc.fastExchange", &htlc.FastExchangeInitiatorViewFactory{}). + RegisterResponder(&hashescrow.LockAcceptView{}, &hashescrow.LockView{}). + RegisterResponder(&hashescrow.ClaimAcceptView{}, &hashescrow.ClaimView{}). + RegisterResponder(&hashescrow.ClaimAcceptView{}, &hashescrow.ClaimDistributionView{}). + RegisterViewFactory("hashescrow.lock", &hashescrow.LockViewFactory{}). + RegisterViewFactory("hashescrow.claim", &hashescrow.ClaimViewFactory{}). RegisterViewFactory("TxFinality", &views3.TxFinalityViewFactory{}) } @@ -264,5 +270,10 @@ func addBob(fscTopology *fsc.Topology) *node.Node { RegisterResponder(&htlc.LockAcceptView{}, &htlc.LockView{}). RegisterResponder(&htlc.FastExchangeResponderView{}, &htlc.FastExchangeInitiatorView{}). RegisterViewFactory("htlc.claim", &htlc.ClaimViewFactory{}). + RegisterResponder(&hashescrow.LockAcceptView{}, &hashescrow.LockView{}). + RegisterResponder(&hashescrow.ClaimAcceptView{}, &hashescrow.ClaimView{}). + RegisterResponder(&hashescrow.ClaimAcceptView{}, &hashescrow.ClaimDistributionView{}). + RegisterViewFactory("hashescrow.lock", &hashescrow.LockViewFactory{}). + RegisterViewFactory("hashescrow.claim", &hashescrow.ClaimViewFactory{}). RegisterViewFactory("TxFinality", &views3.TxFinalityViewFactory{}) } diff --git a/integration/token/interop/views/auditor.go b/integration/token/interop/views/auditor.go index 6dde3c43ba..189d275eee 100644 --- a/integration/token/interop/views/auditor.go +++ b/integration/token/interop/views/auditor.go @@ -14,6 +14,7 @@ import ( "github.com/hyperledger-labs/fabric-smart-client/platform/common/utils/assert" "github.com/hyperledger-labs/fabric-smart-client/platform/view/view" "github.com/hyperledger-labs/fabric-token-sdk/token" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/interop/hashescrow" "github.com/hyperledger-labs/fabric-token-sdk/token/services/interop/htlc" "github.com/hyperledger-labs/fabric-token-sdk/token/services/ttx" ) @@ -68,26 +69,40 @@ func (a *AuditView) Call(context view.Context) (any, error) { for i := range inputs.Count() { input, err := htlc.ToInput(inputs.At(i)) assert.NoError(err, "cannot get htlc input wrapper") - if !input.IsHTLC() { - continue + if input.IsHTLC() { + // check script details, for example make sure the hash is set + script, err := input.Script() + assert.NoError(err, "cannot get htlc script from input") + assert.True(len(script.HashInfo.Hash) > 0, "hash is not set") + } + + hashEscrowInput, err := hashescrow.ToInput(inputs.At(i)) + assert.NoError(err, "cannot get hash escrow input wrapper") + if hashEscrowInput.IsHashEscrow() { + hashEscrowScript, err := hashEscrowInput.Script() + assert.NoError(err, "cannot get hash escrow script from input") + assert.NoError(hashEscrowScript.Validate(), "hash escrow script is not valid") } - // check script details, for example make sure the hash is set - script, err := input.Script() - assert.NoError(err, "cannot get htlc script from input") - assert.True(len(script.HashInfo.Hash) > 0, "hash is not set") } now := time.Now() for i := range outputs.Count() { output, err := htlc.ToOutput(outputs.At(i)) assert.NoError(err, "cannot get htlc output wrapper") - if !output.IsHTLC() { - continue + if output.IsHTLC() { + // check script details + script, err := output.Script() + assert.NoError(err, "cannot get htlc script from output") + assert.NoError(script.Validate(now), "script is not valid") + } + + hashEscrowOutput, err := hashescrow.ToOutput(outputs.At(i)) + assert.NoError(err, "cannot get hash escrow output wrapper") + if hashEscrowOutput.IsHashEscrow() { + hashEscrowScript, err := hashEscrowOutput.Script() + assert.NoError(err, "cannot get hash escrow script from output") + assert.NoError(hashEscrowScript.Validate(), "hash escrow script is not valid") } - // check script details - script, err := output.Script() - assert.NoError(err, "cannot get htlc script from output") - assert.NoError(script.Validate(now), "script is not valid") } return context.RunView(ttx.NewAuditApproveView(w, tx)) diff --git a/integration/token/interop/views/hashescrow/claim.go b/integration/token/interop/views/hashescrow/claim.go new file mode 100644 index 0000000000..7d74e401b9 --- /dev/null +++ b/integration/token/interop/views/hashescrow/claim.go @@ -0,0 +1,298 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package hashescrow + +import ( + "encoding/asn1" + "encoding/json" + "time" + + "github.com/hyperledger-labs/fabric-smart-client/pkg/utils/errors" + "github.com/hyperledger-labs/fabric-smart-client/platform/common/utils/assert" + "github.com/hyperledger-labs/fabric-smart-client/platform/view/services/endpoint" + "github.com/hyperledger-labs/fabric-smart-client/platform/view/services/id" + "github.com/hyperledger-labs/fabric-smart-client/platform/view/services/sig" + "github.com/hyperledger-labs/fabric-smart-client/platform/view/view" + "github.com/hyperledger-labs/fabric-token-sdk/token" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/identity" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/interop/hashescrow" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/logging" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/ttx" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/utils" + session2 "github.com/hyperledger-labs/fabric-token-sdk/token/services/utils/json/session" + token2 "github.com/hyperledger-labs/fabric-token-sdk/token/token" +) + +var logger = logging.MustGetLogger() + +type Claim struct { + TMSID token.TMSID + Wallet string + PreImage []byte +} + +type ClaimView struct { + *Claim +} + +func (r *ClaimView) Call(ctx view.Context) (res any, err error) { + var tx *hashescrow.Transaction + defer func() { + if e := recover(); e != nil { + txID := "none" + if tx != nil { + txID = tx.ID() + } + if err == nil { + err = errors.Errorf("<<<[%s]>>>: %s", txID, e) + } else { + err = errors.Errorf("<<<[%s]>>>: %s", txID, err) + } + } + }() + + claimWallet := hashescrow.GetWallet(ctx, r.Wallet, token.WithTMSID(r.TMSID)) + assert.NotNil(claimWallet, "wallet [%s] not found", r.Wallet) + + var matched *token2.UnspentTokens + runner := utils.NewRetryRunner(logger, 10, 2*time.Second, false) + err = runner.RunWithContext(ctx.Context(), func() error { + var err error + matched, err = hashescrow.Wallet(claimWallet).ListByPreImage(ctx.Context(), r.PreImage) + if err != nil { + return errors.Wrap(err, "failed looking up hash escrow script") + } + if matched.Count() != 1 { + return errors.Errorf("expected only one hash escrow script to match [%s], got [%d]", view.Identity(r.PreImage), matched.Count()) + } + + return nil + }) + assert.NoError(err, "failed looking up hash escrow script") + + idProvider, err := id.GetProvider(ctx) + assert.NoError(err, "failed getting id provider") + tx, err = hashescrow.NewAnonymousTransaction( + ctx, + ttx.WithAuditor(idProvider.Identity("auditor")), + ttx.WithTMSID(r.TMSID), + ) + assert.NoError(err, "failed to create a hash escrow transaction") + matchedToken := matched.At(0) + script, err := scriptFromToken(matchedToken) + assert.NoError(err, "failed reading hash escrow script for [%s]", matchedToken.Id) + claimOwner, _, _, err := script.ResolveOwnerAndHashForPreimage(r.PreImage) + assert.NoError(err, "failed resolving hash escrow claim owner for [%s]", matchedToken.Id) + + assert.NoError(tx.Claim(claimWallet, matchedToken, r.PreImage), "failed adding a hash escrow claim for [%s]", matchedToken.Id) + + _, err = ctx.RunView(ttx.NewCollectEndorsementsView(tx.Transaction)) + assert.NoError(err, "failed to collect endorsements on hash escrow transaction") + + assert.NoError(distributeClaimToCounterparty(ctx, tx, script, claimOwner), "failed distributing hash escrow claim transaction") + + _, err = ctx.RunView(ttx.NewOrderingAndFinalityView(tx.Transaction)) + assert.NoError(err, "failed to commit hash escrow transaction") + + return tx.ID(), nil +} + +func scriptFromToken(tok *token2.UnspentToken) (*hashescrow.Script, error) { + owner, err := identity.UnmarshalTypedIdentity(tok.Owner) + if err != nil { + return nil, errors.Wrap(err, "failed to unmarshal token owner") + } + if owner.Type != hashescrow.HashEscrow { + return nil, errors.Errorf("invalid owner type, expected hash escrow script") + } + + script := &hashescrow.Script{} + if err := json.Unmarshal(owner.Identity, script); err != nil { + return nil, errors.Wrap(err, "failed to unmarshal hash escrow script") + } + + return script, nil +} + +func distributeClaimToCounterparty(ctx view.Context, tx *hashescrow.Transaction, script *hashescrow.Script, claimOwner view.Identity) error { + counterparty := script.Sender + if claimOwner.Equal(script.Sender) { + counterparty = script.Recipient + } + if counterparty.IsNone() || counterparty.Equal(claimOwner) { + return nil + } + if _, err := tx.TokenService().WalletManager().OwnerWallet(ctx.Context(), counterparty); err == nil { + return nil + } + + txRaw, err := transactionBytesWithoutEnvelope(tx) + if err != nil { + return errors.Wrap(err, "failed marshalling claim transaction") + } + + _, err = ctx.RunView(&ClaimDistributionView{ + Counterparty: counterparty, + TxRaw: txRaw, + }) + if err != nil { + return err + } + + return nil +} + +func transactionBytesWithoutEnvelope(tx *hashescrow.Transaction) ([]byte, error) { + // The observer only needs token records; the submitter keeps the envelope for ordering. + payload := tx.Payload + envelope := payload.Envelope + payload.Envelope = nil + defer func() { + payload.Envelope = envelope + }() + + txRaw, err := tx.Bytes() + if err != nil { + return nil, err + } + + if err := assertNoEnvelope(txRaw); err != nil { + return nil, err + } + + return txRaw, nil +} + +type transactionSer struct { + Nonce []byte + Creator []byte + ID string + Network string + Channel string + Namespace string + Signer []byte + Transient []byte + TokenRequest []byte + Envelope []byte +} + +func assertNoEnvelope(txRaw []byte) error { + var ser transactionSer + if _, err := asn1.Unmarshal(txRaw, &ser); err != nil { + return errors.Wrap(err, "failed checking serialized claim transaction") + } + if len(ser.Envelope) != 0 { + return errors.Errorf("expected claim observer transaction without envelope, got envelope length [%d]", len(ser.Envelope)) + } + + return nil +} + +func stripEnvelope(txRaw []byte) ([]byte, error) { + var ser transactionSer + if _, err := asn1.Unmarshal(txRaw, &ser); err != nil { + return nil, errors.Wrap(err, "failed unmarshalling serialized claim transaction") + } + if len(ser.Envelope) == 0 { + return txRaw, nil + } + + ser.Envelope = nil + stripped, err := asn1.Marshal(ser) + if err != nil { + return nil, errors.Wrap(err, "failed marshalling claim transaction without envelope") + } + + return stripped, nil +} + +type ClaimDistributionView struct { + Counterparty view.Identity + TxRaw []byte +} + +func (v *ClaimDistributionView) Call(ctx view.Context) (any, error) { + session, err := ctx.GetSession(ctx.Initiator(), v.Counterparty) + if err != nil { + return nil, errors.Wrap(err, "failed getting counterparty session") + } + + if err := session.SendWithContext(ctx.Context(), v.TxRaw); err != nil { + return nil, errors.Wrap(err, "failed sending claim transaction") + } + + jsonSession := session2.NewFromSession(ctx, session) + ack, err := jsonSession.ReceiveRawWithTimeout(time.Minute) + if err != nil { + return nil, errors.Wrap(err, "failed receiving claim transaction acknowledgement") + } + + longTerm, _, _, err := endpoint.GetService(ctx).Resolve(ctx.Context(), v.Counterparty) + if err != nil { + return nil, errors.Wrap(err, "failed resolving counterparty long-term identity") + } + sigService, err := sig.GetService(ctx) + if err != nil { + return nil, errors.Wrap(err, "failed getting signature service") + } + verifier, err := sigService.GetVerifier(longTerm) + if err != nil { + return nil, errors.Wrap(err, "failed getting counterparty verifier") + } + if err := verifier.Verify(v.TxRaw, ack); err != nil { + return nil, errors.Wrap(err, "failed verifying claim transaction acknowledgement") + } + + return nil, nil +} + +type ClaimAcceptView struct{} + +func (h *ClaimAcceptView) Call(context view.Context) (any, error) { + tx, err := receiveClaimTransaction(context) + assert.NoError(err, "failed to receive hash escrow claim transaction") + + _, err = context.RunView(ttx.NewAcceptView(tx)) + assert.NoError(err, "failed to accept hash escrow claim transaction") + + _, err = context.RunView(ttx.NewFinalityView(tx)) + assert.NoError(err, "hash escrow claim transaction was not committed") + + return tx.ID(), nil +} + +func receiveClaimTransaction(context view.Context) (*ttx.Transaction, error) { + txRaw, err := session2.JSON(context).ReceiveRawWithTimeout(4 * time.Minute) + if err != nil { + return nil, err + } + + txRaw, err = stripEnvelope(txRaw) + if err != nil { + return nil, err + } + + tx, err := ttx.NewTransactionFromBytes(context, txRaw) + if err != nil { + return nil, err + } + if err := tx.IsValid(context.Context()); err != nil { + return nil, errors.Wrapf(err, "invalid transaction %s", tx.ID()) + } + + return tx, nil +} + +type ClaimViewFactory struct{} + +func (p *ClaimViewFactory) NewView(in []byte) (view.View, error) { + f := &ClaimView{Claim: &Claim{}} + err := json.Unmarshal(in, f.Claim) + assert.NoError(err, "failed unmarshalling input") + + return f, nil +} diff --git a/integration/token/interop/views/hashescrow/lock.go b/integration/token/interop/views/hashescrow/lock.go new file mode 100644 index 0000000000..7adec13ea7 --- /dev/null +++ b/integration/token/interop/views/hashescrow/lock.go @@ -0,0 +1,131 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package hashescrow + +import ( + "crypto" + "encoding/json" + + "github.com/hyperledger-labs/fabric-smart-client/pkg/utils/errors" + "github.com/hyperledger-labs/fabric-smart-client/platform/common/utils/assert" + "github.com/hyperledger-labs/fabric-smart-client/platform/view/services/id" + "github.com/hyperledger-labs/fabric-smart-client/platform/view/view" + "github.com/hyperledger-labs/fabric-token-sdk/token" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/interop/hashescrow" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/ttx" + token2 "github.com/hyperledger-labs/fabric-token-sdk/token/token" +) + +type Lock struct { + TMSID token.TMSID + Wallet string + Type token2.Type + Amount uint64 + Recipient view.Identity + RecipientHash []byte + SenderHash []byte + HashFunc crypto.Hash +} + +type LockInfo struct { + TxID string + RecipientHash []byte + SenderHash []byte +} + +type LockView struct { + *Lock +} + +func (hv *LockView) Call(context view.Context) (res any, err error) { + var tx *hashescrow.Transaction + defer func() { + if e := recover(); e != nil { + txID := "none" + if tx != nil { + txID = tx.ID() + } + if err == nil { + err = errors.Errorf("<<<[%s]>>>: %s", txID, e) + } else { + err = errors.Errorf("<<<[%s]>>>: %s", txID, err) + } + } + }() + + me, recipient, err := ttx.ExchangeRecipientIdentities(context, "", hv.Recipient, token.WithTMSID(hv.TMSID)) + assert.NoError(err, "failed getting recipient identity") + + idProvider, err := id.GetProvider(context) + assert.NoError(err, "failed getting id provider") + tx, err = hashescrow.NewAnonymousTransaction( + context, + ttx.WithAuditor(idProvider.Identity("auditor")), + ttx.WithTMSID(hv.TMSID), + ) + assert.NoError(err, "failed creating a hash escrow transaction") + + senderWallet := hashescrow.GetWallet(context, hv.Wallet, token.WithTMSID(hv.TMSID)) + assert.NotNil(senderWallet, "sender wallet [%s] not found", hv.Wallet) + + _, err = tx.Lock( + context.Context(), + senderWallet, + me, + hv.Type, + hv.Amount, + recipient, + hashescrow.WithRecipientHash(hv.RecipientHash), + hashescrow.WithSenderHash(hv.SenderHash), + hashescrow.WithHashFunc(hv.HashFunc), + ) + assert.NoError(err, "failed adding a hash escrow lock operation") + + _, err = context.RunView(ttx.NewCollectEndorsementsView(tx.Transaction)) + assert.NoError(err, "failed to collect endorsements for hash escrow transaction") + + _, err = context.RunView(ttx.NewOrderingAndFinalityView(tx.Transaction)) + assert.NoError(err, "failed to commit hash escrow transaction") + + return &LockInfo{ + TxID: tx.ID(), + RecipientHash: hv.RecipientHash, + SenderHash: hv.SenderHash, + }, nil +} + +type LockViewFactory struct{} + +func (p *LockViewFactory) NewView(in []byte) (view.View, error) { + f := &LockView{Lock: &Lock{}} + err := json.Unmarshal(in, f.Lock) + assert.NoError(err, "failed unmarshalling input") + + return f, nil +} + +type LockAcceptView struct{} + +func (h *LockAcceptView) Call(context view.Context) (any, error) { + _, _, err := ttx.RespondExchangeRecipientIdentities(context) + assert.NoError(err, "failed to respond to identity request") + + tx, err := ttx.ReceiveTransaction(context) + assert.NoError(err, "failed to receive tokens") + + outputs, err := tx.Outputs() + assert.NoError(err, "failed getting outputs") + assert.True(outputs.Count() >= 1, "expected at least one output, got [%d]", outputs.Count()) + + _, err = context.RunView(ttx.NewAcceptView(tx)) + assert.NoError(err, "failed to accept new tokens") + + _, err = context.RunView(ttx.NewFinalityView(tx)) + assert.NoError(err, "new tokens were not committed") + + return tx, nil +} diff --git a/token/core/fabtoken/v1/driver/deserializer.go b/token/core/fabtoken/v1/driver/deserializer.go index ff8e4b5b1d..04e1c7c5a4 100644 --- a/token/core/fabtoken/v1/driver/deserializer.go +++ b/token/core/fabtoken/v1/driver/deserializer.go @@ -13,9 +13,11 @@ import ( "github.com/hyperledger-labs/fabric-token-sdk/token/driver" "github.com/hyperledger-labs/fabric-token-sdk/token/services/identity/boolpolicy" "github.com/hyperledger-labs/fabric-token-sdk/token/services/identity/deserializer" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/identity/interop/hashescrow" "github.com/hyperledger-labs/fabric-token-sdk/token/services/identity/interop/htlc" "github.com/hyperledger-labs/fabric-token-sdk/token/services/identity/multisig" "github.com/hyperledger-labs/fabric-token-sdk/token/services/identity/x509" + hashescrow2 "github.com/hyperledger-labs/fabric-token-sdk/token/services/interop/hashescrow" htlc2 "github.com/hyperledger-labs/fabric-token-sdk/token/services/interop/htlc" ) @@ -29,6 +31,7 @@ func NewDeserializer() *Deserializer { des := deserializer.NewTypedVerifierDeserializerMultiplex() des.AddTypedVerifierDeserializer(x509.IdentityType, deserializer.NewTypedIdentityVerifierDeserializer(&x509.IdentityDeserializer{}, &x509.AuditMatcherDeserializer{})) des.AddTypedVerifierDeserializer(htlc2.ScriptType, htlc.NewTypedIdentityDeserializer(des)) + des.AddTypedVerifierDeserializer(hashescrow2.ScriptType, hashescrow.NewTypedIdentityDeserializer(des)) des.AddTypedVerifierDeserializer(multisig.Multisig, multisig.NewTypedIdentityDeserializer(des, des)) des.AddTypedVerifierDeserializer(boolpolicy.Policy, boolpolicy.NewTypedIdentityDeserializer(des, des)) @@ -51,6 +54,7 @@ func NewEIDRHDeserializer() *EIDRHDeserializer { d := deserializer.NewEIDRHDeserializer() d.AddDeserializer(x509.IdentityType, &x509.AuditInfoDeserializer{}) d.AddDeserializer(htlc2.ScriptType, htlc.NewAuditDeserializer(d)) + d.AddDeserializer(hashescrow2.ScriptType, hashescrow.NewAuditDeserializer(d)) d.AddDeserializer(multisig.Multisig, &multisig.AuditInfoDeserializer{}) d.AddDeserializer(boolpolicy.Policy, &boolpolicy.AuditInfoDeserializer{}) diff --git a/token/core/fabtoken/v1/driver/driver.go b/token/core/fabtoken/v1/driver/driver.go index 6e7bceaeea..25276304e9 100644 --- a/token/core/fabtoken/v1/driver/driver.go +++ b/token/core/fabtoken/v1/driver/driver.go @@ -16,6 +16,7 @@ import ( v1setup "github.com/hyperledger-labs/fabric-token-sdk/token/core/fabtoken/v1/setup" "github.com/hyperledger-labs/fabric-token-sdk/token/core/fabtoken/v1/validator" "github.com/hyperledger-labs/fabric-token-sdk/token/driver" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/interop/hashescrow" "github.com/hyperledger-labs/fabric-token-sdk/token/services/interop/htlc" "github.com/hyperledger-labs/fabric-token-sdk/token/services/logging" "github.com/hyperledger-labs/fabric-token-sdk/token/services/ttx/boolpolicy" @@ -146,6 +147,7 @@ func (d *Driver) NewTokenService(tmsID driver.TMSID, publicParams []byte) (drive authorization := common.NewAuthorizationMultiplexer( common.NewTMSAuthorization(logger, publicParamsManager.PublicParams(), ws), htlc.NewScriptAuth(ws), + hashescrow.NewScriptAuth(ws), multisig.NewEscrowAuth(ws), boolpolicy.NewEscrowAuth(ws), ) diff --git a/token/core/fabtoken/v1/validator/validator.go b/token/core/fabtoken/v1/validator/validator.go index 2f19d90ffc..fe373c1bdf 100644 --- a/token/core/fabtoken/v1/validator/validator.go +++ b/token/core/fabtoken/v1/validator/validator.go @@ -61,6 +61,7 @@ func NewValidator( TransferSignatureValidate, TransferBalanceValidate, TransferHTLCValidate, + TransferHashEscrowValidate, common.TransferApplicationDataValidate[*setup.PublicParams, *actions.Output, *actions.TransferAction, *actions.IssueAction, driver.Deserializer], } transferValidators = append(transferValidators, extraTransferValidators...) diff --git a/token/core/fabtoken/v1/validator/validator_transfer.go b/token/core/fabtoken/v1/validator/validator_transfer.go index a19aed3958..13fd6e932c 100644 --- a/token/core/fabtoken/v1/validator/validator_transfer.go +++ b/token/core/fabtoken/v1/validator/validator_transfer.go @@ -15,7 +15,9 @@ import ( "github.com/hyperledger-labs/fabric-token-sdk/token/core/fabtoken/v1/actions" "github.com/hyperledger-labs/fabric-token-sdk/token/driver" "github.com/hyperledger-labs/fabric-token-sdk/token/services/identity" + hashescrow2 "github.com/hyperledger-labs/fabric-token-sdk/token/services/identity/interop/hashescrow" htlc2 "github.com/hyperledger-labs/fabric-token-sdk/token/services/identity/interop/htlc" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/interop/hashescrow" "github.com/hyperledger-labs/fabric-token-sdk/token/services/interop/htlc" "github.com/hyperledger-labs/fabric-token-sdk/token/token" ) @@ -227,3 +229,87 @@ func TransferHTLCValidate(c context.Context, ctx *Context) error { return nil } + +// TransferHashEscrowValidate validates the hash escrow scripts in a transfer action. +func TransferHashEscrowValidate(c context.Context, ctx *Context) error { + for i, in := range ctx.InputTokens { + owner, err := identity.UnmarshalTypedIdentity(in.GetOwner()) + if err != nil { + return errors.Wrap(err, "failed to unmarshal owner of input token") + } + if owner.Type != hashescrow.HashEscrow { + continue + } + if len(ctx.TransferAction.GetOutputs()) != 1 { + return errors.New("invalid transfer action: a hash escrow script only transfers the ownership of a token") + } + + output := ctx.TransferAction.GetOutputs()[0].(*actions.Output) + tok := output + if ctx.InputTokens[0].Type != tok.Type { + return errors.New("invalid transfer action: type of input does not match type of output") + } + if ctx.InputTokens[0].Quantity != tok.Quantity { + return errors.New("invalid transfer action: quantity of input does not match quantity of output") + } + if output.IsRedeem() { + return errors.New("invalid transfer action: the output corresponding to a hash escrow spending should not be a redeem") + } + + script, err := hashescrow2.VerifyOwner(ctx.InputTokens[0].GetOwner(), tok.Owner) + if err != nil { + return errors.Wrap(err, "failed to verify transfer from hash escrow script") + } + + sigma := ctx.Signatures[i] + claim, err := hashescrow2.ClaimFromSignature(sigma) + if err != nil { + return errors.WithMessagef(err, "failed to parse hash escrow claim signature") + } + resolvedOwner, _, claimedBy, err := hashescrow2.ResolveOwnerAndHash(script, claim.Preimage) + if err != nil { + return errors.WithMessagef(err, "failed to resolve hash escrow claim") + } + if !identity.Identity(resolvedOwner).Equal(tok.Owner) { + return errors.New("invalid transfer action: output owner does not match hash escrow recipient resolved from preimage") + } + metadataKey, err := hashescrow2.ClaimMetadataCheck(ctx.TransferAction, script, claim.Preimage, claimedBy) + if err != nil { + return errors.WithMessagef(err, "failed to check hash escrow claim metadata") + } + ctx.CountMetadataKey(metadataKey) + } + + for _, o := range ctx.TransferAction.GetOutputs() { + out, ok := o.(*actions.Output) + if !ok { + return errors.New("invalid output") + } + if out.IsRedeem() { + continue + } + + owner, err := identity.UnmarshalTypedIdentity(out.Owner) + if err != nil { + return err + } + if owner.Type != hashescrow.HashEscrow { + continue + } + script := &hashescrow.Script{} + err = json.Unmarshal(owner.Identity, script) + if err != nil { + return err + } + if err := script.Validate(); err != nil { + return errors.WithMessagef(err, "hash escrow script invalid") + } + metadataKey, err := hashescrow2.LockMetadataCheck(ctx.TransferAction, script) + if err != nil { + return errors.WithMessagef(err, "failed to check hash escrow lock metadata") + } + ctx.CountMetadataKey(metadataKey) + } + + return nil +} diff --git a/token/core/zkatdlog/nogh/v1/driver/deserializer.go b/token/core/zkatdlog/nogh/v1/driver/deserializer.go index 566e34a2ae..f3444eb169 100644 --- a/token/core/zkatdlog/nogh/v1/driver/deserializer.go +++ b/token/core/zkatdlog/nogh/v1/driver/deserializer.go @@ -16,9 +16,11 @@ import ( "github.com/hyperledger-labs/fabric-token-sdk/token/services/identity/deserializer" "github.com/hyperledger-labs/fabric-token-sdk/token/services/identity/idemix" "github.com/hyperledger-labs/fabric-token-sdk/token/services/identity/idemixnym" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/identity/interop/hashescrow" "github.com/hyperledger-labs/fabric-token-sdk/token/services/identity/interop/htlc" "github.com/hyperledger-labs/fabric-token-sdk/token/services/identity/multisig" "github.com/hyperledger-labs/fabric-token-sdk/token/services/identity/x509" + hashescrow2 "github.com/hyperledger-labs/fabric-token-sdk/token/services/interop/hashescrow" htlc2 "github.com/hyperledger-labs/fabric-token-sdk/token/services/interop/htlc" ) @@ -46,6 +48,7 @@ func NewDeserializer(pp *v1.PublicParams) (*Deserializer, error) { } des.AddTypedVerifierDeserializer(x509.IdentityType, deserializer.NewTypedIdentityVerifierDeserializer(&x509.IdentityDeserializer{}, &x509.AuditMatcherDeserializer{})) des.AddTypedVerifierDeserializer(htlc2.ScriptType, htlc.NewTypedIdentityDeserializer(des)) + des.AddTypedVerifierDeserializer(hashescrow2.ScriptType, hashescrow.NewTypedIdentityDeserializer(des)) des.AddTypedVerifierDeserializer(multisig.Multisig, multisig.NewTypedIdentityDeserializer(des, des)) des.AddTypedVerifierDeserializer(boolpolicy.Policy, boolpolicy.NewTypedIdentityDeserializer(des, des)) @@ -93,6 +96,7 @@ func NewEIDRHDeserializer() *EIDRHDeserializer { d.AddDeserializer(idemixnym.IdentityType, &idemixnym.AuditInfoDeserializer{}) d.AddDeserializer(x509.IdentityType, &x509.AuditInfoDeserializer{}) d.AddDeserializer(htlc2.ScriptType, htlc.NewAuditDeserializer(d)) + d.AddDeserializer(hashescrow2.ScriptType, hashescrow.NewAuditDeserializer(d)) d.AddDeserializer(multisig.Multisig, &multisig.AuditInfoDeserializer{}) d.AddDeserializer(boolpolicy.Policy, &boolpolicy.AuditInfoDeserializer{}) diff --git a/token/core/zkatdlog/nogh/v1/driver/driver.go b/token/core/zkatdlog/nogh/v1/driver/driver.go index a980f27b22..e669e91a19 100644 --- a/token/core/zkatdlog/nogh/v1/driver/driver.go +++ b/token/core/zkatdlog/nogh/v1/driver/driver.go @@ -18,6 +18,7 @@ import ( v1token "github.com/hyperledger-labs/fabric-token-sdk/token/core/zkatdlog/nogh/v1/token" "github.com/hyperledger-labs/fabric-token-sdk/token/core/zkatdlog/nogh/v1/validator" "github.com/hyperledger-labs/fabric-token-sdk/token/driver" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/interop/hashescrow" "github.com/hyperledger-labs/fabric-token-sdk/token/services/interop/htlc" "github.com/hyperledger-labs/fabric-token-sdk/token/services/logging" "github.com/hyperledger-labs/fabric-token-sdk/token/services/ttx/boolpolicy" @@ -150,6 +151,7 @@ func (d *Driver) NewTokenService(tmsID driver.TMSID, publicParams []byte) (drive authorization := common.NewAuthorizationMultiplexer( common.NewTMSAuthorization(logger, ppm.PublicParams(), ws), htlc.NewScriptAuth(ws), + hashescrow.NewScriptAuth(ws), multisig.NewEscrowAuth(ws), boolpolicy.NewEscrowAuth(ws), ) diff --git a/token/core/zkatdlog/nogh/v1/validator/validator.go b/token/core/zkatdlog/nogh/v1/validator/validator.go index f73e2f62fc..90ab050840 100644 --- a/token/core/zkatdlog/nogh/v1/validator/validator.go +++ b/token/core/zkatdlog/nogh/v1/validator/validator.go @@ -74,6 +74,7 @@ func New( TransferUpgradeWitnessValidate, TransferZKProofValidate, TransferHTLCValidate, + TransferHashEscrowValidate, common.TransferApplicationDataValidate[*v1.PublicParams, *token.Token, *transfer.Action, *issue.Action, driver.Deserializer], } transferValidators = append(transferValidators, extraTransferValidators...) diff --git a/token/core/zkatdlog/nogh/v1/validator/validator_transfer.go b/token/core/zkatdlog/nogh/v1/validator/validator_transfer.go index 54bb95a6ad..6308ae00da 100644 --- a/token/core/zkatdlog/nogh/v1/validator/validator_transfer.go +++ b/token/core/zkatdlog/nogh/v1/validator/validator_transfer.go @@ -17,7 +17,9 @@ import ( "github.com/hyperledger-labs/fabric-token-sdk/token/core/zkatdlog/nogh/v1/transfer" "github.com/hyperledger-labs/fabric-token-sdk/token/driver" "github.com/hyperledger-labs/fabric-token-sdk/token/services/identity" + hashescrow2 "github.com/hyperledger-labs/fabric-token-sdk/token/services/identity/interop/hashescrow" htlc2 "github.com/hyperledger-labs/fabric-token-sdk/token/services/identity/interop/htlc" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/interop/hashescrow" "github.com/hyperledger-labs/fabric-token-sdk/token/services/interop/htlc" token2 "github.com/hyperledger-labs/fabric-token-sdk/token/token" ) @@ -213,3 +215,74 @@ func TransferHTLCValidate(c context.Context, ctx *Context) error { return nil } + +// TransferHashEscrowValidate validates the HashEscrow scripts in the transfer action. +func TransferHashEscrowValidate(c context.Context, ctx *Context) error { + for i, in := range ctx.InputTokens { + owner, err := identity.UnmarshalTypedIdentity(in.Owner) + if err != nil { + return errors.Wrap(err, "failed to unmarshal owner of input token") + } + if owner.Type != hashescrow.HashEscrow { + continue + } + if len(ctx.InputTokens) != 1 || len(ctx.TransferAction.GetOutputs()) != 1 { + return ErrInvalidHTLCAction + } + + out, ok := ctx.TransferAction.GetOutputs()[0].(*token.Token) + if !ok || out == nil { + return ErrHTLCOutputNotFound + } + + script, err := hashescrow2.VerifyOwner(ctx.InputTokens[0].Owner, out.Owner) + if err != nil { + return errors.Wrap(err, "failed to verify transfer from hash escrow script") + } + sigma := ctx.Signatures[i] + claim, err := hashescrow2.ClaimFromSignature(sigma) + if err != nil { + return errors.WithMessagef(err, "failed to parse hash escrow claim signature") + } + resolvedOwner, _, claimedBy, err := hashescrow2.ResolveOwnerAndHash(script, claim.Preimage) + if err != nil { + return errors.WithMessagef(err, "failed to resolve hash escrow claim") + } + if !identity.Identity(resolvedOwner).Equal(out.Owner) { + return errors.New("invalid transfer action: output owner does not match hash escrow recipient resolved from preimage") + } + metadataKey, err := hashescrow2.ClaimMetadataCheck(ctx.TransferAction, script, claim.Preimage, claimedBy) + if err != nil { + return errors.WithMessagef(err, "failed to check hash escrow claim metadata") + } + ctx.CountMetadataKey(metadataKey) + } + + for _, o := range ctx.TransferAction.Outputs { + if o.IsRedeem() { + continue + } + owner, err := identity.UnmarshalTypedIdentity(o.Owner) + if err != nil { + return err + } + if owner.Type != hashescrow.HashEscrow { + continue + } + script := &hashescrow.Script{} + err = script.FromBytes(owner.Identity) + if err != nil { + return err + } + if err := script.Validate(); err != nil { + return errors.WithMessagef(err, "hash escrow script invalid") + } + metadataKey, err := hashescrow2.LockMetadataCheck(ctx.TransferAction, script) + if err != nil { + return errors.WithMessagef(err, "failed to check hash escrow lock metadata") + } + ctx.CountMetadataKey(metadataKey) + } + + return nil +} diff --git a/token/driver/wallet.go b/token/driver/wallet.go index f92c46efd7..067ba641ff 100644 --- a/token/driver/wallet.go +++ b/token/driver/wallet.go @@ -207,6 +207,7 @@ const ( HTLCScriptIdentityType IdentityType = 4 MultiSigIdentityType IdentityType = 5 PolicyIdentityType IdentityType = 6 + HashEscrowIdentityType IdentityType = 7 ) // IdentityTypeString identifies the type of identity as a string @@ -219,6 +220,7 @@ const ( HTLCScriptIdentityTypeString IdentityTypeString = "htlc" MultiSigIdentityTypeString IdentityTypeString = "multisig" PolicyIdentityTypeString IdentityTypeString = "policy" + HashEscrowIdentityTypeString IdentityTypeString = "hashescrow" ) // Authorization checks the relationship between a token and different wallet types (owner, issuer, auditor). diff --git a/token/services/identity/interop/hashescrow/deserializer.go b/token/services/identity/interop/hashescrow/deserializer.go new file mode 100644 index 0000000000..f3fe8786a4 --- /dev/null +++ b/token/services/identity/interop/hashescrow/deserializer.go @@ -0,0 +1,174 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package hashescrow + +import ( + "context" + + "github.com/hyperledger-labs/fabric-smart-client/pkg/utils/errors" + "github.com/hyperledger-labs/fabric-token-sdk/token/core/common/encoding/json" + "github.com/hyperledger-labs/fabric-token-sdk/token/driver" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/identity" + idriver "github.com/hyperledger-labs/fabric-token-sdk/token/services/identity/driver" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/interop/hashescrow" +) + +const ( + ScriptType = hashescrow.ScriptType + ScriptTypeString = hashescrow.ScriptTypeString +) + +//go:generate counterfeiter -o mock/deserializer.go -fake-name Deserializer . Deserializer +type Deserializer interface { + DeserializeVerifier(ctx context.Context, id driver.Identity) (driver.Verifier, error) + MatchIdentity(ctx context.Context, id driver.Identity, ai []byte) error +} + +type TypedIdentityDeserializer struct { + deserializer Deserializer +} + +func NewTypedIdentityDeserializer(deserializer Deserializer) *TypedIdentityDeserializer { + return &TypedIdentityDeserializer{deserializer: deserializer} +} + +func (t *TypedIdentityDeserializer) DeserializeVerifier(ctx context.Context, typ identity.Type, raw []byte) (driver.Verifier, error) { + if typ != ScriptType { + return nil, errors.Errorf("cannot deserializer type [%s], expected [%s]", typ, ScriptType) + } + + script := &hashescrow.Script{} + err := json.Unmarshal(raw, script) + if err != nil { + return nil, errors.Errorf("failed to unmarshal TypedIdentity as a hash escrow script") + } + // Ensure sender and recipient identities are still syntactically valid and deserializable. + if _, err = t.deserializer.DeserializeVerifier(ctx, script.Sender); err != nil { + return nil, errors.Errorf("failed to deserialize the identity of the sender in the hash escrow script") + } + if _, err = t.deserializer.DeserializeVerifier(ctx, script.Recipient); err != nil { + return nil, errors.Errorf("failed to deserialize the identity of the recipient in the hash escrow script") + } + v := &hashescrow.Verifier{Script: script} + + return v, nil +} + +func (t *TypedIdentityDeserializer) Recipients(id driver.Identity, typ identity.Type, raw []byte) ([]driver.Identity, error) { + if typ != ScriptType { + return nil, errors.New("unknown identity type") + } + script := &hashescrow.Script{} + err := json.Unmarshal(raw, script) + if err != nil { + return nil, errors.Wrapf(err, "failed to unmarshal hash escrow script") + } + + return []driver.Identity{script.Sender, script.Recipient}, nil +} + +func (t *TypedIdentityDeserializer) GetAuditInfo(ctx context.Context, id driver.Identity, typ identity.Type, raw []byte, p driver.AuditInfoProvider) ([]byte, error) { + if typ != ScriptType { + return nil, errors.Errorf("invalid type, got [%s], expected [%s]", typ, ScriptType) + } + script := &hashescrow.Script{} + if err := json.Unmarshal(raw, script); err != nil { + return nil, errors.Wrapf(err, "failed to unmarshal hash escrow script") + } + + auditInfo := &ScriptInfo{} + var err error + auditInfo.Sender, err = p.GetAuditInfo(ctx, script.Sender) + if err != nil { + return nil, errors.Wrapf(err, "failed getting audit info for hash escrow script [%s]", id.String()) + } + auditInfo.Recipient, err = p.GetAuditInfo(ctx, script.Recipient) + if err != nil { + return nil, errors.Wrapf(err, "failed getting audit info for hash escrow script [%s]", id.String()) + } + + auditInfoRaw, err := json.Marshal(auditInfo) + if err != nil { + return nil, errors.Wrapf(err, "failed marshaling audit info for hash escrow script") + } + + return auditInfoRaw, nil +} + +func (t *TypedIdentityDeserializer) GetAuditInfoMatcher(ctx context.Context, owner driver.Identity, auditInfo []byte) (driver.Matcher, error) { + return &AuditInfoMatcher{ + AuditInfo: auditInfo, + Deserializer: t.deserializer, + }, nil +} + +type AuditDeserializer struct { + AuditInfoDeserializer idriver.AuditInfoDeserializer +} + +func NewAuditDeserializer(auditInfoDeserializer idriver.AuditInfoDeserializer) *AuditDeserializer { + return &AuditDeserializer{AuditInfoDeserializer: auditInfoDeserializer} +} + +func (a *AuditDeserializer) DeserializeAuditInfo(ctx context.Context, identity driver.Identity, raw []byte) (idriver.AuditInfo, error) { + script := &hashescrow.Script{} + err := json.Unmarshal(identity, script) + if err != nil { + return nil, errors.Wrapf(err, "failed to unmarshal hash escrow identity") + } + + si := &ScriptInfo{} + err = json.Unmarshal(raw, si) + if err != nil || (len(si.Sender) == 0 && len(si.Recipient) == 0) { + return nil, errors.Errorf("invalid audit info, failed unmarshal [%s][%d][%d]", string(raw), len(si.Sender), len(si.Recipient)) + } + if len(si.Recipient) == 0 { + return nil, errors.Errorf("no recipient defined") + } + ai, err := a.AuditInfoDeserializer.DeserializeAuditInfo(ctx, script.Recipient, si.Recipient) + if err != nil { + return nil, errors.Wrapf(err, "failed unmarshalling audit info [%s]", raw) + } + + return ai, nil +} + +type AuditInfoMatcher struct { + AuditInfo []byte + Deserializer Deserializer +} + +func (a *AuditInfoMatcher) Match(ctx context.Context, id []byte) error { + scriptInf := &ScriptInfo{} + if err := json.Unmarshal(a.AuditInfo, scriptInf); err != nil { + return errors.Wrapf(err, "failed to unmarshal script info") + } + scriptSender, scriptRecipient, err := GetScriptSenderAndRecipient(id) + if err != nil { + return errors.Wrap(err, "failed getting script sender and recipient") + } + err = a.Deserializer.MatchIdentity(ctx, scriptSender, scriptInf.Sender) + if err != nil { + return errors.Wrapf(err, "failed matching sender identity [%s]", scriptSender.String()) + } + err = a.Deserializer.MatchIdentity(ctx, scriptRecipient, scriptInf.Recipient) + if err != nil { + return errors.Wrapf(err, "failed matching recipient identity [%s]", scriptRecipient.String()) + } + + return nil +} + +func GetScriptSenderAndRecipient(id []byte) (sender, recipient driver.Identity, err error) { + script := &hashescrow.Script{} + err = json.Unmarshal(id, script) + if err != nil { + return nil, nil, errors.Wrapf(err, "failed to unmarshal hash escrow script") + } + + return script.Sender, script.Recipient, nil +} diff --git a/token/services/identity/interop/hashescrow/deserializer_test.go b/token/services/identity/interop/hashescrow/deserializer_test.go new file mode 100644 index 0000000000..ed1360af06 --- /dev/null +++ b/token/services/identity/interop/hashescrow/deserializer_test.go @@ -0,0 +1,177 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package hashescrow_test + +import ( + "context" + "encoding/json" + "errors" + "testing" + + "github.com/hyperledger-labs/fabric-token-sdk/token/driver" + driverMock "github.com/hyperledger-labs/fabric-token-sdk/token/driver/mock" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/identity" + identityDriverMock "github.com/hyperledger-labs/fabric-token-sdk/token/services/identity/driver/mock" + ihe "github.com/hyperledger-labs/fabric-token-sdk/token/services/identity/interop/hashescrow" + he "github.com/hyperledger-labs/fabric-token-sdk/token/services/interop/hashescrow" + "github.com/stretchr/testify/require" +) + +type stubDeserializer struct { + deserializeVerifier func(ctx context.Context, id driver.Identity) (driver.Verifier, error) + matchIdentity func(ctx context.Context, id driver.Identity, ai []byte) error +} + +func (s *stubDeserializer) DeserializeVerifier(ctx context.Context, id driver.Identity) (driver.Verifier, error) { + if s.deserializeVerifier != nil { + return s.deserializeVerifier(ctx, id) + } + + return &driverMock.Verifier{}, nil +} + +func (s *stubDeserializer) MatchIdentity(ctx context.Context, id driver.Identity, ai []byte) error { + if s.matchIdentity != nil { + return s.matchIdentity(ctx, id, ai) + } + + return nil +} + +type fakeAuditInfo struct{} + +func (f *fakeAuditInfo) EnrollmentID() string { return "e" } +func (f *fakeAuditInfo) RevocationHandle() string { return "r" } + +func mkScript(t *testing.T, sender, recipient []byte) []byte { + t.Helper() + s := &he.Script{ + Sender: sender, + Recipient: recipient, + RecipientHashInfo: he.HashInfo{ + Hash: []byte("rh"), + }, + SenderHashInfo: he.HashInfo{ + Hash: []byte("sh"), + }, + } + raw, err := json.Marshal(s) + require.NoError(t, err) + + return raw +} + +func TestGetScriptSenderAndRecipient(t *testing.T) { + raw := mkScript(t, []byte("s"), []byte("r")) + sender, recipient, err := ihe.GetScriptSenderAndRecipient(raw) + require.NoError(t, err) + require.Equal(t, identity.Identity("s"), sender) + require.Equal(t, identity.Identity("r"), recipient) + + _, _, err = ihe.GetScriptSenderAndRecipient([]byte("bad-json")) + require.Error(t, err) +} + +func TestTypedIdentityDeserializerDeserializeVerifier(t *testing.T) { + d := ihe.NewTypedIdentityDeserializer(&stubDeserializer{}) + ctx := t.Context() + + _, err := d.DeserializeVerifier(ctx, identity.Type(999), []byte{}) + require.Error(t, err) + + _, err = d.DeserializeVerifier(ctx, he.ScriptType, []byte("bad-json")) + require.Error(t, err) + + stub := &stubDeserializer{ + deserializeVerifier: func(context.Context, driver.Identity) (driver.Verifier, error) { + return nil, errors.New("nope") + }, + } + d = ihe.NewTypedIdentityDeserializer(stub) + _, err = d.DeserializeVerifier(ctx, he.ScriptType, mkScript(t, []byte("s"), []byte("r"))) + require.Error(t, err) + + d = ihe.NewTypedIdentityDeserializer(&stubDeserializer{}) + v, err := d.DeserializeVerifier(ctx, he.ScriptType, mkScript(t, []byte("s"), []byte("r"))) + require.NoError(t, err) + require.NotNil(t, v) +} + +func TestTypedIdentityDeserializerRecipientsAndAuditInfo(t *testing.T) { + d := ihe.NewTypedIdentityDeserializer(&stubDeserializer{}) + ctx := t.Context() + raw := mkScript(t, []byte("s"), []byte("r")) + + _, err := d.Recipients(nil, identity.Type(999), raw) + require.Error(t, err) + + ids, err := d.Recipients(nil, he.ScriptType, raw) + require.NoError(t, err) + require.Len(t, ids, 2) + require.Equal(t, identity.Identity("s"), ids[0]) + require.Equal(t, identity.Identity("r"), ids[1]) + + p := &driverMock.AuditInfoProvider{} + p.GetAuditInfoReturnsOnCall(0, nil, errors.New("nope")) + _, err = d.GetAuditInfo(ctx, []byte("id"), he.ScriptType, raw, p) + require.Error(t, err) + + p = &driverMock.AuditInfoProvider{} + p.GetAuditInfoReturnsOnCall(0, []byte("sa"), nil) + p.GetAuditInfoReturnsOnCall(1, []byte("ra"), nil) + ai, err := d.GetAuditInfo(ctx, []byte("id"), he.ScriptType, raw, p) + require.NoError(t, err) + + var out ihe.ScriptInfo + require.NoError(t, json.Unmarshal(ai, &out)) + require.Equal(t, []byte("sa"), out.Sender) + require.Equal(t, []byte("ra"), out.Recipient) +} + +func TestAuditDeserializerAndMatcher(t *testing.T) { + ctx := t.Context() + aid := &identityDriverMock.AuditInfoDeserializer{} + ad := ihe.NewAuditDeserializer(aid) + + scriptRaw, err := json.Marshal(&he.Script{Recipient: []byte("r")}) + require.NoError(t, err) + + _, err = ad.DeserializeAuditInfo(ctx, scriptRaw, []byte("bad-json")) + require.Error(t, err) + + siRaw, err := json.Marshal(&ihe.ScriptInfo{Sender: []byte("s")}) + require.NoError(t, err) + _, err = ad.DeserializeAuditInfo(ctx, scriptRaw, siRaw) + require.Error(t, err) + + siRaw, err = json.Marshal(&ihe.ScriptInfo{Recipient: []byte("ra")}) + require.NoError(t, err) + aid.DeserializeAuditInfoReturns(&fakeAuditInfo{}, nil) + ai, err := ad.DeserializeAuditInfo(ctx, scriptRaw, siRaw) + require.NoError(t, err) + require.NotNil(t, ai) + + matcher := &ihe.AuditInfoMatcher{ + AuditInfo: []byte("bad-json"), + Deserializer: &stubDeserializer{ + matchIdentity: func(context.Context, driver.Identity, []byte) error { return nil }, + }, + } + require.Error(t, matcher.Match(ctx, []byte("id"))) + + matcher.AuditInfo, err = json.Marshal(&ihe.ScriptInfo{Sender: []byte("sa"), Recipient: []byte("ra")}) + require.NoError(t, err) + require.Error(t, matcher.Match(ctx, []byte("bad-script"))) + + matcher.Deserializer = &stubDeserializer{ + matchIdentity: func(context.Context, driver.Identity, []byte) error { return errors.New("nope") }, + } + require.Error(t, matcher.Match(ctx, mkScript(t, []byte("s"), []byte("r")))) + + matcher.Deserializer = &stubDeserializer{} + require.NoError(t, matcher.Match(ctx, mkScript(t, []byte("s"), []byte("r")))) +} diff --git a/token/services/identity/interop/hashescrow/info.go b/token/services/identity/interop/hashescrow/info.go new file mode 100644 index 0000000000..5662ed3dd0 --- /dev/null +++ b/token/services/identity/interop/hashescrow/info.go @@ -0,0 +1,29 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package hashescrow + +import ( + "context" + + "github.com/hyperledger-labs/fabric-token-sdk/token/driver" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/interop/hashescrow" +) + +type Script = hashescrow.Script + +// AuditInfoProvider provides access to audit information for a given identity. +// +//go:generate counterfeiter -o mock/aip.go -fake-name AuditInfoProvider . AuditInfoProvider +type AuditInfoProvider interface { + GetAuditInfo(ctx context.Context, identity driver.Identity) ([]byte, error) +} + +// ScriptInfo contains sender and recipient audit info. +type ScriptInfo struct { + Sender []byte + Recipient []byte +} diff --git a/token/services/identity/interop/hashescrow/validator.go b/token/services/identity/interop/hashescrow/validator.go new file mode 100644 index 0000000000..475d6f0531 --- /dev/null +++ b/token/services/identity/interop/hashescrow/validator.go @@ -0,0 +1,118 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package hashescrow + +import ( + "bytes" + "encoding/json" + + "github.com/hyperledger-labs/fabric-smart-client/pkg/utils/errors" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/identity" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/interop/hashescrow" +) + +//go:generate counterfeiter -o mock/action.go -fake-name Action . Action +type Action interface { + GetMetadata() map[string][]byte +} + +const HashEscrow = ScriptType + +// VerifyOwner validates that the output owner is either sender or recipient as encoded in the hash escrow script. +func VerifyOwner(senderRawOwner []byte, outRawOwner []byte) (*hashescrow.Script, error) { + if len(outRawOwner) == 0 { + return nil, errors.Errorf("the output owner must be set") + } + sender, err := identity.UnmarshalTypedIdentity(senderRawOwner) + if err != nil { + return nil, err + } + if sender.Type != HashEscrow { + return nil, errors.Errorf("invalid identity type, expected [%s], got [%s]", HashEscrow, sender.Type) + } + script := &hashescrow.Script{} + err = json.Unmarshal(sender.Identity, script) + if err != nil { + return nil, err + } + if script.Recipient.Equal(outRawOwner) || script.Sender.Equal(outRawOwner) { + return script, nil + } + + return nil, errors.New("owner of output token does not correspond to sender or recipient in hash escrow request") +} + +func ClaimFromSignature(sig []byte) (*hashescrow.ClaimSignature, error) { + claim := &hashescrow.ClaimSignature{} + if err := json.Unmarshal(sig, claim); err != nil { + return nil, errors.Wrapf(err, "failed unmarshalling claim signature [%s]", string(sig)) + } + if len(claim.Preimage) == 0 { + return nil, errors.New("expected a valid claim preimage") + } + + return claim, nil +} + +func ResolveOwnerAndHash(script *hashescrow.Script, preimage []byte) ([]byte, []byte, string, error) { + resolvedOwner, resolvedHash, claimedBy, err := script.ResolveOwnerAndHashForPreimage(preimage) + if err != nil { + return nil, nil, "", errors.Wrap(err, "failed resolving recipient from preimage") + } + + return resolvedOwner, resolvedHash, claimedBy, nil +} + +// ClaimMetadataCheck validates claim metadata and returns the matched metadata key. +func ClaimMetadataCheck(action Action, script *hashescrow.Script, preimage []byte, claimedBy string) (string, error) { + metadata := action.GetMetadata() + if len(metadata) == 0 { + return "", errors.New("cannot find hash escrow unlock preimage, no metadata") + } + key := hashescrow.ClaimKey(script.RecipientHashInfo.Hash, script.SenderHashInfo.Hash) + value, ok := metadata[key] + if !ok { + return "", errors.New("cannot find hash escrow unlock preimage, missing metadata entry") + } + claimValue := &hashescrow.ClaimMetadataValue{} + if err := json.Unmarshal(value, claimValue); err != nil { + return "", errors.Wrapf(err, "cannot unmarshal hash escrow claim metadata value [%s]", string(value)) + } + if !bytes.Equal(claimValue.Preimage, preimage) { + return "", errors.Errorf("invalid action, cannot match hash escrow unlock preimage with metadata [%x]!=[%x]", claimValue.Preimage, preimage) + } + if claimValue.ClaimedBy != claimedBy { + return "", errors.Errorf("invalid action, cannot match hash escrow claimant side with metadata [%s]!=[%s]", claimValue.ClaimedBy, claimedBy) + } + + return key, nil +} + +// LockMetadataCheck validates lock metadata and returns the lock metadata key. +func LockMetadataCheck(action Action, script *hashescrow.Script) (string, error) { + metadata := action.GetMetadata() + if len(metadata) == 0 { + return "", errors.New("cannot find hash escrow lock, no metadata") + } + key := hashescrow.LockKey(script.RecipientHashInfo.Hash, script.SenderHashInfo.Hash) + lockValue, ok := metadata[key] + if !ok { + return "", errors.New("cannot find hash escrow lock, missing metadata entry") + } + lockMetadata := &hashescrow.LockMetadata{} + if err := json.Unmarshal(lockValue, lockMetadata); err != nil { + return "", errors.Wrapf(err, "cannot unmarshal hash escrow lock metadata value [%s]", string(lockValue)) + } + if !bytes.Equal(lockMetadata.RecipientHash, script.RecipientHashInfo.Hash) { + return "", errors.Errorf("invalid action, cannot match recipient hash escrow lock with metadata [%x]!=[%x]", lockMetadata.RecipientHash, script.RecipientHashInfo.Hash) + } + if !bytes.Equal(lockMetadata.SenderHash, script.SenderHashInfo.Hash) { + return "", errors.Errorf("invalid action, cannot match sender hash escrow lock with metadata [%x]!=[%x]", lockMetadata.SenderHash, script.SenderHashInfo.Hash) + } + + return key, nil +} diff --git a/token/services/identity/interop/hashescrow/validator_test.go b/token/services/identity/interop/hashescrow/validator_test.go new file mode 100644 index 0000000000..c42c126bce --- /dev/null +++ b/token/services/identity/interop/hashescrow/validator_test.go @@ -0,0 +1,138 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package hashescrow_test + +import ( + "crypto" + "encoding/json" + "testing" + + "github.com/hyperledger-labs/fabric-token-sdk/token/services/identity" + ihe "github.com/hyperledger-labs/fabric-token-sdk/token/services/identity/interop/hashescrow" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/interop/encoding" + he "github.com/hyperledger-labs/fabric-token-sdk/token/services/interop/hashescrow" + "github.com/stretchr/testify/require" +) + +type actionStub struct { + md map[string][]byte +} + +func (a *actionStub) GetMetadata() map[string][]byte { + return a.md +} + +func wrapScript(t *testing.T, sender, recipient []byte, hash []byte) ([]byte, *he.Script) { + t.Helper() + recipientHash := append([]byte{}, hash...) + senderHash := append([]byte{}, hash...) + senderHash = append(senderHash, byte('2')) + script := &he.Script{ + Sender: sender, + Recipient: recipient, + RecipientHashInfo: he.HashInfo{ + Hash: recipientHash, + HashFunc: crypto.SHA256, + HashEncoding: encoding.Base64, + }, + SenderHashInfo: he.HashInfo{ + Hash: senderHash, + HashFunc: crypto.SHA256, + HashEncoding: encoding.Base64, + }, + } + rawScript, err := json.Marshal(script) + require.NoError(t, err) + rawOwner, err := identity.WrapWithType(he.ScriptType, rawScript) + require.NoError(t, err) + + return rawOwner, script +} + +func TestVerifyOwner(t *testing.T) { + rawOwner, _ := wrapScript(t, []byte("s"), []byte("r"), []byte("h")) + + script, err := ihe.VerifyOwner(rawOwner, []byte("r")) + require.NoError(t, err) + require.Equal(t, identity.Identity("s"), script.Sender) + require.Equal(t, identity.Identity("r"), script.Recipient) + + script, err = ihe.VerifyOwner(rawOwner, []byte("s")) + require.NoError(t, err) + require.Equal(t, identity.Identity("s"), script.Sender) + + _, err = ihe.VerifyOwner(rawOwner, []byte("x")) + require.Error(t, err) + + _, err = ihe.VerifyOwner([]byte("bad"), []byte("r")) + require.Error(t, err) + + typed := identity.TypedIdentity{Type: identity.Type(99), Identity: []byte("x")} + b, err := typed.Bytes() + require.NoError(t, err) + _, err = ihe.VerifyOwner(b, []byte("r")) + require.Error(t, err) +} + +func TestClaimMetadataCheck(t *testing.T) { + _, script := wrapScript(t, []byte("s"), []byte("r"), []byte("h")) + + preimage := []byte("pre") + resolvedHash, err := script.RecipientHashInfo.Image(preimage) + require.NoError(t, err) + script.RecipientHashInfo.Hash = resolvedHash + key := he.ClaimKey(script.RecipientHashInfo.Hash, script.SenderHashInfo.Hash) + value, err := he.ClaimValue(preimage, "recipient") + require.NoError(t, err) + + sigRaw, err := json.Marshal(&he.ClaimSignature{ + Preimage: preimage, + }) + require.NoError(t, err) + claim, err := ihe.ClaimFromSignature(sigRaw) + require.NoError(t, err) + resolvedOwner, _, claimedBy, err := ihe.ResolveOwnerAndHash(script, claim.Preimage) + require.NoError(t, err) + require.Equal(t, identity.Identity("r"), identity.Identity(resolvedOwner)) + require.Equal(t, "recipient", claimedBy) + + act := &actionStub{md: map[string][]byte{key: value}} + got, err := ihe.ClaimMetadataCheck(act, script, claim.Preimage, claimedBy) + require.NoError(t, err) + require.Equal(t, key, got) + + act = &actionStub{md: map[string][]byte{}} + _, err = ihe.ClaimMetadataCheck(act, script, claim.Preimage, claimedBy) + require.Error(t, err) + + _, err = ihe.ClaimFromSignature([]byte("bad-json")) + require.Error(t, err) +} + +func TestLockMetadataCheck(t *testing.T) { + _, script := wrapScript(t, []byte("s"), []byte("r"), []byte("h")) + key := he.LockKey(script.RecipientHashInfo.Hash, script.SenderHashInfo.Hash) + lockValue, err := he.LockValue(script.RecipientHashInfo.Hash, script.SenderHashInfo.Hash) + require.NoError(t, err) + + act := &actionStub{md: map[string][]byte{ + key: lockValue, + }} + got, err := ihe.LockMetadataCheck(act, script) + require.NoError(t, err) + require.Equal(t, key, got) + + act = &actionStub{md: map[string][]byte{}} + _, err = ihe.LockMetadataCheck(act, script) + require.Error(t, err) + + act = &actionStub{md: map[string][]byte{ + key: []byte("bad"), + }} + _, err = ihe.LockMetadataCheck(act, script) + require.Error(t, err) +} diff --git a/token/services/identity/marshal/marshal.go b/token/services/identity/marshal/marshal.go index 3f9bf52ce8..e8037dfd22 100644 --- a/token/services/identity/marshal/marshal.go +++ b/token/services/identity/marshal/marshal.go @@ -131,6 +131,9 @@ func DecodeIdentity(b []byte) (Result, error) { case driver.HTLCScriptIdentityTypeString: r.Int32 = driver.HTLCScriptIdentityType r.IsInt = true + case driver.HashEscrowIdentityTypeString: + r.Int32 = driver.HashEscrowIdentityType + r.IsInt = true } } diff --git a/token/services/identity/marshal/marshal_test.go b/token/services/identity/marshal/marshal_test.go index 10eb8462fc..cd0349a114 100644 --- a/token/services/identity/marshal/marshal_test.go +++ b/token/services/identity/marshal/marshal_test.go @@ -18,6 +18,7 @@ import ( "github.com/hyperledger-labs/fabric-token-sdk/token/services/identity/marshal" "github.com/hyperledger-labs/fabric-token-sdk/token/services/identity/multisig" "github.com/hyperledger-labs/fabric-token-sdk/token/services/identity/x509" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/interop/hashescrow" "github.com/hyperledger-labs/fabric-token-sdk/token/services/interop/htlc" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -540,6 +541,11 @@ func TestStringCompatibility(t *testing.T) { str: htlc.ScriptTypeString, identityType: htlc.ScriptType, }, + { + name: "hashescrow", + str: hashescrow.ScriptTypeString, + identityType: hashescrow.ScriptType, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/token/services/identity/typed.go b/token/services/identity/typed.go index df5e35ff71..c8568e611b 100644 --- a/token/services/identity/typed.go +++ b/token/services/identity/typed.go @@ -73,6 +73,8 @@ func TypeToString(t driver.IdentityType) string { return tdriver.MultiSigIdentityTypeString case tdriver.HTLCScriptIdentityType: return tdriver.HTLCScriptIdentityTypeString + case tdriver.HashEscrowIdentityType: + return tdriver.HashEscrowIdentityTypeString default: return fmt.Sprintf("Type (%d)", t) } diff --git a/token/services/interop/hashescrow/audit.go b/token/services/interop/hashescrow/audit.go new file mode 100644 index 0000000000..95832829f2 --- /dev/null +++ b/token/services/interop/hashescrow/audit.go @@ -0,0 +1,99 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package hashescrow + +import ( + "encoding/json" + + "github.com/hyperledger-labs/fabric-smart-client/pkg/utils/errors" + "github.com/hyperledger-labs/fabric-token-sdk/token" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/identity" +) + +type Input struct { + *token.Input + isHashEscrow bool +} + +func ToInput(i *token.Input) (*Input, error) { + owner, err := identity.UnmarshalTypedIdentity(i.Owner) + if err != nil { + return nil, errors.Wrapf(err, "failed to unmarshal owner") + } + + return &Input{ + Input: i, + isHashEscrow: owner.Type == HashEscrow, + }, nil +} + +func (i *Input) IsHashEscrow() bool { + return i.isHashEscrow +} + +func (i *Input) Script() (*Script, error) { + if !i.isHashEscrow { + return nil, errors.New("this input does not refer to a hash escrow script") + } + + owner, err := identity.UnmarshalTypedIdentity(i.Owner) + if err != nil { + return nil, errors.Wrapf(err, "failed to unmarshal owner") + } + if owner.Type != HashEscrow { + return nil, errors.Errorf("invalid identity type, expected [%s], got [%s]", HashEscrow, owner.Type) + } + script := &Script{} + err = json.Unmarshal(owner.Identity, script) + if err != nil { + return nil, errors.Wrapf(err, "failed to unmarshal hash escrow script") + } + + return script, nil +} + +type Output struct { + *token.Output + isHashEscrow bool +} + +func ToOutput(i *token.Output) (*Output, error) { + owner, err := identity.UnmarshalTypedIdentity(i.Owner) + if err != nil { + return nil, errors.Wrapf(err, "failed to unmarshal owner") + } + + return &Output{ + Output: i, + isHashEscrow: owner.Type == HashEscrow, + }, nil +} + +func (o *Output) IsHashEscrow() bool { + return o.isHashEscrow +} + +func (o *Output) Script() (*Script, error) { + if !o.isHashEscrow { + return nil, errors.New("this output does not refer to a hash escrow script") + } + + owner, err := identity.UnmarshalTypedIdentity(o.Owner) + if err != nil { + return nil, errors.Wrapf(err, "failed to unmarshal owner") + } + if owner.Type != HashEscrow { + return nil, errors.Errorf("invalid identity type, expected [%s], got [%s]", HashEscrow, owner.Type) + } + script := &Script{} + err = json.Unmarshal(owner.Identity, script) + if err != nil { + return nil, errors.Wrapf(err, "failed to unmarshal hash escrow script") + } + + return script, nil +} diff --git a/token/services/interop/hashescrow/keys.go b/token/services/interop/hashescrow/keys.go new file mode 100644 index 0000000000..eaa2350ba3 --- /dev/null +++ b/token/services/interop/hashescrow/keys.go @@ -0,0 +1,72 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package hashescrow + +import ( + "crypto/sha256" + "encoding/hex" + + "github.com/hyperledger-labs/fabric-smart-client/pkg/utils/errors" + "github.com/hyperledger-labs/fabric-token-sdk/token/core/common/encoding/json" +) + +const ( + UnlockPreimage = "hashescrow.upi:" + LockHashes = "hashescrow.lh:" + ClaimMetadata = "hashescrow.cm:" +) + +type LockMetadata struct { + RecipientHash []byte `json:"recipient_hash"` + SenderHash []byte `json:"sender_hash"` +} + +type ClaimMetadataValue struct { + Preimage []byte `json:"preimage"` + ClaimedBy string `json:"claimed_by"` +} + +func aggregateHash(recipientHash, senderHash []byte) []byte { + h := sha256.New() + h.Write(recipientHash) + h.Write([]byte{':'}) + h.Write(senderHash) + + return h.Sum(nil) +} + +func LockKey(recipientHash, senderHash []byte) string { + return LockHashes + hex.EncodeToString(aggregateHash(recipientHash, senderHash)) +} + +func ClaimKey(recipientHash, senderHash []byte) string { + return ClaimMetadata + hex.EncodeToString(aggregateHash(recipientHash, senderHash)) +} + +func LockValue(recipientHash, senderHash []byte) ([]byte, error) { + raw, err := json.Marshal(&LockMetadata{ + RecipientHash: recipientHash, + SenderHash: senderHash, + }) + if err != nil { + return nil, errors.Wrap(err, "failed to marshal hash escrow lock metadata") + } + + return raw, nil +} + +func ClaimValue(preimage []byte, claimedBy string) ([]byte, error) { + raw, err := json.Marshal(&ClaimMetadataValue{ + Preimage: preimage, + ClaimedBy: claimedBy, + }) + if err != nil { + return nil, errors.Wrap(err, "failed to marshal hash escrow claim metadata") + } + + return raw, nil +} diff --git a/token/services/interop/hashescrow/logger.go b/token/services/interop/hashescrow/logger.go new file mode 100644 index 0000000000..880aac0af8 --- /dev/null +++ b/token/services/interop/hashescrow/logger.go @@ -0,0 +1,11 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package hashescrow + +import "github.com/hyperledger-labs/fabric-token-sdk/token/services/logging" + +var logger = logging.MustGetLogger() diff --git a/token/services/interop/hashescrow/script.go b/token/services/interop/hashescrow/script.go new file mode 100644 index 0000000000..94f7402f01 --- /dev/null +++ b/token/services/interop/hashescrow/script.go @@ -0,0 +1,169 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package hashescrow + +import ( + "context" + "encoding/json" + + "github.com/hyperledger-labs/fabric-smart-client/pkg/utils/errors" + "github.com/hyperledger-labs/fabric-smart-client/platform/view/view" + "github.com/hyperledger-labs/fabric-token-sdk/token/driver" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/identity" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/interop/htlc" + token3 "github.com/hyperledger-labs/fabric-token-sdk/token/token" +) + +// HashInfo contains hash configuration used by hash-based escrow scripts. +// Reuse the HTLC hash representation to keep hashing semantics identical. +type HashInfo = htlc.HashInfo + +// Script contains the details of a hash-based escrow lock. +// Either sender or recipient can claim by presenting a valid preimage and signature. +type Script struct { + Sender view.Identity + Recipient view.Identity + RecipientHashInfo HashInfo + SenderHashInfo HashInfo +} + +// Validate performs the following checks: +// - sender must be set +// - recipient must be set +// - both recipient and sender hash info must be valid +func (s *Script) Validate() error { + if s.Sender.IsNone() { + return errors.New("sender not set") + } + if s.Recipient.IsNone() { + return errors.New("recipient not set") + } + if err := s.RecipientHashInfo.Validate(); err != nil { + return errors.WithMessage(err, "recipient hash info invalid") + } + if err := s.SenderHashInfo.Validate(); err != nil { + return errors.WithMessage(err, "sender hash info invalid") + } + + return nil +} + +// ResolveOwnerAndHashForPreimage resolves the output owner from the provided pre-image. +// If the pre-image matches the recipient hash, recipient receives the token. +// If the pre-image matches the sender hash, sender receives the token. +func (s *Script) ResolveOwnerAndHashForPreimage(preimage []byte) (view.Identity, []byte, string, error) { + recipientHash, err := s.RecipientHashInfo.Image(preimage) + if err != nil { + return nil, nil, "", errors.WithMessage(err, "failed computing recipient hash") + } + if err := s.RecipientHashInfo.Compare(recipientHash); err == nil { + return s.Recipient, recipientHash, "recipient", nil + } + + senderHash, err := s.SenderHashInfo.Image(preimage) + if err != nil { + return nil, nil, "", errors.WithMessage(err, "failed computing sender hash") + } + if err := s.SenderHashInfo.Compare(senderHash); err == nil { + return s.Sender, senderHash, "sender", nil + } + + return nil, nil, "", errors.New("preimage does not match any hashescrow hash") +} + +// ResolveRecipientForPreImage is kept for backward compatibility with existing call sites. +func (s *Script) ResolveRecipientForPreImage(preImage []byte) (view.Identity, []byte, error) { + owner, hash, _, err := s.ResolveOwnerAndHashForPreimage(preImage) + if err != nil { + return nil, nil, err + } + + return owner, hash, nil +} + +func (s *Script) FromBytes(raw []byte) error { + return json.Unmarshal(raw, s) +} + +// ScriptAuth implements token ownership checks for hash-based escrow scripts. +type ScriptAuth struct { + WalletService driver.WalletService +} + +func NewScriptAuth(walletService driver.WalletService) *ScriptAuth { + return &ScriptAuth{WalletService: walletService} +} + +// AmIAnAuditor returns false for script ownership. +func (s *ScriptAuth) AmIAnAuditor() bool { + return false +} + +// IsMine returns true if either the sender or recipient belongs to one of our owner wallets. +func (s *ScriptAuth) IsMine(ctx context.Context, tok *token3.Token) (string, []string, bool) { + owner, err := identity.UnmarshalTypedIdentity(tok.Owner) + if err != nil { + logger.DebugfContext(ctx, "Is Mine [%s,%s,%s]? No, failed unmarshalling [%s]", view.Identity(tok.Owner), tok.Type, tok.Quantity, err) + + return "", nil, false + } + if owner.Type != HashEscrow { + logger.DebugfContext(ctx, "Is Mine [%s,%s,%s]? No, owner type is [%s] instead of [%s]", view.Identity(tok.Owner), tok.Type, tok.Quantity, owner.Type, HashEscrow) + + return "", nil, false + } + script := &Script{} + if err := json.Unmarshal(owner.Identity, script); err != nil { + logger.DebugfContext(ctx, "Is Mine [%s,%s,%s]? No, failed unmarshalling [%s]", view.Identity(tok.Owner), tok.Type, tok.Quantity, err) + + return "", nil, false + } + if script.Sender.IsNone() || script.Recipient.IsNone() { + logger.DebugfContext(ctx, "Is Mine [%s,%s,%s]? No, invalid content [%v]", view.Identity(tok.Owner), tok.Type, tok.Quantity, script) + + return "", nil, false + } + + var ids []string + + logger.DebugfContext(ctx, "Is Mine [%s,%s,%s] as sender?", view.Identity(tok.Owner), tok.Type, tok.Quantity) + if wallet, err := s.WalletService.OwnerWallet(ctx, script.Sender); err == nil { + ids = append(ids, senderWallet(wallet)) + } + + logger.DebugfContext(ctx, "Is Mine [%s,%s,%s] as recipient?", view.Identity(tok.Owner), tok.Type, tok.Quantity) + if wallet, err := s.WalletService.OwnerWallet(ctx, script.Recipient); err == nil { + ids = append(ids, recipientWallet(wallet)) + } + + return "", ids, len(ids) != 0 +} + +func (s *ScriptAuth) Issued(ctx context.Context, issuer driver.Identity, tok *token3.Token) bool { + return false +} + +func (s *ScriptAuth) OwnerType(raw []byte) (driver.IdentityType, []byte, error) { + owner, err := identity.UnmarshalTypedIdentity(raw) + if err != nil { + return driver.ZeroIdentityType, nil, err + } + + return owner.Type, owner.Identity, nil +} + +type ownerWallet interface { + ID() string +} + +func senderWallet(w ownerWallet) string { + return "hashescrow.sender" + w.ID() +} + +func recipientWallet(w ownerWallet) string { + return "hashescrow.recipient" + w.ID() +} diff --git a/token/services/interop/hashescrow/script_test.go b/token/services/interop/hashescrow/script_test.go new file mode 100644 index 0000000000..da26e3b160 --- /dev/null +++ b/token/services/interop/hashescrow/script_test.go @@ -0,0 +1,112 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package hashescrow + +import ( + "crypto" + "encoding/json" + "testing" + + "github.com/hyperledger-labs/fabric-token-sdk/token/services/interop/encoding" + "github.com/stretchr/testify/require" +) + +func TestScriptValidate(t *testing.T) { + s := &Script{} + require.EqualError(t, s.Validate(), "sender not set") + + s.Sender = []byte("sender") + require.EqualError(t, s.Validate(), "recipient not set") + + s.Recipient = []byte("recipient") + s.RecipientHashInfo = HashInfo{Hash: []byte("h")} + require.Error(t, s.Validate()) + + s.RecipientHashInfo = HashInfo{Hash: []byte("h"), HashFunc: crypto.SHA256, HashEncoding: encoding.Base64} + s.SenderHashInfo = HashInfo{Hash: []byte("h2"), HashFunc: crypto.SHA256, HashEncoding: encoding.Base64} + require.NoError(t, s.Validate()) +} + +func TestScriptFromBytes(t *testing.T) { + raw, err := json.Marshal(&Script{ + Sender: []byte("sender"), + Recipient: []byte("recipient"), + RecipientHashInfo: HashInfo{ + Hash: []byte("h"), + HashFunc: crypto.SHA256, + HashEncoding: encoding.Base64, + }, + SenderHashInfo: HashInfo{ + Hash: []byte("h2"), + HashFunc: crypto.SHA256, + HashEncoding: encoding.Base64, + }, + }) + require.NoError(t, err) + + s := &Script{} + require.NoError(t, s.FromBytes(raw)) + require.Equal(t, []byte("sender"), []byte(s.Sender)) + require.Equal(t, []byte("recipient"), []byte(s.Recipient)) + require.Equal(t, []byte("h"), s.RecipientHashInfo.Hash) + require.Equal(t, []byte("h2"), s.SenderHashInfo.Hash) + + require.Error(t, s.FromBytes([]byte("bad-json"))) +} + +func TestResolveRecipientForPreImage(t *testing.T) { + s := &Script{ + Sender: []byte("sender"), + Recipient: []byte("recipient"), + RecipientHashInfo: HashInfo{ + HashFunc: crypto.SHA256, + HashEncoding: encoding.Base64, + }, + SenderHashInfo: HashInfo{ + HashFunc: crypto.SHA256, + HashEncoding: encoding.Base64, + }, + } + + recipientPreImage := []byte("recipient-secret") + recipientImage, err := s.RecipientHashInfo.Image(recipientPreImage) + require.NoError(t, err) + s.RecipientHashInfo.Hash = recipientImage + + senderPreImage := []byte("sender-secret") + senderImage, err := s.SenderHashInfo.Image(senderPreImage) + require.NoError(t, err) + s.SenderHashInfo.Hash = senderImage + + owner, image, err := s.ResolveRecipientForPreImage(recipientPreImage) + require.NoError(t, err) + require.Equal(t, []byte("recipient"), []byte(owner)) + require.Equal(t, recipientImage, image) + + owner, image, err = s.ResolveRecipientForPreImage(senderPreImage) + require.NoError(t, err) + require.Equal(t, []byte("sender"), []byte(owner)) + require.Equal(t, senderImage, image) + + _, _, err = s.ResolveRecipientForPreImage([]byte("wrong")) + require.Error(t, err) +} + +func TestKeys(t *testing.T) { + recipientHash := []byte{0x01, 0x02} + senderHash := []byte{0x03, 0x04} + + k := ClaimKey(recipientHash, senderHash) + require.Contains(t, k, "hashescrow.cm:") + + k = LockKey(recipientHash, senderHash) + require.Contains(t, k, "hashescrow.lh:") + + v, err := LockValue(recipientHash, senderHash) + require.NoError(t, err) + require.NotEmpty(t, v) +} diff --git a/token/services/interop/hashescrow/signer.go b/token/services/interop/hashescrow/signer.go new file mode 100644 index 0000000000..898f686594 --- /dev/null +++ b/token/services/interop/hashescrow/signer.go @@ -0,0 +1,74 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package hashescrow + +import ( + "encoding/json" + + "github.com/hyperledger-labs/fabric-smart-client/pkg/utils/errors" +) + +// ClaimSignature carries the preimage used to unlock a hash escrow script. +type ClaimSignature struct { + Preimage []byte +} + +// ClaimSigner embeds the preimage in the signature payload. +// No claimant signature is required: anyone can submit if they know a valid preimage. +type ClaimSigner struct { + Preimage []byte +} + +// Sign returns a serialized claim signature containing the preimage. +func (cs *ClaimSigner) Sign(tokenRequestAndTxID []byte) ([]byte, error) { + claimSignature := ClaimSignature{ + Preimage: cs.Preimage, + } + + return json.Marshal(claimSignature) +} + +// ClaimVerifier checks that the preimage unlocks one of the two script hashes. +type ClaimVerifier struct { + Script *Script +} + +func (cv *ClaimVerifier) Verify(tokenRequestAndTxID, claimSignature []byte) error { + _ = tokenRequestAndTxID + sig := &ClaimSignature{} + err := json.Unmarshal(claimSignature, sig) + if err != nil { + return errors.Wrapf(err, "failed to unmarshal claim signature") + } + + if len(sig.Preimage) == 0 { + return errors.New("invalid claim signature, empty preimage") + } + if cv.Script == nil { + return errors.New("invalid claim verifier, script is nil") + } + if _, _, _, err = cv.Script.ResolveOwnerAndHashForPreimage(sig.Preimage); err != nil { + return errors.WithMessage(err, "preimage does not unlock hash escrow script") + } + + return nil +} + +// Verifier validates claims for hash-based escrow scripts. +// A valid claim carries a preimage that resolves to either recipient or sender. +type Verifier struct { + Script *Script +} + +func (v *Verifier) Verify(msg []byte, sigma []byte) error { + claimVerifier := &ClaimVerifier{Script: v.Script} + if err := claimVerifier.Verify(msg, sigma); err != nil { + return errors.WithMessage(err, "failed verifying hash escrow claim signature") + } + + return nil +} diff --git a/token/services/interop/hashescrow/signer_test.go b/token/services/interop/hashescrow/signer_test.go new file mode 100644 index 0000000000..e1f10cf0b8 --- /dev/null +++ b/token/services/interop/hashescrow/signer_test.go @@ -0,0 +1,91 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package hashescrow_test + +import ( + "crypto" + "encoding/json" + "testing" + + "github.com/hyperledger-labs/fabric-token-sdk/token/services/interop/encoding" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/interop/hashescrow" + "github.com/stretchr/testify/require" +) + +func mkScriptForClaims(t *testing.T) (*hashescrow.Script, []byte, []byte) { + t.Helper() + + s := &hashescrow.Script{ + Sender: []byte("sender"), + Recipient: []byte("recipient"), + RecipientHashInfo: hashescrow.HashInfo{ + HashFunc: crypto.SHA256, + HashEncoding: encoding.Base64, + }, + SenderHashInfo: hashescrow.HashInfo{ + HashFunc: crypto.SHA256, + HashEncoding: encoding.Base64, + }, + } + + recipientPreImage := []byte("recipient-preimage") + recipientImage, err := s.RecipientHashInfo.Image(recipientPreImage) + require.NoError(t, err) + s.RecipientHashInfo.Hash = recipientImage + + senderPreImage := []byte("sender-preimage") + senderImage, err := s.SenderHashInfo.Image(senderPreImage) + require.NoError(t, err) + s.SenderHashInfo.Hash = senderImage + + return s, recipientPreImage, senderPreImage +} + +func TestClaimSignerSign(t *testing.T) { + cs := &hashescrow.ClaimSigner{Preimage: []byte("pre")} + raw, err := cs.Sign([]byte("ignored-msg")) + require.NoError(t, err) + + var sig hashescrow.ClaimSignature + require.NoError(t, json.Unmarshal(raw, &sig)) + require.Equal(t, []byte("pre"), sig.Preimage) +} + +func TestClaimVerifierVerify(t *testing.T) { + s, recipientPreImage, senderPreImage := mkScriptForClaims(t) + cv := &hashescrow.ClaimVerifier{Script: s} + + recipientRaw, err := json.Marshal(&hashescrow.ClaimSignature{Preimage: recipientPreImage}) + require.NoError(t, err) + require.NoError(t, cv.Verify([]byte("msg"), recipientRaw)) + + senderRaw, err := json.Marshal(&hashescrow.ClaimSignature{Preimage: senderPreImage}) + require.NoError(t, err) + require.NoError(t, cv.Verify([]byte("msg"), senderRaw)) + + wrongRaw, err := json.Marshal(&hashescrow.ClaimSignature{Preimage: []byte("wrong")}) + require.NoError(t, err) + require.Error(t, cv.Verify([]byte("msg"), wrongRaw)) + + require.Error(t, cv.Verify([]byte("msg"), []byte("bad-json"))) + require.Error(t, (&hashescrow.ClaimVerifier{}).Verify([]byte("msg"), recipientRaw)) +} + +func TestVerifierVerify(t *testing.T) { + s, recipientPreImage, _ := mkScriptForClaims(t) + v := &hashescrow.Verifier{Script: s} + + sigma, err := json.Marshal(&hashescrow.ClaimSignature{Preimage: recipientPreImage}) + require.NoError(t, err) + require.NoError(t, v.Verify([]byte("msg"), sigma)) + + badSigma, err := json.Marshal(&hashescrow.ClaimSignature{Preimage: []byte("wrong")}) + require.NoError(t, err) + err = v.Verify([]byte("msg"), badSigma) + require.Error(t, err) + require.Contains(t, err.Error(), "failed verifying hash escrow claim signature") +} diff --git a/token/services/interop/hashescrow/transaction.go b/token/services/interop/hashescrow/transaction.go new file mode 100644 index 0000000000..69faa6ac71 --- /dev/null +++ b/token/services/interop/hashescrow/transaction.go @@ -0,0 +1,319 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package hashescrow + +import ( + "context" + "crypto" + "encoding/json" + + "github.com/hyperledger-labs/fabric-smart-client/pkg/utils/errors" + "github.com/hyperledger-labs/fabric-smart-client/platform/view/services/endpoint" + "github.com/hyperledger-labs/fabric-smart-client/platform/view/view" + "github.com/hyperledger-labs/fabric-token-sdk/token" + "github.com/hyperledger-labs/fabric-token-sdk/token/driver" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/identity" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/interop/encoding" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/ttx" + token2 "github.com/hyperledger-labs/fabric-token-sdk/token/token" +) + +const ( + ScriptType = driver.HashEscrowIdentityType + ScriptTypeString = driver.HashEscrowIdentityTypeString + HashEscrow = ScriptType +) + +// WithHash sets a hash attribute to customize the lock command. +// This is kept for backward compatibility and maps to recipient hash. +func WithHash(hash []byte) token.TransferOption { + return WithRecipientHash(hash) +} + +// WithRecipientHash sets recipient-claim hash attribute. +func WithRecipientHash(hash []byte) token.TransferOption { + return func(o *token.TransferOptions) error { + if o.Attributes == nil { + o.Attributes = map[any]any{} + } + o.Attributes["hashescrow.recipientHash"] = hash + + return nil + } +} + +// WithSenderHash sets sender-return hash attribute. +func WithSenderHash(hash []byte) token.TransferOption { + return func(o *token.TransferOptions) error { + if o.Attributes == nil { + o.Attributes = map[any]any{} + } + o.Attributes["hashescrow.senderHash"] = hash + + return nil + } +} + +// WithHashFunc sets a hash function attribute to customize the lock command. +func WithHashFunc(hashFunc crypto.Hash) token.TransferOption { + return func(o *token.TransferOptions) error { + if o.Attributes == nil { + o.Attributes = map[any]any{} + } + o.Attributes["hashescrow.hashFunc"] = hashFunc + + return nil + } +} + +// WithHashEncoding sets a hash encoding attribute to customize the lock command. +func WithHashEncoding(enc encoding.Encoding) token.TransferOption { + return func(o *token.TransferOptions) error { + if o.Attributes == nil { + o.Attributes = map[any]any{} + } + o.Attributes["hashescrow.hashEncoding"] = enc + + return nil + } +} + +func compileTransferOptions(opts ...token.TransferOption) (*token.TransferOptions, error) { + txOptions := &token.TransferOptions{} + for _, opt := range opts { + if err := opt(txOptions); err != nil { + return nil, err + } + } + + return txOptions, nil +} + +type Binder interface { + Bind(ctx context.Context, longTerm view.Identity, ephemeral ...view.Identity) error +} + +// Transaction holds a ttx transaction. +type Transaction struct { + *ttx.Transaction + Binder Binder +} + +func NewTransaction(sp view.Context, signer view.Identity, opts ...ttx.TxOption) (*Transaction, error) { + tx, err := ttx.NewTransaction(sp, signer, opts...) + if err != nil { + return nil, err + } + + return &Transaction{Transaction: tx, Binder: endpoint.GetService(sp)}, nil +} + +func NewAnonymousTransaction(sp view.Context, opts ...ttx.TxOption) (*Transaction, error) { + tx, err := ttx.NewAnonymousTransaction(sp, opts...) + if err != nil { + return nil, err + } + + return &Transaction{Transaction: tx, Binder: endpoint.GetService(sp)}, nil +} + +func NewTransactionFromBytes(ctx view.Context, network, channel string, raw []byte) (*Transaction, error) { + tx, err := ttx.NewTransactionFromBytes(ctx, raw) + if err != nil { + return nil, err + } + + return &Transaction{Transaction: tx, Binder: endpoint.GetService(ctx)}, nil +} + +// Lock appends a hash-based escrow lock action to the token request. +// This lock has no timeout semantics. +func (t *Transaction) Lock(ctx context.Context, wallet *token.OwnerWallet, sender view.Identity, typ token2.Type, value uint64, recipient view.Identity, opts ...token.TransferOption) ([]byte, error) { + options, err := compileTransferOptions(opts...) + if err != nil { + return nil, err + } + if recipient.IsNone() { + return nil, errors.Errorf("must specify a recipient") + } + if sender == nil { + sender, err = wallet.GetRecipientIdentity(ctx) + if err != nil { + return nil, errors.WithMessagef(err, "failed getting sender identity") + } + } + + var recipientHash []byte + var senderHash []byte + hashFunc := crypto.SHA256 + var hashEncoding encoding.Encoding + if options.Attributes != nil { + boxed, ok := options.Attributes["hashescrow.recipientHash"] + if ok { + recipientHash, ok = boxed.([]byte) + if !ok { + return nil, errors.Errorf("expected hashescrow.recipientHash attribute to be []byte, got [%T]", boxed) + } + } + boxed, ok = options.Attributes["hashescrow.senderHash"] + if ok { + senderHash, ok = boxed.([]byte) + if !ok { + return nil, errors.Errorf("expected hashescrow.senderHash attribute to be []byte, got [%T]", boxed) + } + } + boxed, ok = options.Attributes["hashescrow.hashFunc"] + if ok { + hashFunc, ok = boxed.(crypto.Hash) + if !ok { + return nil, errors.Errorf("expected hashescrow.hashFunc attribute to be crypto.Hash, got [%T]", boxed) + } + if hashFunc == 0 { + hashFunc = crypto.SHA256 + } + } + boxed, ok = options.Attributes["hashescrow.hashEncoding"] + if ok { + hashEncoding, ok = boxed.(encoding.Encoding) + if !ok { + return nil, errors.Errorf("expected hashescrow.hashEncoding attribute to be Encoding, got [%T]", boxed) + } + } + } + if len(recipientHash) == 0 { + return nil, errors.New("must specify recipient hash") + } + if len(senderHash) == 0 { + return nil, errors.New("must specify sender hash") + } + if hashFunc == 0 { + hashFunc = crypto.SHA256 + } + + scriptID, script, err := t.recipientAsScript(sender, recipient, recipientHash, senderHash, hashFunc, hashEncoding) + if err != nil { + return nil, err + } + lockValue, err := LockValue(script.RecipientHashInfo.Hash, script.SenderHashInfo.Hash) + if err != nil { + return nil, err + } + + transferOpts := append(opts, + token.WithTransferMetadata(LockKey(script.RecipientHashInfo.Hash, script.SenderHashInfo.Hash), lockValue), + ) + _, err = t.TokenRequest.Transfer( + t.Context, + wallet, + typ, + []uint64{value}, + []view.Identity{scriptID}, + transferOpts..., + ) + if err != nil { + return nil, err + } + + return nil, nil +} + +// Claim appends a claim action to the token request. +// Spending can be submitted by anyone; recipient is resolved from the preimage. +func (t *Transaction) Claim(wallet *token.OwnerWallet, tok *token2.UnspentToken, preImage []byte, opts ...token.TransferOption) error { + if len(preImage) == 0 { + return errors.New("preImage is nil") + } + + q, err := token2.ToQuantity(tok.Quantity, t.TokenRequest.TokenService.PublicParametersManager().PublicParameters().Precision()) + if err != nil { + return errors.Wrapf(err, "failed to convert quantity [%s]", tok.Quantity) + } + owner, err := identity.UnmarshalTypedIdentity(tok.Owner) + if err != nil { + return err + } + if owner.Type != HashEscrow { + return errors.New("invalid owner type, expected hash escrow script") + } + script := &Script{} + if err := json.Unmarshal(owner.Identity, script); err != nil { + return errors.New("failed to unmarshal TypedIdentity as a hash escrow script") + } + + claimIdentity, _, claimedBy, err := script.ResolveOwnerAndHashForPreimage(preImage) + if err != nil { + return errors.Wrap(err, "passed preImage does not match script hashes") + } + + sigService := t.TokenService().SigService() + if err := sigService.RegisterEphemeralSigner( + t.Context, + tok.Owner, + &ClaimSigner{ + Preimage: preImage, + }, + &ClaimVerifier{ + Script: script, + }, + ); err != nil { + return err + } + + if err := t.Binder.Bind(t.Context, claimIdentity, tok.Owner); err != nil { + return err + } + + claimValue, err := ClaimValue(preImage, claimedBy) + if err != nil { + return err + } + + return t.Transfer( + wallet, + tok.Type, + []uint64{q.ToBigInt().Uint64()}, + []view.Identity{claimIdentity}, + append(opts, + token.WithTokenIDs(&tok.Id), + token.WithTransferMetadata(ClaimKey(script.RecipientHashInfo.Hash, script.SenderHashInfo.Hash), claimValue), + )..., + ) +} + +func (t *Transaction) recipientAsScript(sender, recipient view.Identity, recipientHash []byte, senderHash []byte, hashFunc crypto.Hash, hashEncoding encoding.Encoding) (view.Identity, *Script, error) { + script := &Script{ + RecipientHashInfo: HashInfo{ + Hash: recipientHash, + HashFunc: hashFunc, + HashEncoding: hashEncoding, + }, + SenderHashInfo: HashInfo{ + Hash: senderHash, + HashFunc: hashFunc, + HashEncoding: hashEncoding, + }, + Recipient: recipient, + Sender: sender, + } + if err := script.Validate(); err != nil { + return nil, nil, err + } + rawScript, err := json.Marshal(script) + if err != nil { + return nil, nil, err + } + ro := &identity.TypedIdentity{ + Type: HashEscrow, + Identity: rawScript, + } + raw, err := ro.Bytes() + if err != nil { + return nil, nil, err + } + + return raw, script, nil +} diff --git a/token/services/interop/hashescrow/transaction_test.go b/token/services/interop/hashescrow/transaction_test.go new file mode 100644 index 0000000000..73af79a17a --- /dev/null +++ b/token/services/interop/hashescrow/transaction_test.go @@ -0,0 +1,57 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package hashescrow + +import ( + "crypto" + "testing" + + "github.com/hyperledger-labs/fabric-token-sdk/token/services/interop/encoding" + "github.com/stretchr/testify/require" +) + +func TestCompileTransferOptions(t *testing.T) { + opts, err := compileTransferOptions( + WithRecipientHash([]byte("h")), + WithSenderHash([]byte("h2")), + WithHashFunc(crypto.SHA512), + WithHashEncoding(encoding.Hex), + ) + require.NoError(t, err) + require.NotNil(t, opts.Attributes) + require.Equal(t, []byte("h"), opts.Attributes["hashescrow.recipientHash"]) + require.Equal(t, []byte("h2"), opts.Attributes["hashescrow.senderHash"]) + require.Equal(t, crypto.SHA512, opts.Attributes["hashescrow.hashFunc"]) + require.Equal(t, encoding.Hex, opts.Attributes["hashescrow.hashEncoding"]) +} + +func TestRecipientAsScript(t *testing.T) { + tx := &Transaction{} + sender := []byte("sender") + recipient := []byte("recipient") + + raw, script, err := tx.recipientAsScript(sender, recipient, []byte("recipient-hash"), []byte("sender-hash"), crypto.SHA256, encoding.Base64) + require.NoError(t, err) + require.NotNil(t, raw) + require.Equal(t, sender, []byte(script.Sender)) + require.Equal(t, recipient, []byte(script.Recipient)) + require.Equal(t, []byte("recipient-hash"), script.RecipientHashInfo.Hash) + require.Equal(t, []byte("sender-hash"), script.SenderHashInfo.Hash) +} + +func TestRecipientAsScriptBadHashInfo(t *testing.T) { + tx := &Transaction{} + _, _, err := tx.recipientAsScript( + []byte("sender"), + []byte("recipient"), + []byte("recipient-hash"), + []byte("sender-hash"), + crypto.SHA256, + encoding.Encoding(999), + ) + require.Error(t, err) +} diff --git a/token/services/interop/hashescrow/wallet.go b/token/services/interop/hashescrow/wallet.go new file mode 100644 index 0000000000..317666e638 --- /dev/null +++ b/token/services/interop/hashescrow/wallet.go @@ -0,0 +1,94 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package hashescrow + +import ( + "context" + + "github.com/hyperledger-labs/fabric-smart-client/pkg/utils/errors" + "github.com/hyperledger-labs/fabric-smart-client/platform/common/utils/collections/iterators" + "github.com/hyperledger-labs/fabric-smart-client/platform/view/view" + "github.com/hyperledger-labs/fabric-token-sdk/token" + "github.com/hyperledger-labs/fabric-token-sdk/token/driver" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/ttx" + token2 "github.com/hyperledger-labs/fabric-token-sdk/token/token" +) + +type QueryEngine interface { + UnspentTokensIteratorBy(ctx context.Context, id string, tokenType token2.Type) (driver.UnspentTokensIterator, error) +} + +type OwnerWallet struct { + wallet *token.OwnerWallet + queryEngine QueryEngine +} + +func (w *OwnerWallet) ListByPreImage(ctx context.Context, preImage []byte, opts ...token.ListTokensOption) (*token2.UnspentTokens, error) { + compiledOpts, err := token.CompileListTokensOption(opts...) + if err != nil { + return nil, errors.Wrapf(err, "failed to compile options") + } + + tokensByID := map[string]*token2.UnspentToken{} + for _, sender := range []bool{false, true} { + tokens, err := w.filter(ctx, compiledOpts.TokenType, sender, (&PreImageSelector{preImage: preImage}).Filter) + if err != nil { + return nil, err + } + for _, tok := range tokens.Tokens { + tokensByID[tok.Id.String()] = tok + } + } + + tokens := make([]*token2.UnspentToken, 0, len(tokensByID)) + for _, tok := range tokensByID { + tokens = append(tokens, tok) + } + + return &token2.UnspentTokens{Tokens: tokens}, nil +} + +func (w *OwnerWallet) filter(ctx context.Context, tokenType token2.Type, sender bool, selector SelectFunction) (*token2.UnspentTokens, error) { + it, err := w.filterIterator(ctx, tokenType, sender, selector) + if err != nil { + return nil, errors.Wrap(err, "token selection failed") + } + tokens, err := iterators.ReadAllPointers(it) + if err != nil { + return nil, err + } + + return &token2.UnspentTokens{Tokens: tokens}, nil +} + +func (w *OwnerWallet) filterIterator(ctx context.Context, tokenType token2.Type, sender bool, selector SelectFunction) (iterators.Iterator[*token2.UnspentToken], error) { + walletID := recipientWallet(w.wallet) + if sender { + walletID = senderWallet(w.wallet) + } + it, err := w.queryEngine.UnspentTokensIteratorBy(ctx, walletID, tokenType) + if err != nil { + return nil, errors.WithMessagef(err, "failed to get iterator over unspent tokens") + } + + return iterators.Filter(it, IsScript(selector)), nil +} + +func GetWallet(context view.Context, id string, opts ...token.ServiceOption) *token.OwnerWallet { + return ttx.GetWallet(context, id, opts...) +} + +func Wallet(wallet *token.OwnerWallet) *OwnerWallet { + if wallet == nil { + return nil + } + + return &OwnerWallet{ + wallet: wallet, + queryEngine: wallet.TMS().Vault().NewQueryEngine(), + } +} diff --git a/token/services/interop/hashescrow/wallet_filter.go b/token/services/interop/hashescrow/wallet_filter.go new file mode 100644 index 0000000000..ed1fba9c0e --- /dev/null +++ b/token/services/interop/hashescrow/wallet_filter.go @@ -0,0 +1,72 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package hashescrow + +import ( + "encoding/json" + + "github.com/hyperledger-labs/fabric-smart-client/platform/common/utils/collections/iterators" + "github.com/hyperledger-labs/fabric-smart-client/platform/view/view" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/identity" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/logging" + "github.com/hyperledger-labs/fabric-token-sdk/token/token" +) + +type SelectFunction = func(*token.UnspentToken, *Script) (bool, error) + +type PreImageSelector struct { + preImage []byte +} + +func (f *PreImageSelector) Filter(tok *token.UnspentToken, script *Script) (bool, error) { + logger.Debugf("token [%s,%s,%s,%s] contains a hash escrow script? Yes", tok.Id, view.Identity(tok.Owner).UniqueID(), tok.Type, tok.Quantity) + + _, resolvedHash, _, err := script.ResolveOwnerAndHashForPreimage(f.preImage) + if err != nil { + logger.Debugf("hash escrow script does not match pre-image [%s]: [%s]", logging.Base64(f.preImage), err) + + return false, nil + } + logger.Debugf("hash escrow script matches pre-image [%s] with hash [%s]", logging.Base64(f.preImage), logging.Base64(resolvedHash)) + + return true, nil +} + +func IsScript(selector SelectFunction) iterators.Predicate[*token.UnspentToken] { + return func(tok *token.UnspentToken) bool { + owner, err := identity.UnmarshalTypedIdentity(tok.Owner) + if err != nil { + logger.Debugf("Is Mine [%s,%s,%s]? No, failed unmarshalling [%s]", view.Identity(tok.Owner), tok.Type, tok.Quantity, err) + + return false + } + if owner.Type != HashEscrow { + return false + } + + script := &Script{} + if err := json.Unmarshal(owner.Identity, script); err != nil { + logger.Debugf("token [%s,%s,%s,%s] contains a hash escrow script? No", tok.Id, view.Identity(tok.Owner).UniqueID(), tok.Type, tok.Quantity) + + return false + } + if err := script.Validate(); err != nil { + logger.Debugf("token [%s,%s,%s,%s] contains an invalid hash escrow script: [%s]", tok.Id, view.Identity(tok.Owner).UniqueID(), tok.Type, tok.Quantity, err) + + return false + } + + pickItem, err := selector(tok, script) + if err != nil { + logger.Errorf("failed to select (token,script)[%v:%v] pair: %w", tok, script, err) + + return false + } + + return pickItem + } +}