From 13f5dd30c1b777689f16444b963c44db8570d264 Mon Sep 17 00:00:00 2001 From: Angelo De Caro Date: Fri, 24 May 2024 12:52:37 +0200 Subject: [PATCH] asset transfer Signed-off-by: Angelo De Caro --- .github/workflows/tests.yml | 2 + Makefile | 9 +- integration/nwo/token/topology.go | 8 - integration/ports.go | 2 + integration/token/interop/dlog/dlog_test.go | 18 + .../token/interop/fabtoken/fabtoken_test.go | 19 + integration/token/interop/support.go | 161 +++++++ integration/token/interop/tests.go | 136 ++++++ integration/token/interop/topology.go | 123 +++++ integration/token/interop/views/auditor.go | 42 +- .../token/interop/views/pledge/claim.go | 150 ++++++ .../token/interop/views/pledge/fast_pledge.go | 328 +++++++++++++ .../token/interop/views/pledge/pledge.go | 156 ++++++ .../token/interop/views/pledge/reclaim.go | 114 +++++ .../token/interop/views/pledge/redeem.go | 77 +++ .../token/interop/views/pledge/scan.go | 56 +++ interop.mk | 8 + token/core/common/interop/pledge/metadata.go | 53 ++ token/core/common/logging/logger.go | 4 + token/core/fabtoken/driver/deserializer.go | 8 +- token/core/fabtoken/driver/driver.go | 11 +- .../driver/interop/state/fabric/state.go | 454 ++++++++++++++++++ token/core/fabtoken/issue.go | 38 +- token/core/fabtoken/transfer.go | 1 + token/core/fabtoken/validator.go | 2 + token/core/fabtoken/validator_pledge.go | 143 ++++++ token/core/fabtoken/validator_transfer.go | 9 +- token/core/zkatdlog/crypto/audit/auditor.go | 51 +- .../zkatdlog/crypto/validator/validator.go | 2 + .../crypto/validator/validator_pledge.go | 143 ++++++ .../crypto/validator/validator_transfer.go | 9 +- .../core/zkatdlog/nogh/driver/deserializer.go | 10 +- token/core/zkatdlog/nogh/driver/driver.go | 11 +- .../nogh/driver/interop/state/fabric/state.go | 400 +++++++++++++++ token/core/zkatdlog/nogh/issue.go | 14 + token/driver/tms.go | 16 + token/sdk/sdk.go | 17 +- token/sdk/tms/tms.go | 1 + token/services/identity/deserializer/typed.go | 9 +- token/services/interop/audit.go | 139 ++++++ token/services/interop/htlc/audit.go | 95 ---- .../interop/htlc/deserializer.go | 31 +- .../{identity => }/interop/htlc/info.go | 11 +- token/services/interop/htlc/script.go | 16 +- token/services/interop/htlc/signer.go | 10 +- .../{identity => }/interop/htlc/validator.go | 21 +- token/services/interop/pledge/accept.go | 17 + token/services/interop/pledge/approve.go | 275 +++++++++++ token/services/interop/pledge/claim.go | 256 ++++++++++ token/services/interop/pledge/deserializer.go | 164 +++++++ token/services/interop/pledge/distribute.go | 158 ++++++ token/services/interop/pledge/endorsement.go | 65 +++ token/services/interop/pledge/finality.go | 17 + token/services/interop/pledge/logger.go | 11 + token/services/interop/pledge/ordering.go | 17 + token/services/interop/pledge/pledge.go | 99 ++++ token/services/interop/pledge/recipients.go | 179 +++++++ token/services/interop/pledge/reclaim.go | 90 ++++ token/services/interop/pledge/redeem.go | 93 ++++ token/services/interop/pledge/scanner.go | 62 +++ token/services/interop/pledge/script.go | 55 +++ token/services/interop/pledge/signer.go | 105 ++++ token/services/interop/pledge/state.go | 207 ++++++++ token/services/interop/pledge/store.go | 108 +++++ token/services/interop/pledge/stream.go | 71 +++ token/services/interop/pledge/transaction.go | 56 +++ token/services/interop/pledge/wallet.go | 213 ++++++++ token/services/interop/state/driver/driver.go | 54 +++ token/services/interop/state/fabric/driver.go | 41 ++ token/services/interop/state/fabric/ssp.go | 169 +++++++ token/services/interop/state/fabric/url.go | 38 ++ token/services/interop/state/fabric/utils.go | 25 + token/services/interop/state/state.go | 114 +++++ .../services/network/common/rws/keys/keys.go | 25 + .../network/common/rws/translator/prover.go | 128 +++++ .../common/rws/translator/translator.go | 1 + token/services/network/driver/network.go | 4 + token/services/network/fabric/network.go | 9 + token/services/network/fabric/tcc/tcc.go | 104 ++++ token/services/network/network.go | 8 + token/services/network/orion/network.go | 5 +- token/services/ttx/finality.go | 2 +- token/services/ttx/opts.go | 2 +- token/services/ttx/ordering.go | 2 +- token/services/ttx/recipients.go | 4 +- token/services/ttx/transaction.go | 6 +- token/tms.go | 20 +- 87 files changed, 5994 insertions(+), 223 deletions(-) create mode 100644 integration/token/interop/views/pledge/claim.go create mode 100644 integration/token/interop/views/pledge/fast_pledge.go create mode 100644 integration/token/interop/views/pledge/pledge.go create mode 100644 integration/token/interop/views/pledge/reclaim.go create mode 100644 integration/token/interop/views/pledge/redeem.go create mode 100644 integration/token/interop/views/pledge/scan.go create mode 100644 token/core/common/interop/pledge/metadata.go create mode 100644 token/core/fabtoken/driver/interop/state/fabric/state.go create mode 100644 token/core/fabtoken/validator_pledge.go create mode 100644 token/core/zkatdlog/crypto/validator/validator_pledge.go create mode 100644 token/core/zkatdlog/nogh/driver/interop/state/fabric/state.go create mode 100644 token/services/interop/audit.go delete mode 100644 token/services/interop/htlc/audit.go rename token/services/{identity => }/interop/htlc/deserializer.go (79%) rename token/services/{identity => }/interop/htlc/info.go (76%) rename token/services/{identity => }/interop/htlc/validator.go (82%) create mode 100644 token/services/interop/pledge/accept.go create mode 100644 token/services/interop/pledge/approve.go create mode 100644 token/services/interop/pledge/claim.go create mode 100644 token/services/interop/pledge/deserializer.go create mode 100644 token/services/interop/pledge/distribute.go create mode 100644 token/services/interop/pledge/endorsement.go create mode 100644 token/services/interop/pledge/finality.go create mode 100644 token/services/interop/pledge/logger.go create mode 100644 token/services/interop/pledge/ordering.go create mode 100644 token/services/interop/pledge/pledge.go create mode 100644 token/services/interop/pledge/recipients.go create mode 100644 token/services/interop/pledge/reclaim.go create mode 100644 token/services/interop/pledge/redeem.go create mode 100644 token/services/interop/pledge/scanner.go create mode 100644 token/services/interop/pledge/script.go create mode 100644 token/services/interop/pledge/signer.go create mode 100644 token/services/interop/pledge/state.go create mode 100644 token/services/interop/pledge/store.go create mode 100644 token/services/interop/pledge/stream.go create mode 100644 token/services/interop/pledge/transaction.go create mode 100644 token/services/interop/pledge/wallet.go create mode 100644 token/services/interop/state/driver/driver.go create mode 100644 token/services/interop/state/fabric/driver.go create mode 100644 token/services/interop/state/fabric/ssp.go create mode 100644 token/services/interop/state/fabric/url.go create mode 100644 token/services/interop/state/fabric/utils.go create mode 100644 token/services/interop/state/state.go create mode 100644 token/services/network/common/rws/translator/prover.go diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index c338e5a662..b1292eaad9 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -107,12 +107,14 @@ jobs: interop-fabtoken-t4, interop-fabtoken-t5, interop-fabtoken-t6, + interop-fabtoken-t7, interop-dlog-t1, interop-dlog-t2, interop-dlog-t3, interop-dlog-t4, interop-dlog-t5, interop-dlog-t6, + interop-dlog-t7, ] steps: diff --git a/Makefile b/Makefile index d3daa2d601..9eb752844e 100644 --- a/Makefile +++ b/Makefile @@ -3,6 +3,7 @@ FABRIC_VERSION ?= 2.5.0 FABRIC_CA_VERSION ?= 1.5.7 FABRIC_TWO_DIGIT_VERSION = $(shell echo $(FABRIC_VERSION) | cut -d '.' -f 1,2) ORION_VERSION=v0.2.5 +WEAVER_VERSION=1.2.1 # need to install fabric binaries outside of fts tree for now (due to chaincode packaging issues) FABRIC_BINARY_BASE=$(PWD)/../fabric @@ -49,7 +50,7 @@ install-softhsm: ./ci/scripts/install_softhsm.sh .PHONY: docker-images -docker-images: fabric-docker-images orion-server-images monitoring-docker-images +docker-images: fabric-docker-images orion-server-images monitoring-docker-images weaver-docker-images .PHONY: fabric-docker-images fabric-docker-images: @@ -70,6 +71,12 @@ orion-server-images: docker pull orionbcdb/orion-server:$(ORION_VERSION) docker image tag orionbcdb/orion-server:$(ORION_VERSION) orionbcdb/orion-server:latest +.PHONY: weaver-docker-images +weaver-docker-images: + docker pull ghcr.io/hyperledger-labs/weaver-fabric-driver:$(WEAVER_VERSION) + docker image tag ghcr.io/hyperledger-labs/weaver-fabric-driver:$(WEAVER_VERSION) hyperledger-labs/weaver-fabric-driver:latest + docker pull ghcr.io/hyperledger-labs/weaver-relay-server:$(WEAVER_VERSION) + docker image tag ghcr.io/hyperledger-labs/weaver-relay-server:$(WEAVER_VERSION) hyperledger-labs/weaver-relay-server:latest .PHONY: integration-tests-nft-dlog integration-tests-nft-dlog: diff --git a/integration/nwo/token/topology.go b/integration/nwo/token/topology.go index c71ad7cd14..8264b4ca46 100755 --- a/integration/nwo/token/topology.go +++ b/integration/nwo/token/topology.go @@ -7,9 +7,7 @@ SPDX-License-Identifier: Apache-2.0 package token import ( - "github.com/hyperledger-labs/fabric-smart-client/integration/nwo/fsc" "github.com/hyperledger-labs/fabric-smart-client/integration/nwo/fsc/node" - "github.com/hyperledger-labs/fabric-smart-client/pkg/api" "github.com/hyperledger-labs/fabric-token-sdk/integration/nwo/token/topology" "github.com/hyperledger-labs/fabric-token-sdk/token/services/ttx" . "github.com/onsi/gomega" @@ -83,12 +81,6 @@ func (t *Topology) AddTMS(fscNodes []*node.Node, backend BackedTopology, channel return tms } -func (t *Topology) SetSDK(fscTopology *fsc.Topology, sdk api.SDK) { - for _, node := range fscTopology.Nodes { - node.AddSDK(sdk) - } -} - func (t *Topology) GetTMSs() []*topology.TMS { return t.TMSs } diff --git a/integration/ports.go b/integration/ports.go index 5d4d21b5fd..b8ee7271b6 100755 --- a/integration/ports.go +++ b/integration/ports.go @@ -47,6 +47,8 @@ const ( ZKATDLogInteropHTLCSwapNoCrossTwoFabricNetworks ZKATDLogInteropHTLCOrion ZKATDLogInteropHTLCSwapNoCrossWithOrionAndFabricNetworks + FabTokenInteropAssetTransfer + ZKATDLogInteropAssetTransfer ) // StartPortForNode On linux, the default ephemeral port range is 32768-60999 and can be diff --git a/integration/token/interop/dlog/dlog_test.go b/integration/token/interop/dlog/dlog_test.go index 01faee7fa2..f61e4e8e29 100644 --- a/integration/token/interop/dlog/dlog_test.go +++ b/integration/token/interop/dlog/dlog_test.go @@ -145,4 +145,22 @@ var _ = Describe("DLog end to end", func() { }) }) + Describe("Asset Transfer With Two Fabric Networks", func() { + BeforeEach(func() { + var err error + ii, err = integration.New( + integration2.ZKATDLogInteropAssetTransfer.StartPortForNode(), + "", + interop.AssetTransferTopology("dlog", &fabric3.SDK{}, &sdk.SDK{})..., + ) + Expect(err).NotTo(HaveOccurred()) + ii.RegisterPlatformFactory(token.NewPlatformFactory()) + ii.Generate() + ii.Start() + }) + + It("Performed a cross network asset transfer", func() { + interop.TestAssetTransferWithTwoNetworks(ii) + }) + }) }) diff --git a/integration/token/interop/fabtoken/fabtoken_test.go b/integration/token/interop/fabtoken/fabtoken_test.go index 866c824d72..1d16afc8ea 100644 --- a/integration/token/interop/fabtoken/fabtoken_test.go +++ b/integration/token/interop/fabtoken/fabtoken_test.go @@ -145,4 +145,23 @@ var _ = Describe("FabToken end to end", func() { }) }) + Describe("Asset Transfer With Two Fabric Networks", func() { + BeforeEach(func() { + var err error + ii, err = integration.New( + integration2.FabTokenInteropAssetTransfer.StartPortForNode(), + "", + interop.AssetTransferTopology("fabtoken", &fabric3.SDK{}, &sdk.SDK{})..., + ) + Expect(err).NotTo(HaveOccurred()) + ii.RegisterPlatformFactory(token.NewPlatformFactory()) + ii.Generate() + ii.Start() + }) + + It("Performed a cross network asset transfer", func() { + interop.TestAssetTransferWithTwoNetworks(ii) + }) + }) + }) diff --git a/integration/token/interop/support.go b/integration/token/interop/support.go index f1a1b157c5..6f05725fef 100644 --- a/integration/token/interop/support.go +++ b/integration/token/interop/support.go @@ -15,10 +15,12 @@ import ( "github.com/hyperledger-labs/fabric-smart-client/integration" "github.com/hyperledger-labs/fabric-smart-client/integration/nwo/common" + "github.com/hyperledger-labs/fabric-smart-client/pkg/api" 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" "github.com/hyperledger-labs/fabric-token-sdk/integration/token/interop/views/htlc" + "github.com/hyperledger-labs/fabric-token-sdk/integration/token/interop/views/pledge" "github.com/hyperledger-labs/fabric-token-sdk/token" token2 "github.com/hyperledger-labs/fabric-token-sdk/token/token" . "github.com/onsi/gomega" @@ -422,3 +424,162 @@ func scanWithError(network *integration.Infrastructure, id string, hash []byte, Expect(err.Error()).To(ContainSubstring(msg)) } } + +func Pledge(network *integration.Infrastructure, sender, wallet, typ string, amount uint64, receiver, issuer, destNetwork string, deadline time.Duration, opts ...token.ServiceOption) (string, string) { + options, err := token.CompileServiceOptions(opts...) + Expect(err).NotTo(HaveOccurred()) + raw, err := network.Client(sender).CallView("transfer.pledge", common.JSONMarshall(&pledge.Pledge{ + OriginTMSID: options.TMSID(), + Amount: amount, + ReclamationDeadline: deadline, + Type: typ, + DestinationNetworkURL: destNetwork, + Issuer: network.Identity(issuer), + Recipient: network.Identity(receiver), + OriginWallet: wallet, + })) + Expect(err).NotTo(HaveOccurred()) + info := &pledge.Result{} + common.JSONUnmarshal(raw.([]byte), info) + Expect(network.Client(sender).IsTxFinal( + info.TxID, + api.WithNetwork(options.TMSID().Network), + api.WithChannel(options.TMSID().Channel), + )).NotTo(HaveOccurred()) + + return info.TxID, info.PledgeID +} + +func PledgeIDExists(network *integration.Infrastructure, id, pledgeid string, startingTransactionID string, opts ...token.ServiceOption) { + options, err := token.CompileServiceOptions(opts...) + Expect(err).NotTo(HaveOccurred()) + raw, err := network.Client(id).CallView("transfer.scan", common.JSONMarshall(&pledge.Scan{ + TMSID: options.TMSID(), + Timeout: 120 * time.Second, + PledgeID: pledgeid, + StartingTransactionID: startingTransactionID, + })) + Expect(err).NotTo(HaveOccurred()) + var res bool + common.JSONUnmarshal(raw.([]byte), &res) + Expect(res).Should(BeTrue()) +} + +func ScanPledgeIDWithError(network *integration.Infrastructure, id, pledgeid string, startingTransactionID string, errorMsgs []string, opts ...token.ServiceOption) { + options, err := token.CompileServiceOptions(opts...) + Expect(err).NotTo(HaveOccurred()) + _, err = network.Client(id).CallView("transfer.scan", common.JSONMarshall(&pledge.Scan{ + TMSID: options.TMSID(), + Timeout: 120 * time.Second, + PledgeID: pledgeid, + StartingTransactionID: startingTransactionID, + })) + Expect(err).To(HaveOccurred()) + for _, msg := range errorMsgs { + Expect(err.Error()).To(ContainSubstring(msg)) + } +} + +func Reclaim(network *integration.Infrastructure, sender string, wallet string, txid string, opts ...token.ServiceOption) string { + options, err := token.CompileServiceOptions(opts...) + Expect(err).NotTo(HaveOccurred()) + txID, err := network.Client(sender).CallView("transfer.reclaim", common.JSONMarshall(&pledge.Reclaim{ // TokenID contains the identifier of the token to be reclaimed. + TokenID: &token2.ID{TxId: txid, Index: 0}, + WalletID: wallet, + TMSID: options.TMSID(), + })) + Expect(err).NotTo(HaveOccurred()) + Expect(network.Client("alice").IsTxFinal(common.JSONUnmarshalString(txID))).NotTo(HaveOccurred()) + + return common.JSONUnmarshalString(txID) +} + +func ReclaimWithError(network *integration.Infrastructure, sender string, wallet string, txid string, opts ...token.ServiceOption) { + options, err := token.CompileServiceOptions(opts...) + Expect(err).NotTo(HaveOccurred()) + _, err = network.Client(sender).CallView("transfer.reclaim", common.JSONMarshall(&pledge.Reclaim{ // TokenID contains the identifier of the token to be reclaimed. + TokenID: &token2.ID{TxId: txid, Index: 0}, + WalletID: wallet, + TMSID: options.TMSID(), + })) + Expect(err).To(HaveOccurred()) +} + +func Claim(network *integration.Infrastructure, recipient string, issuer string, originTokenID *token2.ID) string { + txid, err := network.Client(recipient).CallView("transfer.claim", common.JSONMarshall(&pledge.Claim{ + OriginTokenID: originTokenID, + Issuer: issuer, + })) + Expect(err).NotTo(HaveOccurred()) + Expect(network.Client(recipient).IsTxFinal(common.JSONUnmarshalString(txid))).NotTo(HaveOccurred()) + + return common.JSONUnmarshalString(txid) +} + +func ClaimWithError(network *integration.Infrastructure, recipient string, issuer string, originTokenID *token2.ID) { + _, err := network.Client(recipient).CallView("transfer.claim", common.JSONMarshall(&pledge.Claim{ + OriginTokenID: originTokenID, + Issuer: issuer, + })) + Expect(err).To(HaveOccurred()) +} + +func RedeemWithTMS(network *integration.Infrastructure, issuer string, tokenID *token2.ID, opts ...token.ServiceOption) string { + options, err := token.CompileServiceOptions(opts...) + Expect(err).NotTo(HaveOccurred()) + txID, err := network.Client(issuer).CallView("transfer.redeem", common.JSONMarshall(&pledge.Redeem{ + TokenID: tokenID, + TMSID: options.TMSID(), + })) + Expect(err).NotTo(HaveOccurred()) + Expect(network.Client(issuer).IsTxFinal(common.JSONUnmarshalString(txID))).NotTo(HaveOccurred()) + + return common.JSONUnmarshalString(txID) +} + +func RedeemWithTMSAndError(network *integration.Infrastructure, issuer string, tokenID *token2.ID, opt token.ServiceOption, errorMsgs ...string) { + options, err := token.CompileServiceOptions(opt) + Expect(err).NotTo(HaveOccurred()) + _, err = network.Client(issuer).CallView("transfer.redeem", common.JSONMarshall(&pledge.Redeem{ + TokenID: tokenID, + TMSID: options.TMSID(), + })) + Expect(err).To(HaveOccurred()) + for _, msg := range errorMsgs { + Expect(err.Error()).To(ContainSubstring(msg)) + } +} + +func FastTransferPledgeClaim(network *integration.Infrastructure, sender, wallet, typ string, amount uint64, receiver, issuer, destNetwork string, deadline time.Duration, opts ...token.ServiceOption) { + options, err := token.CompileServiceOptions(opts...) + Expect(err).NotTo(HaveOccurred()) + + _, err = network.Client(sender).CallView("transfer.fastTransfer", common.JSONMarshall(&pledge.FastPledgeClaim{ + OriginTMSID: options.TMSID(), + Amount: amount, + ReclamationDeadline: deadline, + Type: typ, + DestinationNetworkURL: destNetwork, + Issuer: network.Identity(issuer), + Recipient: network.Identity(receiver), + OriginWallet: wallet, + })) + Expect(err).NotTo(HaveOccurred()) +} + +func FastTransferPledgeReclaim(network *integration.Infrastructure, sender, wallet, typ string, amount uint64, receiver, issuer, destNetwork string, deadline time.Duration, opts ...token.ServiceOption) { + options, err := token.CompileServiceOptions(opts...) + Expect(err).NotTo(HaveOccurred()) + + _, err = network.Client(sender).CallView("transfer.fastPledgeReclaim", common.JSONMarshall(&pledge.FastPledgeReClaim{ + OriginTMSID: options.TMSID(), + Amount: amount, + ReclamationDeadline: deadline, + Type: typ, + DestinationNetworkURL: destNetwork, + Issuer: network.Identity(issuer), + Recipient: network.Identity(receiver), + OriginWallet: wallet, + })) + Expect(err).NotTo(HaveOccurred()) +} diff --git a/integration/token/interop/tests.go b/integration/token/interop/tests.go index d135fb7709..bcc0ad9239 100644 --- a/integration/token/interop/tests.go +++ b/integration/token/interop/tests.go @@ -19,6 +19,8 @@ import ( "github.com/hyperledger-labs/fabric-token-sdk/token" "github.com/hyperledger-labs/fabric-token-sdk/token/services/auditor" "github.com/hyperledger-labs/fabric-token-sdk/token/services/interop/htlc" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/interop/state/fabric" + token2 "github.com/hyperledger-labs/fabric-token-sdk/token/token" . "github.com/onsi/gomega" "github.com/pkg/errors" ) @@ -321,3 +323,137 @@ func TestFastExchange(network *integration.Infrastructure) { CheckBalance(network, "alice", "", "USD", 10, token.WithTMSID(beta)) CheckBalance(network, "bob", "", "USD", 20, token.WithTMSID(beta)) } + +func TestAssetTransferWithTwoNetworks(network *integration.Infrastructure) { + // give some time to the nodes to get the public parameters + time.Sleep(10 * time.Second) + + alpha := token.TMSID{ + Network: "alpha", + Channel: "testchannel", + Namespace: "tns", + } + beta := token.TMSID{ + Network: "beta", + Channel: "testchannel", + Namespace: "tns", + } + + alphaURL := fabric.FabricURL(alpha) + betaURL := fabric.FabricURL(beta) + + RegisterAuditor(network, token.WithTMSID(alpha)) + RegisterAuditor(network, token.WithTMSID(beta)) + + IssueCashWithTMS(network, alpha, "issuerAlpha", "", "USD", 50, "alice") + IssueCashWithTMS(network, alpha, "issuerAlpha", "", "EUR", 10, "alice") + CheckBalance(network, "alice", "", "USD", 50) + CheckBalance(network, "alice", "", "EUR", 10) + + // Pledge + Claim + txid, pledgeid := Pledge(network, "alice", "", "USD", 50, "bob", "issuerAlpha", betaURL, time.Minute*1, token.WithTMSID(alpha)) + CheckBalance(network, "alice", "", "USD", 0) + CheckBalance(network, "bob", "", "USD", 0) + + PledgeIDExists(network, "alice", pledgeid, txid, token.WithTMSID(alpha)) + + Claim(network, "bob", "issuerBeta", &token2.ID{TxId: txid, Index: 0}) + CheckBalance(network, "alice", "", "USD", 0) + CheckBalance(network, "bob", "", "USD", 50) + + time.Sleep(time.Minute * 1) + RedeemWithTMS(network, "issuerAlpha", &token2.ID{TxId: txid, Index: 0}, token.WithTMSID(alpha)) + CheckBalance(network, "alice", "", "USD", 0) + CheckBalance(network, "bob", "", "USD", 50) + + // Pledge + Reclaim + + IssueCashWithTMS(network, alpha, "issuerAlpha", "", "USD", 50, "alice") + CheckBalance(network, "alice", "", "USD", 50, token.WithTMSID(alpha)) + txid, _ = Pledge(network, "alice", "", "USD", 50, "bob", "issuerAlpha", betaURL, time.Second*10, token.WithTMSID(alpha)) + + time.Sleep(time.Second * 15) + Reclaim(network, "alice", "", txid, token.WithTMSID(alpha)) + CheckBalance(network, "alice", "", "USD", 50, token.WithTMSID(alpha)) + CheckBalance(network, "bob", "", "USD", 50) + + RedeemWithTMSAndError(network, "issuerAlpha", &token2.ID{TxId: txid, Index: 0}, token.WithTMSID(alpha)) + CheckBalance(network, "alice", "", "USD", 50, token.WithTMSID(alpha)) + CheckBalance(network, "bob", "", "USD", 50) + + ScanPledgeIDWithError(network, "alice", pledgeid, txid, []string{"timeout reached"}, token.WithTMSID(alpha)) + + // Try to reclaim again + + ReclaimWithError(network, "alice", "", txid, token.WithTMSID(alpha)) + CheckBalance(network, "alice", "", "USD", 50, token.WithTMSID(alpha)) + CheckBalance(network, "bob", "", "USD", 50) + + // Try to claim after reclaim + + ClaimWithError(network, "bob", "issuerBeta", &token2.ID{TxId: txid, Index: 0}) + CheckBalance(network, "alice", "", "USD", 50, token.WithTMSID(alpha)) + CheckBalance(network, "bob", "", "USD", 50) + + // Try to reclaim after claim + + txid, _ = Pledge(network, "alice", "", "USD", 10, "bob", "issuerAlpha", betaURL, time.Minute*1, token.WithTMSID(alpha)) + CheckBalance(network, "alice", "", "USD", 40) + CheckBalance(network, "bob", "", "USD", 50) + + Claim(network, "bob", "issuerBeta", &token2.ID{TxId: txid, Index: 0}) + CheckBalance(network, "alice", "", "USD", 40) + CheckBalance(network, "bob", "", "USD", 60) + + time.Sleep(time.Minute * 1) + + ReclaimWithError(network, "alice", "", txid, token.WithTMSID(alpha)) + CheckBalance(network, "alice", "", "USD", 40) + CheckBalance(network, "bob", "", "USD", 60) + + // Try to claim after claim + + txid, pledgeid = Pledge(network, "bob", "", "USD", 5, "alice", "issuerBeta", alphaURL, time.Minute*1, token.WithTMSID(beta)) + CheckBalance(network, "alice", "", "USD", 40) + CheckBalance(network, "bob", "", "USD", 55) + + PledgeIDExists(network, "bob", pledgeid, txid, token.WithTMSID(beta)) + + Claim(network, "alice", "issuerAlpha", &token2.ID{TxId: txid, Index: 0}) + CheckBalance(network, "alice", "", "USD", 45) + CheckBalance(network, "bob", "", "USD", 55) + + ClaimWithError(network, "alice", "issuerAlpha", &token2.ID{TxId: txid, Index: 0}) + CheckBalance(network, "alice", "", "USD", 45) + CheckBalance(network, "bob", "", "USD", 55) + + time.Sleep(1 * time.Minute) + RedeemWithTMS(network, "issuerBeta", &token2.ID{TxId: txid, Index: 0}, token.WithTMSID(beta)) + CheckBalance(network, "alice", "", "USD", 45) + CheckBalance(network, "bob", "", "USD", 55) + + // Try to redeem again + RedeemWithTMSAndError(network, "issuerBeta", &token2.ID{TxId: txid, Index: 0}, token.WithTMSID(beta), "failed to retrieve pledged token during redeem") + CheckBalance(network, "alice", "", "USD", 45) + CheckBalance(network, "bob", "", "USD", 55) + + // Try to claim or reclaim without pledging + + ClaimWithError(network, "alice", "issuerAlpha", &token2.ID{TxId: "", Index: 0}) + CheckBalance(network, "alice", "", "USD", 45) + CheckBalance(network, "bob", "", "USD", 55) + + ReclaimWithError(network, "alice", "", "", token.WithTMSID(alpha)) + CheckBalance(network, "alice", "", "USD", 45) + CheckBalance(network, "bob", "", "USD", 55) + + // Fast Pledge + Claim + FastTransferPledgeClaim(network, "alice", "", "USD", 10, "bob", "issuerAlpha", betaURL, time.Minute*1, token.WithTMSID(alpha)) + CheckBalance(network, "alice", "", "USD", 35) + CheckBalance(network, "bob", "", "USD", 65) + + // Fast Pledge + Reclaim + FastTransferPledgeReclaim(network, "alice", "", "USD", 10, "bob", "issuerAlpha", betaURL, time.Second*5, token.WithTMSID(alpha)) + CheckBalance(network, "alice", "", "USD", 35) + CheckBalance(network, "bob", "", "USD", 65) +} diff --git a/integration/token/interop/topology.go b/integration/token/interop/topology.go index e925c1cdc2..a53909836e 100644 --- a/integration/token/interop/topology.go +++ b/integration/token/interop/topology.go @@ -11,6 +11,7 @@ import ( "github.com/hyperledger-labs/fabric-smart-client/integration/nwo/fabric" "github.com/hyperledger-labs/fabric-smart-client/integration/nwo/fsc" "github.com/hyperledger-labs/fabric-smart-client/integration/nwo/orion" + "github.com/hyperledger-labs/fabric-smart-client/integration/nwo/weaver" api2 "github.com/hyperledger-labs/fabric-smart-client/pkg/api" fabric3 "github.com/hyperledger-labs/fabric-smart-client/platform/fabric/sdk" orion3 "github.com/hyperledger-labs/fabric-smart-client/platform/orion/sdk" @@ -22,6 +23,7 @@ import ( "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/htlc" + "github.com/hyperledger-labs/fabric-token-sdk/integration/token/interop/views/pledge" sdk "github.com/hyperledger-labs/fabric-token-sdk/token/sdk" ) @@ -529,3 +531,124 @@ func HTLCNoCrossClaimWithOrionTopology(tokenSDKDriver string, sdks ...api2.SDK) } return []api.Topology{f1Topology, orionTopology, tokenTopology, fscTopology} } + +func AssetTransferTopology(tokenSDKDriver string, sdks ...api2.SDK) []api.Topology { + // Define two Fabric topologies + f1Topology := fabric.NewTopologyWithName("alpha").SetDefault() + f1Topology.EnableIdemix() + f1Topology.AddOrganizationsByName("Org1", "Org2") + f1Topology.SetNamespaceApproverOrgs("Org1") + + f2Topology := fabric.NewTopologyWithName("beta") + f2Topology.EnableIdemix() + f2Topology.AddOrganizationsByName("Org3", "Org4") + f2Topology.SetNamespaceApproverOrgs("Org3") + + // FSC + fscTopology := fsc.NewTopology() + fscTopology.SetLogging("token-sdk=debug:fabric-sdk=debug:info", "") + + wTopology := weaver.NewTopology() + wTopology.AddRelayServer(f1Topology, "Org1").AddFabricNetwork(f2Topology) + wTopology.AddRelayServer(f2Topology, "Org3").AddFabricNetwork(f1Topology) + + issuerAlpha := fscTopology.AddNodeByName("issuerAlpha").AddOptions( + fabric.WithNetworkOrganization("alpha", "Org1"), + fabric.WithAnonymousIdentity(), + token.WithIssuerIdentity("issuer.id1", false), + token.WithOwnerIdentity("issuer.id1.owner"), + ) + issuerAlpha.RegisterViewFactory("issue", &views2.IssueCashViewFactory{}) + issuerAlpha.RegisterViewFactory("balance", &views2.BalanceViewFactory{}) + issuerAlpha.RegisterViewFactory("transfer.redeem", &pledge.RedeemViewFactory{}) + issuerAlpha.RegisterResponder(&pledge.IssuerResponderView{}, &pledge.View{}) + issuerAlpha.RegisterResponder(&pledge.IssuerResponderView{}, &pledge.FastPledgeClaimInitiatorView{}) + issuerAlpha.RegisterResponder(&pledge.IssuerResponderView{}, &pledge.FastPledgeReClaimInitiatorView{}) + issuerAlpha.RegisterResponder(&pledge.ReclaimIssuerResponderView{}, &pledge.ReclaimInitiatorView{}) + issuerAlpha.RegisterResponder(&pledge.ClaimIssuerView{}, &pledge.ClaimInitiatorView{}) + issuerAlpha.RegisterResponder(&pledge.ClaimIssuerView{}, &pledge.FastPledgeClaimResponderView{}) + issuerAlpha.RegisterResponder(&pledge.ClaimIssuerView{}, &pledge.FastPledgeReClaimResponderView{}) + issuerAlpha.RegisterViewFactory("TxFinality", &views3.TxFinalityViewFactory{}) + + issuerBeta := fscTopology.AddNodeByName("issuerBeta").AddOptions( + fabric.WithNetworkOrganization("beta", "Org3"), + fabric.WithAnonymousIdentity(), + token.WithIssuerIdentity("issuer.id2", false), + token.WithOwnerIdentity("issuer.id2.owner"), + ) + issuerBeta.RegisterViewFactory("issue", &views2.IssueCashViewFactory{}) + issuerBeta.RegisterViewFactory("balance", &views2.BalanceViewFactory{}) + issuerBeta.RegisterViewFactory("transfer.redeem", &pledge.RedeemViewFactory{}) + issuerBeta.RegisterResponder(&pledge.IssuerResponderView{}, &pledge.View{}) + issuerBeta.RegisterResponder(&pledge.IssuerResponderView{}, &pledge.FastPledgeClaimInitiatorView{}) + issuerBeta.RegisterResponder(&pledge.IssuerResponderView{}, &pledge.FastPledgeReClaimInitiatorView{}) + issuerBeta.RegisterResponder(&pledge.ReclaimIssuerResponderView{}, &pledge.ReclaimInitiatorView{}) + issuerBeta.RegisterResponder(&pledge.ClaimIssuerView{}, &pledge.ClaimInitiatorView{}) + issuerBeta.RegisterResponder(&pledge.ClaimIssuerView{}, &pledge.FastPledgeClaimResponderView{}) + issuerBeta.RegisterResponder(&pledge.ClaimIssuerView{}, &pledge.FastPledgeReClaimResponderView{}) + issuerBeta.RegisterViewFactory("TxFinality", &views3.TxFinalityViewFactory{}) + + auditor := fscTopology.AddNodeByName("auditor").AddOptions( + fabric.WithNetworkOrganization("alpha", "Org1"), + fabric.WithNetworkOrganization("beta", "Org3"), + fabric.WithAnonymousIdentity(), + token.WithAuditorIdentity(false), + ) + auditor.RegisterViewFactory("registerAuditor", &views2.RegisterAuditorViewFactory{}) + auditor.RegisterViewFactory("balance", &views2.BalanceViewFactory{}) + auditor.RegisterViewFactory("TxFinality", &views3.TxFinalityViewFactory{}) + + alice := fscTopology.AddNodeByName("alice").AddOptions( + fabric.WithNetworkOrganization("alpha", "Org2"), + fabric.WithAnonymousIdentity(), + token.WithOwnerIdentity("alice.id1"), + ) + alice.RegisterResponder(&views2.AcceptCashView{}, &views2.IssueCashView{}) + alice.RegisterViewFactory("balance", &views2.BalanceViewFactory{}) + alice.RegisterViewFactory("transfer.claim", &pledge.ClaimInitiatorViewFactory{}) + alice.RegisterViewFactory("transfer.pledge", &pledge.ViewFactory{}) + alice.RegisterViewFactory("transfer.reclaim", &pledge.ReclaimViewFactory{}) + alice.RegisterViewFactory("transfer.fastTransfer", &pledge.FastPledgeClaimInitiatorViewFactory{}) + alice.RegisterViewFactory("transfer.fastPledgeReclaim", &pledge.FastPledgeReClaimInitiatorViewFactory{}) + alice.RegisterViewFactory("transfer.scan", &pledge.ScanViewFactory{}) + alice.RegisterResponder(&pledge.RecipientResponderView{}, &pledge.View{}) + alice.RegisterResponder(&pledge.FastPledgeClaimResponderView{}, &pledge.FastPledgeClaimInitiatorView{}) + alice.RegisterResponder(&pledge.FastPledgeReClaimResponderView{}, &pledge.FastPledgeReClaimInitiatorView{}) + alice.RegisterViewFactory("TxFinality", &views3.TxFinalityViewFactory{}) + + bob := fscTopology.AddNodeByName("bob").AddOptions( + fabric.WithNetworkOrganization("beta", "Org4"), + fabric.WithAnonymousIdentity(), + token.WithOwnerIdentity("bob.id1"), + ) + bob.RegisterResponder(&views2.AcceptCashView{}, &views2.IssueCashView{}) + bob.RegisterViewFactory("balance", &views2.BalanceViewFactory{}) + bob.RegisterViewFactory("transfer.claim", &pledge.ClaimInitiatorViewFactory{}) + bob.RegisterViewFactory("transfer.pledge", &pledge.ViewFactory{}) + bob.RegisterViewFactory("transfer.reclaim", &pledge.ReclaimViewFactory{}) + bob.RegisterViewFactory("transfer.fastTransfer", &pledge.FastPledgeClaimInitiatorViewFactory{}) + bob.RegisterViewFactory("transfer.fastPledgeReclaim", &pledge.FastPledgeReClaimInitiatorViewFactory{}) + bob.RegisterViewFactory("transfer.scan", &pledge.ScanViewFactory{}) + bob.RegisterResponder(&pledge.RecipientResponderView{}, &pledge.View{}) + bob.RegisterResponder(&pledge.FastPledgeClaimResponderView{}, &pledge.FastPledgeClaimInitiatorView{}) + bob.RegisterResponder(&pledge.FastPledgeReClaimResponderView{}, &pledge.FastPledgeReClaimInitiatorView{}) + bob.RegisterViewFactory("TxFinality", &views3.TxFinalityViewFactory{}) + + tokenTopology := token.NewTopology() + tms := tokenTopology.AddTMS(fscTopology.ListNodes("auditor", "issuerAlpha", "alice"), f1Topology, f1Topology.Channels[0].Name, tokenSDKDriver) + common.SetDefaultParams(tokenSDKDriver, tms, true) + fabric2.SetOrgs(tms, "Org1") + tms.AddAuditor(auditor) + + tms = tokenTopology.AddTMS(fscTopology.ListNodes("auditor", "issuerBeta", "bob"), f2Topology, f2Topology.Channels[0].Name, tokenSDKDriver) + common.SetDefaultParams(tokenSDKDriver, tms, true) + fabric2.SetOrgs(tms, "Org3") + tms.AddAuditor(auditor) + + // Add SDKs to FSC Nodes + for _, sdk := range sdks { + fscTopology.AddSDK(sdk) + } + + return []api.Topology{f1Topology, f2Topology, tokenTopology, wTopology, fscTopology} +} diff --git a/integration/token/interop/views/auditor.go b/integration/token/interop/views/auditor.go index 153f853240..61adf19718 100644 --- a/integration/token/interop/views/auditor.go +++ b/integration/token/interop/views/auditor.go @@ -14,7 +14,7 @@ import ( "github.com/hyperledger-labs/fabric-smart-client/platform/view/services/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/htlc" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/interop" "github.com/hyperledger-labs/fabric-token-sdk/token/services/ttx" ) @@ -66,28 +66,40 @@ func (a *AuditView) Call(context view.Context) (interface{}, error) { } for i := 0; i < inputs.Count(); i++ { - input, err := htlc.ToInput(inputs.At(i)) + input, err := interop.ToInput(inputs.At(i)) assert.NoError(err, "cannot get htlc input wrapper") - if !input.IsHTLC() { - continue + + switch { + case input.IsHTLC(): + // check script details + script, err := input.HTLC() + assert.NoError(err, "cannot get htlc script from output") + assert.True(len(script.HashInfo.Hash) > 0, "hash is not set") + assert.NoError(script.WellFormedness(), "htlc script is not valid") + case input.IsPledge(): + // check script details + script, err := input.Pledge() + assert.NoError(err, "cannot get pledge script from input") + assert.NoError(script.WellFormedness(), "pledge 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 := 0; i < outputs.Count(); i++ { - output, err := htlc.ToOutput(outputs.At(i)) + output, err := interop.ToOutput(outputs.At(i)) assert.NoError(err, "cannot get htlc output wrapper") - if !output.IsHTLC() { - continue + switch { + case output.IsHTLC(): + // check script details + script, err := output.HTLC() + assert.NoError(err, "cannot get htlc script from output") + assert.NoError(script.Validate(now), "script is not valid") + case output.IsPledge(): + // check script details + script, err := output.Pledge() + assert.NoError(err, "cannot get pledge script from input") + assert.NoError(script.Validate(now), "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/pledge/claim.go b/integration/token/interop/views/pledge/claim.go new file mode 100644 index 0000000000..544583c2e9 --- /dev/null +++ b/integration/token/interop/views/pledge/claim.go @@ -0,0 +1,150 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package pledge + +import ( + "encoding/json" + + "github.com/hyperledger-labs/fabric-smart-client/platform/fabric" + view3 "github.com/hyperledger-labs/fabric-smart-client/platform/view" + "github.com/hyperledger-labs/fabric-smart-client/platform/view/services/assert" + view2 "github.com/hyperledger-labs/fabric-smart-client/platform/view/services/view" + "github.com/hyperledger-labs/fabric-smart-client/platform/view/view" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/interop/pledge" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/ttx" + "github.com/hyperledger-labs/fabric-token-sdk/token/token" +) + +// Claim contains the input information to claim a token +type Claim struct { + // OriginTokenID is the identifier of the pledged token in the origin network + OriginTokenID *token.ID + // Issuer is the identity of the issuer in the destination network + Issuer string +} + +// ClaimInitiatorView is the view of the initiator of the claim (Bob) +type ClaimInitiatorView struct { + *Claim +} + +func (c *ClaimInitiatorView) Call(context view.Context) (interface{}, error) { + // Retrieve proof of existence of the passed token id + pledges, err := pledge.Vault(context).PledgeByTokenID(c.OriginTokenID) + assert.NoError(err, "failed getting pledge") + assert.Equal(1, len(pledges), "expected one pledge, got [%d]", len(pledges)) + + logger.Debugf("request proof of existence") + proof, err := pledge.RequestProofOfExistence(context, pledges[0]) + assert.NoError(err, "failed to retrieve a valid proof of existence") + assert.NotNil(proof) + + // Request claim to the issuer + wallet, err := pledge.MyOwnerWallet(context) + assert.NoError(err, "failed getting my owner wallet") + me, err := wallet.GetRecipientIdentity() + assert.NoError(err, "failed getting recipient identity from my owner wallet") + + // Contact the issuer, present the pledge proof, and ask to initiate the issue process + fns, err := fabric.GetDefaultFNS(context) + assert.NoError(err, "failed getting default fns") + + session, err := pledge.RequestClaim( + context, + fns.IdentityProvider().Identity(c.Issuer), + pledges[0], + me, + proof, + ) + assert.NoError(err, "failed requesting a claim from the issuer") + + // Now we have an inversion of roles. + // The issuer becomes the initiator of the issue transaction, + // and this node is the responder + return view2.AsResponder(context, session, + func(context view.Context) (interface{}, error) { + // At some point, the recipient receives the token transaction that in the meantime has been assembled + tx, err := pledge.ReceiveTransaction(context) + assert.NoError(err, "failed to receive transaction") + + // The recipient can perform any check on the transaction + outputs, err := tx.Outputs() + assert.NoError(err, "failed getting outputs") + assert.True(outputs.Count() > 0) + assert.True(outputs.ByRecipient(me).Count() > 0) + output := outputs.At(0) + assert.NotNil(output, "failed getting the output") + assert.NoError(err, "failed parsing quantity") + assert.Equal(pledges[0].Amount, output.Quantity.ToBigInt().Uint64()) + assert.Equal(pledges[0].TokenType, output.Type) + assert.Equal(me, output.Owner) + + // If everything is fine, the recipient accepts and sends back her signature. + _, err = context.RunView(pledge.NewAcceptView(tx)) + assert.NoError(err, "failed to accept the claim transaction") + + // Before completing, the recipient waits for finality of the transaction + _, err = context.RunView(pledge.NewFinalityView(tx)) + assert.NoError(err, "the claim transaction was not committed") + + // Delete pledges + err = pledge.Vault(context).Delete(pledges) + assert.NoError(err, "failed deleting pledges") + + return tx.ID(), nil + }, + ) +} + +type ClaimInitiatorViewFactory struct{} + +func (c *ClaimInitiatorViewFactory) NewView(in []byte) (view.View, error) { + f := &ClaimInitiatorView{Claim: &Claim{}} + err := json.Unmarshal(in, f.Claim) + assert.NoError(err, "failed unmarshalling input") + + return f, nil +} + +// ClaimIssuerView is the view of the issuer responding to the claim interactive protocol. +type ClaimIssuerView struct{} + +func (c *ClaimIssuerView) Call(context view.Context) (interface{}, error) { + // Receive claim request + req, err := pledge.ReceiveClaimRequest(context) + assert.NoError(err, "failed to receive claim request") + + // Validate and check Proof + err = pledge.ValidateClaimRequest(context, req) + assert.NoError(err, "failed validating claim request") + logger.Debugf("claim request valid, preparing claim transaction [%s]", err) + + // At this point, the issuer is ready to prepare the token transaction. + tx, err := pledge.NewAnonymousTransaction( + context, + ttx.WithAuditor(view3.GetIdentityProvider(context).Identity("auditor")), + ) + assert.NoError(err, "failed creating a new transaction") + + // The issuer adds a new claim operation to the transaction following the instruction received. + wallet, err := pledge.GetIssuerWallet(context, "") + assert.NoError(err, "failed to get issuer's wallet") + + err = tx.Claim(wallet, req.TokenType, req.Quantity, req.Recipient, req.OriginTokenID, req.OriginNetwork, req.PledgeProof) + assert.NoError(err, "failed adding a claim action") + + // The issuer is ready to collect all the required signatures. + // Invoke the Token Chaincode to collect endorsements on the Token Request and prepare the relative Fabric transaction. + _, err = context.RunView(pledge.NewCollectEndorsementsView(tx)) + assert.NoError(err, "failed to collect endorsements on claim transaction") + + // Last but not least, the issuer sends the transaction for ordering and waits for transaction finality. + _, err = context.RunView(pledge.NewOrderingAndFinalityView(tx)) + assert.NoError(err, "failed to commit claim transaction") + + return tx.ID(), nil +} diff --git a/integration/token/interop/views/pledge/fast_pledge.go b/integration/token/interop/views/pledge/fast_pledge.go new file mode 100644 index 0000000000..91d7a7197d --- /dev/null +++ b/integration/token/interop/views/pledge/fast_pledge.go @@ -0,0 +1,328 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package pledge + +import ( + "encoding/json" + "time" + + "github.com/hyperledger-labs/fabric-smart-client/platform/fabric" + view3 "github.com/hyperledger-labs/fabric-smart-client/platform/view" + "github.com/hyperledger-labs/fabric-smart-client/platform/view/services/assert" + view2 "github.com/hyperledger-labs/fabric-smart-client/platform/view/services/view" + "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/pledge" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/interop/state" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/ttx" + token2 "github.com/hyperledger-labs/fabric-token-sdk/token/token" + "github.com/pkg/errors" +) + +// FastPledgeClaim contains the input information for a pledge+claim +type FastPledgeClaim struct { + // OriginTMSID identifies the TMS to use to perform the token operation + OriginTMSID token.TMSID + // OriginWallet is the identifier of the wallet that owns the tokens to transfer in the origin network + OriginWallet string + // Type of tokens to transfer + Type string + // Amount to transfer + Amount uint64 + // Issuer is the identity of the issuer's FSC node + Issuer view.Identity + // Recipient is the identity of the recipient's FSC node + Recipient view.Identity + // DestinationNetworkURL is the destination network's url to transfer the token to + DestinationNetworkURL string + // ReclamationDeadline is the time after which we can reclaim the funds in case they were not transferred + ReclamationDeadline time.Duration +} + +type FastPledgeClaimInitiatorViewFactory struct{} + +func (f *FastPledgeClaimInitiatorViewFactory) NewView(in []byte) (view.View, error) { + v := &FastPledgeClaimInitiatorView{FastPledgeClaim: &FastPledgeClaim{}} + err := json.Unmarshal(in, v.FastPledgeClaim) + assert.NoError(err, "failed unmarshalling input") + + return v, nil +} + +type FastPledgeClaimInitiatorView struct { + *FastPledgeClaim +} + +func (v *FastPledgeClaimInitiatorView) Call(context view.Context) (interface{}, error) { + // Collect recipient's token-sdk identity + recipient, err := pledge.RequestPledgeRecipientIdentity(context, v.Recipient, v.DestinationNetworkURL, token.WithTMSID(v.OriginTMSID)) + assert.NoError(err, "failed getting recipient identity") + + // Collect issuer's token-sdk identity + issuer, err := pledge.RequestRecipientIdentity(context, v.Issuer, token.WithTMSID(v.OriginTMSID)) + assert.NoError(err, "failed getting recipient identity") + + // Create a new transaction + tx, err := pledge.NewAnonymousTransaction( + context, + ttx.WithAuditor(view3.GetIdentityProvider(context).Identity("auditor")), + ttx.WithTMSID(v.OriginTMSID), + ) + assert.NoError(err, "failed created a new transaction") + + // The sender will select tokens owned by this wallet + senderWallet := pledge.GetWallet(context, v.OriginWallet, token.WithTMSID(v.OriginTMSID)) + assert.NotNil(senderWallet, "sender wallet [%s] not found", v.OriginWallet) + + _, err = tx.Pledge(senderWallet, v.DestinationNetworkURL, v.ReclamationDeadline, recipient, issuer, v.Type, v.Amount) + assert.NoError(err, "failed pledging") + + // Collect signatures + _, err = context.RunView(pledge.NewCollectEndorsementsView(tx)) + assert.NoError(err, "failed to sign pledge transaction") + + // Last but not least, the issuer sends the transaction for ordering and waits for transaction finality. + _, err = context.RunView(pledge.NewOrderingAndFinalityView(tx)) + assert.NoError(err, "failed to commit issue transaction") + + // Inform the recipient of the pledge, + // recall that the recipient might be aware of only the other network + _, err = context.RunView(pledge.NewDistributePledgeInfoView(tx)) + assert.NoError(err, "failed to send the pledge info") + + time.Sleep(v.ReclamationDeadline) + + // Request proof of non-existence for the passed token, + // we expect the token to exist in the destination network + w, err := pledge.GetOwnerWallet(context, v.OriginWallet, token.WithTMSID(v.OriginTMSID)) + assert.NoError(err, "failed to retrieve wallet of owner during reclaim") + + tokenID := &token2.ID{TxId: tx.ID()} + _, script, err := pledge.Wallet(context, w).GetPledgedToken(tokenID) + assert.NoError(err, "failed to retrieve token to be reclaimed") + assert.False(time.Now().Before(script.Deadline), "cannot reclaim token yet; deadline has not elapsed yet") + + logger.Debugf("request proof of non-existence") + _, err = pledge.RequestProofOfNonExistence(context, tokenID, v.OriginTMSID, script) + assert.Error(err, "retrieve proof of non-existence should fail") + assert.Equal(state.TokenExistsError, errors.Cause(err), "token should exist") + + return nil, nil +} + +type FastPledgeClaimResponderView struct{} + +func (v *FastPledgeClaimResponderView) Call(context view.Context) (interface{}, error) { + me, err := pledge.RespondRequestPledgeRecipientIdentity(context) + assert.NoError(err, "failed to respond to identity request") + + // At some point, the recipient receives the pledge info + pledgeInfo, err := pledge.ReceivePledgeInfo(context) + assert.NoError(err, "failed to receive pledge info") + + // Perform any check that is needed to validate the pledge. + logger.Debugf("The pledge info is %v", pledgeInfo) + assert.Equal(me, pledgeInfo.Script.Recipient, "recipient is different [%s]!=[%s]", me, pledgeInfo.Script.Recipient) + + // Store the pledge and send a notification back + _, err = context.RunView(pledge.NewAcceptPledgeIndoView(pledgeInfo)) + assert.NoError(err, "failed accepting pledge info") + + // Retrieve proof of existence of the passed token id + pledges, err := pledge.Vault(context).PledgeByTokenID(pledgeInfo.TokenID) + assert.NoError(err, "failed getting pledge") + assert.Equal(1, len(pledges), "expected one pledge, got [%d]", len(pledges)) + + logger.Debugf("request proof of existence") + proof, err := pledge.RequestProofOfExistence(context, pledges[0]) + assert.NoError(err, "failed to retrieve a valid proof of existence") + assert.NotNil(proof) + + // Request claim to the issuer + logger.Debugf("Request claim to the issuer") + wallet, err := pledge.MyOwnerWallet(context) + assert.NoError(err, "failed getting my owner wallet") + me, err = wallet.GetRecipientIdentity() + assert.NoError(err, "failed getting recipient identity from my owner wallet") + + var session view.Session + _, err = view2.AsInitiatorCall(context, v, func(context view.Context) (interface{}, error) { + fns, err := fabric.GetDefaultFNS(context) + assert.NoError(err, "failed getting default fns") + session, err = pledge.RequestClaim( + context, + fns.IdentityProvider().Identity("issuerBeta"), // TODO get issuer + pledges[0], + me, + proof, + ) + assert.NoError(err, "failed requesting a claim from the issuer") + return nil, nil + }) + assert.NoError(err, "failed to request claim") + + return view2.AsResponder(context, session, + func(context view.Context) (interface{}, error) { + logger.Debugf("Respond to the issuer...") + + // At some point, the recipient receives the token transaction that in the meantime has been assembled + tx, err := pledge.ReceiveTransaction(context) + assert.NoError(err, "failed to receive transaction") + + // The recipient can perform any check on the transaction + outputs, err := tx.Outputs() + assert.NoError(err, "failed getting outputs") + assert.True(outputs.Count() > 0) + assert.True(outputs.ByRecipient(me).Count() > 0) + output := outputs.At(0) + assert.NotNil(output, "failed getting the output") + assert.NoError(err, "failed parsing quantity") + assert.Equal(pledges[0].Amount, output.Quantity.ToBigInt().Uint64()) + assert.Equal(pledges[0].TokenType, output.Type) + assert.Equal(me, output.Owner) + + // If everything is fine, the recipient accepts and sends back her signature. + _, err = context.RunView(pledge.NewAcceptView(tx)) + assert.NoError(err, "failed to accept the claim transaction") + + // Before completing, the recipient waits for finality of the transaction + _, err = context.RunView(pledge.NewFinalityView(tx)) + assert.NoError(err, "the claim transaction was not committed") + + // Delete pledges + err = pledge.Vault(context).Delete(pledges) + assert.NoError(err, "failed deleting pledges") + + logger.Debugf("Respond to the issuer...done") + + return tx.ID(), nil + }, + ) +} + +// FastPledgeReClaim contains the input information for a pledge+reclaim +type FastPledgeReClaim struct { + // OriginTMSID identifies the TMS to use to perform the token operation + OriginTMSID token.TMSID + // OriginWallet is the identifier of the wallet that owns the tokens to transfer in the origin network + OriginWallet string + // Type of tokens to transfer + Type string + // Amount to transfer + Amount uint64 + // Issuer is the identity of the issuer's FSC node + Issuer view.Identity + // Recipient is the identity of the recipient's FSC node + Recipient view.Identity + // DestinationNetworkURL is the destination network's url to transfer the token to + DestinationNetworkURL string + // ReclamationDeadline is the time after which we can reclaim the funds in case they were not transferred + ReclamationDeadline time.Duration + // PledgeID is the unique identifier of the pledge + PledgeID string +} + +type FastPledgeReClaimInitiatorViewFactory struct{} + +func (f *FastPledgeReClaimInitiatorViewFactory) NewView(in []byte) (view.View, error) { + v := &FastPledgeReClaimInitiatorView{FastPledgeReClaim: &FastPledgeReClaim{}} + err := json.Unmarshal(in, v.FastPledgeReClaim) + assert.NoError(err, "failed unmarshalling input") + + return v, nil +} + +type FastPledgeReClaimInitiatorView struct { + *FastPledgeReClaim +} + +func (v *FastPledgeReClaimInitiatorView) Call(context view.Context) (interface{}, error) { + // Collect recipient's token-sdk identity + recipient, err := pledge.RequestPledgeRecipientIdentity(context, v.Recipient, v.DestinationNetworkURL, token.WithTMSID(v.OriginTMSID)) + assert.NoError(err, "failed getting recipient identity") + + // Collect issuer's token-sdk identity + issuer, err := pledge.RequestRecipientIdentity(context, v.Issuer, token.WithTMSID(v.OriginTMSID)) + assert.NoError(err, "failed getting recipient identity") + + // Create a new transaction + tx, err := pledge.NewAnonymousTransaction( + context, + ttx.WithAuditor(view3.GetIdentityProvider(context).Identity("auditor")), + ttx.WithTMSID(v.OriginTMSID), + ) + assert.NoError(err, "failed created a new transaction") + + // The sender will select tokens owned by this wallet + senderWallet := pledge.GetWallet(context, v.OriginWallet, token.WithTMSID(v.OriginTMSID)) + assert.NotNil(senderWallet, "sender wallet [%s] not found", v.OriginWallet) + + _, err = tx.Pledge(senderWallet, v.DestinationNetworkURL, v.ReclamationDeadline, recipient, issuer, v.Type, v.Amount) + assert.NoError(err, "failed pledging") + + // Collect signatures + _, err = context.RunView(pledge.NewCollectEndorsementsView(tx)) + assert.NoError(err, "failed to sign pledge transaction") + + // Last but not least, the issuer sends the transaction for ordering and waits for transaction finality. + _, err = context.RunView(pledge.NewOrderingAndFinalityView(tx)) + assert.NoError(err, "failed to commit issue transaction") + + // Inform the recipient of the pledge, + // recall that the recipient might be aware of only the other network + _, err = context.RunView(pledge.NewDistributePledgeInfoView(tx)) + assert.NoError(err, "failed to send the pledge info") + + time.Sleep(v.ReclamationDeadline) + + // Reclaim, here we are executing the reclaim protocol, therefore + // we initiate it with a fresh context + tokenID := &token2.ID{TxId: tx.ID()} + _, err = view2.Initiate(context, &ReclaimInitiatorView{ + &Reclaim{ + TMSID: v.OriginTMSID, + TokenID: tokenID, + WalletID: v.OriginWallet, + Retry: false, + }, + }) + assert.NoError(err, "failed to reclaim [%s:%s:%s]", tokenID, v.OriginTMSID, v.OriginWallet) + + return nil, nil +} + +type FastPledgeReClaimResponderView struct{} + +func (v *FastPledgeReClaimResponderView) Call(context view.Context) (interface{}, error) { + me, err := pledge.RespondRequestPledgeRecipientIdentity(context) + assert.NoError(err, "failed to respond to identity request") + + // At some point, the recipient receives the pledge info + pledgeInfo, err := pledge.ReceivePledgeInfo(context) + assert.NoError(err, "failed to receive pledge info") + + // Perform any check that is needed to validate the pledge. + logger.Debugf("The pledge info is %v", pledgeInfo) + assert.Equal(me, pledgeInfo.Script.Recipient, "recipient is different [%s]!=[%s]", me, pledgeInfo.Script.Recipient) + + // Store the pledge and send a notification back + _, err = context.RunView(pledge.NewAcceptPledgeIndoView(pledgeInfo)) + assert.NoError(err, "failed accepting pledge info") + + // Retrieve proof of existence of the passed token id + pledges, err := pledge.Vault(context).PledgeByTokenID(pledgeInfo.TokenID) + assert.NoError(err, "failed getting pledge") + assert.Equal(1, len(pledges), "expected one pledge, got [%d]", len(pledges)) + + logger.Debugf("request proof of existence") + proof, err := pledge.RequestProofOfExistence(context, pledges[0]) + assert.NoError(err, "failed to retrieve a valid proof of existence") + assert.NotNil(proof) + + // Don't claim the token + return nil, nil +} diff --git a/integration/token/interop/views/pledge/pledge.go b/integration/token/interop/views/pledge/pledge.go new file mode 100644 index 0000000000..e76e90d3f5 --- /dev/null +++ b/integration/token/interop/views/pledge/pledge.go @@ -0,0 +1,156 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package pledge + +import ( + "encoding/json" + "time" + + view2 "github.com/hyperledger-labs/fabric-smart-client/platform/view" + "github.com/hyperledger-labs/fabric-smart-client/platform/view/services/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/pledge" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/ttx" +) + +type Result struct { + TxID string + PledgeID string +} + +// Pledge contains the input information for a transfer +type Pledge struct { + // OriginTMSID identifies the TMS to use to perform the token operation. + OriginTMSID token.TMSID + // OriginWallet is the identifier of the wallet that owns the tokens to transfer in the origin network + OriginWallet string + // Type of tokens to transfer + Type string + // Amount to transfer + Amount uint64 + // Issuer is the identity of the issuer's FSC node + Issuer view.Identity + // Recipient is the identity of the recipient's FSC node + Recipient view.Identity + // DestinationNetworkURL is the destination network's url to transfer the token to + DestinationNetworkURL string + // ReclamationDeadline is the time after which we can reclaim the funds in case they were not transferred + ReclamationDeadline time.Duration +} + +// View is the view of the initiator of a pledge operation +type View struct { + *Pledge +} + +func (v *View) Call(context view.Context) (interface{}, error) { + // Collect recipient's token-sdk identity + recipient, err := pledge.RequestPledgeRecipientIdentity(context, v.Recipient, v.DestinationNetworkURL, token.WithTMSID(v.OriginTMSID)) + assert.NoError(err, "failed getting recipient identity") + + // Collect issuer's token-sdk identity + // TODO: shall we ask for the issuer identity here and not the owner identity? + issuer, err := pledge.RequestRecipientIdentity(context, v.Issuer, token.WithTMSID(v.OriginTMSID)) + assert.NoError(err, "failed getting recipient identity") + + // Create a new transaction + tx, err := pledge.NewAnonymousTransaction( + context, + ttx.WithAuditor(view2.GetIdentityProvider(context).Identity("auditor")), + ttx.WithTMSID(v.OriginTMSID), + ) + assert.NoError(err, "failed created a new transaction") + + // The sender will select tokens owned by this wallet + senderWallet := pledge.GetWallet(context, v.OriginWallet, token.WithTMSID(v.OriginTMSID)) + assert.NotNil(senderWallet, "sender wallet [%s] not found", v.OriginWallet) + + pledgeID, err := tx.Pledge(senderWallet, v.DestinationNetworkURL, v.ReclamationDeadline, recipient, issuer, v.Type, v.Amount) + assert.NoError(err, "failed pledging") + + // Collect signatures + _, err = context.RunView(pledge.NewCollectEndorsementsView(tx)) + assert.NoError(err, "failed to sign pledge transaction") + + // Last but not least, the issuer sends the transaction for ordering and waits for transaction finality. + _, err = context.RunView(pledge.NewOrderingAndFinalityView(tx)) + assert.NoError(err, "failed to commit issue transaction") + + // Inform the recipient of the pledge, + // recall that the recipient might be aware of only the other network + _, err = context.RunView(pledge.NewDistributePledgeInfoView(tx)) + assert.NoError(err, "failed to send the pledge info") + + return json.Marshal(&Result{TxID: tx.ID(), PledgeID: pledgeID}) +} + +type ViewFactory struct{} + +func (f *ViewFactory) NewView(in []byte) (view.View, error) { + v := &View{Pledge: &Pledge{}} + err := json.Unmarshal(in, v.Pledge) + assert.NoError(err, "failed unmarshalling input") + + return v, nil +} + +type RecipientResponderView struct{} + +func (p *RecipientResponderView) Call(context view.Context) (interface{}, error) { + me, err := pledge.RespondRequestPledgeRecipientIdentity(context) + assert.NoError(err, "failed to respond to identity request") + + // At some point, the recipient receives the pledge info + pledgeInfo, err := pledge.ReceivePledgeInfo(context) + assert.NoError(err, "failed to receive pledge info") + + // Perform any check that is needed to validate the pledge. + logger.Debugf("The pledge info is %v", pledgeInfo) + assert.Equal(me, pledgeInfo.Script.Recipient, "recipient is different [%s]!=[%s]", me, pledgeInfo.Script.Recipient) + + // TODO: check pledgeInfo.Script.DestinationNetwork + + // Store the pledge and send a notification back + _, err = context.RunView(pledge.NewAcceptPledgeIndoView(pledgeInfo)) + assert.NoError(err, "failed accepting pledge info") + + return nil, nil +} + +type IssuerResponderView struct{} + +func (p *IssuerResponderView) Call(context view.Context) (interface{}, error) { + me, err := pledge.RespondRequestRecipientIdentity(context) + assert.NoError(err, "failed to respond to identity request") + + // At some point, the recipient receives the token transaction that in the meantime has been assembled + tx, err := pledge.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()) + outputs = outputs.ByScript() + assert.True(outputs.Count() == 1, "expected only one pledge output, got [%d]", outputs.Count()) + script := outputs.ScriptAt(0) + assert.NotNil(script, "expected a pledge script") + assert.Equal(me, script.Issuer, "Expected pledge script to have me (%x) as an issuer but it has %x instead", me, script.Issuer) + + // If everything is fine, the recipient accepts and sends back her signature. + // Notice that, a signature from the recipient might or might not be required to make the transaction valid. + // This depends on the driver implementation. + _, err = context.RunView(pledge.NewAcceptView(tx)) + assert.NoError(err, "failed to accept new tokens") + + // The issue is in the same Fabric network of the pledge + // Before completing, the recipient waits for finality of the transaction + _, err = context.RunView(pledge.NewFinalityView(tx)) + assert.NoError(err, "new tokens were not committed") + + return nil, nil +} diff --git a/integration/token/interop/views/pledge/reclaim.go b/integration/token/interop/views/pledge/reclaim.go new file mode 100644 index 0000000000..201dfdf9a3 --- /dev/null +++ b/integration/token/interop/views/pledge/reclaim.go @@ -0,0 +1,114 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package pledge + +import ( + "encoding/json" + "time" + + view2 "github.com/hyperledger-labs/fabric-smart-client/platform/view" + "github.com/hyperledger-labs/fabric-smart-client/platform/view/services/assert" + "github.com/hyperledger-labs/fabric-smart-client/platform/view/services/flogging" + "github.com/hyperledger-labs/fabric-smart-client/platform/view/view" + token2 "github.com/hyperledger-labs/fabric-token-sdk/token" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/interop/pledge" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/ttx" + "github.com/hyperledger-labs/fabric-token-sdk/token/token" + "github.com/pkg/errors" +) + +var logger = flogging.MustGetLogger("token-sdk.asset.transfer") + +// Reclaim contains the input information for a reclaim +type Reclaim struct { + TMSID token2.TMSID + // TokenID contains the identifier of the token to be reclaimed. + TokenID *token.ID + // WalletID is the identifier of the wallet that the tokens will be reclaimed to + WalletID string + // Retry tells if a retry must happen in case of a failure + Retry bool +} + +type ReclaimInitiatorView struct { + *Reclaim +} + +func (rv *ReclaimInitiatorView) Call(context view.Context) (interface{}, error) { + logger.Debugf("caller [%s]", context.Initiator()) + // Request proof of non-existence for the passed token + w, err := pledge.GetOwnerWallet(context, rv.WalletID, token2.WithTMSID(rv.TMSID)) + assert.NoError(err, "failed to retrieve wallet of owner during reclaim") + + token, script, err := pledge.Wallet(context, w).GetPledgedToken(rv.TokenID) + assert.NoError(err, "failed to retrieve token to be reclaimed") + if time.Now().Before(script.Deadline) { + return nil, errors.Errorf("cannot reclaim token yet; deadline has not elapsed yet") + } + + logger.Debugf("request proof of non-existence") + proof, err := pledge.RequestProofOfNonExistence(context, rv.TokenID, rv.TMSID, script) + if err != nil { + return nil, errors.Wrapf(err, "failed to retrieve proof of non-existence") + } + + // At this point, Alice contacts the issuer's FSC node + // to ask for the issuer's signature on the TokenID + issuerSignature, err := pledge.RequestIssuerSignature(context, rv.TokenID, rv.TMSID, script, proof) + assert.NoError(err, "failed getting issuer's signature") + assert.NotNil(issuerSignature) + + // At this point, alice is ready to prepare the token transaction. + tx, err := pledge.NewAnonymousTransaction( + context, + ttx.WithAuditor(view2.GetIdentityProvider(context).Identity("auditor")), + ttx.WithTMSID(rv.TMSID), + ) + assert.NoError(err, "failed to create a new transaction") + + // Create reclaim transaction + err = tx.Reclaim(w, token, issuerSignature, rv.TokenID, proof) + assert.NoError(err, "failed creating transaction") + + // Alice is ready to collect all the required signatures. + // Invoke the Token Chaincode to collect endorsements on the Token Request and prepare the relative Fabric transaction. + // This is all done in one shot running the following view. + // Before completing, all recipients receive the approved Fabric transaction. + // Depending on the token driver implementation, the recipient's signature might or might not be needed to make + // the token transaction valid. + _, err = context.RunView(pledge.NewCollectEndorsementsView(tx)) + assert.NoError(err, "failed to sign transaction") + + // Send to the ordering service and wait for finality + _, err = context.RunView(pledge.NewOrderingAndFinalityView(tx)) + assert.NoError(err, "failed asking ordering") + + return tx.ID(), nil +} + +type ReclaimViewFactory struct{} + +func (rvf *ReclaimViewFactory) NewView(in []byte) (view.View, error) { + rv := &ReclaimInitiatorView{Reclaim: &Reclaim{}} + err := json.Unmarshal(in, rv.Reclaim) + assert.NoError(err, "failed unmarshalling input") + return rv, nil +} + +type ReclaimIssuerResponderView struct { + WalletID string + Network string +} + +func (i *ReclaimIssuerResponderView) Call(context view.Context) (interface{}, error) { + _, err := pledge.RespondRequestIssuerSignature(context, i.WalletID) + if err != nil { + return nil, errors.Wrapf(err, "failed to respond to signature request") + } + + return nil, nil +} diff --git a/integration/token/interop/views/pledge/redeem.go b/integration/token/interop/views/pledge/redeem.go new file mode 100644 index 0000000000..f7c51adae2 --- /dev/null +++ b/integration/token/interop/views/pledge/redeem.go @@ -0,0 +1,77 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package pledge + +import ( + "encoding/json" + + view2 "github.com/hyperledger-labs/fabric-smart-client/platform/view" + "github.com/hyperledger-labs/fabric-smart-client/platform/view/services/assert" + "github.com/hyperledger-labs/fabric-smart-client/platform/view/view" + token2 "github.com/hyperledger-labs/fabric-token-sdk/token" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/interop/pledge" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/ttx" + "github.com/hyperledger-labs/fabric-token-sdk/token/token" +) + +// Redeem contains the input information for a redeem +type Redeem struct { + TMSID token2.TMSID + // TokenID contains the identifier of the token to be redeemed + TokenID *token.ID +} + +type RedeemView struct { + *Redeem +} + +func (rv *RedeemView) Call(context view.Context) (interface{}, error) { + w, err := pledge.GetIssuerWallet(context, "") + assert.NoError(err, "failed to retrieve wallet of issuer during redeem") + + wallet := pledge.NewIssuerWallet(context, w) + t, script, err := wallet.GetPledgedToken(rv.TokenID) + assert.NoError(err, "failed to retrieve pledged token during redeem") + + // make sure token was in fact claimed in the other network + proof, err := pledge.RequestProofOfTokenWithMetadataExistence(context, rv.TokenID, rv.TMSID, script) + assert.NoError(err, "failed to retrieve and verify proof of token existence") + + // Create a new transaction + tx, err := pledge.NewAnonymousTransaction( + context, + ttx.WithAuditor(view2.GetIdentityProvider(context).Identity("auditor")), + ttx.WithTMSID(rv.TMSID), + ) + assert.NoError(err, "failed created a new transaction") + + ow, err := pledge.GetOwnerWallet(context, "") + assert.NoError(err, "failed to retrieve owner wallet") + + err = tx.RedeemPledge(ow, t, rv.TokenID, proof) + assert.NoError(err, "failed adding redeem") + + // Collect signatures + _, err = context.RunView(pledge.NewCollectEndorsementsView(tx)) + assert.NoError(err, "failed to sign redeem transaction") + + // Sends the transaction for ordering and wait for transaction finality + _, err = context.RunView(pledge.NewOrderingAndFinalityView(tx)) + assert.NoError(err, "failed to commit redeem transaction") + + return tx.ID(), nil +} + +type RedeemViewFactory struct{} + +func (rvf *RedeemViewFactory) NewView(in []byte) (view.View, error) { + v := &RedeemView{Redeem: &Redeem{}} + err := json.Unmarshal(in, v.Redeem) + assert.NoError(err, "failed unmarshalling input") + + return v, nil +} diff --git a/integration/token/interop/views/pledge/scan.go b/integration/token/interop/views/pledge/scan.go new file mode 100644 index 0000000000..df31edf82e --- /dev/null +++ b/integration/token/interop/views/pledge/scan.go @@ -0,0 +1,56 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package pledge + +import ( + "encoding/json" + "time" + + "github.com/hyperledger-labs/fabric-smart-client/platform/view/services/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/pledge" +) + +// Scan contains the input information for a scan of a matching pledge id +type Scan struct { + // TMSID identifies the TMS to use to perform the token operation + TMSID token.TMSID + // PlegdeID is the identifier to use in the scan + PledgeID string + // Timeout of the scan + Timeout time.Duration + // StartingTransactionID is the transaction id from which to start the scan. + // If empty, the scan starts from the genesis block + StartingTransactionID string +} + +type ScanView struct { + *Scan +} + +func (s *ScanView) Call(context view.Context) (interface{}, error) { + b, err := pledge.IDExists( + context, + s.PledgeID, + s.Timeout, + token.WithTMSID(s.TMSID), + pledge.WithStartingTransaction(s.StartingTransactionID), + ) + assert.NoError(err, "failed to scan for pledge id") + return b, nil +} + +type ScanViewFactory struct{} + +func (s *ScanViewFactory) NewView(in []byte) (view.View, error) { + f := &ScanView{Scan: &Scan{}} + err := json.Unmarshal(in, f.Scan) + assert.NoError(err, "failed unmarshalling input") + + return f, nil +} diff --git a/interop.mk b/interop.mk index 6a728158c9..584a032b2a 100644 --- a/interop.mk +++ b/interop.mk @@ -30,6 +30,10 @@ integration-tests-interop-dlog-t5: integration-tests-interop-dlog-t6: cd ./integration/token/interop/dlog; export FAB_BINS=$(FAB_BINS); ginkgo $(GINKGO_TEST_OPTS) --focus "HTLC No Cross Claim with Orion and Fabric Networks" . +.PHONY: integration-tests-interop-dlog-t7 +integration-tests-interop-dlog-t7: + cd ./integration/token/interop/dlog; export FAB_BINS=$(FAB_BINS); ginkgo $(GINKGO_TEST_OPTS) --focus "Asset Transfer With Two Fabric Networks" . + .PHONY: integration-tests-interop-fabtoken-t1 integration-tests-interop-fabtoken-t1: cd ./integration/token/interop/fabtoken; export FAB_BINS=$(FAB_BINS); ginkgo $(GINKGO_TEST_OPTS) --focus "HTLC Single Fabric Network" . @@ -53,3 +57,7 @@ integration-tests-interop-fabtoken-t5: .PHONY: integration-tests-interop-fabtoken-t6 integration-tests-interop-fabtoken-t6: cd ./integration/token/interop/fabtoken; export FAB_BINS=$(FAB_BINS); ginkgo $(GINKGO_TEST_OPTS) --focus "HTLC No Cross Claim with Orion and Fabric Networks" . + +.PHONY: integration-tests-interop-fabtoken-t7 +integration-tests-interop-fabtoken-t7: + cd ./integration/token/interop/fabtoken; export FAB_BINS=$(FAB_BINS); ginkgo $(GINKGO_TEST_OPTS) --focus "Asset Transfer With Two Fabric Networks" . diff --git a/token/core/common/interop/pledge/metadata.go b/token/core/common/interop/pledge/metadata.go new file mode 100644 index 0000000000..3317ba1211 --- /dev/null +++ b/token/core/common/interop/pledge/metadata.go @@ -0,0 +1,53 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package pledge + +import ( + "encoding/json" + + "github.com/hyperledger-labs/fabric-smart-client/platform/view/services/hash" + "github.com/hyperledger-labs/fabric-token-sdk/token/driver" + "github.com/hyperledger-labs/fabric-token-sdk/token/token" + "github.com/pkg/errors" +) + +type IssueMetadata struct { + // OriginTokenID is the identifier of the pledged token in the origin network + OriginTokenID *token.ID + // OriginNetwork is the network where the pledge took place + OriginNetwork string +} + +func IssueActionMetadata(attributes map[string][]byte, opts *driver.IssueOptions) (map[string][]byte, error) { + var metadata *IssueMetadata + var proof []byte + if len(opts.Attributes) != 0 { + tokenID, ok1 := opts.Attributes["github.com/hyperledger-labs/fabric-token-sdk/token/services/interop/pledge/tokenID"] + network, ok2 := opts.Attributes["github.com/hyperledger-labs/fabric-token-sdk/token/services/interop/pledge/network"] + proofOpt, ok3 := opts.Attributes["github.com/hyperledger-labs/fabric-token-sdk/token/services/interop/pledge/proof"] + if ok1 && ok2 { + metadata = &IssueMetadata{ + OriginTokenID: tokenID.(*token.ID), + OriginNetwork: network.(string), + } + } + if ok3 { + proof = proofOpt.([]byte) + } + } + if metadata != nil { + marshalled, err := json.Marshal(metadata) + key := hash.Hashable(marshalled).String() + if err != nil { + return nil, errors.Wrapf(err, "failed marshaling metadata; origin network [%s]; origin tokenID [%s]", metadata.OriginNetwork, metadata.OriginTokenID) + } + attributes[key] = marshalled + attributes[key+"proof_of_claim"] = proof + } + + return attributes, nil +} diff --git a/token/core/common/logging/logger.go b/token/core/common/logging/logger.go index 3c566c705c..ea27a0a010 100644 --- a/token/core/common/logging/logger.go +++ b/token/core/common/logging/logger.go @@ -33,6 +33,10 @@ type Logger interface { IsEnabledFor(level zapcore.Level) bool } +func MustGetLogger(name string) Logger { + return flogging.MustGetLogger(name) +} + func DriverLogger(prefix string, networkID string, channel string, namespace string) Logger { return flogging.MustGetLogger(loggerName(prefix, networkID, channel, namespace)) } diff --git a/token/core/fabtoken/driver/deserializer.go b/token/core/fabtoken/driver/deserializer.go index 0b089df9a3..790cc37552 100644 --- a/token/core/fabtoken/driver/deserializer.go +++ b/token/core/fabtoken/driver/deserializer.go @@ -10,10 +10,10 @@ import ( "github.com/hyperledger-labs/fabric-token-sdk/token/core/common" "github.com/hyperledger-labs/fabric-token-sdk/token/core/fabtoken" "github.com/hyperledger-labs/fabric-token-sdk/token/services/identity/deserializer" - "github.com/hyperledger-labs/fabric-token-sdk/token/services/identity/interop/htlc" "github.com/hyperledger-labs/fabric-token-sdk/token/services/identity/msp" "github.com/hyperledger-labs/fabric-token-sdk/token/services/identity/msp/x509" htlc2 "github.com/hyperledger-labs/fabric-token-sdk/token/services/interop/htlc" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/interop/pledge" ) // Deserializer deserializes verifiers associated with issuers, owners, and auditors @@ -25,7 +25,8 @@ type Deserializer struct { func NewDeserializer() *Deserializer { m := deserializer.NewTypedVerifierDeserializerMultiplex(&x509.AuditMatcherDeserializer{}) m.AddTypedVerifierDeserializer(msp.X509Identity, deserializer.NewTypedIdentityVerifierDeserializer(&x509.MSPIdentityDeserializer{})) - m.AddTypedVerifierDeserializer(htlc2.ScriptType, htlc.NewTypedIdentityDeserializer(m)) + m.AddTypedVerifierDeserializer(htlc2.ScriptType, htlc2.NewTypedIdentityDeserializer(m)) + m.AddTypedVerifierDeserializer(pledge.ScriptType, pledge.NewTypedIdentityDeserializer(m)) return &Deserializer{ Deserializer: common.NewDeserializer( @@ -52,6 +53,7 @@ type EIDRHDeserializer = deserializer.EIDRHDeserializer func NewEIDRHDeserializer() *EIDRHDeserializer { d := deserializer.NewEIDRHDeserializer() d.AddDeserializer(msp.X509Identity, &x509.AuditInfoDeserializer{}) - d.AddDeserializer(htlc2.ScriptType, htlc.NewAuditDeserializer(&x509.AuditInfoDeserializer{})) + d.AddDeserializer(htlc2.ScriptType, htlc2.NewAuditDeserializer(&x509.AuditInfoDeserializer{})) + d.AddDeserializer(pledge.ScriptType, pledge.NewAuditDeserializer(&x509.AuditInfoDeserializer{})) return d } diff --git a/token/core/fabtoken/driver/driver.go b/token/core/fabtoken/driver/driver.go index faffffd945..4e25f8dd77 100644 --- a/token/core/fabtoken/driver/driver.go +++ b/token/core/fabtoken/driver/driver.go @@ -11,9 +11,11 @@ import ( "github.com/hyperledger-labs/fabric-token-sdk/token" "github.com/hyperledger-labs/fabric-token-sdk/token/core" "github.com/hyperledger-labs/fabric-token-sdk/token/core/common" + "github.com/hyperledger-labs/fabric-token-sdk/token/core/common/interop/pledge" "github.com/hyperledger-labs/fabric-token-sdk/token/core/common/logging" "github.com/hyperledger-labs/fabric-token-sdk/token/core/config" "github.com/hyperledger-labs/fabric-token-sdk/token/core/fabtoken" + _ "github.com/hyperledger-labs/fabric-token-sdk/token/core/fabtoken/driver/interop/state/fabric" "github.com/hyperledger-labs/fabric-token-sdk/token/driver" "github.com/hyperledger-labs/fabric-token-sdk/token/services/identity" config2 "github.com/hyperledger-labs/fabric-token-sdk/token/services/identity/config" @@ -141,7 +143,14 @@ func (d *Driver) NewTokenService(sp driver.ServiceProvider, networkID string, ch common.NewSerializer(), deserializer, tmsConfig, - fabtoken.NewIssueService(publicParamsManager, ws, deserializer), + fabtoken.NewIssueService( + publicParamsManager, + ws, + deserializer, + []fabtoken.IssueMetadataProviderFunc{ + pledge.IssueActionMetadata, + }, + ), fabtoken.NewTransferService(logger, publicParamsManager, ws, common.NewVaultTokenLoader(qe), deserializer), fabtoken.NewAuditorService(), fabtoken.NewTokensService(), diff --git a/token/core/fabtoken/driver/interop/state/fabric/state.go b/token/core/fabtoken/driver/interop/state/fabric/state.go new file mode 100644 index 0000000000..c07e28ad69 --- /dev/null +++ b/token/core/fabtoken/driver/interop/state/fabric/state.go @@ -0,0 +1,454 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package fabric + +import ( + "encoding/base64" + "encoding/json" + "strings" + "time" + + "github.com/hyperledger-labs/fabric-token-sdk/token/core/fabtoken" + + "github.com/hyperledger-labs/fabric-smart-client/platform/fabric" + "github.com/hyperledger-labs/fabric-smart-client/platform/fabric/services/weaver" + "github.com/hyperledger-labs/fabric-token-sdk/token/core/common/logging" + driver2 "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/pledge" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/interop/state" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/interop/state/driver" + fabric3 "github.com/hyperledger-labs/fabric-token-sdk/token/services/interop/state/fabric" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/network/common/rws/keys" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/network/common/rws/translator" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/network/fabric/tcc" + "github.com/hyperledger-labs/fabric-token-sdk/token/token" + "github.com/pkg/errors" +) + +type RelayProvider interface { + Relay(fns *fabric.NetworkService) *weaver.Relay +} + +type PledgeVault interface { + PledgeByTokenID(tokenID *token.ID) ([]*pledge.Info, error) +} + +type GetFabricNetworkServiceFunc = func(string) (*fabric.NetworkService, error) + +type StateQueryExecutor struct { + Logger logging.Logger + RelayProvider RelayProvider + TargetNetworkURL string + RelaySelector *fabric.NetworkService +} + +func NewStateQueryExecutor( + Logger logging.Logger, + relayProvider RelayProvider, + targetNetworkURL string, + relaySelector *fabric.NetworkService, +) (*StateQueryExecutor, error) { + if err := fabric3.CheckFabricScheme(targetNetworkURL); err != nil { + return nil, err + } + return &StateQueryExecutor{ + Logger: Logger, + RelayProvider: relayProvider, + TargetNetworkURL: targetNetworkURL, + RelaySelector: relaySelector, + }, nil +} + +func (p *StateQueryExecutor) Exist(tokenID *token.ID) ([]byte, error) { + raw, err := json.Marshal(tokenID) + if err != nil { + return nil, err + } + + // get local relay + relay := p.RelayProvider.Relay(p.RelaySelector) + + // Query + p.Logger.Debugf("Query [%s] for proof of existence of token [%s], input [%s]", p.TargetNetworkURL, tokenID.String(), base64.StdEncoding.EncodeToString(raw)) + query, err := relay.ToFabric().Query(p.TargetNetworkURL, tcc.ProofOfTokenExistenceQuery, base64.StdEncoding.EncodeToString(raw)) + if err != nil { + return nil, err + } + res, err := query.Call() + if err != nil { + // todo: move this to the query executor + errMsg := err.Error() + switch { + case strings.Contains(errMsg, "failed to confirm if token with ID"): + return nil, errors.WithMessagef(state.TokenDoesNotExistError, "%s", err) + default: + return nil, err + } + } + + return res.Proof() +} + +func (p *StateQueryExecutor) DoesNotExist(tokenID *token.ID, origin string, deadline time.Time) ([]byte, error) { + req := &tcc.ProofOfTokenNonExistenceRequest{ + Deadline: deadline, + OriginNetwork: origin, + TokenID: tokenID, + } + raw, err := json.Marshal(req) + if err != nil { + return nil, err + } + + // get local relay + relay := p.RelayProvider.Relay(p.RelaySelector) + + // Query + p.Logger.Debugf("Query [%s] for proof of non-existence of token [%s], input [%s]", p.TargetNetworkURL, tokenID.String(), base64.StdEncoding.EncodeToString(raw)) + query, err := relay.ToFabric().Query(p.TargetNetworkURL, tcc.ProofOfTokenNonExistenceQuery, base64.StdEncoding.EncodeToString(raw)) + if err != nil { + return nil, err + } + res, err := query.Call() + if err != nil { + errMsg := err.Error() + switch { + case strings.Contains(errMsg, "failed to confirm if token from network"): + return nil, errors.WithMessagef(state.TokenExistsError, "%s", err) + default: + return nil, err + } + } + + return res.Proof() +} + +// ExistsWithMetadata returns a proof that a token with metadata including the passed token ID and origin network exists +// in the network this query executor targets +func (p *StateQueryExecutor) ExistsWithMetadata(tokenID *token.ID, origin string) ([]byte, error) { + req := &tcc.ProofOfTokenMetadataExistenceRequest{ + OriginNetwork: origin, + TokenID: tokenID, + } + raw, err := json.Marshal(req) + if err != nil { + return nil, errors.Wrapf(err, "failed to marshal proof of token metadata [%s]", req) + } + + // Get local relay + relay := p.RelayProvider.Relay(p.RelaySelector) + + // Query + p.Logger.Debugf("Query [%s] for proof of existence of metadata with token [%s], input [%s]", p.TargetNetworkURL, tokenID.String(), base64.StdEncoding.EncodeToString(raw)) + query, err := relay.ToFabric().Query(p.TargetNetworkURL, tcc.ProofOfTokenMetadataExistenceQuery, base64.StdEncoding.EncodeToString(raw)) + if err != nil { + return nil, errors.Wrapf(err, "failed to query proof of metadata [%s]", req) + } + res, err := query.Call() + if err != nil { + errMsg := err.Error() + switch { + case strings.Contains(errMsg, "failed to confirm if token from network"): + return nil, errors.WithMessagef(state.TokenExistsError, "%s", err) + default: + return nil, err + } + } + + return res.Proof() +} + +type StateVerifier struct { + Logger logging.Logger + RelayProvider RelayProvider + NetworkURL string + RelaySelector *fabric.NetworkService + PledgeVault PledgeVault + GetFabricNetworkService GetFabricNetworkServiceFunc +} + +func NewStateVerifier( + Logger logging.Logger, + relayProvider RelayProvider, + PledgeVault PledgeVault, + GetFabricNetworkService GetFabricNetworkServiceFunc, + networkURL string, + relaySelector *fabric.NetworkService, +) (*StateVerifier, error) { + if err := fabric3.CheckFabricScheme(networkURL); err != nil { + return nil, err + } + return &StateVerifier{ + Logger: Logger, + RelayProvider: relayProvider, + NetworkURL: networkURL, + RelaySelector: relaySelector, + PledgeVault: PledgeVault, + GetFabricNetworkService: GetFabricNetworkService, + }, nil +} + +func (v *StateVerifier) VerifyProofExistence(proofRaw []byte, tokenID *token.ID, metadata []byte) error { + // Get local relay + relay := v.RelayProvider.Relay(v.RelaySelector) + + // Parse proof + proof, err := relay.ToFabric().ProofFromBytes(proofRaw) + if err != nil { + return errors.Wrapf(err, "failed to unmarshal claim proof") + } + if err := proof.Verify(); err != nil { + return errors.Wrapf(err, "failed to verify pledge proof") + } + + // todo check that address in proof matches source network + + rwset, err := proof.RWSet() + if err != nil { + return errors.Wrapf(err, "failed to unmarshal claim proof") + } + + key, err := keys.CreateProofOfExistenceKey(tokenID) + if err != nil { + return err + } + tmsID, err := fabric3.FabricURLToTMSID(v.NetworkURL) + if err != nil { + return err + } + raw, err := rwset.GetState(tmsID.Namespace, key) + if err != nil { + return errors.Wrapf(err, "failed to check proof of existence") + } + if len(raw) == 0 { + return errors.Errorf("failed to check proof of existence, missing key-value pair") + } + tok := &token.Token{} + err = json.Unmarshal(raw, tok) + if err != nil { + return err + } + // Validate against pledge + v.Logger.Debugf("verify proof of existence for token id [%s]", tokenID) + pledges, err := v.PledgeVault.PledgeByTokenID(tokenID) + if err != nil { + v.Logger.Errorf("failed retrieving pledge info for token id [%s]: [%s]", tokenID, err) + return errors.WithMessagef(err, "failed getting pledge for [%s]", tokenID) + } + if len(pledges) != 1 { + v.Logger.Errorf("failed retrieving pledge info for token id [%s]: no info found", tokenID) + return errors.Errorf("expected one pledge, got [%d]", len(pledges)) + } + info := pledges[0] + v.Logger.Debugf("found pledge info for token id [%s]: [%s]", tokenID, info.Source) + + if tok.Type != info.TokenType { + return errors.Errorf("type of pledge token does not match type in claim request") + } + q, err := token.ToQuantity(tok.Quantity, 64) + if err != nil { + return err + } + expectedQ := token.NewQuantityFromUInt64(info.Amount) + if expectedQ.Cmp(q) != 0 { + return errors.Errorf("quantity in pledged token is different from quantity in claim request") + } + owner, err := identity.UnmarshalTypedIdentity(tok.Owner.Raw) + if err != nil { + return err + } + if owner.Type != pledge.ScriptType { + return err + } + script := &pledge.Script{} + err = json.Unmarshal(owner.Identity, script) + if err != nil { + return err + } + if script.Recipient == nil { + return errors.Errorf("script in proof encodes invalid recipient") + } + if !script.Recipient.Equal(info.Script.Recipient) { + return errors.Errorf("recipient in claim request does not match recipient in proof") + } + if script.Deadline != info.Script.Deadline { + return errors.Errorf("deadline in claim request does not match deadline in proof") + } + if script.DestinationNetwork != info.Script.DestinationNetwork { + return errors.Errorf("destination network in claim request does not match destination network in proof [%s vs.%s]", info.Script.DestinationNetwork, script.DestinationNetwork) + } + + return nil +} + +func (v *StateVerifier) VerifyProofNonExistence(proofRaw []byte, tokenID *token.ID, origin string, deadline time.Time) error { + // v.NetworkURL is the network from which the proof comes from + tokenOriginNetworkTMSID, err := fabric3.FabricURLToTMSID(origin) + if err != nil { + return errors.Wrapf(err, "failed to parse network url") + } + // get local relay + fns, err := v.GetFabricNetworkService(tokenOriginNetworkTMSID.Network) + if err != nil { + return errors.Wrapf(err, "failed to get fabric network service for network [%s]", tokenOriginNetworkTMSID.Network) + } + relay := v.RelayProvider.Relay(fns) + + // parse proof + proof, err := relay.ToFabric().ProofFromBytes(proofRaw) + if err != nil { + return errors.Wrapf(err, "failed to umarshal proof") + } + + rwset, err := proof.RWSet() + if err != nil { + return errors.Wrapf(err, "failed to retrieve RWset") + } + + key, err := keys.CreateProofOfNonExistenceKey(tokenID, origin) + if err != nil { + return errors.Wrapf(err, "failed creating key for proof of non-existence") + } + + proofSourceNetworkTMSID, err := fabric3.FabricURLToTMSID(v.NetworkURL) + if err != nil { + return err + } + raw, err := rwset.GetState(proofSourceNetworkTMSID.Namespace, key) + if err != nil { + return errors.Wrapf(err, "failed to check proof of non-existence") + } + p := &translator.ProofOfTokenMetadataNonExistence{} + if raw == nil { + return errors.Errorf("could not find proof of non-existence") + } + err = json.Unmarshal(raw, p) + if err != nil { + return errors.Wrapf(err, "failed to unmarshal proof of non-existence") + } + if p.Deadline != deadline { + return errors.Errorf("deadline in reclaim request does not match deadline in proof of non-existence") + } + if p.TokenID.String() != tokenID.String() { + return errors.Errorf("token ID in reclaim request does not match token ID in proof of non-existence") + } + if p.Origin != fabric3.FabricURL(tokenOriginNetworkTMSID) { + return errors.Errorf("origin in reclaim request does not match origin in proof of non-existence") + } + + // todo check that address in proof is the destination network + + err = proof.Verify() + if err != nil { + return errors.Wrapf(err, "invalid proof of non-existence") + } + + return nil +} + +// VerifyProofTokenWithMetadataExistence verifies that a proof of existence of a token +// with metadata including the given token ID and origin network, in the target network is valid +func (v *StateVerifier) VerifyProofTokenWithMetadataExistence(proofRaw []byte, tokenID *token.ID, origin string) error { + // v.NetworkURL is the network from which the proof comes from + tokenOriginNetworkTMSID, err := fabric3.FabricURLToTMSID(origin) + if err != nil { + return errors.Wrapf(err, "failed to parse network url") + } + + // get local relay + fns, err := v.GetFabricNetworkService(tokenOriginNetworkTMSID.Network) + if err != nil { + return errors.Wrapf(err, "failed to get fabric network service for network [%s]", tokenOriginNetworkTMSID.Network) + } + relay := v.RelayProvider.Relay(fns) + + // parse proof + proof, err := relay.ToFabric().ProofFromBytes(proofRaw) + if err != nil { + return errors.Wrapf(err, "failed to umarshal proof") + } + + rwset, err := proof.RWSet() + if err != nil { + return errors.Wrapf(err, "failed to retrieve RWset") + } + + key, err := keys.CreateProofOfMetadataExistenceKey(tokenID, origin) + if err != nil { + return errors.Wrapf(err, "failed creating key for proof of token existence") + } + + proofSourceNetworkTMSID, err := fabric3.FabricURLToTMSID(v.NetworkURL) + if err != nil { + return err + } + raw, err := rwset.GetState(proofSourceNetworkTMSID.Namespace, key) + if err != nil { + return errors.Wrapf(err, "failed to check proof of token existence") + } + p := &translator.ProofOfTokenMetadataExistence{} + if raw == nil { + return errors.Errorf("could not find proof of token existence") + } + err = json.Unmarshal(raw, p) + if err != nil { + return errors.Wrapf(err, "failed to unmarshal proof of token existence") + } + if p.TokenID.String() != tokenID.String() { + return errors.Errorf("token ID in redeem request does not match token ID in proof of token existence") + } + if p.Origin != fabric3.FabricURL(tokenOriginNetworkTMSID) { + return errors.Errorf("origin in redeem request does not match origin in proof of token existence") + } + + // todo check that address in proof is the destination network + + err = proof.Verify() + if err != nil { + return errors.Wrapf(err, "invalid proof of token existence") + } + + return nil +} + +type StateDriver struct { + Logger logging.Logger +} + +func NewStateDriver(logger logging.Logger) *StateDriver { + return &StateDriver{Logger: logger} +} + +func (d *StateDriver) NewStateQueryExecutor(sp driver2.ServiceProvider, url string) (driver.StateQueryExecutor, error) { + fns, err := fabric.GetDefaultFNS(sp) + if err != nil { + return nil, errors.Wrapf(err, "failed to get default FNS") + } + return NewStateQueryExecutor(d.Logger, weaver.GetProvider(sp), url, fns) +} + +func (d *StateDriver) NewStateVerifier(sp driver2.ServiceProvider, url string) (driver.StateVerifier, error) { + fns, err := fabric.GetDefaultFNS(sp) + if err != nil { + return nil, errors.Wrapf(err, "failed to get default FNS") + } + return NewStateVerifier( + d.Logger, + weaver.GetProvider(sp), + pledge.Vault(sp), + func(id string) (*fabric.NetworkService, error) { + return fabric.GetFabricNetworkService(sp, id) + }, + url, + fns, + ) +} + +func init() { + fabric3.RegisterStateDriver(fabtoken.PublicParameters, NewStateDriver(logging.MustGetLogger("token-sdk.core.fabtoken"))) +} diff --git a/token/core/fabtoken/issue.go b/token/core/fabtoken/issue.go index 0a73f21d03..4825ccf965 100644 --- a/token/core/fabtoken/issue.go +++ b/token/core/fabtoken/issue.go @@ -12,14 +12,27 @@ import ( "github.com/pkg/errors" ) +type IssueMetadataProviderFunc = func(attributes map[string][]byte, opts *driver.IssueOptions) (map[string][]byte, error) + type IssueService struct { - PublicParamsManager driver.PublicParamsManager - WalletService driver.WalletService - Deserializer driver.Deserializer + PublicParamsManager driver.PublicParamsManager + WalletService driver.WalletService + Deserializer driver.Deserializer + IssueMetadataProviders []IssueMetadataProviderFunc } -func NewIssueService(publicParamsManager driver.PublicParamsManager, walletService driver.WalletService, deserializer driver.Deserializer) *IssueService { - return &IssueService{PublicParamsManager: publicParamsManager, WalletService: walletService, Deserializer: deserializer} +func NewIssueService( + publicParamsManager driver.PublicParamsManager, + walletService driver.WalletService, + deserializer driver.Deserializer, + IssueMetadataProviders []IssueMetadataProviderFunc, +) *IssueService { + return &IssueService{ + PublicParamsManager: publicParamsManager, + WalletService: walletService, + Deserializer: deserializer, + IssueMetadataProviders: IssueMetadataProviders, + } } // Issue returns an IssueAction as a function of the passed arguments @@ -65,7 +78,20 @@ func (s *IssueService) Issue(issuerIdentity driver.Identity, tokenType string, v outputsMetadata = append(outputsMetadata, metaRaw) } - action := &IssueAction{Issuer: issuerIdentity, Outputs: outs} + actionMetadata := map[string][]byte{} + var err error + for _, provider := range s.IssueMetadataProviders { + actionMetadata, err = provider(actionMetadata, opts) + if err != nil { + return nil, nil, errors.Wrapf(err, "failed generating token action metadata") + } + } + + action := &IssueAction{ + Issuer: issuerIdentity, + Outputs: outs, + Metadata: actionMetadata, + } outputs, err := action.GetSerializedOutputs() if err != nil { return nil, nil, err diff --git a/token/core/fabtoken/transfer.go b/token/core/fabtoken/transfer.go index dd4f4574ea..d098288398 100644 --- a/token/core/fabtoken/transfer.go +++ b/token/core/fabtoken/transfer.go @@ -121,6 +121,7 @@ func (s *TransferService) Transfer(txID string, wallet driver.OwnerWallet, token if err != nil { return nil, nil, errors.Wrapf(err, "failed getting audit info for recipient identity [%s]", receiver.String()) } + s.Logger.Debugf("receiver audit info for [%s] is [%s]", receiver, auditInfo) receiverAuditInfos = append(receiverAuditInfos, auditInfo...) } outputs, err := transfer.GetSerializedOutputs() diff --git a/token/core/fabtoken/validator.go b/token/core/fabtoken/validator.go index 1ba246358b..4e39b412d0 100644 --- a/token/core/fabtoken/validator.go +++ b/token/core/fabtoken/validator.go @@ -50,11 +50,13 @@ func NewValidator(logger logging.Logger, pp *PublicParams, deserializer driver.D TransferSignatureValidate, TransferBalanceValidate, TransferHTLCValidate, + TransferPledgeValidate, } transferValidators = append(transferValidators, extraValidators...) issueValidators := []ValidateIssueFunc{ IssueValidate, + IssuePledgeValidate, } return common.NewValidator[*PublicParams, *token.Token, *TransferAction, *IssueAction]( diff --git a/token/core/fabtoken/validator_pledge.go b/token/core/fabtoken/validator_pledge.go new file mode 100644 index 0000000000..77ec9f502e --- /dev/null +++ b/token/core/fabtoken/validator_pledge.go @@ -0,0 +1,143 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package fabtoken + +import ( + "bytes" + "encoding/json" + "fmt" + "strconv" + "time" + + "github.com/hyperledger-labs/fabric-token-sdk/token/services/identity" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/interop/pledge" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/network/common/rws/keys" + "github.com/pkg/errors" +) + +func IssuePledgeValidate(ctx *Context) error { + for k := range ctx.IssueAction.Metadata { + ctx.CountMetadataKey(k) + } + return nil +} + +func TransferPledgeValidate(ctx *Context) error { + ctx.Logger.Debug("pledge validation starts") + for _, in := range ctx.InputTokens { + id, err := identity.UnmarshalTypedIdentity(in.Owner.Raw) + if err != nil { + return errors.Wrap(err, "failed to unmarshal owner of input token") + } + if id.Type == pledge.ScriptType { + if len(ctx.InputTokens) != 1 || len(ctx.TransferAction.GetOutputs()) != 1 { + return errors.Errorf("invalid transfer action: a pledge script only transfers the ownership of a token") + } + out := ctx.TransferAction.GetOutputs()[0].(*Output) + tok := out.Output + sender, err := identity.UnmarshalTypedIdentity(ctx.InputTokens[0].Owner.Raw) + if err != nil { + return err + } + script := &pledge.Script{} + err = json.Unmarshal(sender.Identity, script) + if err != nil { + return errors.Wrap(err, "failed to unmarshal pledge script") + } + if time.Now().Before(script.Deadline) { + return errors.New("cannot reclaim pledge yet: wait for timeout to elapse.") + } + + key, err := constructMetadataKey(ctx.TransferAction) + if err != nil { + return errors.Wrap(err, "failed constructing metadata key") + } + + if out.IsRedeem() { + redeemKey := pledge.RedeemPledgeKey + key + v, ok := ctx.TransferAction.GetMetadata()[redeemKey] + if !ok { + return errors.Errorf("empty metadata of redeem for pledge script with identifier %s", redeemKey) + } + if v == nil { + return errors.Errorf("invalid metadatata of redeem for pledge script with identifier %s, metadata should contain a proof", redeemKey) + } + ctx.CountMetadataKey(redeemKey) + continue + } + if !script.Sender.Equal(tok.Owner.Raw) { + return errors.Errorf("recipient of token does not correspond to the sender of reclaim request") + } + + reclaimKey := pledge.MetadataReclaimKey + key + v, ok := ctx.TransferAction.GetMetadata()[reclaimKey] + if !ok { + return errors.Errorf("empty metadata of reclaim with identifier %s", reclaimKey) + } + if v == nil { + return errors.Errorf("invalid metadatata of reclaim with identifier %s, metadata should contain a proof", reclaimKey) + } + ctx.CountMetadataKey(reclaimKey) + } + } + + for _, o := range ctx.TransferAction.GetOutputs() { + out, ok := o.(*Output) + if !ok { + return errors.Errorf("invalid output") + } + if out.IsRedeem() { + continue + } + owner, err := identity.UnmarshalTypedIdentity(out.Output.Owner.Raw) + if err != nil { + return err + } + if owner.Type == pledge.ScriptType { + script := &pledge.Script{} + err = json.Unmarshal(owner.Identity, script) + if err != nil { + return err + } + if script.Deadline.Before(time.Now()) { + return errors.Errorf("pledge script is invalid: expiration date has already passed") + } + v, ok := ctx.TransferAction.GetMetadata()[pledge.MetadataKey+script.ID] + if !ok { + return errors.Errorf("empty metadata for pledge script with identifier %s", script.ID) + } + if !bytes.Equal(v, []byte("1")) { + return errors.Errorf("invalid metadatata for pledge script with identifier %s", script.ID) + } + ctx.CountMetadataKey(pledge.MetadataKey + script.ID) + } + } + return nil +} + +func constructMetadataKey(action *TransferAction) (string, error) { + inputs, err := action.GetInputs() + if err != nil { + return "", errors.Wrap(err, "failed to retrieve input IDs from action") + } + if len(inputs) != 1 { + return "", errors.New("invalid transfer action, does not carry a single input") + } + prefix, components, err := keys.SplitCompositeKey(inputs[0]) + if err != nil { + return "", errors.Wrapf(err, "unable to split input as key") + } + if prefix != keys.TokenKeyPrefix { + return "", errors.Errorf("expected prefix [%s], got [%s], skipping", keys.TokenKeyPrefix, prefix) + } + txID := components[0] + index, err := strconv.ParseUint(components[1], 10, 64) + if err != nil { + return "", errors.Errorf("invalid index for key [%s]", inputs[0]) + } + return fmt.Sprintf(".%d.%s", index, txID), nil +} diff --git a/token/core/fabtoken/validator_transfer.go b/token/core/fabtoken/validator_transfer.go index f6f6f721ae..8a08ba3600 100644 --- a/token/core/fabtoken/validator_transfer.go +++ b/token/core/fabtoken/validator_transfer.go @@ -12,7 +12,6 @@ import ( "github.com/hyperledger-labs/fabric-token-sdk/token/driver" "github.com/hyperledger-labs/fabric-token-sdk/token/services/identity" - htlc2 "github.com/hyperledger-labs/fabric-token-sdk/token/services/identity/interop/htlc" "github.com/hyperledger-labs/fabric-token-sdk/token/services/interop/htlc" "github.com/hyperledger-labs/fabric-token-sdk/token/token" "github.com/pkg/errors" @@ -120,18 +119,18 @@ func TransferHTLCValidate(ctx *Context) error { } // check owner field - script, op, err := htlc2.VerifyOwner(ctx.InputTokens[0].Owner.Raw, tok.Owner.Raw, now) + script, op, err := htlc.VerifyOwner(ctx.InputTokens[0].Owner.Raw, tok.Owner.Raw, now) if err != nil { return errors.Wrap(err, "failed to verify transfer from htlc script") } // check metadata sigma := ctx.Signatures[i] - metadataKey, err := htlc2.MetadataClaimKeyCheck(ctx.TransferAction, script, op, sigma) + metadataKey, err := htlc.MetadataClaimKeyCheck(ctx.TransferAction, script, op, sigma) if err != nil { return errors.WithMessagef(err, "failed to check htlc metadata") } - if op != htlc2.Reclaim { + if op != htlc.Reclaim { ctx.CountMetadataKey(metadataKey) } } @@ -160,7 +159,7 @@ func TransferHTLCValidate(ctx *Context) error { if err := script.Validate(now); err != nil { return errors.WithMessagef(err, "htlc script invalid") } - metadataKey, err := htlc2.MetadataLockKeyCheck(ctx.TransferAction, script) + metadataKey, err := htlc.MetadataLockKeyCheck(ctx.TransferAction, script) if err != nil { return errors.WithMessagef(err, "failed to check htlc metadata") } diff --git a/token/core/zkatdlog/crypto/audit/auditor.go b/token/core/zkatdlog/crypto/audit/auditor.go index 42b13c4223..29dffd8106 100644 --- a/token/core/zkatdlog/crypto/audit/auditor.go +++ b/token/core/zkatdlog/crypto/audit/auditor.go @@ -18,9 +18,9 @@ import ( "github.com/hyperledger-labs/fabric-token-sdk/token/core/zkatdlog/crypto/transfer" "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/identity/interop/htlc" "github.com/hyperledger-labs/fabric-token-sdk/token/services/identity/msp" - htlc2 "github.com/hyperledger-labs/fabric-token-sdk/token/services/interop/htlc" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/interop/htlc" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/interop/pledge" "github.com/pkg/errors" ) @@ -268,15 +268,17 @@ func InspectTokenOwner(des Deserializer, token *AuditableToken, index int) error return errors.Wrapf(err, "owner at index [%d] does not match the provided opening", index) } return nil - case htlc2.ScriptType: - return inspectTokenOwnerOfScript(des, token, index) + case htlc.ScriptType: + return inspectTokenOwnerOfHTLCScript(des, token, index) + case pledge.ScriptType: + return inspectTokenOwnerOfPledgeScript(des, token, index) default: return errors.Errorf("identity type [%s] not recognized", ro.Type) } } -func inspectTokenOwnerOfScript(des Deserializer, token *AuditableToken, index int) error { +func inspectTokenOwnerOfHTLCScript(des Deserializer, token *AuditableToken, index int) error { owner, err := identity.UnmarshalTypedIdentity(token.Token.Owner) if err != nil { return errors.Errorf("input owner at index [%d] cannot be unmarshalled", index) @@ -317,6 +319,45 @@ func inspectTokenOwnerOfScript(des Deserializer, token *AuditableToken, index in return nil } +func inspectTokenOwnerOfPledgeScript(des Deserializer, token *AuditableToken, index int) error { + owner, err := identity.UnmarshalTypedIdentity(token.Token.Owner) + if err != nil { + return errors.Errorf("input owner at index [%d] cannot be unmarshalled", index) + } + scriptInf := &htlc.ScriptInfo{} + if err := json.Unmarshal(token.Owner.OwnerInfo, scriptInf); err != nil { + return errors.Wrapf(err, "failed to unmarshal script info") + } + scriptSender, _, scriptIssuer, err := pledge.GetScriptSenderAndRecipient(owner) + if err != nil { + return errors.Wrap(err, "failed getting script sender and recipient") + } + + sender, err := des.GetOwnerMatcher(scriptInf.Sender) + if err != nil { + return errors.Wrapf(err, "failed to get owner matcher from pledge script sender [%s]", string(scriptInf.Sender)) + } + ro, err := identity.UnmarshalTypedIdentity(scriptSender) + if err != nil { + return errors.Wrapf(err, "failed to retrieve raw owner from sender in pledge script") + } + // If this is a reclaim, match it against the sender. + // If this is a redeem, match it against the issuer. + if err := sender.Match(ro.Identity); err != nil { + // Check if this can be matched to the issuer + ro, err := identity.UnmarshalTypedIdentity(scriptIssuer) + if err != nil { + return errors.Wrapf(err, "failed to retrieve raw owner from issuer in pledge script") + } + if err := sender.Match(ro.Identity); err != nil { + return errors.Wrapf(err, "token at index [%d] does not match the provided opening [%s]", index, string(scriptInf.Sender)) + } + } + + // TODO: recipient is in another network + return nil +} + // GetAuditInfoForIssues returns an array of AuditableToken for each issue action // It takes a deserializer, an array of serialized issue actions and an array of issue metadata. func GetAuditInfoForIssues(issues [][]byte, metadata []driver.IssueMetadata) ([][]*AuditableToken, error) { diff --git a/token/core/zkatdlog/crypto/validator/validator.go b/token/core/zkatdlog/crypto/validator/validator.go index 732d877925..c3ad780d37 100644 --- a/token/core/zkatdlog/crypto/validator/validator.go +++ b/token/core/zkatdlog/crypto/validator/validator.go @@ -53,11 +53,13 @@ func New(logger logging.Logger, pp *crypto.PublicParams, deserializer driver.Des TransferSignatureValidate, TransferZKProofValidate, TransferHTLCValidate, + TransferPledgeValidate, } transferValidators = append(transferValidators, extraValidators...) issueValidators := []ValidateIssueFunc{ IssueValidate, + IssuePledgeValidate, } return common.NewValidator[*crypto.PublicParams, *token.Token, *transfer.TransferAction, *issue.IssueAction]( diff --git a/token/core/zkatdlog/crypto/validator/validator_pledge.go b/token/core/zkatdlog/crypto/validator/validator_pledge.go new file mode 100644 index 0000000000..f8f9c037b1 --- /dev/null +++ b/token/core/zkatdlog/crypto/validator/validator_pledge.go @@ -0,0 +1,143 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package validator + +import ( + "bytes" + "encoding/json" + "fmt" + "strconv" + "time" + + "github.com/hyperledger-labs/fabric-token-sdk/token/core/zkatdlog/crypto/token" + "github.com/hyperledger-labs/fabric-token-sdk/token/core/zkatdlog/crypto/transfer" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/identity" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/interop/pledge" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/network/common/rws/keys" + "github.com/pkg/errors" +) + +func IssuePledgeValidate(ctx *Context) error { + for k := range ctx.IssueAction.Metadata { + ctx.CountMetadataKey(k) + } + return nil +} + +func TransferPledgeValidate(ctx *Context) error { + for _, in := range ctx.InputTokens { + id, err := identity.UnmarshalTypedIdentity(in.Owner) + if err != nil { + return errors.Wrap(err, "failed to unmarshal owner of input token") + } + if id.Type == pledge.ScriptType { + if len(ctx.InputTokens) != 1 || len(ctx.TransferAction.GetOutputs()) != 1 { + return errors.Errorf("invalid transfer action: a pledge script only transfers the ownership of a token") + } + out := ctx.TransferAction.GetOutputs()[0].(*token.Token) + sender, err := identity.UnmarshalTypedIdentity(ctx.InputTokens[0].Owner) + if err != nil { + return err + } + script := &pledge.Script{} + err = json.Unmarshal(sender.Identity, script) + if err != nil { + return err + } + if time.Now().Before(script.Deadline) { + return errors.New("cannot reclaim pledge yet: wait for timeout to elapse.") + } + + key, err := constructMetadataKey(ctx.TransferAction) + if err != nil { + return errors.Wrap(err, "failed constructing metadata key") + } + + if out.IsRedeem() { + redeemKey := pledge.RedeemPledgeKey + key + v, ok := ctx.TransferAction.GetMetadata()[redeemKey] + if !ok { + return errors.Errorf("empty metadata of redeem for pledge script with identifier %s", redeemKey) + } + if v == nil { + return errors.Errorf("invalid metadatata of redeem for pledge script with identifier %s, metadata should contain a proof", redeemKey) + } + ctx.CountMetadataKey(redeemKey) + continue + } + if !script.Sender.Equal(out.Owner) { + return errors.New("recipient of token does not correspond to sender of reclaim request") + } + + reclaimKey := pledge.MetadataReclaimKey + key + v, ok := ctx.TransferAction.GetMetadata()[reclaimKey] + if !ok { + return errors.Errorf("empty metadata for pledge script with identifier %s", reclaimKey) + } + if v == nil { + return errors.Errorf("invalid metadatata for pledge script with identifier %s, metadata should contain a proof", reclaimKey) + } + ctx.CountMetadataKey(reclaimKey) + } + } + + for _, o := range ctx.TransferAction.GetOutputs() { + out, ok := o.(*token.Token) + if !ok { + return errors.Errorf("invalid output") + } + if out.IsRedeem() { + continue + } + owner, err := identity.UnmarshalTypedIdentity(out.Owner) + if err != nil { + return err + } + if owner.Type == pledge.ScriptType { + script := &pledge.Script{} + err = json.Unmarshal(owner.Identity, script) + if err != nil { + return err + } + if script.Deadline.Before(time.Now()) { + return errors.Errorf("pledge script is invalid: expiration date has already passed") + } + v, ok := ctx.TransferAction.GetMetadata()[pledge.MetadataKey+script.ID] + if !ok { + return errors.Errorf("empty metadata for pledge script with identifier %s", script.ID) + } + if !bytes.Equal(v, []byte("1")) { + return errors.Errorf("invalid metadatata for pledge script with identifier %s", script.ID) + } + ctx.CountMetadataKey(pledge.MetadataKey + script.ID) + } + } + return nil +} + +func constructMetadataKey(action *transfer.TransferAction) (string, error) { + inputs, err := action.GetInputs() + if err != nil { + return "", errors.Wrap(err, "failed to retrieve input IDs from action") + } + if len(inputs) != 1 { + return "", errors.New("invalid transfer action, does not carry a single input") + } + prefix, components, err := keys.SplitCompositeKey(inputs[0]) + if err != nil { + return "", errors.Wrapf(err, "unable to split input as key") + } + if prefix != keys.TokenKeyPrefix { + return "", errors.Errorf("expected prefix [%s], got [%s], skipping", keys.TokenKeyPrefix, prefix) + } + txID := components[0] + index, err := strconv.ParseUint(components[1], 10, 64) + if err != nil { + return "", errors.Errorf("invalid index for key [%s]", inputs[0]) + } + return fmt.Sprintf(".%d.%s", index, txID), nil +} diff --git a/token/core/zkatdlog/crypto/validator/validator_transfer.go b/token/core/zkatdlog/crypto/validator/validator_transfer.go index 742cef4eac..85f9e0f566 100644 --- a/token/core/zkatdlog/crypto/validator/validator_transfer.go +++ b/token/core/zkatdlog/crypto/validator/validator_transfer.go @@ -15,7 +15,6 @@ import ( "github.com/hyperledger-labs/fabric-token-sdk/token/core/zkatdlog/crypto/transfer" "github.com/hyperledger-labs/fabric-token-sdk/token/driver" "github.com/hyperledger-labs/fabric-token-sdk/token/services/identity" - htlc2 "github.com/hyperledger-labs/fabric-token-sdk/token/services/identity/interop/htlc" "github.com/hyperledger-labs/fabric-token-sdk/token/services/interop/htlc" "github.com/pkg/errors" ) @@ -94,18 +93,18 @@ func TransferHTLCValidate(ctx *Context) error { out := ctx.TransferAction.GetOutputs()[0].(*token.Token) // check that owner field in output is correct - script, op, err := htlc2.VerifyOwner(ctx.InputTokens[0].Owner, out.Owner, now) + script, op, err := htlc.VerifyOwner(ctx.InputTokens[0].Owner, out.Owner, now) if err != nil { return errors.Wrap(err, "failed to verify transfer from htlc script") } // check metadata sigma := ctx.Signatures[i] - metadataKey, err := htlc2.MetadataClaimKeyCheck(ctx.TransferAction, script, op, sigma) + metadataKey, err := htlc.MetadataClaimKeyCheck(ctx.TransferAction, script, op, sigma) if err != nil { return errors.WithMessagef(err, "failed to check htlc metadata") } - if op != htlc2.Reclaim { + if op != htlc.Reclaim { ctx.CountMetadataKey(metadataKey) } } @@ -132,7 +131,7 @@ func TransferHTLCValidate(ctx *Context) error { if err := script.Validate(now); err != nil { return errors.WithMessagef(err, "htlc script invalid") } - metadataKey, err := htlc2.MetadataLockKeyCheck(ctx.TransferAction, script) + metadataKey, err := htlc.MetadataLockKeyCheck(ctx.TransferAction, script) if err != nil { return errors.WithMessagef(err, "failed to check htlc metadata") } diff --git a/token/core/zkatdlog/nogh/driver/deserializer.go b/token/core/zkatdlog/nogh/driver/deserializer.go index 5c43abbbc3..10551b3228 100644 --- a/token/core/zkatdlog/nogh/driver/deserializer.go +++ b/token/core/zkatdlog/nogh/driver/deserializer.go @@ -11,11 +11,11 @@ import ( "github.com/hyperledger-labs/fabric-token-sdk/token/core/zkatdlog/crypto" "github.com/hyperledger-labs/fabric-token-sdk/token/core/zkatdlog/crypto/token" "github.com/hyperledger-labs/fabric-token-sdk/token/services/identity/deserializer" - "github.com/hyperledger-labs/fabric-token-sdk/token/services/identity/interop/htlc" "github.com/hyperledger-labs/fabric-token-sdk/token/services/identity/msp" "github.com/hyperledger-labs/fabric-token-sdk/token/services/identity/msp/idemix" "github.com/hyperledger-labs/fabric-token-sdk/token/services/identity/msp/x509" - htlc2 "github.com/hyperledger-labs/fabric-token-sdk/token/services/interop/htlc" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/interop/htlc" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/interop/pledge" "github.com/pkg/errors" ) @@ -35,7 +35,8 @@ func NewDeserializer(pp *crypto.PublicParams) (*Deserializer, error) { } m := deserializer.NewTypedVerifierDeserializerMultiplex(idemixDes) m.AddTypedVerifierDeserializer(msp.IdemixIdentity, deserializer.NewTypedIdentityVerifierDeserializer(idemixDes)) - m.AddTypedVerifierDeserializer(htlc2.ScriptType, htlc.NewTypedIdentityDeserializer(m)) + m.AddTypedVerifierDeserializer(htlc.ScriptType, htlc.NewTypedIdentityDeserializer(m)) + m.AddTypedVerifierDeserializer(pledge.ScriptType, pledge.NewTypedIdentityDeserializer(m)) return &Deserializer{ Deserializer: common.NewDeserializer( @@ -80,6 +81,7 @@ type EIDRHDeserializer = deserializer.EIDRHDeserializer func NewEIDRHDeserializer() *EIDRHDeserializer { d := deserializer.NewEIDRHDeserializer() d.AddDeserializer(msp.IdemixIdentity, &idemix.AuditInfoDeserializer{}) - d.AddDeserializer(htlc2.ScriptType, htlc.NewAuditDeserializer(&idemix.AuditInfoDeserializer{})) + d.AddDeserializer(htlc.ScriptType, htlc.NewAuditDeserializer(&idemix.AuditInfoDeserializer{})) + d.AddDeserializer(pledge.ScriptType, pledge.NewAuditDeserializer(&idemix.AuditInfoDeserializer{})) return d } diff --git a/token/core/zkatdlog/nogh/driver/driver.go b/token/core/zkatdlog/nogh/driver/driver.go index 6aa5bd4404..432f54e675 100644 --- a/token/core/zkatdlog/nogh/driver/driver.go +++ b/token/core/zkatdlog/nogh/driver/driver.go @@ -11,12 +11,14 @@ import ( "github.com/hyperledger-labs/fabric-token-sdk/token" "github.com/hyperledger-labs/fabric-token-sdk/token/core" "github.com/hyperledger-labs/fabric-token-sdk/token/core/common" + "github.com/hyperledger-labs/fabric-token-sdk/token/core/common/interop/pledge" "github.com/hyperledger-labs/fabric-token-sdk/token/core/common/logging" "github.com/hyperledger-labs/fabric-token-sdk/token/core/config" "github.com/hyperledger-labs/fabric-token-sdk/token/core/zkatdlog/crypto" token3 "github.com/hyperledger-labs/fabric-token-sdk/token/core/zkatdlog/crypto/token" "github.com/hyperledger-labs/fabric-token-sdk/token/core/zkatdlog/crypto/validator" zkatdlog "github.com/hyperledger-labs/fabric-token-sdk/token/core/zkatdlog/nogh" + _ "github.com/hyperledger-labs/fabric-token-sdk/token/core/zkatdlog/nogh/driver/interop/state/fabric" "github.com/hyperledger-labs/fabric-token-sdk/token/driver" "github.com/hyperledger-labs/fabric-token-sdk/token/services/identity" config2 "github.com/hyperledger-labs/fabric-token-sdk/token/services/identity/config" @@ -155,7 +157,14 @@ func (d *Driver) NewTokenService(sp driver.ServiceProvider, networkID string, ch common.NewSerializer(), deserializer, tmsConfig, - zkatdlog.NewIssueService(ppm, ws, deserializer), + zkatdlog.NewIssueService( + ppm, + ws, + deserializer, + []zkatdlog.IssueMetadataProviderFunc{ + pledge.IssueActionMetadata, + }, + ), zkatdlog.NewTransferService(logger, ppm, ws, common.NewVaultLedgerTokenAndMetadataLoader[*token3.Token, *token3.Metadata](qe, tokDeserializer), deserializer), zkatdlog.NewAuditorService(logger, ppm, common.NewLedgerTokenLoader[*token3.Token](logger, qe, tokDeserializer), deserializer), zkatdlog.NewTokensService(ppm), diff --git a/token/core/zkatdlog/nogh/driver/interop/state/fabric/state.go b/token/core/zkatdlog/nogh/driver/interop/state/fabric/state.go new file mode 100644 index 0000000000..8938e3eab8 --- /dev/null +++ b/token/core/zkatdlog/nogh/driver/interop/state/fabric/state.go @@ -0,0 +1,400 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package fabric + +import ( + "encoding/base64" + "encoding/json" + "strings" + "time" + + "github.com/hyperledger-labs/fabric-token-sdk/token/core/zkatdlog/crypto" + + "github.com/hyperledger-labs/fabric-token-sdk/token/core/common/logging" + + "github.com/hyperledger-labs/fabric-smart-client/platform/fabric" + "github.com/hyperledger-labs/fabric-smart-client/platform/fabric/services/weaver" + driver2 "github.com/hyperledger-labs/fabric-token-sdk/token/driver" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/interop/pledge" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/interop/state" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/interop/state/driver" + fabric2 "github.com/hyperledger-labs/fabric-token-sdk/token/services/interop/state/fabric" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/network/common/rws/keys" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/network/common/rws/translator" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/network/fabric/tcc" + "github.com/hyperledger-labs/fabric-token-sdk/token/token" + "github.com/pkg/errors" +) + +type RelayProvider interface { + Relay(fns *fabric.NetworkService) *weaver.Relay +} + +type PledgeVault interface { + PledgeByTokenID(tokenID *token.ID) ([]*pledge.Info, error) +} + +type GetFabricNetworkServiceFunc = func(string) (*fabric.NetworkService, error) + +type StateQueryExecutor struct { + Logger logging.Logger + RelayProvider RelayProvider + TargetNetworkURL string + RelaySelector *fabric.NetworkService +} + +func NewStateQueryExecutor( + Logger logging.Logger, + RelayProvider RelayProvider, + targetNetworkURL string, + relaySelector *fabric.NetworkService, +) (*StateQueryExecutor, error) { + if err := fabric2.CheckFabricScheme(targetNetworkURL); err != nil { + return nil, err + } + return &StateQueryExecutor{ + Logger: Logger, + RelayProvider: RelayProvider, + TargetNetworkURL: targetNetworkURL, + RelaySelector: relaySelector, + }, nil +} + +func (p *StateQueryExecutor) Exist(tokenID *token.ID) ([]byte, error) { + raw, err := json.Marshal(tokenID) + if err != nil { + return nil, err + } + + relay := p.RelayProvider.Relay(p.RelaySelector) + p.Logger.Debugf("Query [%s] for proof of existence of token [%s], input [%s]", p.TargetNetworkURL, tokenID.String(), base64.StdEncoding.EncodeToString(raw)) + + query, err := relay.ToFabric().Query(p.TargetNetworkURL, tcc.ProofOfTokenExistenceQuery, base64.StdEncoding.EncodeToString(raw)) + if err != nil { + return nil, err + } + res, err := query.Call() + if err != nil { + // todo: move this to the query executor + errMsg := err.Error() + switch { + case strings.Contains(errMsg, "failed to confirm if token with ID"): + return nil, errors.WithMessagef(state.TokenDoesNotExistError, "%s", err) + default: + return nil, err + } + } + + return res.Proof() +} + +func (p *StateQueryExecutor) DoesNotExist(tokenID *token.ID, origin string, deadline time.Time) ([]byte, error) { + req := &tcc.ProofOfTokenNonExistenceRequest{ + Deadline: deadline, + OriginNetwork: origin, + TokenID: tokenID, + } + raw, err := json.Marshal(req) + if err != nil { + return nil, err + } + relay := p.RelayProvider.Relay(p.RelaySelector) + + p.Logger.Debugf("Query [%s] for proof of non-existence of token [%s], input [%s]", p.TargetNetworkURL, tokenID.String(), base64.StdEncoding.EncodeToString(raw)) + + query, err := relay.ToFabric().Query(p.TargetNetworkURL, tcc.ProofOfTokenNonExistenceQuery, base64.StdEncoding.EncodeToString(raw)) + if err != nil { + return nil, err + } + res, err := query.Call() + if err != nil { + errMsg := err.Error() + switch { + case strings.Contains(errMsg, "failed to confirm if token from network"): + return nil, errors.WithMessagef(state.TokenExistsError, "%s", err) + default: + return nil, err + } + } + + return res.Proof() +} + +// ExistsWithMetadata returns a proof that a token with metadata including the passed token ID and origin network exists +// in the network this query executor targets +func (p *StateQueryExecutor) ExistsWithMetadata(tokenID *token.ID, origin string) ([]byte, error) { + req := &tcc.ProofOfTokenMetadataExistenceRequest{ + OriginNetwork: origin, + TokenID: tokenID, + } + raw, err := json.Marshal(req) + if err != nil { + return nil, err + } + relay := p.RelayProvider.Relay(p.RelaySelector) + + p.Logger.Debugf("Query [%s] for proof of existence of metadata with token [%s], input [%s]", p.TargetNetworkURL, tokenID.String(), base64.StdEncoding.EncodeToString(raw)) + + query, err := relay.ToFabric().Query(p.TargetNetworkURL, tcc.ProofOfTokenMetadataExistenceQuery, base64.StdEncoding.EncodeToString(raw)) + if err != nil { + return nil, err + } + res, err := query.Call() + if err != nil { + errMsg := err.Error() + switch { + case strings.Contains(errMsg, "failed to confirm if token from network"): + return nil, errors.WithMessagef(state.TokenExistsError, "%s", err) + default: + return nil, err + } + } + + return res.Proof() +} + +type StateVerifier struct { + Logger logging.Logger + RelayProvider RelayProvider + NetworkURL string + RelaySelector *fabric.NetworkService + PledgeVault PledgeVault + GetFabricNetworkService GetFabricNetworkServiceFunc +} + +func NewStateVerifier( + Logger logging.Logger, + relayProvider RelayProvider, + PledgeVault PledgeVault, + GetFabricNetworkService GetFabricNetworkServiceFunc, + networkURL string, + relaySelector *fabric.NetworkService, +) (*StateVerifier, error) { + if err := fabric2.CheckFabricScheme(networkURL); err != nil { + return nil, err + } + return &StateVerifier{ + Logger: Logger, + RelayProvider: relayProvider, + NetworkURL: networkURL, + RelaySelector: relaySelector, + PledgeVault: PledgeVault, + GetFabricNetworkService: GetFabricNetworkService, + }, nil +} + +func (v *StateVerifier) VerifyProofExistence(proofRaw []byte, tokenID *token.ID, metadata []byte) error { + relay := v.RelayProvider.Relay(v.RelaySelector) + proof, err := relay.ToFabric().ProofFromBytes(proofRaw) + if err != nil { + return errors.Wrapf(err, "failed to unmarshal claim proof") + } + if err := proof.Verify(); err != nil { + return errors.Wrapf(err, "failed to verify pledge proof") + } + + // todo check that address in proof matches source network + + rwset, err := proof.RWSet() + if err != nil { + return errors.Wrapf(err, "failed to unmarshal claim proof") + } + + key, err := keys.CreateProofOfExistenceKey(tokenID) + if err != nil { + return err + } + tmsID, err := fabric2.FabricURLToTMSID(v.NetworkURL) + if err != nil { + return err + } + raw, err := rwset.GetState(tmsID.Namespace, key) + if err != nil { + return errors.Wrapf(err, "failed to check proof of existence") + } + if len(raw) == 0 { + return errors.Errorf("failed to check proof of existence, missing key-value pair") + } + + // Validate against pledge + v.Logger.Debugf("verify proof of existence for token id [%s]", tokenID) + pledges, err := v.PledgeVault.PledgeByTokenID(tokenID) + if err != nil { + v.Logger.Errorf("failed retrieving pledge info for token id [%s]: [%s]", tokenID, err) + return errors.WithMessagef(err, "failed getting pledge for [%s]", tokenID) + } + if len(pledges) != 1 { + v.Logger.Errorf("failed retrieving pledge info for token id [%s]: no info found", tokenID) + return errors.Errorf("expected one pledge, got [%d]", len(pledges)) + } + info := pledges[0] + v.Logger.Debugf("found pledge info for token id [%s]: [%s]", tokenID, info.Source) + + // TODO compare token type and quantity and script, as done in fabtoken driver + + return nil +} + +func (v *StateVerifier) VerifyProofNonExistence(proofRaw []byte, tokenID *token.ID, origin string, deadline time.Time) error { + // v.NetworkURL is the network from which the proof comes from + tokenOriginNetworkTMSID, err := fabric2.FabricURLToTMSID(origin) + if err != nil { + return errors.Wrapf(err, "failed to parse network url") + } + fns, err := v.GetFabricNetworkService(tokenOriginNetworkTMSID.Network) + if err != nil { + return errors.Wrapf(err, "failed to get fabric network service for network [%s]", tokenOriginNetworkTMSID.Network) + } + relay := v.RelayProvider.Relay(fns) + proof, err := relay.ToFabric().ProofFromBytes(proofRaw) + if err != nil { + return errors.Wrapf(err, "failed to umarshal proof") + } + + rwset, err := proof.RWSet() + if err != nil { + return errors.Wrapf(err, "failed to retrieve RWset") + } + + key, err := keys.CreateProofOfNonExistenceKey(tokenID, origin) + if err != nil { + return errors.Wrapf(err, "failed creating key for proof of non-existence") + } + + proofSourceNetworkTMSID, err := fabric2.FabricURLToTMSID(v.NetworkURL) + if err != nil { + return err + } + raw, err := rwset.GetState(proofSourceNetworkTMSID.Namespace, key) + if err != nil { + return errors.Wrapf(err, "failed to check proof of non-existence") + } + p := &translator.ProofOfTokenMetadataNonExistence{} + if raw == nil { + return errors.Errorf("could not find proof of non-existence") + } + err = json.Unmarshal(raw, p) + if err != nil { + return errors.Wrapf(err, "failed to unmarshal proof of non-existence") + } + if p.Deadline != deadline { + return errors.Errorf("deadline in reclaim request does not match deadline in proof of non-existence") + } + if p.TokenID.String() != tokenID.String() { + return errors.Errorf("token ID in reclaim request does not match token ID in proof of non-existence") + } + if p.Origin != fabric2.FabricURL(tokenOriginNetworkTMSID) { + return errors.Errorf("origin in reclaim request does not match origin in proof of non-existence") + } + + // todo check that address in proof is the destination network + + err = proof.Verify() + if err != nil { + return errors.Wrapf(err, "invalid proof of non-existence") + } + + return nil +} + +// VerifyProofTokenWithMetadataExistence verifies that a proof of existence of a token +// with metadata including the given token ID and origin network, in the target network is valid +func (v *StateVerifier) VerifyProofTokenWithMetadataExistence(proofRaw []byte, tokenID *token.ID, origin string) error { + // v.NetworkURL is the network from which the proof comes from + tokenOriginNetworkTMSID, err := fabric2.FabricURLToTMSID(origin) + if err != nil { + return errors.Wrapf(err, "failed to parse network url") + } + fns, err := v.GetFabricNetworkService(tokenOriginNetworkTMSID.Network) + if err != nil { + return err + } + relay := v.RelayProvider.Relay(fns) + proof, err := relay.ToFabric().ProofFromBytes(proofRaw) + if err != nil { + return errors.Wrapf(err, "failed to umarshal proof") + } + + rwset, err := proof.RWSet() + if err != nil { + return errors.Wrapf(err, "failed to retrieve RWset") + } + + key, err := keys.CreateProofOfMetadataExistenceKey(tokenID, origin) + if err != nil { + return errors.Wrapf(err, "failed creating key for proof of token existence") + } + + proofSourceNetworkTMSID, err := fabric2.FabricURLToTMSID(v.NetworkURL) + if err != nil { + return err + } + raw, err := rwset.GetState(proofSourceNetworkTMSID.Namespace, key) + if err != nil { + return errors.Wrapf(err, "failed to check proof of token existence") + } + p := &translator.ProofOfTokenMetadataExistence{} + if raw == nil { + return errors.Errorf("could not find proof of token existence") + } + err = json.Unmarshal(raw, p) + if err != nil { + return errors.Wrapf(err, "failed to unmarshal proof of token existence") + } + if p.TokenID.String() != tokenID.String() { + return errors.Errorf("token ID in redeem request does not match token ID in proof of token existence") + } + if p.Origin != fabric2.FabricURL(tokenOriginNetworkTMSID) { + return errors.Errorf("origin in redeem request does not match origin in proof of token existence") + } + + // todo check that address in proof is the destination network + + err = proof.Verify() + if err != nil { + return errors.Wrapf(err, "invalid proof of token existence") + } + + return nil +} + +type StateDriver struct { + Logger logging.Logger +} + +func NewStateDriver(logger logging.Logger) *StateDriver { + return &StateDriver{Logger: logger} +} + +func (d *StateDriver) NewStateQueryExecutor(sp driver2.ServiceProvider, url string) (driver.StateQueryExecutor, error) { + fns, err := fabric.GetDefaultFNS(sp) + if err != nil { + return nil, errors.Wrapf(err, "failed to get default FNS") + } + return NewStateQueryExecutor(d.Logger, weaver.GetProvider(sp), url, fns) +} + +func (d *StateDriver) NewStateVerifier(sp driver2.ServiceProvider, url string) (driver.StateVerifier, error) { + fns, err := fabric.GetDefaultFNS(sp) + if err != nil { + return nil, errors.Wrapf(err, "failed to get default FNS") + } + return NewStateVerifier( + d.Logger, + weaver.GetProvider(sp), + pledge.Vault(sp), + func(id string) (*fabric.NetworkService, error) { + return fabric.GetFabricNetworkService(sp, id) + }, + url, + fns, + ) +} + +func init() { + fabric2.RegisterStateDriver(crypto.DLogPublicParameters, NewStateDriver(logging.MustGetLogger("token-sdk.core.zkatdlog"))) +} diff --git a/token/core/zkatdlog/nogh/issue.go b/token/core/zkatdlog/nogh/issue.go index 6bd392c64e..a4e03a991b 100644 --- a/token/core/zkatdlog/nogh/issue.go +++ b/token/core/zkatdlog/nogh/issue.go @@ -15,21 +15,26 @@ import ( "github.com/pkg/errors" ) +type IssueMetadataProviderFunc = func(attributes map[string][]byte, opts *driver.IssueOptions) (map[string][]byte, error) + type IssueService struct { PublicParametersManager common2.PublicParametersManager[*crypto.PublicParams] WalletService driver.WalletService Deserializer driver.Deserializer + IssueMetadataProviders []IssueMetadataProviderFunc } func NewIssueService( publicParametersManager common2.PublicParametersManager[*crypto.PublicParams], walletService driver.WalletService, deserializer driver.Deserializer, + IssueMetadataProviders []IssueMetadataProviderFunc, ) *IssueService { return &IssueService{ PublicParametersManager: publicParametersManager, WalletService: walletService, Deserializer: deserializer, + IssueMetadataProviders: IssueMetadataProviders, } } @@ -65,6 +70,15 @@ func (s *IssueService) Issue(issuerIdentity driver.Identity, tokenType string, v return nil, nil, err } + actionMetadata := map[string][]byte{} + for _, provider := range s.IssueMetadataProviders { + actionMetadata, err = provider(actionMetadata, opts) + if err != nil { + return nil, nil, errors.Wrapf(err, "failed generating token action metadata") + } + } + action.Metadata = actionMetadata + var outputsMetadata [][]byte for _, meta := range zkOutputsMetadata { raw, err := meta.Serialize() diff --git a/token/driver/tms.go b/token/driver/tms.go index c17a0af196..b19013c90b 100644 --- a/token/driver/tms.go +++ b/token/driver/tms.go @@ -13,6 +13,22 @@ import ( "github.com/pkg/errors" ) +// TMSID models a TMS identifier +type TMSID struct { + Network string + Channel string + Namespace string +} + +// String returns a string representation of the TMSID +func (t TMSID) String() string { + return fmt.Sprintf("%s,%s,%s", t.Network, t.Channel, t.Namespace) +} + +func (t TMSID) Equal(tmsid TMSID) bool { + return t.Network == tmsid.Network && t.Channel == tmsid.Channel && t.Namespace == tmsid.Namespace +} + // TokenManagerService is the entry point of the Driver API and gives access to the rest of the API type TokenManagerService interface { IssueService() IssueService diff --git a/token/sdk/sdk.go b/token/sdk/sdk.go index 5f4b2d8e88..cba388f833 100644 --- a/token/sdk/sdk.go +++ b/token/sdk/sdk.go @@ -40,6 +40,8 @@ import ( "github.com/hyperledger-labs/fabric-token-sdk/token/services/identitydb" _ "github.com/hyperledger-labs/fabric-token-sdk/token/services/identitydb/db/sql" "github.com/hyperledger-labs/fabric-token-sdk/token/services/interop/htlc" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/interop/pledge" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/interop/state" "github.com/hyperledger-labs/fabric-token-sdk/token/services/logging" "github.com/hyperledger-labs/fabric-token-sdk/token/services/network" _ "github.com/hyperledger-labs/fabric-token-sdk/token/services/network/fabric" @@ -101,7 +103,10 @@ func (p *SDK) Install() error { ) assert.NoError(p.registry.RegisterService(tmsProvider)) - fabricNSP, _ := fabric.GetNetworkServiceProvider(p.registry) + fabricNSP, err := fabric.GetNetworkServiceProvider(p.registry) + if err != nil { + logger.Errorf("Failed to get fabric network provider: %s", err) + } orionNSP, _ := orion2.GetNetworkServiceProvider(p.registry) tokenDBManager := tokendb.NewManager(configProvider, dbconfig.NewConfig(configProvider, "tokendb.persistence.type", "db.persistence.type")) @@ -154,7 +159,11 @@ func (p *SDK) Install() error { tmsp, tokenDBManager, publisher, - tokens2.NewAuthorizationMultiplexer(&tokens2.TMSAuthorization{}, &htlc.ScriptOwnership{}), + tokens2.NewAuthorizationMultiplexer( + &tokens2.TMSAuthorization{}, + &htlc.ScriptOwnership{}, + &pledge.ScriptOwnership{}, + ), tokens2.NewIssuedMultiplexer(&tokens2.WalletIssued{}), storage.NewDBEntriesStorage("tokens", kvs.GetService(p.registry)), ) @@ -181,6 +190,10 @@ func (p *SDK) Install() error { assert.NoError(orion.InstallViews(view2.GetRegistry(p.registry)), "failed to install custodian views") } + // State Service Provider + provider := state.NewServiceProvider(p.registry) + assert.NoError(p.registry.RegisterService(provider), "failed registering interoperability prover provider") + // Install metrics assert.NoError( p.registry.RegisterService(ttx.NewMetrics(metrics.GetProvider(p.registry))), diff --git a/token/sdk/tms/tms.go b/token/sdk/tms/tms.go index d47487778f..a5ee987d80 100644 --- a/token/sdk/tms/tms.go +++ b/token/sdk/tms/tms.go @@ -92,6 +92,7 @@ func (p *PostInitializer) ConnectNetwork(networkID, channel, namespace string) e n, err := p.fabricNetworkService(networkID) if err != nil { // ORION + logger.Errorf("failed to fabric connect network, trying orion [%s]", err) // register processor ons, err := p.orionNetworkService(networkID) diff --git a/token/services/identity/deserializer/typed.go b/token/services/identity/deserializer/typed.go index c44eece914..c6ccfff592 100644 --- a/token/services/identity/deserializer/typed.go +++ b/token/services/identity/deserializer/typed.go @@ -8,16 +8,19 @@ package deserializer import ( "github.com/hyperledger-labs/fabric-smart-client/platform/view/services/hash" + "github.com/hyperledger-labs/fabric-token-sdk/token" "github.com/hyperledger-labs/fabric-token-sdk/token/core/common" "github.com/hyperledger-labs/fabric-token-sdk/token/driver" "github.com/hyperledger-labs/fabric-token-sdk/token/services/identity" "github.com/pkg/errors" ) +type AuditInfoProvider = driver.AuditInfoProvider + type TypedVerifierDeserializer interface { - DeserializeVerifier(typ string, raw []byte) (driver.Verifier, error) - Recipients(id driver.Identity, typ string, raw []byte) ([]driver.Identity, error) - GetOwnerAuditInfo(id driver.Identity, typ string, raw []byte, p driver.AuditInfoProvider) ([][]byte, error) + DeserializeVerifier(typ string, raw []byte) (token.Verifier, error) + Recipients(id token.Identity, typ string, raw []byte) ([]token.Identity, error) + GetOwnerAuditInfo(id token.Identity, typ string, raw []byte, p AuditInfoProvider) ([][]byte, error) } // AuditMatcherDeserializer deserializes raw bytes into a matcher, which allows an auditor to match an identity to an enrollment ID diff --git a/token/services/interop/audit.go b/token/services/interop/audit.go new file mode 100644 index 0000000000..c9ca42d3bd --- /dev/null +++ b/token/services/interop/audit.go @@ -0,0 +1,139 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package interop + +import ( + "encoding/json" + + "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/htlc" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/interop/pledge" + "github.com/pkg/errors" +) + +type Input struct { + *token.Input + isHTLC bool + isPledge 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, + isHTLC: owner.Type == htlc.ScriptType, + isPledge: owner.Type == pledge.ScriptType, + }, nil +} + +func (i *Input) IsHTLC() bool { + return i.isHTLC +} + +func (i *Input) IsPledge() bool { + return i.isPledge +} + +func (i *Input) HTLC() (*htlc.Script, error) { + if !i.isHTLC { + return nil, errors.New("this input does not refer to an HTLC script") + } + owner, err := identity.UnmarshalTypedIdentity(i.Owner) + if err != nil { + return nil, errors.Wrapf(err, "failed to unmarshal owner") + } + script := &htlc.Script{} + err = json.Unmarshal(owner.Identity, script) + if err != nil { + return nil, errors.Wrapf(err, "failed to unmarshal HTLC script") + } + return script, nil +} + +func (i *Input) Pledge() (*pledge.Script, error) { + if !i.isPledge { + return nil, errors.New("this input does not refer to a pledge script") + } + owner, err := identity.UnmarshalTypedIdentity(i.Owner) + if err != nil { + return nil, errors.Wrapf(err, "failed to unmarshal owner") + } + script := &pledge.Script{} + err = json.Unmarshal(owner.Identity, script) + if err != nil { + return nil, errors.Wrapf(err, "failed to unmarshal pledge script") + } + return script, nil +} + +type Output struct { + *token.Output + isHTLC bool + isPledge bool +} + +func ToOutput(o *token.Output) (*Output, error) { + if o.Owner != nil { + owner, err := identity.UnmarshalTypedIdentity(o.Owner) + if err != nil { + return nil, errors.Wrapf(err, "failed to unmarshal owner") + } + return &Output{ + Output: o, + isHTLC: owner.Type == htlc.ScriptType, + isPledge: owner.Type == pledge.ScriptType, + }, nil + } + return &Output{ + Output: o, + }, nil + +} + +func (o *Output) IsHTLC() bool { + return o.isHTLC +} + +func (o *Output) IsPledge() bool { + return o.isPledge +} + +func (o *Output) HTLC() (*htlc.Script, error) { + if !o.isHTLC { + return nil, errors.New("this output does not refer to an HTLC script") + } + owner, err := identity.UnmarshalTypedIdentity(o.Owner) + if err != nil { + return nil, errors.Wrapf(err, "failed to unmarshal owner") + } + script := &htlc.Script{} + err = json.Unmarshal(owner.Identity, script) + if err != nil { + return nil, errors.Wrapf(err, "failed to unmrshal HTLC script") + } + return script, nil +} + +func (o *Output) Pledge() (*pledge.Script, error) { + if !o.isPledge { + return nil, errors.New("this output does not refer to a pledge script") + } + owner, err := identity.UnmarshalTypedIdentity(o.Owner) + if err != nil { + return nil, errors.Wrapf(err, "failed to unmarshal owner") + } + script := &pledge.Script{} + err = json.Unmarshal(owner.Identity, script) + if err != nil { + return nil, errors.Wrapf(err, "failed to unmrshal pledge script") + } + return script, nil +} diff --git a/token/services/interop/htlc/audit.go b/token/services/interop/htlc/audit.go deleted file mode 100644 index 421aef818f..0000000000 --- a/token/services/interop/htlc/audit.go +++ /dev/null @@ -1,95 +0,0 @@ -/* -Copyright IBM Corp. All Rights Reserved. - -SPDX-License-Identifier: Apache-2.0 -*/ - -package htlc - -import ( - "encoding/json" - - "github.com/hyperledger-labs/fabric-token-sdk/token" - "github.com/hyperledger-labs/fabric-token-sdk/token/services/identity" - "github.com/pkg/errors" -) - -type Input struct { - *token.Input - isHTLC 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, - isHTLC: owner.Type == ScriptType, - }, nil -} - -func (i *Input) IsHTLC() bool { - return i.isHTLC -} - -func (i *Input) Script() (*Script, error) { - if !i.isHTLC { - return nil, errors.New("this input does not refer to an HTLC script") - } - - owner, err := identity.UnmarshalTypedIdentity(i.Owner) - if err != nil { - return nil, errors.Wrapf(err, "failed to unmarshal owner") - } - if owner.Type != ScriptType { - return nil, errors.Errorf("invalid identity type, expected [%s], got [%s]", ScriptType, owner.Type) - } - script := &Script{} - err = json.Unmarshal(owner.Identity, script) - if err != nil { - return nil, errors.Wrapf(err, "failed to unmrshal HTLC script") - } - return script, nil -} - -type Output struct { - *token.Output - isHTLC 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, - isHTLC: owner.Type == ScriptType, - }, nil -} - -func (o *Output) IsHTLC() bool { - return o.isHTLC -} - -func (o *Output) Script() (*Script, error) { - if !o.isHTLC { - return nil, errors.New("this output does not refer to an HTLC script") - } - - owner, err := identity.UnmarshalTypedIdentity(o.Owner) - if err != nil { - return nil, errors.Wrapf(err, "failed to unmarshal owner") - } - if owner.Type != ScriptType { - return nil, errors.Errorf("invalid identity type, expected [%s], got [%s]", ScriptType, owner.Type) - } - script := &Script{} - err = json.Unmarshal(owner.Identity, script) - if err != nil { - return nil, errors.Wrapf(err, "failed to unmrshal HTLC script") - } - return script, nil -} diff --git a/token/services/identity/interop/htlc/deserializer.go b/token/services/interop/htlc/deserializer.go similarity index 79% rename from token/services/identity/interop/htlc/deserializer.go rename to token/services/interop/htlc/deserializer.go index 95f330378a..2e33a51082 100644 --- a/token/services/identity/interop/htlc/deserializer.go +++ b/token/services/interop/htlc/deserializer.go @@ -9,14 +9,13 @@ package htlc import ( "encoding/json" - "github.com/hyperledger-labs/fabric-token-sdk/token/driver" + "github.com/hyperledger-labs/fabric-token-sdk/token" "github.com/hyperledger-labs/fabric-token-sdk/token/services/identity/deserializer" - "github.com/hyperledger-labs/fabric-token-sdk/token/services/interop/htlc" "github.com/pkg/errors" ) type VerifierDES interface { - DeserializeVerifier(id driver.Identity) (driver.Verifier, error) + DeserializeVerifier(id token.Identity) (token.Verifier, error) } type TypedIdentityDeserializer struct { @@ -27,17 +26,17 @@ func NewTypedIdentityDeserializer(verifierDeserializer VerifierDES) *TypedIdenti return &TypedIdentityDeserializer{VerifierDeserializer: verifierDeserializer} } -func (t *TypedIdentityDeserializer) DeserializeVerifier(typ string, raw []byte) (driver.Verifier, error) { - if typ != htlc.ScriptType { - return nil, errors.Errorf("cannot deserializer type [%s], expected [%s]", typ, htlc.ScriptType) +func (t *TypedIdentityDeserializer) DeserializeVerifier(typ string, raw []byte) (token.Verifier, error) { + if typ != ScriptType { + return nil, errors.Errorf("cannot deserializer type [%s], expected [%s]", typ, ScriptType) } - script := &htlc.Script{} + script := &Script{} err := json.Unmarshal(raw, script) if err != nil { return nil, errors.Errorf("failed to unmarshal TypedIdentity as an htlc script") } - v := &htlc.Verifier{} + v := &Verifier{} v.Sender, err = t.VerifierDeserializer.DeserializeVerifier(script.Sender) if err != nil { return nil, errors.Errorf("failed to unmarshal the identity of the sender in the htlc script") @@ -53,24 +52,24 @@ func (t *TypedIdentityDeserializer) DeserializeVerifier(typ string, raw []byte) return v, nil } -func (t *TypedIdentityDeserializer) Recipients(id driver.Identity, typ string, raw []byte) ([]driver.Identity, error) { - if typ != htlc.ScriptType { +func (t *TypedIdentityDeserializer) Recipients(id token.Identity, typ string, raw []byte) ([]token.Identity, error) { + if typ != ScriptType { return nil, errors.New("unknown identity type") } - script := &htlc.Script{} + script := &Script{} err := json.Unmarshal(raw, script) if err != nil { return nil, errors.Wrapf(err, "failed to unmarshal htlc script") } - return []driver.Identity{script.Recipient}, nil + return []token.Identity{script.Recipient}, nil } -func (t *TypedIdentityDeserializer) GetOwnerAuditInfo(id driver.Identity, typ string, raw []byte, p driver.AuditInfoProvider) ([][]byte, error) { - if typ != htlc.ScriptType { - return nil, errors.Errorf("invalid type, got [%s], expected [%s]", typ, htlc.ScriptType) +func (t *TypedIdentityDeserializer) GetOwnerAuditInfo(id token.Identity, typ string, raw []byte, p deserializer.AuditInfoProvider) ([][]byte, error) { + if typ != ScriptType { + return nil, errors.Errorf("invalid type, got [%s], expected [%s]", typ, ScriptType) } - script := &htlc.Script{} + script := &Script{} var err error err = json.Unmarshal(raw, script) if err != nil { diff --git a/token/services/identity/interop/htlc/info.go b/token/services/interop/htlc/info.go similarity index 76% rename from token/services/identity/interop/htlc/info.go rename to token/services/interop/htlc/info.go index bdfa05af5a..0869b54be4 100644 --- a/token/services/identity/interop/htlc/info.go +++ b/token/services/interop/htlc/info.go @@ -9,14 +9,13 @@ package htlc import ( "encoding/json" - "github.com/hyperledger-labs/fabric-token-sdk/token/driver" + "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/htlc" "github.com/pkg/errors" ) type AuditInfoProvider interface { - GetAuditInfo(identity driver.Identity) ([]byte, error) + GetAuditInfo(identity token.Identity) ([]byte, error) } // ScriptInfo includes info about the sender and the recipient @@ -34,9 +33,9 @@ func (si *ScriptInfo) Unmarshal(raw []byte) error { } // GetScriptSenderAndRecipient returns the script's sender and recipient according to the type of the given owner -func GetScriptSenderAndRecipient(ro *identity.TypedIdentity) (sender, recipient driver.Identity, err error) { - if ro.Type == htlc.ScriptType { - script := &htlc.Script{} +func GetScriptSenderAndRecipient(ro *identity.TypedIdentity) (sender, recipient token.Identity, err error) { + if ro.Type == ScriptType { + script := &Script{} err = json.Unmarshal(ro.Identity, script) if err != nil { return nil, nil, errors.Wrapf(err, "failed to unmarshal htlc script") diff --git a/token/services/interop/htlc/script.go b/token/services/interop/htlc/script.go index 753d2981a5..8f09901925 100644 --- a/token/services/interop/htlc/script.go +++ b/token/services/interop/htlc/script.go @@ -74,18 +74,28 @@ type Script struct { // - The deadline must be after the passed time reference // - HashInfo must be Available func (s *Script) Validate(timeReference time.Time) error { + if err := s.WellFormedness(); err != nil { + return err + } + if s.Deadline.Before(timeReference) { + return errors.New("expiration date has already passed") + } + return nil +} + +func (s *Script) WellFormedness() error { if s.Sender.IsNone() { return errors.New("sender not set") } if s.Recipient.IsNone() { return errors.New("recipient not set") } - if s.Deadline.Before(timeReference) { - return errors.New("expiration date has already passed") - } if err := s.HashInfo.Validate(); err != nil { return err } + if len(s.HashInfo.Hash) == 0 { + return errors.New("hash is not set") + } return nil } diff --git a/token/services/interop/htlc/signer.go b/token/services/interop/htlc/signer.go index e9036b5504..b71f13eaf9 100644 --- a/token/services/interop/htlc/signer.go +++ b/token/services/interop/htlc/signer.go @@ -11,7 +11,7 @@ import ( "encoding/json" "time" - "github.com/hyperledger-labs/fabric-token-sdk/token/driver" + "github.com/hyperledger-labs/fabric-token-sdk/token" "github.com/pkg/errors" ) @@ -23,7 +23,7 @@ type ClaimSignature struct { // ClaimSigner is the signer for the claim of an htlc script type ClaimSigner struct { - Recipient driver.Signer + Recipient token.Signer Preimage []byte } @@ -51,7 +51,7 @@ func concatTokenRequestTxIDPreimage(tokenRequestAndTxID []byte, preImage []byte) // ClaimVerifier is the verifier of a ClaimSignature type ClaimVerifier struct { - Recipient driver.Verifier + Recipient token.Verifier HashInfo HashInfo } @@ -87,8 +87,8 @@ func (cv *ClaimVerifier) Verify(tokenRequestAndTxID, claimSignature []byte) erro // Verifier checks if an htlc script can be claimed or reclaimed type Verifier struct { - Recipient driver.Verifier - Sender driver.Verifier + Recipient token.Verifier + Sender token.Verifier Deadline time.Time HashInfo HashInfo } diff --git a/token/services/identity/interop/htlc/validator.go b/token/services/interop/htlc/validator.go similarity index 82% rename from token/services/identity/interop/htlc/validator.go rename to token/services/interop/htlc/validator.go index 3eb6c13822..d6c4d4cb0c 100644 --- a/token/services/identity/interop/htlc/validator.go +++ b/token/services/interop/htlc/validator.go @@ -12,7 +12,6 @@ import ( "time" "github.com/hyperledger-labs/fabric-token-sdk/token/services/identity" - "github.com/hyperledger-labs/fabric-token-sdk/token/services/interop/htlc" "github.com/pkg/errors" ) @@ -29,15 +28,15 @@ type Action interface { } // VerifyOwner validates the owners of the transfer in the htlc script -func VerifyOwner(senderRawOwner []byte, outRawOwner []byte, now time.Time) (*htlc.Script, OperationType, error) { +func VerifyOwner(senderRawOwner []byte, outRawOwner []byte, now time.Time) (*Script, OperationType, error) { sender, err := identity.UnmarshalTypedIdentity(senderRawOwner) if err != nil { return nil, None, err } - if sender.Type != htlc.ScriptType { - return nil, None, errors.Errorf("invalid identity type, expected [%s], got [%s]", htlc.ScriptType, sender.Type) + if sender.Type != ScriptType { + return nil, None, errors.Errorf("invalid identity type, expected [%s], got [%s]", ScriptType, sender.Type) } - script := &htlc.Script{} + script := &Script{} err = json.Unmarshal(sender.Identity, script) if err != nil { return nil, None, err @@ -59,14 +58,14 @@ func VerifyOwner(senderRawOwner []byte, outRawOwner []byte, now time.Time) (*htl } // MetadataClaimKeyCheck checks that the claim key is in place -func MetadataClaimKeyCheck(action Action, script *htlc.Script, op OperationType, sig []byte) (string, error) { +func MetadataClaimKeyCheck(action Action, script *Script, op OperationType, sig []byte) (string, error) { if op == Reclaim { // No metadata in this case return "", nil } // Unmarshal signature to ClaimSignature - claim := &htlc.ClaimSignature{} + claim := &ClaimSignature{} if err := json.Unmarshal(sig, claim); err != nil { return "", errors.Wrapf(err, "failed unmarshalling claim signature [%s]", string(sig)) } @@ -84,7 +83,7 @@ func MetadataClaimKeyCheck(action Action, script *htlc.Script, op OperationType, if err != nil { return "", errors.Wrapf(err, "failed to compute image of [%x]", claim.Preimage) } - key := htlc.ClaimKey(image) + key := ClaimKey(image) value, ok := metadata[key] if !ok { return "", errors.New("cannot find htlc pre-image, missing metadata entry") @@ -97,17 +96,17 @@ func MetadataClaimKeyCheck(action Action, script *htlc.Script, op OperationType, } // MetadataLockKeyCheck checks that the lock key is in place -func MetadataLockKeyCheck(action Action, script *htlc.Script) (string, error) { +func MetadataLockKeyCheck(action Action, script *Script) (string, error) { metadata := action.GetMetadata() if len(metadata) == 0 { return "", errors.New("cannot find htlc lock, no metadata") } - key := htlc.LockKey(script.HashInfo.Hash) + key := LockKey(script.HashInfo.Hash) value, ok := metadata[key] if !ok { return "", errors.New("cannot find htlc lock, missing metadata entry") } - if !bytes.Equal(value, htlc.LockValue(script.HashInfo.Hash)) { + if !bytes.Equal(value, LockValue(script.HashInfo.Hash)) { return "", errors.Errorf("invalid action, cannot match htlc lock with metadata [%x]!=[%x]", value, script.HashInfo.Hash) } return key, nil diff --git a/token/services/interop/pledge/accept.go b/token/services/interop/pledge/accept.go new file mode 100644 index 0000000000..db6f7e9990 --- /dev/null +++ b/token/services/interop/pledge/accept.go @@ -0,0 +1,17 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package pledge + +import ( + "github.com/hyperledger-labs/fabric-smart-client/platform/view/view" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/ttx" +) + +// NewAcceptView returns an instance of the ttx acceptView struct +func NewAcceptView(tx *Transaction) view.View { + return ttx.NewAcceptView(tx.Transaction) +} diff --git a/token/services/interop/pledge/approve.go b/token/services/interop/pledge/approve.go new file mode 100644 index 0000000000..1c771b5b63 --- /dev/null +++ b/token/services/interop/pledge/approve.go @@ -0,0 +1,275 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package pledge + +import ( + "encoding/json" + "fmt" + "time" + + "github.com/hyperledger-labs/fabric-smart-client/platform/view/services/hash" + "github.com/hyperledger-labs/fabric-smart-client/platform/view/services/session" + "github.com/hyperledger-labs/fabric-smart-client/platform/view/view" + tokn "github.com/hyperledger-labs/fabric-token-sdk/token" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/interop/state" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/network" + "github.com/hyperledger-labs/fabric-token-sdk/token/token" + "github.com/pkg/errors" +) + +type IssuerApprovalRequest struct { + OriginTMSID tokn.TMSID + TokenID *token.ID + Proof []byte + Destination string + RequestorSignature []byte +} + +func (r *IssuerApprovalRequest) Bytes() ([]byte, error) { + return json.Marshal(r) +} + +func (r *IssuerApprovalRequest) FromBytes(raw []byte) error { + return json.Unmarshal(raw, r) +} + +type IssuerApprovalResponse struct { + Signature []byte +} + +func (r *IssuerApprovalResponse) Bytes() ([]byte, error) { + return json.Marshal(r) +} + +func (r *IssuerApprovalResponse) FromBytes(raw []byte) error { + return json.Unmarshal(raw, r) +} + +type RequestIssuerSignatureView struct { + originTMSID tokn.TMSID + sender view.Identity + issuer view.Identity + tokenID *token.ID + reclaimProof []byte + network string + pledgeID string +} + +func RequestIssuerSignature(context view.Context, tokenID *token.ID, originTMSID tokn.TMSID, script *Script, proof []byte) ([]byte, error) { + boxed, err := context.RunView(&RequestIssuerSignatureView{ + originTMSID: originTMSID, + sender: script.Sender, + issuer: script.Issuer, + tokenID: tokenID, + reclaimProof: proof, + network: script.DestinationNetwork, + pledgeID: script.ID, + }) + if err != nil { + return nil, err + } + return boxed.([]byte), nil +} + +func (v *RequestIssuerSignatureView) Call(context view.Context) (interface{}, error) { + logger.Debugf("RequestIssuerSignatureView:caller [%s]", context.Initiator()) + + session, err := context.GetSession(context.Initiator(), v.issuer) + if err != nil { + return nil, err + } + + // Ask for issuer's signature + req := &IssuerApprovalRequest{ + OriginTMSID: v.originTMSID, + TokenID: v.tokenID, + Destination: v.network, + Proof: v.reclaimProof, + } + + reqRaw, err := req.Bytes() + if err != nil { + return nil, errors.Wrapf(err, "failed marshalling issuer signature request") + } + // sign request + logger.Debugf("sign request [%s]", v.sender) + signer, err := tokn.GetManagementService(context, tokn.WithTMSID(v.originTMSID)).SigService().GetSigner(v.sender) + if err != nil { + return nil, err + } + msg := append(reqRaw, v.sender.Bytes()...) + req.RequestorSignature, err = signer.Sign(msg) + if err != nil { + return nil, err + } + verifier, err := tokn.GetManagementService(context, tokn.WithTMSID(req.OriginTMSID)).SigService().OwnerVerifier(v.sender) + if err != nil { + return nil, errors.Wrapf(err, "failed to retrieve a verifier to check sender signature") + } + if err := verifier.Verify(msg, req.RequestorSignature); err != nil { + return nil, errors.Wrapf(err, "failed to double-verify sender signature") + } + + reqRaw, err = req.Bytes() + if err != nil { + return nil, errors.Wrapf(err, "failed marshalling issuer signature request") + } + err = session.Send(reqRaw) + if err != nil { + return nil, err + } + + // Wait to receive a signature + ch := session.Receive() + var payload []byte + select { + case msg := <-ch: + payload = msg.Payload + if msg.Status == view.ERROR { + return nil, errors.Errorf("failed requesting approval [%s]", string(payload)) + } + case <-time.After(60 * time.Second): + return nil, errors.New("time out reached") + } + logger.Debugf("received approval response [%v]", payload) + + res := &IssuerApprovalResponse{} + err = res.FromBytes(payload) + if err != nil { + return nil, errors.Wrapf(err, "failed to unmarshal approval response [%s]", string(payload)) + } + // check if signature is valid + // TODO: The issuer here is identified with it is owner identity. Shall we have the issuer identity? + verifier, err = tokn.GetManagementService(context, tokn.WithTMSID(req.OriginTMSID)).SigService().OwnerVerifier(v.issuer) + if err != nil { + return nil, errors.Wrapf(err, "failed to retrieve a verifier to check issuer signature") + } + + err = verifier.Verify([]byte(v.pledgeID), res.Signature) + if err != nil { + return nil, errors.Wrapf(err, "invalid issuer signature") + } + return res.Signature, nil +} + +type RequestIssuerSignatureResponderView struct { + walletID string +} + +func RespondRequestIssuerSignature(context view.Context, walletID string) ([]byte, error) { + sig, err := context.RunView(&RequestIssuerSignatureResponderView{walletID: walletID}) + if err != nil { + return nil, err + } + return sig.([]byte), nil +} + +func (v *RequestIssuerSignatureResponderView) Call(context view.Context) (interface{}, error) { + s, payload, err := session.ReadFirstMessage(context) + if err != nil { + return nil, err + } + + req := &IssuerApprovalRequest{} + if err := req.FromBytes(payload); err != nil { + return nil, errors.Wrapf(err, "failed unmarshalling signature request") + } + w, err := GetIssuerWallet(context, v.walletID) + if err != nil { + return nil, err + } + + wallet := NewIssuerWallet(context, w) + _, script, err := wallet.GetPledgedToken(req.TokenID) + if err != nil { + return nil, err + } + // check validity of reclaim + if time.Now().Before(script.Deadline) { + return nil, errors.Errorf("cannot reclaim token yet; deadline has not elapsed yet") + } + if req.Destination != script.DestinationNetwork { + return nil, errors.Errorf("destination network in reclaim request does not match destination network in pledged token") + } + // access control check + logger.Debugf("verify request [%s]", script.Sender) + verifier, err := tokn.GetManagementService(context, tokn.WithTMSID(req.OriginTMSID)).SigService().OwnerVerifier(script.Sender) + if err != nil { + return nil, errors.Wrapf(err, "failed to retrieve a verifier to check sender signature") + } + request := &IssuerApprovalRequest{ + OriginTMSID: req.OriginTMSID, + TokenID: req.TokenID, + Proof: req.Proof, + Destination: req.Destination, + } + toBeVerified, err := json.Marshal(request) + if err != nil { + return nil, err + } + err = verifier.Verify(append(toBeVerified, script.Sender...), req.RequestorSignature) + if err != nil { + return nil, errors.Wrapf(err, "failed to verify reclaim request signature") + } + + // verify proof before returning it + net := network.GetInstance(context, req.OriginTMSID.Network, req.OriginTMSID.Channel) + if net == nil { + return nil, errors.Errorf("cannot find network for [%s]", req.OriginTMSID) + } + origin := net.InteropURL(req.OriginTMSID.Namespace) + + ssp, err := state.GetServiceProvider(context) + if err != nil { + return nil, errors.WithMessage(err, "failed getting state service provider") + } + stateProofVerifier, err := ssp.Verifier(script.DestinationNetwork) + if err != nil { + return nil, errors.WithMessagef(err, "failed getting verifier for [%s]", origin) + } + if err := stateProofVerifier.VerifyProofNonExistence(req.Proof, req.TokenID, origin, script.Deadline); err != nil { + return nil, errors.WithMessagef(err, "failed verifying proof of existence for [%s]", origin) + } + + // sign + signer, err := tokn.GetManagementService(context, tokn.WithTMSID(req.OriginTMSID)).SigService().GetSigner(script.Issuer) + if err != nil { + return nil, err + } + + sigma, err := signer.Sign([]byte(script.ID)) + if err != nil { + return nil, err + } + + ver, err := tokn.GetManagementService(context, tokn.WithTMSID(req.OriginTMSID)).SigService().OwnerVerifier(script.Issuer) + if err != nil { + return nil, err + } + if err := ver.Verify([]byte(script.ID), sigma); err != nil { + return nil, errors.Wrapf(err, "failed to verify issuer signature [%s]", script.Issuer) + } + + logger.Debugf("produced signature by (me) [%s,%s,%s]", + hash.Hashable(req.TokenID.String()).String(), + hash.Hashable(sigma).String(), + script.Issuer.UniqueID(), + ) + res := &IssuerApprovalResponse{Signature: sigma} + resRaw, err := res.Bytes() + if err != nil { + return nil, err + } + fmt.Printf("sent approval response [%v]\n", resRaw) + err = s.Send(resRaw) + if err != nil { + return nil, err + } + fmt.Printf("sent approval response [%v]\n", resRaw) + + return resRaw, nil +} diff --git a/token/services/interop/pledge/claim.go b/token/services/interop/pledge/claim.go new file mode 100644 index 0000000000..6bb4c399bd --- /dev/null +++ b/token/services/interop/pledge/claim.go @@ -0,0 +1,256 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package pledge + +import ( + "encoding/json" + "time" + + view2 "github.com/hyperledger-labs/fabric-smart-client/platform/view" + "github.com/hyperledger-labs/fabric-smart-client/platform/view/services/session" + "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/state" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/network" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/ttx" + token2 "github.com/hyperledger-labs/fabric-token-sdk/token/token" + "github.com/pkg/errors" +) + +func (t *Transaction) Claim(issuerWallet *token.IssuerWallet, typ string, value uint64, recipient view.Identity, originTokenID *token2.ID, originNetwork string, proof []byte) error { + if typ == "" { + return errors.Errorf("must specify a type") + } + if value == 0 { + return errors.Errorf("must specify a value") + } + if recipient.IsNone() { + return errors.Errorf("must specify a recipient") + } + if originTokenID == nil { + return errors.Errorf("must specify the origin token ID") + } + if originNetwork == "" { + return errors.Errorf("must specify the origin network") + } + if proof == nil { + return errors.Errorf("must provide a proof") + } + + _, err := t.TokenRequest.Issue(issuerWallet, recipient, typ, value, WithMetadata(originTokenID, originNetwork, proof)) + return err +} + +func WithMetadata(tokenID *token2.ID, network string, proof []byte) token.IssueOption { + return func(options *token.IssueOptions) error { + if options.Attributes == nil { + options.Attributes = make(map[interface{}]interface{}) + } + options.Attributes["github.com/hyperledger-labs/fabric-token-sdk/token/services/interop/pledge/tokenID"] = tokenID + options.Attributes["github.com/hyperledger-labs/fabric-token-sdk/token/services/interop/pledge/network"] = network + options.Attributes["github.com/hyperledger-labs/fabric-token-sdk/token/services/interop/pledge/proof"] = proof + return nil + } +} + +type ClaimRequest struct { + TokenType string + Quantity uint64 + Recipient view.Identity + RecipientAuditInfo []byte + ClaimDeadline time.Time + OriginTokenID *token2.ID + OriginNetwork string + PledgeProof []byte + RequestorSignature []byte +} + +func (cr *ClaimRequest) Bytes() ([]byte, error) { + return json.Marshal(cr) +} + +type claimInitiatorView struct { + issuer view.Identity + recipient view.Identity + pledgeInfo *Info + pledgeProof []byte +} + +func RequestClaim(context view.Context, issuer view.Identity, pledgeInfo *Info, recipient view.Identity, pledgeProof []byte) (view.Session, error) { + boxed, err := context.RunView(&claimInitiatorView{ + issuer: issuer, + pledgeInfo: pledgeInfo, + recipient: recipient, + pledgeProof: pledgeProof, + }) + if err != nil { + return nil, err + } + return boxed.(view.Session), err +} + +func (v *claimInitiatorView) Call(context view.Context) (interface{}, error) { + session, err := context.GetSession(context.Initiator(), v.issuer) + if err != nil { + return nil, err + } + + w := token.GetManagementService(context).WalletManager().OwnerWallet(v.recipient) + if w == nil { + return nil, errors.Wrapf(err, "cannot find owner wallet for recipient [%s]", v.recipient) + } + auditInfo, err := w.GetAuditInfo(v.recipient) + if err != nil { + return nil, errors.Wrapf(err, "failed getting audit info for recipient [%s]", v.recipient) + } + + req := &ClaimRequest{ + TokenType: v.pledgeInfo.TokenType, + Quantity: v.pledgeInfo.Amount, + Recipient: v.recipient, + RecipientAuditInfo: auditInfo, + ClaimDeadline: v.pledgeInfo.Script.Deadline, + OriginTokenID: v.pledgeInfo.TokenID, + OriginNetwork: v.pledgeInfo.Source, + PledgeProof: v.pledgeProof, + } + reqRaw, err := req.Bytes() + if err != nil { + return nil, errors.Wrapf(err, "failed marshalling claim request") + } + signer, err := token.GetManagementService(context).SigService().GetSigner(v.recipient) + if err != nil { + return nil, err + } + req.RequestorSignature, err = signer.Sign(append(reqRaw, context.Me().Bytes()...)) + if err != nil { + return nil, err + } + reqRaw, err = req.Bytes() + if err != nil { + return nil, errors.Wrapf(err, "failed marshalling claim request") + } + err = session.Send(reqRaw) + if err != nil { + return nil, err + } + + return session, nil +} + +type receiveClaimRequestView struct{} + +func ReceiveClaimRequest(context view.Context) (*ClaimRequest, error) { + req, err := context.RunView(&receiveClaimRequestView{}) + if err != nil { + return nil, err + } + return req.(*ClaimRequest), nil +} + +func (v *receiveClaimRequestView) Call(context view.Context) (interface{}, error) { + s := session.JSON(context) + req := &ClaimRequest{} + if err := s.Receive(&req); err != nil { + return nil, errors.Wrapf(err, "failed unmarshalling claim request") + } + tms := token.GetManagementService(context) + verifier, err := tms.SigService().OwnerVerifier(req.Recipient) + if err != nil { + return nil, errors.Wrapf(err, "failed to retrieve a verifier to check sender signature [%s]", req.Recipient) + } + request := &ClaimRequest{ + TokenType: req.TokenType, + Quantity: req.Quantity, + Recipient: req.Recipient, + RecipientAuditInfo: req.RecipientAuditInfo, + ClaimDeadline: req.ClaimDeadline, + OriginTokenID: req.OriginTokenID, + OriginNetwork: req.OriginNetwork, + PledgeProof: req.PledgeProof, + } + toBeVerified, err := json.Marshal(request) + if err != nil { + return nil, err + } + err = verifier.Verify(append(toBeVerified, s.Session().Info().Caller.Bytes()...), req.RequestorSignature) + if err != nil { + return nil, errors.Wrapf(err, "failed to verify claim request signature") + } + + if err := view2.GetEndpointService(context).Bind( + s.Session().Info().Caller, + req.Recipient, + ); err != nil { + return nil, errors.Wrapf(err, "failed binding caller's identity to request's recipient") + } + + if err := tms.WalletManager().RegisterRecipientIdentity(&token.RecipientData{ + Identity: req.Recipient, + AuditInfo: req.RecipientAuditInfo}); err != nil { + return nil, errors.Wrapf(err, "failed registering request recipient info") + } + + return req, nil +} + +func ValidateClaimRequest(context view.Context, req *ClaimRequest, opts ...ttx.TxOption) error { + txOpts, err := ttx.CompileTXOptions(opts...) + if err != nil { + return errors.WithMessage(err, "failed compiling tx options") + } + tms := token.GetManagementService(context, token.WithTMSID(txOpts.TMSID)) + if tms == nil { + return errors.Errorf("cannot find tms for [%s]", txOpts.TMSID) + } + + tmsID := tms.ID() + net := network.GetInstance(context, tmsID.Network, tmsID.Channel) + if net == nil { + return errors.Errorf("cannot find network for [%s]", tmsID) + } + destination := net.InteropURL(tmsID.Namespace) + + info := &Info{ + Amount: req.Quantity, + TokenID: req.OriginTokenID, + TokenMetadata: nil, + TokenType: req.TokenType, + Source: req.OriginNetwork, + Script: &Script{ + Deadline: req.ClaimDeadline, + Recipient: req.Recipient, + DestinationNetwork: destination, + }, + } + + if err := Vault(context).Store(info); err != nil { + return errors.WithMessagef(err, "failed storing temporary pledge info for [%s]", info.Source) + } + ssp, err := state.GetServiceProvider(context) + if err != nil { + return errors.WithMessage(err, "failed getting state service provider") + } + v, err := ssp.Verifier(info.Source) + if err != nil { + return errors.WithMessagef(err, "failed getting verifier for [%s]", info.Source) + } + // todo check that address in proof matches the source network + // todo check that destination network matches issuer's network + err = v.VerifyProofExistence(req.PledgeProof, req.OriginTokenID, info.TokenMetadata) + if err != nil { + logger.Errorf("proof of existence in claim request is not valid valid [%s]", err) + return errors.WithMessagef(err, "failed verifying proof of existence for [%s]", info.Source) + } + logger.Debugf("proof of existence in claim request is valid [%s]", err) + + if !req.ClaimDeadline.After(time.Now()) { + return errors.Errorf("deadline for claim has elapsed") + } + + return nil +} diff --git a/token/services/interop/pledge/deserializer.go b/token/services/interop/pledge/deserializer.go new file mode 100644 index 0000000000..93abba7a5a --- /dev/null +++ b/token/services/interop/pledge/deserializer.go @@ -0,0 +1,164 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package pledge + +import ( + "encoding/json" + "runtime/debug" + + "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/identity/deserializer" + "github.com/pkg/errors" +) + +// ScriptInfo includes info about the sender and the recipient +type ScriptInfo struct { + Sender []byte + Recipient []byte +} + +func (si *ScriptInfo) Marshal() ([]byte, error) { + return json.Marshal(si) +} + +func (si *ScriptInfo) Unmarshal(raw []byte) error { + return json.Unmarshal(raw, si) +} + +type VerifierDES interface { + DeserializeVerifier(id token.Identity) (token.Verifier, error) +} + +type TypedIdentityDeserializer struct { + VerifierDeserializer VerifierDES +} + +func NewTypedIdentityDeserializer(verifierDeserializer VerifierDES) *TypedIdentityDeserializer { + return &TypedIdentityDeserializer{VerifierDeserializer: verifierDeserializer} +} + +func (t *TypedIdentityDeserializer) DeserializeVerifier(typ string, raw []byte) (token.Verifier, error) { + if typ != ScriptType { + return nil, errors.Errorf("cannot deserializer type [%s], expected [%s]", typ, ScriptType) + } + + script := &Script{} + err := json.Unmarshal(raw, script) + if err != nil { + return nil, errors.Errorf("failed to unmarshal TypedIdentity as an htlc script") + } + v := &Verifier{} + v.Sender, err = t.VerifierDeserializer.DeserializeVerifier(script.Sender) + if err != nil { + return nil, errors.Wrapf(err, "failed to unmarshal the identity of the sender [%v]", script.Sender.String()) + } + v.Issuer, err = t.VerifierDeserializer.DeserializeVerifier(script.Issuer) + if err != nil { + return nil, errors.Wrapf(err, "failed to unmarshal the identity of the issuer [%s]", script.Issuer.String()) + } + v.PledgeID = script.ID + return v, nil +} + +func (t *TypedIdentityDeserializer) Recipients(id token.Identity, typ string, raw []byte) ([]token.Identity, error) { + logger.Infof("pledge, get recipients for [%s][%s]", id, typ) + if typ != ScriptType { + return nil, errors.New("unknown identity type") + } + + script := &Script{} + err := json.Unmarshal(raw, script) + if err != nil { + return nil, errors.Wrapf(err, "failed to unmarshal htlc script") + } + return []token.Identity{script.Issuer}, nil +} + +func (t *TypedIdentityDeserializer) GetOwnerAuditInfo(id token.Identity, typ string, raw []byte, p deserializer.AuditInfoProvider) ([][]byte, error) { + logger.Infof("1. pledge, get owner audit info for [%s][%s]", id, typ) + if typ != ScriptType { + return nil, errors.Errorf("invalid type, got [%s], expected [%s]", typ, ScriptType) + } + script := &Script{} + var err error + err = json.Unmarshal(raw, script) + if err != nil { + return nil, errors.Wrapf(err, "failed to unmarshal htlc script") + } + + logger.Infof("2. pledge, get owner audit info for [%s][%s]", id, typ) + auditInfo := &ScriptInfo{} + auditInfo.Sender, err = p.GetAuditInfo(script.Sender) + if err != nil { + return nil, errors.Wrapf(err, "failed getting audit info for sender of pledge script [%s]", view.Identity(raw).String()) + } + + logger.Infof("3. pledge, get owner audit info for [%s][%s]", id, typ) + if len(auditInfo.Sender) == 0 { // in case this is a redeem we need to check the script issuer (and not the script sender) + auditInfo.Sender, err = p.GetAuditInfo(script.Issuer) + if err != nil { + return nil, errors.Wrapf(err, "failed getting audit info for issuer of pledge script [%s]", view.Identity(raw).String()) + } + if len(auditInfo.Sender) == 0 { + return nil, errors.Errorf("failed getting audit info for pledge script [%s]", view.Identity(raw).String()) + } + } + + logger.Infof("4. pledge, get owner audit info for [%s][%s]", id, typ) + // Notice that recipient is in another network, but the issuer is + // the actual recipient of the script because it is in the same network. + auditInfo.Recipient, err = p.GetAuditInfo(script.Issuer) + if err != nil { + return nil, errors.Wrapf(err, "failed getting audit info for issuer of pledge script [%s]", view.Identity(raw).String()) + } + + logger.Infof("5. pledge, get owner audit info for [%s][%s] [%s]", id, typ, debug.Stack()) + auditInfoRaw, err := json.Marshal(auditInfo) + if err != nil { + return nil, errors.Wrapf(err, "failed marshaling audit info for script") + } + return [][]byte{auditInfoRaw}, nil +} + +type AuditDeserializer struct { + AuditInfoDeserializer deserializer.AuditInfoDeserializer +} + +func NewAuditDeserializer(auditInfoDeserializer deserializer.AuditInfoDeserializer) *AuditDeserializer { + return &AuditDeserializer{AuditInfoDeserializer: auditInfoDeserializer} +} + +func (a *AuditDeserializer) DeserializeAuditInfo(bytes []byte) (deserializer.AuditInfo, error) { + si := &ScriptInfo{} + err := json.Unmarshal(bytes, si) + if err != nil || (len(si.Sender) == 0 && len(si.Recipient) == 0) { + return nil, errors.Errorf("ivalid audit info, failed unmarshal [%s][%d][%d]", string(bytes), len(si.Sender), len(si.Recipient)) + } + if len(si.Recipient) == 0 { + return nil, errors.Errorf("no recipient defined") + } + ai, err := a.AuditInfoDeserializer.DeserializeAuditInfo(si.Recipient) + if err != nil { + return nil, errors.Wrapf(err, "failed unamrshalling audit info [%s]", bytes) + } + return ai, nil +} + +// GetScriptSenderAndRecipient returns the script's sender, recipient, and issuer +func GetScriptSenderAndRecipient(ro *identity.TypedIdentity) (sender, recipient, issuer token.Identity, err error) { + if ro.Type == ScriptType { + script := &Script{} + err = json.Unmarshal(ro.Identity, script) + if err != nil { + return nil, nil, nil, errors.Wrapf(err, "failed to unmarshal htlc script") + } + return script.Sender, script.Recipient, script.Issuer, nil + } + return nil, nil, nil, errors.New("unknown identity type") +} diff --git a/token/services/interop/pledge/distribute.go b/token/services/interop/pledge/distribute.go new file mode 100644 index 0000000000..e9485154de --- /dev/null +++ b/token/services/interop/pledge/distribute.go @@ -0,0 +1,158 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package pledge + +import ( + "encoding/json" + + "github.com/hyperledger-labs/fabric-smart-client/platform/view/services/session" + "github.com/hyperledger-labs/fabric-smart-client/platform/view/view" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/network" + token2 "github.com/hyperledger-labs/fabric-token-sdk/token/token" + "github.com/pkg/errors" +) + +type Info struct { + // Source is the url of the network where the pledge is supposed to be + Source string + TokenType string + Amount uint64 + // TokenID is the ID of the token. + TokenID *token2.ID + TokenMetadata []byte + Script *Script +} + +func (i *Info) Bytes() ([]byte, error) { + return json.Marshal(i) +} + +func (i *Info) FromBytes(raw []byte) error { + return json.Unmarshal(raw, i) +} + +type DistributePledgeView struct { + tx *Transaction +} + +func NewDistributePledgeInfoView(tx *Transaction) *DistributePledgeView { + return &DistributePledgeView{ + tx: tx, + } +} + +func (v *DistributePledgeView) Call(context view.Context) (interface{}, error) { + outputs, err := v.tx.Outputs() + if err != nil { + return nil, errors.WithMessagef(err, "failed getting outputs") + } + if outputs.Count() < 1 { + return nil, errors.WithMessagef(err, "expected at least one output, got [%d]", outputs.Count()) + } + inputs, err := v.tx.TokenRequest.Inputs() + if err != nil { + return nil, errors.WithMessagef(err, "failed getting inputs") + } + if inputs.Count() < 1 { + return nil, errors.WithMessagef(err, "expected at least one input, got [%d]", inputs.Count()) + } + + var ret []*Info + for i := 0; i < outputs.Count(); i++ { + script := outputs.ScriptAt(i) + if script == nil { + continue + } + output := outputs.At(i) + + tokenID := &token2.ID{ + TxId: v.tx.ID(), + Index: uint64(i), + } + // TODO: retrieve token's metadata + + tmsID := v.tx.TokenService().ID() + net := network.GetInstance(context, tmsID.Network, tmsID.Channel) + if net == nil { + return nil, errors.Errorf("cannot find network for [%s]", tmsID) + } + info := &Info{ + Source: net.InteropURL(tmsID.Namespace), + TokenType: output.Type, + Amount: output.Quantity.ToBigInt().Uint64(), + TokenID: tokenID, + TokenMetadata: nil, + Script: script, + } + + session, err := context.GetSession(context.Initiator(), script.Recipient) + if err != nil { + return nil, err + } + infoRaw, err := info.Bytes() + if err != nil { + return nil, errors.Wrapf(err, "failed marshalling pledge info") + } + err = session.Send(infoRaw) + if err != nil { + return nil, err + } + + // Wait for a signed ack, but who should sign? What if recipient is an identity that this node does + // not recognize? + ret = append(ret, info) + } + + return ret, nil +} + +type pledgeReceiverView struct{} + +func ReceivePledgeInfo(context view.Context) (*Info, error) { + info, err := context.RunView(&pledgeReceiverView{}) + if err != nil { + return nil, err + } + return info.(*Info), nil +} + +func (v *pledgeReceiverView) Call(context view.Context) (interface{}, error) { + _, payload, err := session.ReadFirstMessage(context) + if err != nil { + return nil, err + } + info := &Info{} + if err := info.FromBytes(payload); err != nil { + return nil, errors.Wrapf(err, "failed unmarshalling pledge info") + } + + return info, nil +} + +type AcceptPledgeIndoView struct { + info *Info +} + +func NewAcceptPledgeIndoView(info *Info) *AcceptPledgeIndoView { + return &AcceptPledgeIndoView{ + info: info, + } +} + +func (a *AcceptPledgeIndoView) Call(context view.Context) (interface{}, error) { + // Store info + if err := Vault(context).Store(a.info); err != nil { + return nil, errors.Wrapf(err, "failed storing pledge info") + } + + // raw, err := a.info.Bytes() + // if err != nil { + // return nil, errors.Wrapf(err, "failed marshalling info to raw") + // } + + return nil, nil +} diff --git a/token/services/interop/pledge/endorsement.go b/token/services/interop/pledge/endorsement.go new file mode 100644 index 0000000000..2ee411170f --- /dev/null +++ b/token/services/interop/pledge/endorsement.go @@ -0,0 +1,65 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package pledge + +import ( + "time" + + "github.com/hyperledger-labs/fabric-smart-client/platform/view/view" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/ttx" + "github.com/pkg/errors" +) + +// NewCollectEndorsementsView returns an instance of the ttx collectEndorsementsView struct +func NewCollectEndorsementsView(tx *Transaction) view.View { + return ttx.NewCollectEndorsementsView(tx.Transaction) +} + +type ReceiveTransactionView struct { +} + +// NewReceiveTransactionView returns an instance of receiveTransactionView struct +func NewReceiveTransactionView() *ReceiveTransactionView { + return &ReceiveTransactionView{} +} + +func (f *ReceiveTransactionView) Call(context view.Context) (interface{}, error) { + // Wait to receive a transaction back + ch := context.Session().Receive() + + select { + case msg := <-ch: + if msg.Status == view.ERROR { + return nil, errors.New(string(msg.Payload)) + } + tx, err := NewTransactionFromBytes(context, msg.Payload) + if err != nil { + return nil, err + } + return tx, nil + case <-time.After(240 * time.Second): + return nil, errors.New("timeout reached") + } +} + +// ReceiveTransaction executes the receiveTransactionView and returns the received transaction +func ReceiveTransaction(context view.Context) (*Transaction, error) { + logger.Debugf("receive a new transaction...") + + txBoxed, err := context.RunView(NewReceiveTransactionView()) + if err != nil { + return nil, err + } + + cctx, ok := txBoxed.(*Transaction) + if !ok { + return nil, errors.Errorf("received transaction of wrong type [%T]", cctx) + } + logger.Debugf("received transaction with id [%s]", cctx.ID()) + + return cctx, nil +} diff --git a/token/services/interop/pledge/finality.go b/token/services/interop/pledge/finality.go new file mode 100644 index 0000000000..eb340d73c2 --- /dev/null +++ b/token/services/interop/pledge/finality.go @@ -0,0 +1,17 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package pledge + +import ( + "github.com/hyperledger-labs/fabric-smart-client/platform/view/view" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/ttx" +) + +// NewFinalityView returns an instance of the ttx FinalityView +func NewFinalityView(tx *Transaction) view.View { + return ttx.NewFinalityView(tx.Transaction) +} diff --git a/token/services/interop/pledge/logger.go b/token/services/interop/pledge/logger.go new file mode 100644 index 0000000000..2b7d8287fb --- /dev/null +++ b/token/services/interop/pledge/logger.go @@ -0,0 +1,11 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package pledge + +import "github.com/hyperledger-labs/fabric-smart-client/platform/view/services/flogging" + +var logger = flogging.MustGetLogger("token-sdk.services.pledge") diff --git a/token/services/interop/pledge/ordering.go b/token/services/interop/pledge/ordering.go new file mode 100644 index 0000000000..c3bf2f2a9f --- /dev/null +++ b/token/services/interop/pledge/ordering.go @@ -0,0 +1,17 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package pledge + +import ( + "github.com/hyperledger-labs/fabric-smart-client/platform/view/view" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/ttx" +) + +// NewOrderingAndFinalityView returns a new instance of the ttx orderingAndFinalityView struct +func NewOrderingAndFinalityView(tx *Transaction) view.View { + return ttx.NewOrderingAndFinalityView(tx.Transaction) +} diff --git a/token/services/interop/pledge/pledge.go b/token/services/interop/pledge/pledge.go new file mode 100644 index 0000000000..3258f70e6f --- /dev/null +++ b/token/services/interop/pledge/pledge.go @@ -0,0 +1,99 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package pledge + +import ( + "crypto/rand" + "encoding/hex" + "encoding/json" + "time" + + "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/pkg/errors" +) + +const ( + MetadataKey = "metadata.pledge" + defaultDeadlineOffset = time.Hour +) + +func (t *Transaction) Pledge(wallet *token.OwnerWallet, destNetwork string, deadline time.Duration, recipient view.Identity, issuer view.Identity, typ string, value uint64) (string, error) { + if deadline == 0 { + deadline = defaultDeadlineOffset + } + if destNetwork == "" { + return "", errors.Errorf("must specify a destination network") + } + if issuer.IsNone() { + return "", errors.Errorf("must specify an issuer") + } + if recipient.IsNone() { + return "", errors.Errorf("must specify a recipient") + } + pledgeID, err := generatePledgeID() + if err != nil { + return "", errors.Wrapf(err, "failed to generate pledge ID") + } + me, err := wallet.GetRecipientIdentity() + if err != nil { + return "", err + } + script, err := t.recipientAsScript(me, destNetwork, deadline, recipient, issuer, pledgeID) + if err != nil { + return "", err + } + _, err = t.TokenRequest.Transfer( + wallet, + typ, + []uint64{value}, + []view.Identity{script}, + token.WithTransferMetadata(MetadataKey+pledgeID, []byte("1")), + ) + return pledgeID, err +} + +func (t *Transaction) recipientAsScript(sender view.Identity, destNetwork string, deadline time.Duration, recipient view.Identity, issuer view.Identity, pledgeID string) (view.Identity, error) { + script := Script{ + Deadline: time.Now().Add(deadline), + DestinationNetwork: destNetwork, + Recipient: recipient, + Issuer: issuer, + Sender: sender, + ID: pledgeID, + } + rawScript, err := json.Marshal(script) + if err != nil { + return nil, err + } + + ro := &identity.TypedIdentity{ + Type: ScriptType, + Identity: rawScript, + } + return ro.Bytes() +} + +// generatePledgeID generates a pledgeID randomly +func generatePledgeID() (string, error) { + nonce, err := getRandomNonce() + if err != nil { + return "", errors.New("failed generating random nonce for pledgeID") + } + return hex.EncodeToString(nonce), nil +} + +// getRandomNonce generates a random nonce using the package math/rand +func getRandomNonce() ([]byte, error) { + key := make([]byte, 24) + _, err := rand.Read(key) + if err != nil { + return nil, errors.Wrap(err, "error getting random bytes") + } + return key, nil +} diff --git a/token/services/interop/pledge/recipients.go b/token/services/interop/pledge/recipients.go new file mode 100644 index 0000000000..f0dec0c322 --- /dev/null +++ b/token/services/interop/pledge/recipients.go @@ -0,0 +1,179 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package pledge + +import ( + view2 "github.com/hyperledger-labs/fabric-smart-client/platform/view" + session2 "github.com/hyperledger-labs/fabric-smart-client/platform/view/services/session" + "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/state" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/ttx" + "github.com/pkg/errors" +) + +type RecipientData = token.RecipientData + +type RecipientRequest struct { + NetworkURL string + WalletID []byte +} + +type RequestRecipientIdentityView struct { + TMSID token.TMSID + DestNetwork string + Other view.Identity +} + +// RequestPledgeRecipientIdentity executes the RequestRecipientIdentityView. +// The sender contacts the recipient's FSC node identified via the passed view identity. +// The sender gets back the identity the recipient wants to use to assign ownership of tokens. +func RequestPledgeRecipientIdentity(context view.Context, recipient view.Identity, destNetwork string, opts ...token.ServiceOption) (view.Identity, error) { + options, err := token.CompileServiceOptions(opts...) + if err != nil { + return nil, err + } + pseudonymBoxed, err := context.RunView(&RequestRecipientIdentityView{ + TMSID: options.TMSID(), + DestNetwork: destNetwork, + Other: recipient, + }) + if err != nil { + return nil, err + } + return pseudonymBoxed.(view.Identity), nil +} + +func (f RequestRecipientIdentityView) Call(context view.Context) (interface{}, error) { + logger.Debugf("request recipient to [%s] for TMS [%s]", f.Other, f.TMSID) + + tms := token.GetManagementService(context, token.WithTMSID(f.TMSID)) + + if w := tms.WalletManager().OwnerWallet(f.Other); w != nil { + recipient, err := w.GetRecipientIdentity() + if err != nil { + return nil, err + } + return recipient, nil + } else { + session, err := session2.NewJSON(context, context.Initiator(), f.Other) + if err != nil { + return nil, err + } + + // Ask for identity + err = session.Send(&RecipientRequest{ + NetworkURL: f.DestNetwork, + WalletID: f.Other, + }) + if err != nil { + return nil, errors.Wrapf(err, "failed to send recipient request") + } + + // Wait to receive a view identity + recipientData := &RecipientData{} + if err := session.Receive(recipientData); err != nil { + return nil, errors.Wrapf(err, "failed to receive recipient data") + } + //if err := tms.WalletManager().RegisterRecipientIdentity(recipientData); err != nil { + // return nil, err + //} + + // Update the Endpoint Resolver + if err := view2.GetEndpointService(context).Bind(f.Other, recipientData.Identity); err != nil { + return nil, err + } + + return recipientData.Identity, nil + } +} + +type RespondRequestPledgeRecipientIdentityView struct { + Wallet string +} + +// RespondRequestPledgeRecipientIdentity executes the RespondRequestPledgeRecipientIdentityView. +// The recipient sends back the identity to receive ownership of tokens. +// The identity is taken from the wallet +func RespondRequestPledgeRecipientIdentity(context view.Context) (view.Identity, error) { + id, err := context.RunView(&RespondRequestPledgeRecipientIdentityView{}) + if err != nil { + return nil, err + } + return id.(view.Identity), nil +} + +func (s *RespondRequestPledgeRecipientIdentityView) Call(context view.Context) (interface{}, error) { + session := session2.JSON(context) + recipientRequest := &RecipientRequest{} + if err := session.Receive(recipientRequest); err != nil { + return nil, errors.Wrapf(err, "failed unmarshalling recipient request") + } + + wallet := s.Wallet + if len(wallet) == 0 && len(recipientRequest.WalletID) != 0 { + wallet = string(recipientRequest.WalletID) + } + + ssp, err := state.GetServiceProvider(context) + if err != nil { + return nil, errors.Errorf("failed to load state service provider") + } + tmsID, err := ssp.URLToTMSID(recipientRequest.NetworkURL) + if err != nil { + return nil, errors.Wrapf(err, "failed parsing destination [%s]", recipientRequest.NetworkURL) + } + w := GetWallet( + context, + wallet, + token.WithTMSID(tmsID), + ) + if w == nil { + return nil, errors.Errorf("unable to get wallet %s in %s", wallet, tmsID) + } + recipientIdentity, err := w.GetRecipientIdentity() + if err != nil { + return nil, errors.Wrapf(err, "failed getting recipient identity for wallet [%s]", wallet) + } + auditInfo, err := w.GetAuditInfo(recipientIdentity) + if err != nil { + return nil, errors.Wrapf(err, "failed getting audit info for recipient [%s], wallet [%s]", recipientIdentity, wallet) + } + metadata, err := w.GetTokenMetadata(recipientIdentity) + if err != nil { + return nil, errors.Wrapf(err, "failed getting token metadata for recipient [%s], wallet [%s]", recipientIdentity, wallet) + } + + // Step 3: send the public key back to the invoker + err = session.Send(&RecipientData{ + Identity: recipientIdentity, + AuditInfo: auditInfo, + TokenMetadata: metadata, + }) + if err != nil { + return nil, err + } + + // Update the Endpoint Resolver + resolver := view2.GetEndpointService(context) + err = resolver.Bind(context.Me(), recipientIdentity) + if err != nil { + return nil, err + } + + return recipientIdentity, nil +} + +// RequestRecipientIdentity executes the RequestRecipientIdentityView. +func RequestRecipientIdentity(context view.Context, recipient view.Identity, opts ...token.ServiceOption) (view.Identity, error) { + return ttx.RequestRecipientIdentity(context, recipient, opts...) +} + +// RespondRequestRecipientIdentity executes the RespondRequestRecipientIdentityView. +func RespondRequestRecipientIdentity(context view.Context) (view.Identity, error) { + return ttx.RespondRequestRecipientIdentity(context) +} diff --git a/token/services/interop/pledge/reclaim.go b/token/services/interop/pledge/reclaim.go new file mode 100644 index 0000000000..b7bf3ef17c --- /dev/null +++ b/token/services/interop/pledge/reclaim.go @@ -0,0 +1,90 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package pledge + +import ( + "encoding/json" + "fmt" + + "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" + token2 "github.com/hyperledger-labs/fabric-token-sdk/token/token" + "github.com/pkg/errors" +) + +const ( + MetadataReclaimKey = "metadata.reclaim" +) + +func (t *Transaction) Reclaim(wallet *token.OwnerWallet, tok *token2.UnspentToken, issuerSignature []byte, tokenID *token2.ID, proof []byte) error { + if proof == nil { + return errors.New("must provide proof") + } + if tokenID == nil { + return errors.New("must provide token ID") + } + + 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.Raw) + if err != nil { + return err + } + + if owner.Type != ScriptType { + return errors.Errorf("invalid owner type, expected a pledge script") + } + + script := &Script{} + err = json.Unmarshal(owner.Identity, script) + if err != nil { + return errors.Errorf("failed to unmarshal TypedIdentity as a pledge script") + } + + // Register the signer for the reclaim + sigService := t.TokenService().SigService() + signer, err := sigService.GetSigner(script.Sender) + if err != nil { + return err + } + verifier, err := sigService.OwnerVerifier(script.Sender) + if err != nil { + return err + } + // TODO: script.Issues is an owner identity, shall we switch to issuer identity? + issuer, err := sigService.OwnerVerifier(script.Issuer) + if err != nil { + return err + } + reclaimSigner := &Signer{Sender: signer, IssuerSignature: issuerSignature} + reclaimVerifier := &Verifier{ + Sender: verifier, + Issuer: issuer, + PledgeID: script.ID, + } + logger.Debugf("registering signer for reclaim...") + if err := sigService.RegisterSigner( + tok.Owner.Raw, + reclaimSigner, + reclaimVerifier, + ); err != nil { + return err + } + + if err := t.Binder.Bind(script.Sender, tok.Owner.Raw); err != nil { + return err + } + + proofKey := MetadataReclaimKey + fmt.Sprintf(".%d.%s", tokenID.Index, tokenID.TxId) + + _, err = t.TokenRequest.Transfer(wallet, tok.Type, []uint64{q.ToBigInt().Uint64()}, []view.Identity{script.Sender}, token.WithTokenIDs(tok.Id), token.WithTransferMetadata(proofKey, proof)) + return err +} diff --git a/token/services/interop/pledge/redeem.go b/token/services/interop/pledge/redeem.go new file mode 100644 index 0000000000..7bde69ae06 --- /dev/null +++ b/token/services/interop/pledge/redeem.go @@ -0,0 +1,93 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package pledge + +import ( + "encoding/json" + "fmt" + + "github.com/hyperledger-labs/fabric-token-sdk/token" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/identity" + token2 "github.com/hyperledger-labs/fabric-token-sdk/token/token" + "github.com/pkg/errors" +) + +const ( + RedeemPledgeKey = "metadata.redeemPledge" +) + +// RedeemPledge appends a redeem action to the request. The action will be prepared using the provided owner wallet. +// The action redeems the passed token. +func (t *Transaction) RedeemPledge(wallet *token.OwnerWallet, tok *token2.UnspentToken, tokenID *token2.ID, proof []byte) error { + if proof == nil { + return errors.New("must provide proof") + } + if tokenID == nil { + return errors.New("must provide token ID") + } + + 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.Raw) + if err != nil { + return err + } + script := &Script{} + if owner.Type != ScriptType { + return errors.Errorf("invalid owner type, expected a pledge script") + } + + err = json.Unmarshal(owner.Identity, script) + if err != nil { + return errors.Errorf("failed to unmarshal TypedIdentity as a pledge script") + } + + // Register the signer for the redeem + sigService := t.TokenService().SigService() + signer, err := sigService.GetSigner(script.Issuer) + if err != nil { + return err + } + verifier, err := sigService.OwnerVerifier(script.Issuer) + if err != nil { + return err + } + // TODO: script.Issues is an owner identity, shall we switch to issuer identity? + if err != nil { + return err + } + redeemSigner := &Signer{Issuer: signer} + redeemVerifier := &Verifier{ + Issuer: verifier, + } + logger.Debugf("registering signer for redeem...") + if err := sigService.RegisterSigner( + tok.Owner.Raw, + redeemSigner, + redeemVerifier, + ); err != nil { + return err + } + + if err := t.Binder.Bind(script.Issuer, tok.Owner.Raw); err != nil { + return err + } + + proofKey := RedeemPledgeKey + fmt.Sprintf(".%d.%s", tokenID.Index, tokenID.TxId) + + err = t.TokenRequest.Redeem( + wallet, + tok.Type, + q.ToBigInt().Uint64(), + token.WithTokenIDs(tok.Id), + token.WithTransferMetadata(proofKey, proof), + ) + return err +} diff --git a/token/services/interop/pledge/scanner.go b/token/services/interop/pledge/scanner.go new file mode 100644 index 0000000000..208883c782 --- /dev/null +++ b/token/services/interop/pledge/scanner.go @@ -0,0 +1,62 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package pledge + +import ( + "time" + + "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/network" + "github.com/pkg/errors" +) + +const ( + ScanForPledgeIDStartingTransaction = "pledge.IDExists.StartingTransaction" +) + +// WithStartingTransaction sets the starting transaction for the scan +func WithStartingTransaction(txID string) token.ServiceOption { + return func(o *token.ServiceOptions) error { + if o.Params == nil { + o.Params = map[string]interface{}{} + } + o.Params[ScanForPledgeIDStartingTransaction] = txID + return nil + } +} + +// IDExists scans the ledger for a pledge identifier, taking into account the timeout +// IDExists returns true, if entry identified by key (MetadataKey+pledgeID) is occupied. +func IDExists(ctx view.Context, pledgeID string, timeout time.Duration, opts ...token.ServiceOption) (bool, error) { + logger.Infof("scanning for pledgeID of [%s] with timeout [%s]", pledgeID, timeout) + tokenOptions, err := token.CompileServiceOptions(opts...) + if err != nil { + return false, err + } + tms := token.GetManagementService(ctx, opts...) + + net := network.GetInstance(ctx, tms.Network(), tms.Channel()) + if net == nil { + return false, errors.Errorf("cannot find network [%s:%s]", tms.Namespace(), tms.Channel()) + } + + startingTxID, err := tokenOptions.ParamAsString(ScanForPledgeIDStartingTransaction) + if err != nil { + return false, errors.Wrapf(err, "invalid starting transaction param") + } + + pledgeKey := MetadataKey + pledgeID + v, err := net.LookupTransferMetadataKey(tms.Namespace(), startingTxID, pledgeKey, timeout, false, opts...) + if err != nil { + return false, errors.Wrapf(err, "failed to lookup transfer metadata for pledge ID [%s]", pledgeID) + } + if len(v) != 0 { + return true, nil + } + return false, nil +} diff --git a/token/services/interop/pledge/script.go b/token/services/interop/pledge/script.go new file mode 100644 index 0000000000..2746517dd7 --- /dev/null +++ b/token/services/interop/pledge/script.go @@ -0,0 +1,55 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package pledge + +import ( + "time" + + "github.com/hyperledger-labs/fabric-smart-client/platform/view/view" + "github.com/pkg/errors" +) + +const ScriptType = "pledge" // pledge script + +type Script struct { + Sender view.Identity + Recipient view.Identity + DestinationNetwork string + Deadline time.Time + Issuer view.Identity + ID string +} + +// Validate checks that all fields of pledge script are correctly set +func (s *Script) Validate(timeReference time.Time) error { + if err := s.WellFormedness(); err != nil { + return err + } + if s.Deadline.Before(timeReference) { + return errors.New("invalid pledge script: deadline already elapsed") + } + return nil +} + +func (s *Script) WellFormedness() error { + if s.Sender.IsNone() { + return errors.New("invalid pledge script: empty sender") + } + if s.Recipient.IsNone() { + return errors.New("invalid pledge script: empty recipient") + } + if s.DestinationNetwork == "" { + return errors.New("invalid pledge script: empty destination network") + } + if s.Issuer.IsNone() { + return errors.New("invalid pledge script: empty issuer") + } + if s.ID == "" { + return errors.New("invalid pledge script: empty identifier") + } + return nil +} diff --git a/token/services/interop/pledge/signer.go b/token/services/interop/pledge/signer.go new file mode 100644 index 0000000000..35497bac85 --- /dev/null +++ b/token/services/interop/pledge/signer.go @@ -0,0 +1,105 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package pledge + +import ( + "encoding/json" + + "github.com/hyperledger-labs/fabric-smart-client/platform/view/services/hash" + "github.com/hyperledger-labs/fabric-token-sdk/token" + "github.com/pkg/errors" +) + +// Signer for a pledge script +type Signer struct { + // Sender corresponds to the sender of the token at time of pledge + // this is used during reclaim + Sender token.Signer + // Issuer corresponds to the issuer in the origin network + // this is used during redeem + Issuer token.Signer + // IssuerSignature is the signature from the issuer in the origin network + // it attests to whether a pledged token has been successfully claimed or not + // this is used during reclaim + IssuerSignature []byte +} + +// Signature encodes the signature that spends a pledge script +type Signature struct { + Reclaim bool + // this is empty in case of redeem + SenderSignature []byte + IssuerSignature []byte +} + +func (s *Signer) Sign(message []byte) ([]byte, error) { + sigma := Signature{} + var err error + if s.Issuer == nil { + if s.Sender == nil { + return nil, errors.New("please initialize pledge signer correctly: empty sender") + } + sigma.Reclaim = true + sigma.IssuerSignature = s.IssuerSignature + + message = append(message, s.IssuerSignature...) + sigma.SenderSignature, err = s.Sender.Sign(message) + if err != nil { + return nil, err + } + logger.Debugf("reclaim signature on message [%s]", hash.Hashable(message).String()) + } else { + sigma.Reclaim = false + sigma.IssuerSignature, err = s.Issuer.Sign(message) + if err != nil { + return nil, err + } + logger.Debugf("redeem signature on message [%s]", hash.Hashable(message).String()) + } + raw, err := json.Marshal(sigma) + if err != nil { + return nil, err + } + return raw, nil +} + +// Verifier of a Signature it is uniquely linked to a the pledge script identified by +// PledgeID +type Verifier struct { + // Sender in the pledge script + Sender token.Verifier + // Issuer is the issuer in the pledge script + Issuer token.Verifier + // PledgeID identifies the pledge script + PledgeID string +} + +// Verify checks if a signature is a valid signature on the message with respect to TransferVerifier +func (v *Verifier) Verify(message, sigma []byte) error { + sig := &Signature{} + err := json.Unmarshal(sigma, sig) + if err != nil { + return errors.Wrapf(err, "failed unmarshalling signature [%s] on message [%s]", hash.Hashable(sigma).String(), hash.Hashable(message).String()) + } + // this is a redeem + if !sig.Reclaim { + if err := v.Issuer.Verify(message, sig.IssuerSignature); err != nil { + return errors.Wrapf(err, "failed verifying signature [%s] on message [%s], this is not a valid redeem", hash.Hashable(sigma).String(), hash.Hashable(message).String()) + } + return nil + } + // this is a reclaim + message = append(message, sig.IssuerSignature...) + if err := v.Sender.Verify(message, sig.SenderSignature); err != nil { + return errors.Wrapf(err, "failed verifying signature [%s] on message [%s], this is not a valid reclaim", hash.Hashable(sigma).String(), hash.Hashable(message).String()) + } + err = v.Issuer.Verify([]byte(v.PledgeID), sig.IssuerSignature) + if err != nil { + return errors.Wrapf(err, "failed verifying reclaim issuer signature [%s:%s]", v.PledgeID, hash.Hashable(sig.IssuerSignature).String()) + } + return nil +} diff --git a/token/services/interop/pledge/state.go b/token/services/interop/pledge/state.go new file mode 100644 index 0000000000..ef520b615d --- /dev/null +++ b/token/services/interop/pledge/state.go @@ -0,0 +1,207 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package pledge + +import ( + "time" + + "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/state" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/network" + token2 "github.com/hyperledger-labs/fabric-token-sdk/token/token" + "github.com/pkg/errors" +) + +type CollectProofOfExistenceView struct { + tokenID *token2.ID + source string +} + +func NewCollectProofOfExistenceView(tokenID *token2.ID, source string) *CollectProofOfExistenceView { + return &CollectProofOfExistenceView{ + tokenID: tokenID, + source: source, + } +} + +func (c *CollectProofOfExistenceView) Call(context view.Context) (interface{}, error) { + // get a query executor for the target network that should contain the pledge + ssp, err := state.GetServiceProvider(context) + if err != nil { + return nil, errors.WithMessage(err, "failed getting state service provider") + } + p, err := ssp.QueryExecutor(c.source) + if err != nil { + return nil, errors.WithMessagef(err, "failed getting prover for [%s]", c.source) + } + return p.Exist(c.tokenID) +} + +type CollectProofOfNonExistenceView struct { + origin string + tokenID *token2.ID + deadline time.Time + destination string +} + +func NewCollectProofOfNonExistenceView(tokenID *token2.ID, origin string, deadline time.Time, destination string) *CollectProofOfNonExistenceView { + return &CollectProofOfNonExistenceView{ + origin: origin, + tokenID: tokenID, + deadline: deadline, + destination: destination, + } +} + +func (c *CollectProofOfNonExistenceView) Call(context view.Context) (interface{}, error) { + ssp, err := state.GetServiceProvider(context) + if err != nil { + return nil, errors.WithMessage(err, "failed getting state service provider") + } + p, err := ssp.QueryExecutor(c.destination) + if err != nil { + return nil, errors.WithMessagef(err, "failed getting prover for [%s]", c.destination) + } + return p.DoesNotExist(c.tokenID, c.origin, c.deadline) +} + +type CollectProofOfTokenWithMetadataExistenceView struct { + origin string + tokenID *token2.ID + destination string +} + +func NewCollectProofOfTokenWithMetadataExistenceView(tokenID *token2.ID, origin string, destination string) *CollectProofOfTokenWithMetadataExistenceView { + return &CollectProofOfTokenWithMetadataExistenceView{ + origin: origin, + tokenID: tokenID, + destination: destination, + } +} + +func (c *CollectProofOfTokenWithMetadataExistenceView) Call(context view.Context) (interface{}, error) { + ssp, err := state.GetServiceProvider(context) + if err != nil { + return nil, errors.WithMessage(err, "failed getting state service provider") + } + p, err := ssp.QueryExecutor(c.destination) + if err != nil { + return nil, errors.WithMessagef(err, "failed getting prover for [%s]", c.destination) + } + return p.ExistsWithMetadata(c.tokenID, c.origin) +} + +// RequestProofOfExistence requests a proof of the existence of a pledge corresponding to the passed information +func RequestProofOfExistence(context view.Context, info *Info) ([]byte, error) { + // collect proof + boxed, err := context.RunView(NewCollectProofOfExistenceView(info.TokenID, info.Source)) + if err != nil { + return nil, err + } + proof, ok := boxed.([]byte) + if !ok { + return nil, errors.Errorf("failed to collect proof of existence") + } + + // verify proof before returning it + // get a proof verifier for the network that generated the proof + ssp, err := state.GetServiceProvider(context) + if err != nil { + return nil, errors.WithMessage(err, "failed getting state service provider") + } + v, err := ssp.Verifier(info.Source) + if err != nil { + return nil, errors.WithMessagef(err, "failed getting verifier for [%s]", info.Source) + } + if err := v.VerifyProofExistence(proof, info.TokenID, info.TokenMetadata); err != nil { + return nil, errors.WithMessagef(err, "failed verifying proof of existence for [%s]", info.Source) + } + + return proof, nil +} + +// RequestProofOfNonExistence request a proof of non-existence of the given token, originally created in the given network, +// in the destination network identified by the given script. +// If no error is returned, the proof is valid with the respect to the given script. +func RequestProofOfNonExistence(context view.Context, tokenID *token2.ID, originTMSID token.TMSID, script *Script) ([]byte, error) { + // collect proof + net := network.GetInstance(context, originTMSID.Network, originTMSID.Channel) + if net == nil { + return nil, errors.Errorf("cannot find network for [%s]", originTMSID) + } + originNetwork := net.InteropURL(originTMSID.Namespace) + + boxed, err := context.RunView(NewCollectProofOfNonExistenceView( + tokenID, + originNetwork, + script.Deadline, + script.DestinationNetwork, + )) + if err != nil { + return nil, err + } + proof, ok := boxed.([]byte) + if !ok { + return nil, errors.Errorf("failed to collect proof of non-existence") + } + + // verify proof before returning it + ssp, err := state.GetServiceProvider(context) + if err != nil { + return nil, errors.WithMessage(err, "failed getting state service provider") + } + v, err := ssp.Verifier(script.DestinationNetwork) + if err != nil { + return nil, errors.WithMessagef(err, "failed getting verifier for [%s]", script.DestinationNetwork) + } + if err := v.VerifyProofNonExistence(proof, tokenID, originNetwork, script.Deadline); err != nil { + return nil, errors.WithMessagef(err, "failed verifying proof of non-existence for [%s]", originNetwork) + } + + return proof, nil +} + +// RequestProofOfTokenWithMetadataExistence request a proof of a token existence with the given token ID and origin network, +// in the destination network identified by the given script. +// If no error is returned, the proof is valid with the respect to the given script. +func RequestProofOfTokenWithMetadataExistence(context view.Context, tokenID *token2.ID, originTMSID token.TMSID, script *Script) ([]byte, error) { + // collect proof + net := network.GetInstance(context, originTMSID.Network, originTMSID.Channel) + if net == nil { + return nil, errors.Errorf("cannot find network for [%s]", originTMSID) + } + originNetwork := net.InteropURL(originTMSID.Namespace) + + boxed, err := context.RunView(NewCollectProofOfTokenWithMetadataExistenceView( + tokenID, + originNetwork, + script.DestinationNetwork, + )) + if err != nil { + return nil, errors.WithMessagef(err, "failed collecting proof of token existence for [%s][%s]", originTMSID, tokenID) + } + proof, ok := boxed.([]byte) + if !ok { + return nil, errors.Errorf("failed to collect proof of token existence") + } + + // verify proof before returning it + ssp, err := state.GetServiceProvider(context) + if err != nil { + return nil, errors.WithMessage(err, "failed getting state service provider") + } + v, err := ssp.Verifier(script.DestinationNetwork) + if err != nil { + return nil, errors.WithMessagef(err, "failed getting verifier for [%s]", script.DestinationNetwork) + } + if err := v.VerifyProofTokenWithMetadataExistence(proof, tokenID, originNetwork); err != nil { + return nil, errors.WithMessagef(err, "failed verifying proof of token existence for [%s]", originNetwork) + } + + return proof, nil +} diff --git a/token/services/interop/pledge/store.go b/token/services/interop/pledge/store.go new file mode 100644 index 0000000000..b7a3cc51c3 --- /dev/null +++ b/token/services/interop/pledge/store.go @@ -0,0 +1,108 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package pledge + +import ( + "github.com/hyperledger-labs/fabric-smart-client/platform/view" + "github.com/hyperledger-labs/fabric-smart-client/platform/view/services/hash" + "github.com/hyperledger-labs/fabric-smart-client/platform/view/services/kvs" + "github.com/hyperledger-labs/fabric-token-sdk/token/token" + "github.com/pkg/errors" +) + +type store interface { + Exists(id string) bool + Put(id string, state interface{}) error + Get(id string, state interface{}) error + Delete(id string) error + GetByPartialCompositeID(prefix string, attrs []string) (kvs.Iterator, error) +} + +type VaultStore struct { + store store +} + +func Vault(sf view.ServiceProvider) *VaultStore { + return &VaultStore{ + store: kvs.GetService(sf), + } +} + +func (ps *VaultStore) Store(info *Info) error { + raw, err := info.Bytes() + if err != nil { + return errors.Wrapf(err, "failed marshalling info to raw") + } + key, err := kvs.CreateCompositeKey( + "github.com/hyperledger-labs/fabric-token-sdk/token/services/interop/pledge", + []string{ + "pledge", + "info", + hash.Hashable(raw).String(), + }, + ) + if err != nil { + return errors.Wrapf(err, "failed creating key for info [%v]", info) + } + return ps.store.Put( + key, + info, + ) +} + +func (ps *VaultStore) PledgeByTokenID(tokenID *token.ID) ([]*Info, error) { + if tokenID == nil { + return nil, errors.Errorf("passed nil token id") + } + it, err := ps.store.GetByPartialCompositeID( + "github.com/hyperledger-labs/fabric-token-sdk/token/services/interop/pledge", + []string{ + "pledge", + "info", + }, + ) + if err != nil { + return nil, errors.Wrapf(err, "failed getting iterator over pledges") + } + + var res []*Info + for it.HasNext() { + var info *Info + if _, err := it.Next(&info); err != nil { + return nil, errors.Wrapf(err, "failed getting next pledge info") + } + if info.TokenID.TxId == tokenID.TxId && info.TokenID.Index == tokenID.Index { + res = append(res, info) + } + } + + return res, nil +} + +func (ps *VaultStore) Delete(pledges []*Info) error { + for _, info := range pledges { + raw, err := info.Bytes() + if err != nil { + return errors.Wrapf(err, "failed marshalling info to raw") + } + key, err := kvs.CreateCompositeKey( + "github.com/hyperledger-labs/fabric-token-sdk/token/services/interop/pledge", + []string{ + "pledge", + "info", + hash.Hashable(raw).String(), + }, + ) + if err != nil { + return errors.Wrapf(err, "failed creating key for info [%v]", info) + } + if err := ps.store.Delete(key); err != nil { + return errors.WithMessagef(err, "failed deleting [%s]", key) + } + } + return nil +} diff --git a/token/services/interop/pledge/stream.go b/token/services/interop/pledge/stream.go new file mode 100644 index 0000000000..1e0fce1f33 --- /dev/null +++ b/token/services/interop/pledge/stream.go @@ -0,0 +1,71 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package pledge + +import ( + "encoding/json" + + "github.com/hyperledger-labs/fabric-token-sdk/token/services/identity" + + "github.com/hyperledger-labs/fabric-smart-client/platform/view/view" + "github.com/hyperledger-labs/fabric-token-sdk/token" +) + +type OutputStream struct { + *token.OutputStream +} + +func NewOutputStream(outputs *token.OutputStream) *OutputStream { + return &OutputStream{OutputStream: outputs} +} + +func (o *OutputStream) Filter(f func(t *token.Output) bool) *OutputStream { + return NewOutputStream(o.OutputStream.Filter(f)) +} + +func (o *OutputStream) ByRecipient(id view.Identity) *OutputStream { + return o.Filter(func(t *token.Output) bool { + return id.Equal(t.Owner) + }) +} + +func (o *OutputStream) ByType(typ string) *OutputStream { + return o.Filter(func(t *token.Output) bool { + return t.Type == typ + }) +} + +func (o *OutputStream) ByScript() *OutputStream { + return o.Filter(func(t *token.Output) bool { + owner, err := identity.UnmarshalTypedIdentity(t.Owner) + if err != nil { + return false + } + return owner.Type == ScriptType + }) +} + +func (o *OutputStream) ScriptAt(i int) *Script { + tok := o.OutputStream.At(i) + owner, err := identity.UnmarshalTypedIdentity(tok.Owner) + if err != nil { + logger.Debugf("failed unmarshalling raw owner [%s]: [%s]", tok, err) + return nil + } + if owner.Type == ScriptType { + script := &Script{} + if err := json.Unmarshal(owner.Identity, script); err != nil { + logger.Debugf("failed unmarshalling pledge script [%s]: [%s]", tok, err) + return nil + } + if script.Sender.IsNone() || script.Recipient.IsNone() { + return nil + } + return script + } + return nil +} diff --git a/token/services/interop/pledge/transaction.go b/token/services/interop/pledge/transaction.go new file mode 100644 index 0000000000..4ba4ba4bb3 --- /dev/null +++ b/token/services/interop/pledge/transaction.go @@ -0,0 +1,56 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package pledge + +import ( + view2 "github.com/hyperledger-labs/fabric-smart-client/platform/view" + "github.com/hyperledger-labs/fabric-smart-client/platform/view/view" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/ttx" +) + +type Binder interface { + Bind(longTerm view.Identity, ephemeral view.Identity) error +} + +// Transaction holds a ttx transaction +type Transaction struct { + *ttx.Transaction + Binder Binder +} + +// NewAnonymousTransaction returns a new anonymous token transaction customized with the passed opts +func NewAnonymousTransaction(ctx view.Context, opts ...ttx.TxOption) (*Transaction, error) { + tx, err := ttx.NewAnonymousTransaction(ctx, opts...) + if err != nil { + return nil, err + } + return &Transaction{ + Transaction: tx, + Binder: view2.GetEndpointService(ctx), + }, nil +} + +// NewTransactionFromBytes returns a new transaction from the passed bytes +func NewTransactionFromBytes(ctx view.Context, raw []byte) (*Transaction, error) { + tx, err := ttx.NewTransactionFromBytes(ctx, raw) + if err != nil { + return nil, err + } + return &Transaction{ + Transaction: tx, + Binder: view2.GetEndpointService(ctx), + }, nil +} + +// Outputs returns a new OutputStream of the transaction's outputs +func (t *Transaction) Outputs() (*OutputStream, error) { + outs, err := t.TokenRequest.Outputs() + if err != nil { + return nil, err + } + return NewOutputStream(outs), nil +} diff --git a/token/services/interop/pledge/wallet.go b/token/services/interop/pledge/wallet.go new file mode 100644 index 0000000000..94ccb6bcc4 --- /dev/null +++ b/token/services/interop/pledge/wallet.go @@ -0,0 +1,213 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package pledge + +import ( + "encoding/json" + + "github.com/hyperledger-labs/fabric-smart-client/platform/view" + view2 "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/network" + token2 "github.com/hyperledger-labs/fabric-token-sdk/token/token" + "github.com/pkg/errors" +) + +func MyOwnerWallet(sp view.ServiceProvider) (*token.OwnerWallet, error) { + w := token.GetManagementService(sp).WalletManager().OwnerWallet("") + if w == nil { + return nil, errors.Errorf("owner Wallet needs to be initialized") + } + return w, nil +} + +func GetOwnerWallet(sp view.ServiceProvider, id string, opts ...token.ServiceOption) (*token.OwnerWallet, error) { + w := token.GetManagementService(sp, opts...).WalletManager().OwnerWallet(id) + if w == nil { + return nil, errors.Errorf("owner Wallet needs to be initialized") + } + return w, nil +} + +func GetIssuerWallet(sp view.ServiceProvider, id string, opts ...token.ServiceOption) (*token.IssuerWallet, error) { + w := token.GetManagementService(sp, opts...).WalletManager().IssuerWallet(id) + if w == nil { + return nil, errors.Errorf("issuer Wallet needs to be initialized") + } + return w, nil +} + +type QueryService interface { + // TODO: switch to UnspentTokensIteratorBy(id, typ string) (UnspentTokensIterator, error) + ListUnspentTokens() (*token2.UnspentTokens, error) +} + +type IssuerWallet struct { + wallet *token.IssuerWallet + queryService QueryService +} + +func NewIssuerWallet(sp view.ServiceProvider, wallet *token.IssuerWallet) *IssuerWallet { + tmsID := wallet.TMS().ID() + net := network.GetInstance(sp, tmsID.Network, tmsID.Channel) + if net == nil { + logger.Errorf("could not find network [%s]", tmsID) + return nil + } + v, err := net.Vault(tmsID.Namespace) + if err != nil { + logger.Errorf("failed to get vault for [%s]: [%s]", tmsID, err) + } + + return &IssuerWallet{ + wallet: wallet, + queryService: v.QueryEngine(), + } +} + +func (w *IssuerWallet) GetPledgedToken(tokenID *token2.ID) (*token2.UnspentToken, *Script, error) { + unspentTokens, err := w.queryService.ListUnspentTokens() + if err != nil { + return nil, nil, errors.Wrap(err, "token selection failed") + } + return retrievePledgedToken(unspentTokens, tokenID) +} + +type OwnerWallet struct { + wallet *token.OwnerWallet + queryService QueryService +} + +// GetWallet returns the wallet whose id is the passed id. +// If the passed id is empty, GetWallet has the same behaviour of MyWallet. +// It returns nil, if no wallet is found. +func GetWallet(sp view.ServiceProvider, id string, opts ...token.ServiceOption) *token.OwnerWallet { + w := token.GetManagementService(sp, opts...).WalletManager().OwnerWallet(id) + if w == nil { + return nil + } + return w +} + +func Wallet(sp view.ServiceProvider, wallet *token.OwnerWallet) *OwnerWallet { + tmsID := wallet.TMS().ID() + net := network.GetInstance(sp, tmsID.Network, tmsID.Channel) + if net == nil { + logger.Errorf("could not find network [%s]", tmsID) + return nil + } + v, err := net.Vault(tmsID.Namespace) + if err != nil { + logger.Errorf("failed to get vault for [%s]: [%s]", tmsID, err) + } + + return &OwnerWallet{ + wallet: wallet, + queryService: v.QueryEngine(), + } +} + +func (w *OwnerWallet) GetPledgedToken(tokenID *token2.ID) (*token2.UnspentToken, *Script, error) { + unspentTokens, err := w.queryService.ListUnspentTokens() + if err != nil { + return nil, nil, errors.Wrap(err, "token selection failed") + } + + return retrievePledgedToken(unspentTokens, tokenID) +} + +func retrievePledgedToken(unspentTokens *token2.UnspentTokens, tokenID *token2.ID) (*token2.UnspentToken, *Script, error) { + logger.Debugf("[%d] unspent tokens found, search [%s]", len(unspentTokens.Tokens), tokenID) + + var res []*token2.UnspentToken + var scripts []*Script + for _, tok := range unspentTokens.Tokens { + if tok.Id.String() == tokenID.String() { + owner, err := identity.UnmarshalTypedIdentity(tok.Owner.Raw) + if err != nil { + return nil, nil, errors.Wrapf(err, "failed to unmarshal owner") + } + logger.Debugf("found token [%s] with type [%s]", tokenID, owner.Type) + if owner.Type == ScriptType { + res = append(res, tok) + script := &Script{} + if err := json.Unmarshal(owner.Identity, script); err != nil { + return nil, nil, errors.Wrapf(err, "failed unmarshalling pledge script") + } + scripts = append(scripts, script) + } + } + } + if len(res) > 1 { + return nil, nil, errors.Errorf("multiple pledged tokens with the same identifier [%s]", tokenID.String()) + } + if len(res) == 0 { + return nil, nil, errors.Errorf("no pledged token exists with identifier [%s]", tokenID.String()) + } + return res[0], scripts[0], nil +} + +type ScriptOwnership struct{} + +func (s *ScriptOwnership) AmIAnAuditor(tms *token.ManagementService) bool { + return false +} + +func (s *ScriptOwnership) IsMine(tms *token.ManagementService, tok *token2.Token) ([]string, bool) { + identity, err := identity.UnmarshalTypedIdentity(tok.Owner.Raw) + if err != nil { + logger.Debugf("Is Mine [%s,%s,%s]? No, failed unmarshalling [%s]", view2.Identity(tok.Owner.Raw), tok.Type, tok.Quantity, err) + return nil, false + } + if identity.Type != ScriptType { + return nil, false + } + + script := &Script{} + if err := json.Unmarshal(identity.Identity, script); err != nil { + logger.Debugf("Is Mine [%s,%s,%s]? No, failed unmarshalling [%s]", view2.Identity(tok.Owner.Raw), tok.Type, tok.Quantity, err) + return nil, false + } + if script.Sender.IsNone() || script.Recipient.IsNone() || script.Issuer.IsNone() { + logger.Debugf("Is Mine [%s,%s,%s]? No, invalid content [%v]", view2.Identity(tok.Owner.Raw), tok.Type, tok.Quantity, script) + return nil, false + } + + // I'm either a sender, recipient, or issuer + var ids []string + for _, beneficiary := range []struct { + identity view2.Identity + desc string + prefix string + }{ + { + identity: script.Sender, + desc: "sender", + prefix: "pledge.sender", + }, + { + identity: script.Recipient, + desc: "recipient", + prefix: "pledge.recipient", + }, + { + identity: script.Issuer, + desc: "issuer", + prefix: "pledge.issuer", + }, + } { + logger.Debugf("Is Mine [%s,%s,%s] as a %s?", view2.Identity(tok.Owner.Raw), tok.Type, tok.Quantity, beneficiary.desc) + // TODO: differentiate better + if wallet := tms.WalletManager().OwnerWallet(beneficiary.identity); wallet != nil { + logger.Debugf("Is Mine [%s,%s,%s] as a %s? Yes", view2.Identity(tok.Owner.Raw), tok.Type, tok.Quantity, beneficiary.desc) + ids = append(ids, beneficiary.prefix+wallet.ID()) + } + } + logger.Debugf("Is Mine [%s,%s,%s]? [%b] with [%s]", view2.Identity(tok.Owner.Raw), tok.Type, tok.Quantity, len(ids) != 0, ids) + return ids, len(ids) != 0 +} diff --git a/token/services/interop/state/driver/driver.go b/token/services/interop/state/driver/driver.go new file mode 100644 index 0000000000..73d08dc6c5 --- /dev/null +++ b/token/services/interop/state/driver/driver.go @@ -0,0 +1,54 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package driver + +import ( + "time" + + token2 "github.com/hyperledger-labs/fabric-token-sdk/token" + "github.com/hyperledger-labs/fabric-token-sdk/token/token" +) + +// StateQueryExecutor models a prover of token related states +type StateQueryExecutor interface { + // Exist returns a proof that the passed token exists in the network this query executor targets + Exist(tokenID *token.ID) ([]byte, error) + // DoesNotExist returns a proof that the passed token, originated in the given network, does not exist + // in the network this query executor targets + DoesNotExist(tokenID *token.ID, origin string, deadline time.Time) ([]byte, error) + // ExistsWithMetadata returns a proof that a token with metadata including the passed token ID and origin network exists + // in the network this query executor targets + ExistsWithMetadata(tokenID *token.ID, origin string) ([]byte, error) +} + +// StateVerifier is used to verify proofs related to the state of tokens in a target network +type StateVerifier interface { + // VerifyProofExistence verifies that a proof of existence of the passed token in the target network is valid + VerifyProofExistence(proof []byte, tokenID *token.ID, metadata []byte) error + // VerifyProofNonExistence verifies that a proof of non-existence of the given token, + // originated in the given network, in the target network is valid + VerifyProofNonExistence(proof []byte, tokenID *token.ID, origin string, deadline time.Time) error + // VerifyProofTokenWithMetadataExistence verifies that a proof of existence of a token + // with metadata including the given token ID and origin network, in the target network is valid + VerifyProofTokenWithMetadataExistence(proof []byte, tokenID *token.ID, origin string) error +} + +// StateServiceProvider manages state-related services +type StateServiceProvider interface { + // QueryExecutor returns an instance of a query executor to requests proofs from the network identified by the passed url + QueryExecutor(url string) (StateQueryExecutor, error) + // Verifier returns an instance of a verifier of proofs generated by the network identified by the passed url + Verifier(url string) (StateVerifier, error) + // TODO: comment + URLToTMSID(url string) (token2.TMSID, error) +} + +// SSPDriver models a driver factory for state-related services +type SSPDriver interface { + // New returns an instance of a state service provider + New(sp token2.ServiceProvider) (StateServiceProvider, error) +} diff --git a/token/services/interop/state/fabric/driver.go b/token/services/interop/state/fabric/driver.go new file mode 100644 index 0000000000..32b347bd04 --- /dev/null +++ b/token/services/interop/state/fabric/driver.go @@ -0,0 +1,41 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package fabric + +import ( + "sync" + + "github.com/hyperledger-labs/fabric-token-sdk/token" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/interop/state/driver" +) + +type StateDriver interface { + // NewStateQueryExecutor returns a new StateQueryExecutor for the given URL + NewStateQueryExecutor(sp token.ServiceProvider, url string) (driver.StateQueryExecutor, error) + // NewStateVerifier returns a new StateVerifier for the given url + NewStateVerifier(sp token.ServiceProvider, url string) (driver.StateVerifier, error) +} + +var ( + driversMu sync.RWMutex + drivers = make(map[string]StateDriver) +) + +// RegisterStateDriver makes an SSPDriver available by the provided name. +// If Register is called twice with the same name or if driver is nil, +// it panics. +func RegisterStateDriver(name string, driver StateDriver) { + driversMu.Lock() + defer driversMu.Unlock() + if driver == nil { + panic("Register driver is nil") + } + if _, dup := drivers[name]; dup { + panic("Register called twice for driver " + name) + } + drivers[name] = driver +} diff --git a/token/services/interop/state/fabric/ssp.go b/token/services/interop/state/fabric/ssp.go new file mode 100644 index 0000000000..9e8bcffcc4 --- /dev/null +++ b/token/services/interop/state/fabric/ssp.go @@ -0,0 +1,169 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package fabric + +import ( + "sync" + + "github.com/hyperledger-labs/fabric-smart-client/platform/fabric" + "github.com/hyperledger-labs/fabric-smart-client/platform/fabric/services/weaver" + "github.com/hyperledger-labs/fabric-token-sdk/token" + "github.com/hyperledger-labs/fabric-token-sdk/token/core" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/interop/state" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/interop/state/driver" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/logging" + "github.com/pkg/errors" +) + +const ( + QueryPublicParamsFunction = "queryPublicParams" +) + +var logger = logging.MustGetLogger("token-sdk.state") + +type StateServiceProvider struct { + sp token.ServiceProvider + mu sync.RWMutex + queryExecutors map[string]driver.StateQueryExecutor + verifiers map[string]driver.StateVerifier +} + +func NewStateServiceProvider(sp token.ServiceProvider) *StateServiceProvider { + return &StateServiceProvider{ + sp: sp, + mu: sync.RWMutex{}, + queryExecutors: map[string]driver.StateQueryExecutor{}, + verifiers: map[string]driver.StateVerifier{}, + } +} + +func (f *StateServiceProvider) QueryExecutor(url string) (driver.StateQueryExecutor, error) { + f.mu.Lock() + defer f.mu.Unlock() + + qe, ok := f.queryExecutors[url] + if ok { + return qe, nil + } + + // Fetch public parameters, if not fetched already + ppRaw, err := f.fetchPublicParameters(url) + if err != nil { + return nil, errors.Wrapf(err, "failed fetching public parameters from [%s]", url) + } + pp, err := core.PublicParametersFromBytes(ppRaw) + if err != nil { + return nil, errors.Wrapf(err, "failed unmarshalling public parameters from [%s]", url) + } + + driver, ok := drivers[pp.Identifier()] + if !ok { + return nil, errors.Errorf("invalid public parameters type, got [%s]", pp.Identifier()) + } + qe, err = driver.NewStateQueryExecutor(f.sp, url) + if err != nil { + return nil, errors.Wrapf(err, "failed instantiating state query executor from [%s]", url) + } + v, err := driver.NewStateVerifier(f.sp, url) + if err != nil { + return nil, errors.Wrapf(err, "failed instantiating state verifier from [%s]", url) + } + f.queryExecutors[url] = qe + f.verifiers[url] = v + + return qe, nil +} + +func (f *StateServiceProvider) Verifier(url string) (driver.StateVerifier, error) { + f.mu.Lock() + defer f.mu.Unlock() + + v, ok := f.verifiers[url] + if ok { + return v, nil + } + + var identifier string + + // Check if the url refers to a TMS known by this node, then create and return just a verifier + tmsID, err := FabricURLToTMSID(url) + if err != nil { + return nil, errors.Wrapf(err, "failed parsing url [%s]", url) + } + tms := token.GetManagementService(f.sp, token.WithTMSID(tmsID)) + if tms != nil { + identifier = tms.PublicParametersManager().PublicParameters().Identifier() + } else { + // If not, fetch public parameters, if not fetched already + ppRaw, err := f.fetchPublicParameters(url) + if err != nil { + return nil, errors.Wrapf(err, "failed fetching public parameters from [%s]", url) + } + pp, err := core.PublicParametersFromBytes(ppRaw) + if err != nil { + return nil, errors.Wrapf(err, "failed unmarshalling public parameters from [%s]", url) + } + identifier = pp.Identifier() + } + + driver, ok := drivers[identifier] + if !ok { + return nil, errors.Errorf("invalid public parameters type, got [%s]", identifier) + } + v, err = driver.NewStateVerifier(f.sp, url) + if err != nil { + return nil, errors.Wrapf(err, "failed instantiating state verifier from [%s]", url) + } + f.verifiers[url] = v + + return v, nil +} + +func (f *StateServiceProvider) URLToTMSID(url string) (token.TMSID, error) { + return FabricURLToTMSID(url) +} + +func (f *StateServiceProvider) fetchPublicParameters(url string) ([]byte, error) { + fns, err := fabric.GetDefaultFNS(f.sp) + if err != nil { + return nil, errors.Wrapf(err, "failed getting default FNS for [%s]", url) + } + relay := weaver.GetProvider(f.sp).Relay(fns) + logger.Debugf("query [%s] for the public parameters", url) + + query, err := relay.ToFabric().Query(url, QueryPublicParamsFunction) + if err != nil { + return nil, err + } + res, err := query.Call() + if err != nil { + return nil, err + } + return res.Result(), nil +} + +type SSPDriver struct { + mu sync.RWMutex + queryExecutors map[string]driver.StateQueryExecutor + verifiers map[string]driver.StateVerifier +} + +func NewSSPDriver() *SSPDriver { + return &SSPDriver{ + mu: sync.RWMutex{}, + queryExecutors: map[string]driver.StateQueryExecutor{}, + verifiers: map[string]driver.StateVerifier{}, + } +} + +func (f *SSPDriver) New(sp token.ServiceProvider) (driver.StateServiceProvider, error) { + return NewStateServiceProvider(sp), nil +} + +func init() { + state.RegisterSSPDriver("fabric", NewSSPDriver()) +} diff --git a/token/services/interop/state/fabric/url.go b/token/services/interop/state/fabric/url.go new file mode 100644 index 0000000000..68734921d4 --- /dev/null +++ b/token/services/interop/state/fabric/url.go @@ -0,0 +1,38 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package fabric + +import ( + "fmt" + url2 "net/url" + "strings" + + "github.com/hyperledger-labs/fabric-token-sdk/token" + "github.com/pkg/errors" +) + +func FabricURL(tms token.TMSID) string { + return fmt.Sprintf("fabric://%s.%s.%s/", tms.Network, tms.Channel, tms.Namespace) +} + +func FabricURLToTMSID(url string) (token.TMSID, error) { + u, err := url2.Parse(url) + if err != nil { + return token.TMSID{}, errors.Wrapf(err, "failed parsing url") + } + if u.Scheme != "fabric" { + return token.TMSID{}, errors.Errorf("invalid scheme, expected fabric, got [%s]", u.Scheme) + } + + res := strings.Split(u.Host, ".") + if len(res) != 3 { + return token.TMSID{}, errors.Errorf("invalid host, expected 3 components, found [%d,%v]", len(res), res) + } + return token.TMSID{ + Network: res[0], Channel: res[1], Namespace: res[2], + }, nil +} diff --git a/token/services/interop/state/fabric/utils.go b/token/services/interop/state/fabric/utils.go new file mode 100644 index 0000000000..205cbff176 --- /dev/null +++ b/token/services/interop/state/fabric/utils.go @@ -0,0 +1,25 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package fabric + +import ( + url2 "net/url" + + "github.com/pkg/errors" +) + +// CheckFabricScheme returns an error is the given url is not valid or its scheme is not equal to fabric +func CheckFabricScheme(url string) error { + u, err := url2.Parse(url) + if err != nil { + return errors.Wrapf(err, "failed parsing url [%s]", url) + } + if u.Scheme != "fabric" { + return errors.Errorf("invalid scheme, expected fabric, got [%s] in url [%s]", u.Scheme, url) + } + return nil +} diff --git a/token/services/interop/state/state.go b/token/services/interop/state/state.go new file mode 100644 index 0000000000..d737e07c3d --- /dev/null +++ b/token/services/interop/state/state.go @@ -0,0 +1,114 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package state + +import ( + url2 "net/url" + "sync" + + "github.com/hyperledger-labs/fabric-smart-client/platform/view" + "github.com/hyperledger-labs/fabric-token-sdk/token" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/interop/state/driver" + "github.com/pkg/errors" +) + +var ( + sspDriverMu sync.RWMutex + sspDriver = make(map[string]driver.SSPDriver) +) + +// RegisterSSPDriver makes an SSPDriver available by the provided name. +// If Register is called twice with the same name or if ssp is nil, +// it panics. +func RegisterSSPDriver(name string, driver driver.SSPDriver) { + sspDriverMu.Lock() + defer sspDriverMu.Unlock() + if driver == nil { + panic("Register ssp is nil") + } + if _, dup := sspDriver[name]; dup { + panic("Register called twice for ssp " + name) + } + sspDriver[name] = driver +} + +var ( + // TokenExistsError is returned when the token already exists + TokenExistsError = errors.New("token exists") + // TokenDoesNotExistError is returned when the token does not exist + TokenDoesNotExistError = errors.New("token does not exists") +) + +type ServiceProvider struct { + sp view.ServiceProvider + + sspsMu sync.RWMutex + ssps map[string]driver.StateServiceProvider +} + +func NewServiceProvider(sp view.ServiceProvider) *ServiceProvider { + return &ServiceProvider{ + sp: sp, + ssps: map[string]driver.StateServiceProvider{}, + } +} + +func (p *ServiceProvider) QueryExecutor(url string) (driver.StateQueryExecutor, error) { + ssp, err := p.ssp(url) + if err != nil { + return nil, errors.WithMessagef(err, "failed to get ssp for url [%s]", url) + } + return ssp.QueryExecutor(url) +} + +func (p *ServiceProvider) Verifier(url string) (driver.StateVerifier, error) { + ssp, err := p.ssp(url) + if err != nil { + return nil, errors.WithMessagef(err, "failed to get ssp for url [%s]", url) + } + return ssp.Verifier(url) +} + +func (p *ServiceProvider) URLToTMSID(url string) (token.TMSID, error) { + ssp, err := p.ssp(url) + if err != nil { + return token.TMSID{}, errors.WithMessagef(err, "failed to get ssp for url [%s]", url) + } + return ssp.URLToTMSID(url) +} + +func (p *ServiceProvider) ssp(url string) (driver.StateServiceProvider, error) { + p.sspsMu.Lock() + defer p.sspsMu.Unlock() + + ssp, ok := p.ssps[url] + if !ok { + u, err := url2.Parse(url) + if err != nil { + return nil, errors.Wrapf(err, "failed parsing url") + } + provider, ok := sspDriver[u.Scheme] + if !ok { + return nil, errors.Errorf("invalid scheme, expected fabric, got [%s]", u.Scheme) + } + ssp, err = provider.New(p.sp) + if err != nil { + return nil, errors.Wrapf(err, "failed getting state service provider for [%s]", u.Scheme) + } + p.ssps[url] = ssp + } + return ssp, nil +} + +// GetServiceProvider returns an instance of a state service provider +func GetServiceProvider(sp token.ServiceProvider) (*ServiceProvider, error) { + s, err := sp.GetService(&ServiceProvider{}) + if err != nil { + return nil, errors.Wrap(err, "failed getting state service provider") + } + return s.(*ServiceProvider), nil +} diff --git a/token/services/network/common/rws/keys/keys.go b/token/services/network/common/rws/keys/keys.go index 1b03bc5f25..a6f8c338b5 100644 --- a/token/services/network/common/rws/keys/keys.go +++ b/token/services/network/common/rws/keys/keys.go @@ -10,6 +10,8 @@ import ( "strconv" "unicode/utf8" + token2 "github.com/hyperledger-labs/fabric-token-sdk/token" + "github.com/hyperledger-labs/fabric-token-sdk/token/token" "github.com/pkg/errors" ) @@ -29,6 +31,10 @@ const ( SerialNumber = "sn" IssueActionMetadata = "iam" TransferActionMetadata = "tam" + + ProofOfExistencePrefix = "pe" + ProofOfNonExistencePrefix = "pne" + ProofOfMetadataExistencePrefix = "pme" ) func SplitCompositeKey(compositeKey string) (string, []string, error) { @@ -124,3 +130,22 @@ func ValidateCompositeKeyAttribute(str string) error { } return nil } + +func CreateProofOfExistenceKey(tokenId *token.ID) (string, error) { + id := token2.Hashable(tokenId.String()).String() + return CreateCompositeKey(ProofOfExistencePrefix, []string{id}) +} + +func CreateProofOfNonExistenceKey(tokenID *token.ID, origin string) (string, error) { + return CreateCompositeKey(ProofOfNonExistencePrefix, []string{ + token2.Hashable(tokenID.String()).String(), + token2.Hashable(origin).String(), + }) +} + +func CreateProofOfMetadataExistenceKey(tokenID *token.ID, origin string) (string, error) { + return CreateCompositeKey(ProofOfMetadataExistencePrefix, []string{ + token2.Hashable(tokenID.String()).String(), + token2.Hashable(origin).String(), + }) +} diff --git a/token/services/network/common/rws/translator/prover.go b/token/services/network/common/rws/translator/prover.go new file mode 100644 index 0000000000..d618b6dd6d --- /dev/null +++ b/token/services/network/common/rws/translator/prover.go @@ -0,0 +1,128 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package translator + +import ( + "encoding/json" + "time" + + "github.com/hyperledger-labs/fabric-smart-client/platform/view/services/hash" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/network/common/rws/keys" + "github.com/hyperledger-labs/fabric-token-sdk/token/token" + "github.com/pkg/errors" +) + +type Metadata struct { + // OriginTokenID is the identifier of the pledged token in the origin network + OriginTokenID *token.ID + // OriginNetwork is the network where the pledge took place + OriginNetwork string +} + +type ProofOfTokenMetadataNonExistence struct { + Origin string + TokenID *token.ID + Deadline time.Time +} + +type ProofOfTokenMetadataExistence struct { + Origin string + TokenID *token.ID +} + +// ProveTokenExists queries whether a token with the given token ID exists +func (w *Translator) ProveTokenExists(tokenId *token.ID) error { + key, err := keys.CreateTokenKey(tokenId.TxId, tokenId.Index) + if err != nil { + return err + } + tok, err := w.RWSet.GetState(w.namespace, key) + if err != nil { + return err + } + if tok == nil { + return errors.Errorf("value at key [%s] is empty", tokenId) + } + key, err = keys.CreateProofOfExistenceKey(tokenId) + if err != nil { + return err + } + err = w.RWSet.SetState(w.namespace, key, tok) + if err != nil { + return err + } + return nil +} + +// ProveTokenDoesNotExist queries whether a token with metadata including the given token ID and origin network does not exist +func (w *Translator) ProveTokenDoesNotExist(tokenID *token.ID, origin string, deadline time.Time) error { + if time.Now().Before(deadline) { + return errors.Errorf("deadline has not elapsed yet") + } + metadata, err := json.Marshal(&Metadata{OriginTokenID: tokenID, OriginNetwork: origin}) + if err != nil { + return errors.Errorf("failed to marshal token metadata") + } + key, err := keys.CreateIssueActionMetadataKey(hash.Hashable(metadata).String()) + if err != nil { + return err + } + tok, err := w.RWSet.GetState(w.namespace, key) + if err != nil { + return err + } + if tok != nil { + return errors.Errorf("value at key [%s] is not empty", key) + } + proof := &ProofOfTokenMetadataNonExistence{Origin: origin, TokenID: tokenID, Deadline: deadline} + raw, err := json.Marshal(proof) + if err != nil { + return err + } + key, err = keys.CreateProofOfNonExistenceKey(tokenID, origin) + if err != nil { + return err + } + err = w.RWSet.SetState(w.namespace, key, raw) + if err != nil { + return err + } + return nil +} + +// ProveTokenWithMetadataExists queries whether a token with metadata including the given token ID and origin network exists +func (w *Translator) ProveTokenWithMetadataExists(tokenID *token.ID, origin string) error { + metadata, err := json.Marshal(&Metadata{OriginTokenID: tokenID, OriginNetwork: origin}) + if err != nil { + return errors.Errorf("failed to marshal token metadata") + } + key, err := keys.CreateIssueActionMetadataKey(hash.Hashable(metadata).String()) + if err != nil { + return err + } + tok, err := w.RWSet.GetState(w.namespace, key) + if err != nil { + return err + } + if tok == nil { + return errors.Errorf("value at key [%s] is empty", key) + } + proof := &ProofOfTokenMetadataExistence{Origin: origin, TokenID: tokenID} + raw, err := json.Marshal(proof) + if err != nil { + return err + } + key, err = keys.CreateProofOfMetadataExistenceKey(tokenID, origin) + if err != nil { + return err + } + err = w.RWSet.SetState(w.namespace, key, raw) + if err != nil { + return err + } + return nil +} diff --git a/token/services/network/common/rws/translator/translator.go b/token/services/network/common/rws/translator/translator.go index 553f3cfc31..5f8613ae81 100644 --- a/token/services/network/common/rws/translator/translator.go +++ b/token/services/network/common/rws/translator/translator.go @@ -346,6 +346,7 @@ func (w *Translator) commitIssueAction(issueAction IssueAction) error { if len(raw) != 0 { return errors.Errorf("entry with issue metadata key [%s] is already occupied by [%s]", key, string(raw)) } + logger.Infof("commit issue metadata with key [%s]", key) if err := w.RWSet.SetState(w.namespace, k, value); err != nil { return err } diff --git a/token/services/network/driver/network.go b/token/services/network/driver/network.go index 1a4b1830c1..816ec06cf0 100644 --- a/token/services/network/driver/network.go +++ b/token/services/network/driver/network.go @@ -91,3 +91,7 @@ type Network interface { // ProcessNamespace indicates to the commit pipeline to process all transaction in the passed namespace ProcessNamespace(namespace string) error } + +type Interoperability interface { + InteropURL(namespace string) string +} diff --git a/token/services/network/fabric/network.go b/token/services/network/fabric/network.go index c6bd843a2b..d23620f21b 100644 --- a/token/services/network/fabric/network.go +++ b/token/services/network/fabric/network.go @@ -21,6 +21,7 @@ import ( "github.com/hyperledger-labs/fabric-smart-client/platform/view/view" token2 "github.com/hyperledger-labs/fabric-token-sdk/token" "github.com/hyperledger-labs/fabric-token-sdk/token/sdk/common" + fabric2 "github.com/hyperledger-labs/fabric-token-sdk/token/services/interop/state/fabric" "github.com/hyperledger-labs/fabric-token-sdk/token/services/network/common/rws/keys" "github.com/hyperledger-labs/fabric-token-sdk/token/services/network/driver" "github.com/hyperledger-labs/fabric-token-sdk/token/services/vault" @@ -396,6 +397,14 @@ func (n *Network) ProcessNamespace(namespace string) error { return nil } +func (n *Network) InteropURL(namespace string) string { + return fabric2.FabricURL(token2.TMSID{ + Network: n.Name(), + Channel: n.Channel(), + Namespace: namespace, + }) +} + type FinalityListener struct { net *Network root driver.FinalityListener diff --git a/token/services/network/fabric/tcc/tcc.go b/token/services/network/fabric/tcc/tcc.go index 7f1669b06e..51c6347e67 100644 --- a/token/services/network/fabric/tcc/tcc.go +++ b/token/services/network/fabric/tcc/tcc.go @@ -13,6 +13,7 @@ import ( "os" "runtime/debug" "sync" + "time" "github.com/hyperledger-labs/fabric-token-sdk/token" "github.com/hyperledger-labs/fabric-token-sdk/token/core/common" @@ -33,6 +34,10 @@ const ( AreTokensSpent = "areTokensSpent" PublicParamsPathVarEnv = "PUBLIC_PARAMS_FILE_PATH" + + ProofOfTokenExistenceQuery = "proof_of_token_existence" + ProofOfTokenNonExistenceQuery = "proof_of_token_non_existence" + ProofOfTokenMetadataExistenceQuery = "proof_of_token_metadata_existence" ) type Agent interface { @@ -59,6 +64,17 @@ type PublicParameters interface { GraphHiding() bool } +type ProofOfTokenNonExistenceRequest struct { + TokenID *token2.ID + OriginNetwork string + Deadline time.Time +} + +type ProofOfTokenMetadataExistenceRequest struct { + TokenID *token2.ID + OriginNetwork string +} + type TokenChaincode struct { initOnce sync.Once LogLevel string @@ -128,6 +144,21 @@ func (cc *TokenChaincode) Invoke(stub shim.ChaincodeStubInterface) (res pb.Respo return shim.Error("request to check if tokens are spent is empty") } return cc.AreTokensSpent(args[1], stub) + case ProofOfTokenExistenceQuery: + if len(args) != 2 { + return shim.Error(fmt.Sprintf("(ProofOfTokenExistenceQuery) invalid number of arguments, expected 2, got [%d]", len(args))) + } + return cc.ProofOfTokenExistenceQuery(args[1], stub) + case ProofOfTokenNonExistenceQuery: + if len(args) != 2 { + return shim.Error(fmt.Sprintf("(ProofOfTokenNonExistenceQuery) invalid number of arguments, expected 2, got [%d]", len(args))) + } + return cc.ProofOfTokenNonExistenceQuery(args[1], stub) + case ProofOfTokenMetadataExistenceQuery: + if len(args) != 2 { + return shim.Error(fmt.Sprintf("(ProofOfTokenMetadataExistenceQuery) invalid number of arguments, expected 2, got [%d]", len(args))) + } + return cc.ProofOfTokenMetadataExistenceQuery(args[1], stub) default: return shim.Error(fmt.Sprintf("function not [%s] recognized", f)) } @@ -303,3 +334,76 @@ func (cc *TokenChaincode) AreTokensSpent(idsRaw []byte, stub shim.ChaincodeStubI } return shim.Success(raw) } + +func (cc *TokenChaincode) ProofOfTokenExistenceQuery(idRaw []byte, stub shim.ChaincodeStubInterface) pb.Response { + raw, err := base64.StdEncoding.DecodeString(string(idRaw)) + if err != nil { + return shim.Error(fmt.Sprintf("(ProofOfTokenExistenceQuery) invalid argument [%s]: failed unmarshalling [%s]", string(idRaw), err)) + } + tokenId := &token2.ID{} + if err := json.Unmarshal(raw, tokenId); err != nil { + return shim.Error(fmt.Sprintf("(ProofOfTokenExistenceQuery) invalid argument [%s]: failed unmarshalling [%s]", string(idRaw), err)) + } + return cc.proveTokenExists(tokenId, stub) +} + +func (cc *TokenChaincode) proveTokenExists(tokenId *token2.ID, stub shim.ChaincodeStubInterface) pb.Response { + logger.Infof("proof of existence [%s]", tokenId.String()) + logger.Infof("generate proof of existence...") + rwset := &rwsWrapper{stub: stub} + p := translator.New("", rwset, "") + if err := p.ProveTokenExists(tokenId); err != nil { + return shim.Error(fmt.Sprintf("failed to confirm if token with ID [%s] exists", tokenId)) + } + logger.Infof("proof of existence...done.") + return shim.Success(nil) +} + +func (cc *TokenChaincode) ProofOfTokenNonExistenceQuery(reqRaw []byte, stub shim.ChaincodeStubInterface) pb.Response { + raw, err := base64.StdEncoding.DecodeString(string(reqRaw)) + if err != nil { + return shim.Error(fmt.Sprintf("(ProofOfTokenNonExistenceQuery) invalid argument [%s]: failed unmarshalling [%s]", string(reqRaw), err)) + } + request := &ProofOfTokenNonExistenceRequest{} + if err := json.Unmarshal(raw, request); err != nil { + return shim.Error(fmt.Sprintf("(ProofOfTokenNonExistenceQuery) invalid argument [%s]: failed unmarshalling [%s]", string(reqRaw), err)) + } + return cc.proveTokenDoesNotExist(request.TokenID, request.OriginNetwork, request.Deadline, stub) +} + +func (cc *TokenChaincode) proveTokenDoesNotExist(tokenID *token2.ID, origin string, deadline time.Time, stub shim.ChaincodeStubInterface) pb.Response { + logger.Infof("proof of non existence of token [%s] from network [%s]", tokenID.String(), origin) + logger.Infof("generate proof of non-existence...") + rwset := &rwsWrapper{stub: stub} + p := translator.New("", rwset, "") + if err := p.ProveTokenDoesNotExist(tokenID, origin, deadline); err != nil { + return shim.Error(fmt.Sprintf("failed to confirm if token from network [%s] and with key [%s] does not exist", origin, tokenID.String())) + } + logger.Infof("proof of non existence...done.") + return shim.Success(nil) +} + +func (cc *TokenChaincode) ProofOfTokenMetadataExistenceQuery(reqRaw []byte, stub shim.ChaincodeStubInterface) pb.Response { + raw, err := base64.StdEncoding.DecodeString(string(reqRaw)) + if err != nil { + return shim.Error(fmt.Sprintf("(ProofOfTokenMetadataExistenceQuery) invalid argument [%s]: failed unmarshalling [%s]", string(reqRaw), err)) + } + request := &ProofOfTokenMetadataExistenceRequest{} + if err := json.Unmarshal(raw, request); err != nil { + return shim.Error(fmt.Sprintf("(ProofOfTokenMetadataExistenceQuery) invalid argument [%s]: failed unmarshalling [%s]", string(reqRaw), err)) + } + return cc.proveTokenWithMetadataExist(request.TokenID, request.OriginNetwork, stub) +} + +func (cc *TokenChaincode) proveTokenWithMetadataExist(tokenID *token2.ID, origin string, stub shim.ChaincodeStubInterface) pb.Response { + logger.Infof("proof of existence of token with metadata [%s] and network [%s]", tokenID.String(), origin) + logger.Infof("generate proof of existence...") + rwset := &rwsWrapper{stub: stub} + p := translator.New("", rwset, "") + if err := p.ProveTokenWithMetadataExists(tokenID, origin); err != nil { + fmt.Println(err.Error()) + return shim.Error(fmt.Sprintf("failed to confirm if token from network [%s] and with key [%s] exist", origin, tokenID.String())) + } + logger.Infof("proof of non existence...done.") + return shim.Success(nil) +} diff --git a/token/services/network/network.go b/token/services/network/network.go index 352644d96c..527ae41b9d 100644 --- a/token/services/network/network.go +++ b/token/services/network/network.go @@ -387,6 +387,14 @@ func (n *Network) ProcessNamespace(namespace string) error { return n.n.ProcessNamespace(namespace) } +func (n *Network) InteropURL(namespace string) string { + interoperability, ok := n.n.(driver.Interoperability) + if !ok { + panic("interoperability not supported") + } + return interoperability.InteropURL(namespace) +} + // Provider returns an instance of network provider type Provider struct { sp token.ServiceProvider diff --git a/token/services/network/orion/network.go b/token/services/network/orion/network.go index 8555f20b5c..d360c163d2 100644 --- a/token/services/network/orion/network.go +++ b/token/services/network/orion/network.go @@ -11,15 +11,14 @@ import ( "fmt" "time" - "github.com/hyperledger-labs/fabric-token-sdk/token/sdk/common" - "github.com/hyperledger-labs/fabric-token-sdk/token/services/network/common/rws/keys" - "github.com/hyperledger-labs/fabric-smart-client/platform/orion" view2 "github.com/hyperledger-labs/fabric-smart-client/platform/view" "github.com/hyperledger-labs/fabric-smart-client/platform/view/services/events" "github.com/hyperledger-labs/fabric-smart-client/platform/view/view" token2 "github.com/hyperledger-labs/fabric-token-sdk/token" + "github.com/hyperledger-labs/fabric-token-sdk/token/sdk/common" "github.com/hyperledger-labs/fabric-token-sdk/token/services/db" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/network/common/rws/keys" "github.com/hyperledger-labs/fabric-token-sdk/token/services/network/driver" "github.com/hyperledger-labs/fabric-token-sdk/token/services/vault" "github.com/hyperledger-labs/fabric-token-sdk/token/token" diff --git a/token/services/ttx/finality.go b/token/services/ttx/finality.go index e724b3bdf7..7106c1550f 100644 --- a/token/services/ttx/finality.go +++ b/token/services/ttx/finality.go @@ -48,7 +48,7 @@ func NewFinalityWithOpts(opts ...TxOption) *finalityView { // If the transaction is final, the vault is updated. func (f *finalityView) Call(ctx view.Context) (interface{}, error) { // Compile options - options, err := compile(f.opts...) + options, err := CompileTXOptions(f.opts...) if err != nil { return nil, errors.Wrapf(err, "failed to compile options") } diff --git a/token/services/ttx/opts.go b/token/services/ttx/opts.go index 0f53883f39..659275681b 100644 --- a/token/services/ttx/opts.go +++ b/token/services/ttx/opts.go @@ -24,7 +24,7 @@ type TxOptions struct { NetworkTxID network.TxID } -func compile(opts ...TxOption) (*TxOptions, error) { +func CompileTXOptions(opts ...TxOption) (*TxOptions, error) { txOptions := &TxOptions{} for _, opt := range opts { if err := opt(txOptions); err != nil { diff --git a/token/services/ttx/ordering.go b/token/services/ttx/ordering.go index f939e46f27..ecebbec91c 100644 --- a/token/services/ttx/ordering.go +++ b/token/services/ttx/ordering.go @@ -34,7 +34,7 @@ func NewOrderingViewWithOpts(opts ...TxOption) *orderingView { // 1. It broadcasts the token transaction to the proper backend. func (o *orderingView) Call(context view.Context) (interface{}, error) { // Compile options - options, err := compile(o.opts...) + options, err := CompileTXOptions(o.opts...) if err != nil { return nil, errors.Wrapf(err, "failed to compile options") } diff --git a/token/services/ttx/recipients.go b/token/services/ttx/recipients.go index 5ce938e6c7..8bcd9aac84 100644 --- a/token/services/ttx/recipients.go +++ b/token/services/ttx/recipients.go @@ -441,7 +441,9 @@ func (s *RespondExchangeRecipientIdentitiesView) Call(context view.Context) (int ts := token.GetManagementService(context, token.WithTMSID(request.TMSID)) other := request.RecipientData.Identity if err := ts.WalletManager().RegisterRecipientIdentity(&RecipientData{ - Identity: other, AuditInfo: request.RecipientData.AuditInfo, TokenMetadata: request.RecipientData.TokenMetadata, + Identity: other, + AuditInfo: request.RecipientData.AuditInfo, + TokenMetadata: request.RecipientData.TokenMetadata, }); err != nil { return nil, err } diff --git a/token/services/ttx/transaction.go b/token/services/ttx/transaction.go index 6178b52848..6c9cf917e7 100644 --- a/token/services/ttx/transaction.go +++ b/token/services/ttx/transaction.go @@ -41,7 +41,7 @@ type Transaction struct { // NewAnonymousTransaction returns a new anonymous token transaction customized with the passed opts func NewAnonymousTransaction(sp view.Context, opts ...TxOption) (*Transaction, error) { - txOpts, err := compile(opts...) + txOpts, err := CompileTXOptions(opts...) if err != nil { return nil, errors.WithMessage(err, "failed compiling tx options") } @@ -56,7 +56,7 @@ func NewAnonymousTransaction(sp view.Context, opts ...TxOption) (*Transaction, e // A valid signer is a signer that the target network recognizes as so. For example, in case of fabric, the signer must be a valid fabric identity. // If the passed signer is nil, then the default identity is used. func NewTransaction(sp view.Context, signer view.Identity, opts ...TxOption) (*Transaction, error) { - txOpts, err := compile(opts...) + txOpts, err := CompileTXOptions(opts...) if err != nil { return nil, errors.WithMessage(err, "failed compiling tx options") } @@ -134,7 +134,7 @@ func NewTransactionFromBytes(sp view.Context, raw []byte) (*Transaction, error) } func ReceiveTransaction(context view.Context, opts ...TxOption) (*Transaction, error) { - opt, err := compile(opts...) + opt, err := CompileTXOptions(opts...) if err != nil { return nil, errors.WithMessagef(err, "failed to parse options") } diff --git a/token/tms.go b/token/tms.go index 5261efc904..056684ed5b 100644 --- a/token/tms.go +++ b/token/tms.go @@ -18,26 +18,10 @@ import ( var logger = logging.MustGetLogger("token-sdk") // TMSID models a TMS identifier -type TMSID struct { - Network string - Channel string - Namespace string -} - -// String returns a string representation of the TMSID -func (t TMSID) String() string { - return fmt.Sprintf("%s,%s,%s", t.Network, t.Channel, t.Namespace) -} - -func (t TMSID) Equal(tmsid TMSID) bool { - return t.Network == tmsid.Network && t.Channel == tmsid.Channel && t.Namespace == tmsid.Namespace -} +type TMSID = driver.TMSID // ServiceProvider is used to return instances of a given type -type ServiceProvider interface { - // GetService returns an instance of the given type - GetService(v interface{}) (interface{}, error) -} +type ServiceProvider = driver.ServiceProvider // ServiceOptions is used to configure the service type ServiceOptions struct {