diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index f420ad09c..88565dc0e 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -106,12 +106,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 7c88d184e..96fb38f9b 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/ports.go b/integration/ports.go index 5d4d21b5f..b8ee7271b 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 1f6eb5aed..ad3759c17 100644 --- a/integration/token/interop/dlog/dlog_test.go +++ b/integration/token/interop/dlog/dlog_test.go @@ -142,4 +142,23 @@ 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")..., + ) + 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 92696f412..95baaf61c 100644 --- a/integration/token/interop/fabtoken/fabtoken_test.go +++ b/integration/token/interop/fabtoken/fabtoken_test.go @@ -142,4 +142,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")..., + ) + 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 d01306f0d..c0018268e 100644 --- a/integration/token/interop/support.go +++ b/integration/token/interop/support.go @@ -19,6 +19,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" + pledge2 "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" @@ -359,7 +360,7 @@ func HTLCCheckExistenceReceivedExpiredByHash(network *integration.Infrastructure } } -func htlcClaim(network *integration.Infrastructure, tmsID token.TMSID, id string, wallet string, preImage []byte, errorMsgs ...string) string { +func HTLCClaim(network *integration.Infrastructure, tmsID token.TMSID, id string, wallet string, preImage []byte, errorMsgs ...string) string { txIDBoxed, err := network.Client(id).CallView("htlc.claim", common.JSONMarshall(&htlc.Claim{ TMSID: tmsID, Wallet: wallet, @@ -398,7 +399,7 @@ func htlcClaim(network *integration.Infrastructure, tmsID token.TMSID, id string } } -func fastExchange(network *integration.Infrastructure, id string, recipient string, tmsID1 token.TMSID, typ1 string, amount1 uint64, tmsID2 token.TMSID, typ2 string, amount2 uint64, deadline time.Duration) { +func FastExchange(network *integration.Infrastructure, id string, recipient string, tmsID1 token.TMSID, typ1 string, amount1 uint64, tmsID2 token.TMSID, typ2 string, amount2 uint64, deadline time.Duration) { _, err := network.Client(id).CallView("htlc.fastExchange", common.JSONMarshall(&htlc.FastExchange{ Recipient: network.Identity(recipient), TMSID1: tmsID1, @@ -414,7 +415,7 @@ func fastExchange(network *integration.Infrastructure, id string, recipient stri time.Sleep(10 * time.Second) } -func scan(network *integration.Infrastructure, id string, hash []byte, hashFunc crypto.Hash, startingTransactionID string, opts ...token.ServiceOption) { +func Scan(network *integration.Infrastructure, id string, hash []byte, hashFunc crypto.Hash, startingTransactionID string, opts ...token.ServiceOption) { options, err := token.CompileServiceOptions(opts...) Expect(err).NotTo(HaveOccurred()) @@ -428,7 +429,7 @@ func scan(network *integration.Infrastructure, id string, hash []byte, hashFunc Expect(err).NotTo(HaveOccurred()) } -func scanWithError(network *integration.Infrastructure, id string, hash []byte, hashFunc crypto.Hash, startingTransactionID string, errorMsgs []string, opts ...token.ServiceOption) { +func ScanWithError(network *integration.Infrastructure, id string, hash []byte, hashFunc crypto.Hash, startingTransactionID string, errorMsgs []string, opts ...token.ServiceOption) { options, err := token.CompileServiceOptions(opts...) Expect(err).NotTo(HaveOccurred()) @@ -444,3 +445,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(&pledge2.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 := &pledge2.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(&pledge2.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(&pledge2.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(&pledge2.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(&pledge2.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(&pledge2.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(&pledge2.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(&pledge2.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(&pledge2.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(&pledge2.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(&pledge2.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 8d6e840c8..ed4017727 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" + pledge2 "github.com/hyperledger-labs/fabric-token-sdk/token/services/interop/pledge" + token2 "github.com/hyperledger-labs/fabric-token-sdk/token/token" . "github.com/onsi/gomega" "github.com/pkg/errors" ) @@ -79,7 +81,7 @@ func TestHTLCSingleNetwork(network *integration.Infrastructure) { CheckBalanceWithLockedAndHolding(network, "alice", "", "EUR", 0, 0, 0, -1) CheckBalanceWithLockedAndHolding(network, "bob", "", "EUR", 30, 0, 0, -1) CheckBalanceWithLockedAndHolding(network, "bob", "", "USD", 0, 0, 10, -1) - htlcClaim(network, defaultTMSID, "bob", "", preImage, "deadline elapsed") + HTLCClaim(network, defaultTMSID, "bob", "", preImage, "deadline elapsed") CheckBalanceWithLockedAndHolding(network, "alice", "", "USD", 110, 0, 0, -1) CheckBalanceWithLockedAndHolding(network, "alice", "", "EUR", 0, 0, 0, -1) CheckBalanceWithLockedAndHolding(network, "bob", "", "EUR", 30, 0, 0, -1) @@ -98,7 +100,7 @@ func TestHTLCSingleNetwork(network *integration.Infrastructure) { CheckBalanceWithLockedAndHolding(network, "bob", "", "EUR", 30, 0, 0, -1) CheckBalanceWithLockedAndHolding(network, "bob", "", "USD", 0, 20, 0, -1) - failedClaimTXID := htlcClaim(network, defaultTMSID, "bob", "", preImage) + failedClaimTXID := HTLCClaim(network, defaultTMSID, "bob", "", preImage) CheckBalanceWithLockedAndHolding(network, "alice", "", "USD", 100, 0, 0, -1) CheckBalanceWithLockedAndHolding(network, "alice", "", "EUR", 0, 0, 0, -1) CheckBalanceWithLockedAndHolding(network, "bob", "", "EUR", 30, 0, 0, -1) @@ -184,8 +186,8 @@ func TestHTLCTwoNetworks(network *integration.Infrastructure) { _, preImage, hash := HTLCLock(network, alpha, "alice", "", "EUR", 10, "bob", 1*time.Hour, nil, 0) HTLCLock(network, beta, "bob", "", "USD", 10, "alice", 1*time.Hour, hash, 0) - htlcClaim(network, beta, "alice", "", preImage) - htlcClaim(network, alpha, "bob", "", preImage) + HTLCClaim(network, beta, "alice", "", preImage) + HTLCClaim(network, alpha, "bob", "", preImage) CheckBalanceWithLockedAndHolding(network, "alice", "", "EUR", 20, 0, 0, -1, token.WithTMSID(alpha)) CheckBalanceWithLockedAndHolding(network, "bob", "", "EUR", 10, 0, 0, -1, token.WithTMSID(alpha)) @@ -194,8 +196,8 @@ func TestHTLCTwoNetworks(network *integration.Infrastructure) { // Try to claim again and get an error - htlcClaim(network, beta, "alice", "", preImage, "expected only one htlc script to match") - htlcClaim(network, alpha, "bob", "", preImage, "expected only one htlc script to match") + HTLCClaim(network, beta, "alice", "", preImage, "expected only one htlc script to match") + HTLCClaim(network, alpha, "bob", "", preImage, "expected only one htlc script to match") CheckBalanceWithLockedAndHolding(network, "alice", "", "EUR", 20, 0, 0, -1, token.WithTMSID(alpha)) CheckBalanceWithLockedAndHolding(network, "bob", "", "EUR", 10, 0, 0, -1, token.WithTMSID(alpha)) @@ -204,8 +206,8 @@ func TestHTLCTwoNetworks(network *integration.Infrastructure) { // Try to claim without locking - htlcClaim(network, beta, "alice", "", nil, "expected only one htlc script to match") - htlcClaim(network, alpha, "bob", "", nil, "expected only one htlc script to match") + HTLCClaim(network, beta, "alice", "", nil, "expected only one htlc script to match") + HTLCClaim(network, alpha, "bob", "", nil, "expected only one htlc script to match") CheckBalanceWithLockedAndHolding(network, "alice", "", "EUR", 20, 0, 0, -1, token.WithTMSID(alpha)) CheckBalanceWithLockedAndHolding(network, "bob", "", "EUR", 10, 0, 0, -1, token.WithTMSID(alpha)) @@ -253,13 +255,13 @@ func TestHTLCNoCrossClaimTwoNetworks(network *integration.Infrastructure) { aliceLockTxID, preImage, hash := HTLCLock(network, alpha, "alice", "alice.id1", "EUR", 10, "alice.id2", 30*time.Second, nil, 0) bobLockTxID, _, _ := HTLCLock(network, beta, "bob", "bob.id1", "USD", 10, "bob.id2", 30*time.Second, hash, 0) - go func() { htlcClaim(network, alpha, "alice", "alice.id2", preImage) }() - go func() { htlcClaim(network, beta, "bob", "bob.id2", preImage) }() - scan(network, "alice", hash, crypto.SHA256, "", token.WithTMSID(alpha)) - scan(network, "alice", hash, crypto.SHA256, aliceLockTxID, token.WithTMSID(alpha)) + go func() { HTLCClaim(network, alpha, "alice", "alice.id2", preImage) }() + go func() { HTLCClaim(network, beta, "bob", "bob.id2", preImage) }() + Scan(network, "alice", hash, crypto.SHA256, "", token.WithTMSID(alpha)) + Scan(network, "alice", hash, crypto.SHA256, aliceLockTxID, token.WithTMSID(alpha)) - scan(network, "bob", hash, crypto.SHA256, "", token.WithTMSID(beta)) - scan(network, "bob", hash, crypto.SHA256, bobLockTxID, token.WithTMSID(beta)) + Scan(network, "bob", hash, crypto.SHA256, "", token.WithTMSID(beta)) + Scan(network, "bob", hash, crypto.SHA256, bobLockTxID, token.WithTMSID(beta)) CheckBalanceWithLockedAndHolding(network, "alice", "alice.id1", "EUR", 20, 0, 0, -1, token.WithTMSID(alpha)) CheckBalanceWithLockedAndHolding(network, "alice", "alice.id2", "EUR", 10, 0, 0, -1, token.WithTMSID(alpha)) @@ -267,7 +269,7 @@ func TestHTLCNoCrossClaimTwoNetworks(network *integration.Infrastructure) { CheckBalanceWithLockedAndHolding(network, "bob", "bob.id2", "USD", 10, 0, 0, -1, token.WithTMSID(beta)) txID := IssueCashWithTMS(network, alpha, "issuer", "", "EUR", 30, "alice.id1") - scanWithError(network, "alice", hash, crypto.SHA256, txID, []string{"timeout reached"}, token.WithTMSID(alpha)) + ScanWithError(network, "alice", hash, crypto.SHA256, txID, []string{"timeout reached"}, token.WithTMSID(alpha)) CheckPublicParams(network, token.TMSID{}, "alice", "bob") CheckPublicParams(network, alpha, "issuer", "auditor") @@ -305,7 +307,7 @@ func TestFastExchange(network *integration.Infrastructure) { IssueCashWithTMS(network, beta, "issuer", "", "USD", 30, "bob") CheckBalance(network, "bob", "", "USD", 30, token.WithTMSID(beta)) - fastExchange(network, "alice", "bob", alpha, "EUR", 10, beta, "USD", 10, 1*time.Hour) + FastExchange(network, "alice", "bob", alpha, "EUR", 10, beta, "USD", 10, 1*time.Hour) CheckBalance(network, "alice", "", "EUR", 20, token.WithTMSID(alpha)) CheckBalance(network, "bob", "", "EUR", 10, token.WithTMSID(alpha)) @@ -313,3 +315,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 := pledge2.FabricURL(alpha) + betaURL := pledge2.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 1a4974f0c..6dea763ac 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" fabric3 "github.com/hyperledger-labs/fabric-smart-client/platform/fabric/sdk" "github.com/hyperledger-labs/fabric-token-sdk/integration/nwo/token" fabric2 "github.com/hyperledger-labs/fabric-token-sdk/integration/nwo/token/fabric" @@ -19,6 +20,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" + pledge2 "github.com/hyperledger-labs/fabric-token-sdk/integration/token/interop/views/pledge" sdk "github.com/hyperledger-labs/fabric-token-sdk/token/sdk" ) @@ -505,3 +507,122 @@ func HTLCNoCrossClaimWithOrionTopology(tokenSDKDriver string) []api.Topology { return []api.Topology{f1Topology, orionTopology, tokenTopology, fscTopology} } + +func AssetTransferTopology(tokenSDKDriver string) []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("db.driver.badger=info:debug", "") + + 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(), + fabric.WithDefaultNetwork("alpha"), + token.WithIssuerIdentity("issuer.id1"), + token.WithOwnerIdentity("issuer.id1.owner"), + ) + issuerAlpha.RegisterViewFactory("issue", &views2.IssueCashViewFactory{}) + issuerAlpha.RegisterViewFactory("balance", &views2.BalanceViewFactory{}) + issuerAlpha.RegisterViewFactory("transfer.redeem", &pledge2.RedeemViewFactory{}) + issuerAlpha.RegisterResponder(&pledge2.IssuerResponderView{}, &pledge2.View{}) + issuerAlpha.RegisterResponder(&pledge2.IssuerResponderView{}, &pledge2.FastPledgeClaimInitiatorView{}) + issuerAlpha.RegisterResponder(&pledge2.IssuerResponderView{}, &pledge2.FastPledgeReClaimInitiatorView{}) + issuerAlpha.RegisterResponder(&pledge2.ReclaimIssuerResponderView{}, &pledge2.ReclaimInitiatorView{}) + issuerAlpha.RegisterResponder(&pledge2.ClaimIssuerView{}, &pledge2.ClaimInitiatorView{}) + issuerAlpha.RegisterResponder(&pledge2.ClaimIssuerView{}, &pledge2.FastPledgeClaimResponderView{}) + issuerAlpha.RegisterResponder(&pledge2.ClaimIssuerView{}, &pledge2.FastPledgeReClaimResponderView{}) + + issuerBeta := fscTopology.AddNodeByName("issuerBeta").AddOptions( + fabric.WithNetworkOrganization("beta", "Org3"), + fabric.WithAnonymousIdentity(), + fabric.WithDefaultNetwork("beta"), + token.WithIssuerIdentity("issuer.id2"), + token.WithOwnerIdentity("issuer.id2.owner"), + ) + issuerBeta.RegisterViewFactory("issue", &views2.IssueCashViewFactory{}) + issuerBeta.RegisterViewFactory("balance", &views2.BalanceViewFactory{}) + issuerBeta.RegisterViewFactory("transfer.redeem", &pledge2.RedeemViewFactory{}) + issuerBeta.RegisterResponder(&pledge2.IssuerResponderView{}, &pledge2.View{}) + issuerBeta.RegisterResponder(&pledge2.IssuerResponderView{}, &pledge2.FastPledgeClaimInitiatorView{}) + issuerBeta.RegisterResponder(&pledge2.IssuerResponderView{}, &pledge2.FastPledgeReClaimInitiatorView{}) + issuerBeta.RegisterResponder(&pledge2.ReclaimIssuerResponderView{}, &pledge2.ReclaimInitiatorView{}) + issuerBeta.RegisterResponder(&pledge2.ClaimIssuerView{}, &pledge2.ClaimInitiatorView{}) + issuerBeta.RegisterResponder(&pledge2.ClaimIssuerView{}, &pledge2.FastPledgeClaimResponderView{}) + issuerBeta.RegisterResponder(&pledge2.ClaimIssuerView{}, &pledge2.FastPledgeReClaimResponderView{}) + + auditor := fscTopology.AddNodeByName("auditor").AddOptions( + fabric.WithNetworkOrganization("alpha", "Org1"), + fabric.WithNetworkOrganization("beta", "Org3"), + fabric.WithAnonymousIdentity(), + token.WithAuditorIdentity(), + ) + auditor.RegisterViewFactory("registerAuditor", &views2.RegisterAuditorViewFactory{}) + auditor.RegisterViewFactory("balance", &views2.BalanceViewFactory{}) + + alice := fscTopology.AddNodeByName("alice").AddOptions( + fabric.WithNetworkOrganization("alpha", "Org2"), + fabric.WithAnonymousIdentity(), + fabric.WithDefaultNetwork("alpha"), + token.WithOwnerIdentity("alice.id1"), + ) + alice.RegisterResponder(&views2.AcceptCashView{}, &views2.IssueCashView{}) + alice.RegisterViewFactory("balance", &views2.BalanceViewFactory{}) + alice.RegisterViewFactory("transfer.claim", &pledge2.ClaimInitiatorViewFactory{}) + alice.RegisterViewFactory("transfer.pledge", &pledge2.ViewFactory{}) + alice.RegisterViewFactory("transfer.reclaim", &pledge2.ReclaimViewFactory{}) + alice.RegisterViewFactory("transfer.fastTransfer", &pledge2.FastPledgeClaimInitiatorViewFactory{}) + alice.RegisterViewFactory("transfer.fastPledgeReclaim", &pledge2.FastPledgeReClaimInitiatorViewFactory{}) + alice.RegisterViewFactory("transfer.scan", &pledge2.ScanViewFactory{}) + alice.RegisterResponder(&pledge2.RecipientResponderView{}, &pledge2.View{}) + alice.RegisterResponder(&pledge2.FastPledgeClaimResponderView{}, &pledge2.FastPledgeClaimInitiatorView{}) + alice.RegisterResponder(&pledge2.FastPledgeReClaimResponderView{}, &pledge2.FastPledgeReClaimInitiatorView{}) + + bob := fscTopology.AddNodeByName("bob").AddOptions( + fabric.WithNetworkOrganization("beta", "Org4"), + fabric.WithAnonymousIdentity(), + fabric.WithDefaultNetwork("beta"), + token.WithOwnerIdentity("bob.id1"), + ) + bob.RegisterResponder(&views2.AcceptCashView{}, &views2.IssueCashView{}) + bob.RegisterViewFactory("balance", &views2.BalanceViewFactory{}) + bob.RegisterViewFactory("transfer.claim", &pledge2.ClaimInitiatorViewFactory{}) + bob.RegisterViewFactory("transfer.pledge", &pledge2.ViewFactory{}) + bob.RegisterViewFactory("transfer.reclaim", &pledge2.ReclaimViewFactory{}) + bob.RegisterViewFactory("transfer.fastTransfer", &pledge2.FastPledgeClaimInitiatorViewFactory{}) + bob.RegisterViewFactory("transfer.fastPledgeReclaim", &pledge2.FastPledgeReClaimInitiatorViewFactory{}) + bob.RegisterViewFactory("transfer.scan", &pledge2.ScanViewFactory{}) + bob.RegisterResponder(&pledge2.RecipientResponderView{}, &pledge2.View{}) + bob.RegisterResponder(&pledge2.FastPledgeClaimResponderView{}, &pledge2.FastPledgeClaimInitiatorView{}) + bob.RegisterResponder(&pledge2.FastPledgeReClaimResponderView{}, &pledge2.FastPledgeReClaimInitiatorView{}) + + tokenTopology := token.NewTopology() + tokenTopology.SetSDK(fscTopology, &sdk.SDK{}) + tms := tokenTopology.AddTMS(fscTopology.ListNodes("auditor", "issuerAlpha", "alice"), f1Topology, f1Topology.Channels[0].Name, tokenSDKDriver) + tms.SetTokenGenPublicParams("100", "2") + fabric2.SetOrgs(tms, "Org1") + tms.AddAuditor(auditor) + + tms = tokenTopology.AddTMS(fscTopology.ListNodes("auditor", "issuerBeta", "bob"), f2Topology, f2Topology.Channels[0].Name, tokenSDKDriver) + tms.SetTokenGenPublicParams("100", "2") + fabric2.SetOrgs(tms, "Org3") + tms.AddAuditor(auditor) + + // Add Fabric SDK to FSC Nodes + fscTopology.AddSDK(&fabric3.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 f8e79efad..71da5184b 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" ) @@ -65,28 +65,37 @@ 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 + if input.IsHTLC() { + // check script details, for example make sure the hash is set + htlc, err := input.HTLC() + assert.NoError(err, "cannot get htlc script from input") + assert.True(len(htlc.HashInfo.Hash) > 0, "hash is not set") + } else { + if input.IsPledge() { + // check that one can retrieve pledge + _, err := input.Pledge() + assert.NoError(err, "cannot get pledge script from input") + } } - // 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 + htlc, err := output.HTLC() + assert.NoError(err, "cannot get htlc script from output") + assert.NoError(htlc.Validate(now), "script is not valid") + case output.IsPledge(): + pledge, err := output.Pledge() + assert.NoError(err, "cannot get pledge script from output") + assert.NoError(pledge.Validate(), "invalid pledge script") } - // 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 000000000..a7c6ace5d --- /dev/null +++ b/integration/token/interop/views/pledge/claim.go @@ -0,0 +1,147 @@ +/* +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 + session, err := pledge.RequestClaim( + context, + fabric.GetDefaultIdentityProvider(context).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 000000000..c6276f895 --- /dev/null +++ b/integration/token/interop/views/pledge/fast_pledge.go @@ -0,0 +1,325 @@ +/* +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/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(pledge.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) { + session, err = pledge.RequestClaim( + context, + fabric.GetDefaultIdentityProvider(context).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 000000000..e76e90d3f --- /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 000000000..201dfdf9a --- /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 000000000..f7c51adae --- /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 000000000..df31edf82 --- /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 6a728158c..584a032b2 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/fabtoken/actions.go b/token/core/fabtoken/actions.go index aba2d91c0..4c8201373 100644 --- a/token/core/fabtoken/actions.go +++ b/token/core/fabtoken/actions.go @@ -47,6 +47,13 @@ func (t *Output) IsRedeem() bool { return len(t.Output.Owner.Raw) == 0 } +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 +} + // IssueAction encodes a fabtoken Issue type IssueAction struct { // issuer's public key diff --git a/token/core/fabtoken/deserializer.go b/token/core/fabtoken/deserializer.go index df15ff964..de5b323ab 100644 --- a/token/core/fabtoken/deserializer.go +++ b/token/core/fabtoken/deserializer.go @@ -10,10 +10,10 @@ import ( "encoding/json" "github.com/hyperledger-labs/fabric-smart-client/platform/view/view" - "github.com/hyperledger-labs/fabric-token-sdk/token/core/identity" "github.com/hyperledger-labs/fabric-token-sdk/token/core/identity/msp/x509" - "github.com/hyperledger-labs/fabric-token-sdk/token/core/interop/htlc" "github.com/hyperledger-labs/fabric-token-sdk/token/driver" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/interop" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/owner" "github.com/pkg/errors" ) @@ -36,7 +36,7 @@ func NewDeserializer() *Deserializer { return &Deserializer{ auditorDeserializer: &x509.MSPIdentityDeserializer{}, issuerDeserializer: &x509.MSPIdentityDeserializer{}, - ownerDeserializer: htlc.NewDeserializer(identity.NewRawOwnerIdentityDeserializer(&x509.MSPIdentityDeserializer{})), + ownerDeserializer: interop.NewDeserializer(owner.NewTypedIdentityDeserializer(&x509.MSPIdentityDeserializer{})), } } @@ -80,7 +80,7 @@ func (e *EnrollmentService) GetEnrollmentID(auditInfo []byte) (string, error) { } // Try to unmarshal it as ScriptInfo - si := &htlc.ScriptInfo{} + si := &interop.ScriptInfo{} err := json.Unmarshal(auditInfo, si) if err == nil && (len(si.Sender) != 0 || len(si.Recipient) != 0) { if len(si.Recipient) != 0 { @@ -108,7 +108,7 @@ func (e *EnrollmentService) GetRevocationHandler(auditInfo []byte) (string, erro } // Try to unmarshal it as ScriptInfo - si := &htlc.ScriptInfo{} + si := &interop.ScriptInfo{} err := json.Unmarshal(auditInfo, si) if err == nil && (len(si.Sender) != 0 || len(si.Recipient) != 0) { if len(si.Recipient) != 0 { diff --git a/token/core/fabtoken/driver/driver.go b/token/core/fabtoken/driver/driver.go index 7d52c7802..33b457a7c 100644 --- a/token/core/fabtoken/driver/driver.go +++ b/token/core/fabtoken/driver/driver.go @@ -7,22 +7,42 @@ SPDX-License-Identifier: Apache-2.0 package driver import ( + fabric2 "github.com/hyperledger-labs/fabric-smart-client/platform/fabric" + weaver2 "github.com/hyperledger-labs/fabric-smart-client/platform/fabric/services/weaver" "github.com/hyperledger-labs/fabric-smart-client/platform/view" "github.com/hyperledger-labs/fabric-smart-client/platform/view/services/kvs" "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/config" "github.com/hyperledger-labs/fabric-token-sdk/token/core/fabtoken" + fabric3 "github.com/hyperledger-labs/fabric-token-sdk/token/core/fabtoken/driver/state/fabric" "github.com/hyperledger-labs/fabric-token-sdk/token/core/fabtoken/ppm" "github.com/hyperledger-labs/fabric-token-sdk/token/core/identity" "github.com/hyperledger-labs/fabric-token-sdk/token/core/identity/msp" "github.com/hyperledger-labs/fabric-token-sdk/token/core/identity/msp/common" + "github.com/hyperledger-labs/fabric-token-sdk/token/core/state/fabric" "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/network" "github.com/pkg/errors" ) -type Driver struct { +type Driver struct{} + +func (d *Driver) NewStateQueryExecutor(sp driver.ServiceProvider, url string) (driver.StateQueryExecutor, error) { + return fabric3.NewStateQueryExecutor(weaver2.GetProvider(sp), url, fabric2.GetDefaultFNS(sp)) +} + +func (d *Driver) NewStateVerifier(sp driver.ServiceProvider, url string) (driver.StateVerifier, error) { + return fabric3.NewStateVerifier( + weaver2.GetProvider(sp), + pledge.Vault(sp), + func(id string) *fabric2.NetworkService { + return fabric2.GetFabricNetworkService(sp, id) + }, + url, + fabric2.GetDefaultFNS(sp), + ) } func (d *Driver) PublicParametersFromBytes(params []byte) (driver.PublicParameters, error) { @@ -208,5 +228,7 @@ func (d *Driver) NewWalletService(sp view.ServiceProvider, networkID string, cha } func init() { - core.Register(fabtoken.PublicParameters, &Driver{}) + d := &Driver{} + core.Register(fabtoken.PublicParameters, d) + fabric.RegisterStateDriver(fabtoken.PublicParameters, d) } diff --git a/token/core/fabtoken/driver/state/fabric/state.go b/token/core/fabtoken/driver/state/fabric/state.go new file mode 100644 index 000000000..e36866217 --- /dev/null +++ b/token/core/fabtoken/driver/state/fabric/state.go @@ -0,0 +1,386 @@ +/* +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-smart-client/platform/fabric" + weaver2 "github.com/hyperledger-labs/fabric-smart-client/platform/fabric/services/weaver" + "github.com/hyperledger-labs/fabric-smart-client/platform/view/services/flogging" + fabric2 "github.com/hyperledger-labs/fabric-token-sdk/token/core/state/fabric" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/interop/pledge" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/network/fabric/tcc" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/owner" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/vault/keys" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/vault/translator" + "github.com/hyperledger-labs/fabric-token-sdk/token/token" + "github.com/pkg/errors" +) + +var logger = flogging.MustGetLogger("token-sdk.fabtoken") + +type RelayProvider interface { + Relay(fns *fabric.NetworkService) *weaver2.Relay +} + +type PledgeVault interface { + PledgeByTokenID(tokenID *token.ID) ([]*pledge.Info, error) +} + +type GetFabricNetworkServiceFunc = func(string) *fabric.NetworkService + +type StateQueryExecutor struct { + RelayProvider RelayProvider + TargetNetworkURL string + RelaySelector *fabric.NetworkService +} + +func NewStateQueryExecutor(relayProvider RelayProvider, targetNetworkURL string, relaySelector *fabric.NetworkService) (*StateQueryExecutor, error) { + if err := fabric2.CheckFabricScheme(targetNetworkURL); err != nil { + return nil, err + } + return &StateQueryExecutor{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 + 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(pledge.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 + 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(pledge.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 + } + + // Get local relay + relay := p.RelayProvider.Relay(p.RelaySelector) + + // Query + 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(pledge.TokenExistsError, "%s", err) + default: + return nil, err + } + } + + return res.Proof() +} + +type StateVerifier struct { + RelayProvider RelayProvider + NetworkURL string + RelaySelector *fabric.NetworkService + PledgeVault PledgeVault + GetFabricNetworkService GetFabricNetworkServiceFunc +} + +func NewStateVerifier(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{ + 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 := pledge.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 + logger.Debugf("verify proof of existence for token id [%s]", tokenID) + pledges, err := v.PledgeVault.PledgeByTokenID(tokenID) + if err != nil { + 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 { + 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] + 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 := owner.UnmarshallTypedIdentity(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 := pledge.FabricURLToTMSID(origin) + if err != nil { + return errors.Wrapf(err, "failed to parse network url") + } + // get local relay + relay := v.RelayProvider.Relay(v.GetFabricNetworkService(tokenOriginNetworkTMSID.Network)) + + // 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 := pledge.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 != pledge.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 := pledge.FabricURLToTMSID(origin) + if err != nil { + return errors.Wrapf(err, "failed to parse network url") + } + + // get local relay + relay := v.RelayProvider.Relay(v.GetFabricNetworkService(tokenOriginNetworkTMSID.Network)) + + // 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 := pledge.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 != pledge.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 +} diff --git a/token/core/fabtoken/issuer.go b/token/core/fabtoken/issuer.go index 6e6351a19..32bf9a717 100644 --- a/token/core/fabtoken/issuer.go +++ b/token/core/fabtoken/issuer.go @@ -7,6 +7,9 @@ SPDX-License-Identifier: Apache-2.0 package fabtoken import ( + "encoding/json" + + "github.com/hyperledger-labs/fabric-smart-client/platform/view/services/hash" "github.com/hyperledger-labs/fabric-smart-client/platform/view/view" "github.com/hyperledger-labs/fabric-token-sdk/token/driver" token2 "github.com/hyperledger-labs/fabric-token-sdk/token/token" @@ -56,12 +59,49 @@ func (s *Service) Issue(issuerIdentity view.Identity, tokenType string, values [ metas = append(metas, metaRaw) } - meta := &driver.IssueMetadata{ + md, err := getIssueActionMetadata(opts) + if err != nil { + return nil, nil, errors.Wrapf(err, "failed getting issue action metadata") + } + + issueAction := &IssueAction{ + Issuer: issuerIdentity, + Outputs: outs, + Metadata: md, + } + issueMetadata := &driver.IssueMetadata{ Issuer: issuerIdentity, TokenInfo: metas, } + return issueAction, issueMetadata, nil +} - return &IssueAction{Issuer: issuerIdentity, Outputs: outs}, meta, nil +func getIssueActionMetadata(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.(*token2.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) + } + return map[string][]byte{key: marshalled, key + "proof_of_claim": proof}, nil + } + return nil, nil } // VerifyIssue checks if the outputs of an IssueAction match the passed tokenInfos diff --git a/token/core/fabtoken/sender.go b/token/core/fabtoken/sender.go index b8b9e6232..35f188d65 100644 --- a/token/core/fabtoken/sender.go +++ b/token/core/fabtoken/sender.go @@ -9,16 +9,18 @@ package fabtoken import ( "github.com/hyperledger-labs/fabric-smart-client/platform/view/view" "github.com/hyperledger-labs/fabric-token-sdk/token/core/common" - "github.com/hyperledger-labs/fabric-token-sdk/token/core/identity" - "github.com/hyperledger-labs/fabric-token-sdk/token/core/interop/htlc" "github.com/hyperledger-labs/fabric-token-sdk/token/driver" - token2 "github.com/hyperledger-labs/fabric-token-sdk/token/token" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/interop" + "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/owner" + "github.com/hyperledger-labs/fabric-token-sdk/token/token" "github.com/pkg/errors" ) // Transfer returns a TransferAction as a function of the passed arguments // It also returns the corresponding TransferMetadata -func (s *Service) Transfer(txID string, wallet driver.OwnerWallet, ids []*token2.ID, Outputs []*token2.Token, opts *driver.TransferOptions) (driver.TransferAction, *driver.TransferMetadata, error) { +func (s *Service) Transfer(txID string, wallet driver.OwnerWallet, ids []*token.ID, Outputs []*token.Token, opts *driver.TransferOptions) (driver.TransferAction, *driver.TransferMetadata, error) { // select inputs inputIDs, inputTokens, err := s.TokenLoader.GetTokens(ids) if err != nil { @@ -65,24 +67,32 @@ func (s *Service) Transfer(txID string, wallet driver.OwnerWallet, ids []*token2 receivers = append(receivers, output.Output.Owner.Raw) continue } - owner, err := identity.UnmarshallRawOwner(output.Output.Owner.Raw) + identity, err := owner.UnmarshallTypedIdentity(output.Output.Owner.Raw) if err != nil { return nil, nil, errors.Wrap(err, "failed to unmarshal owner of input token") } - if owner.Type == identity.SerializedIdentityType { + if identity.Type == owner.SerializedIdentityType { receivers = append(receivers, output.Output.Owner.Raw) continue } - _, recipient, err := htlc.GetScriptSenderAndRecipient(owner) + _, recipient, issuer, err := interop.GetScriptSenderAndRecipient(identity) if err != nil { return nil, nil, errors.Wrap(err, "failed getting script sender and recipient") } - receivers = append(receivers, recipient) + if identity.Type == htlc.ScriptType { + receivers = append(receivers, recipient) + continue + } + if identity.Type == pledge.ScriptType { + receivers = append(receivers, issuer) + continue + } + return nil, nil, errors.Errorf("owner's type not recognized [%s]", identity.Type) } var senderAuditInfos [][]byte for _, t := range inputTokens { - auditInfo, err := htlc.GetOwnerAuditInfo(t.Owner.Raw, s) + auditInfo, err := interop.GetOwnerAuditInfo(t.Owner.Raw, s) if err != nil { return nil, nil, errors.Wrapf(err, "failed getting audit info for sender identity [%s]", view.Identity(t.Owner.Raw).String()) } @@ -91,9 +101,9 @@ func (s *Service) Transfer(txID string, wallet driver.OwnerWallet, ids []*token2 var receiverAuditInfos [][]byte for _, output := range outs { - auditInfo, err := htlc.GetOwnerAuditInfo(output.Output.Owner.Raw, s) + auditInfo, err := interop.GetOwnerAuditInfo(output.Output.Owner.Raw, s) if err != nil { - return nil, nil, errors.Wrapf(err, "failed getting audit info for recipient identity [%s]", view.Identity(output.Output.Owner.Raw).String()) + return nil, nil, errors.Wrapf(err, "failed getting audit info for receiver identity [%s]", view.Identity(output.Output.Owner.Raw).String()) } receiverAuditInfos = append(receiverAuditInfos, auditInfo) } diff --git a/token/core/fabtoken/validator.go b/token/core/fabtoken/validator.go index fda73bd2d..4142a065f 100644 --- a/token/core/fabtoken/validator.go +++ b/token/core/fabtoken/validator.go @@ -41,6 +41,7 @@ func NewValidator(pp *PublicParams, deserializer driver.Deserializer, extraValid TransferSignatureValidate, TransferBalanceValidate, TransferHTLCValidate, + TransferPledgeValidate, } transferValidators = append(transferValidators, extraValidators...) v := &Validator{ diff --git a/token/core/fabtoken/validator_pledge.go b/token/core/fabtoken/validator_pledge.go new file mode 100644 index 000000000..3dcd0d8c7 --- /dev/null +++ b/token/core/fabtoken/validator_pledge.go @@ -0,0 +1,137 @@ +/* +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/owner" + + "github.com/hyperledger-labs/fabric-token-sdk/token/services/interop/pledge" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/vault/keys" + "github.com/pkg/errors" +) + +func TransferPledgeValidate(ctx *Context) error { + logger.Debug("pledge validation starts") + for _, in := range ctx.InputTokens { + identity, err := owner.UnmarshallTypedIdentity(in.Owner.Raw) + if err != nil { + return errors.Wrap(err, "failed to unmarshal owner of input token") + } + if identity.Type == pledge.ScriptType { + if len(ctx.InputTokens) != 1 || len(ctx.Action.GetOutputs()) != 1 { + return errors.Errorf("invalid transfer action: a pledge script only transfers the ownership of a token") + } + out := ctx.Action.GetOutputs()[0].(*Output) + tok := out.Output + sender, err := owner.UnmarshallTypedIdentity(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.Action) + if err != nil { + return errors.Wrap(err, "failed constructing metadata key") + } + + if out.IsRedeem() { + redeemKey := pledge.RedeemPledgeKey + key + v, ok := ctx.Action.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.Action.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.Action.GetOutputs() { + out, ok := o.(*Output) + if !ok { + return errors.Errorf("invalid output") + } + if out.IsRedeem() { + continue + } + owner, err := owner.UnmarshallTypedIdentity(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.Action.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 53b750ea8..a301cd20b 100644 --- a/token/core/fabtoken/validator_transfer.go +++ b/token/core/fabtoken/validator_transfer.go @@ -11,10 +11,9 @@ import ( "time" "github.com/hyperledger-labs/fabric-smart-client/platform/view/view" - "github.com/hyperledger-labs/fabric-token-sdk/token/core/identity" - htlc2 "github.com/hyperledger-labs/fabric-token-sdk/token/core/interop/htlc" "github.com/hyperledger-labs/fabric-token-sdk/token/driver" "github.com/hyperledger-labs/fabric-token-sdk/token/services/interop/htlc" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/owner" "github.com/hyperledger-labs/fabric-token-sdk/token/token" "github.com/pkg/errors" ) @@ -108,7 +107,7 @@ func TransferHTLCValidate(ctx *Context) error { now := time.Now() for i, in := range ctx.InputTokens { - owner, err := identity.UnmarshallRawOwner(in.Owner.Raw) + owner, err := owner.UnmarshallTypedIdentity(in.Owner.Raw) if err != nil { return errors.Wrap(err, "failed to unmarshal owner of input token") } @@ -133,18 +132,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.Action, script, op, sigma) + metadataKey, err := htlc.MetadataClaimKeyCheck(ctx.Action, 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 it is an htlc script then the deadline must still be valid - owner, err := identity.UnmarshallRawOwner(out.Output.Owner.Raw) + owner, err := owner.UnmarshallTypedIdentity(out.Output.Owner.Raw) if err != nil { return err } @@ -173,7 +172,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.Action, script) + metadataKey, err := htlc.MetadataLockKeyCheck(ctx.Action, script) if err != nil { return errors.WithMessagef(err, "failed to check htlc metadata") } diff --git a/token/core/fabtoken/wallet.go b/token/core/fabtoken/wallet.go index 8d8bc416a..a2907d16b 100644 --- a/token/core/fabtoken/wallet.go +++ b/token/core/fabtoken/wallet.go @@ -13,6 +13,7 @@ import ( "github.com/hyperledger-labs/fabric-token-sdk/token/core/identity" "github.com/hyperledger-labs/fabric-token-sdk/token/core/identity/msp/common" "github.com/hyperledger-labs/fabric-token-sdk/token/driver" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/owner" "github.com/hyperledger-labs/fabric-token-sdk/token/services/vault/keys" "github.com/hyperledger-labs/fabric-token-sdk/token/token" "github.com/pkg/errors" @@ -74,11 +75,11 @@ func (s *WalletService) RegisterRecipientIdentity(data *driver.RecipientData) er } // match identity and audit info - recipient, err := identity.UnmarshallRawOwner(data.Identity) + recipient, err := owner.UnmarshallTypedIdentity(data.Identity) if err != nil { return errors.Wrapf(err, "failed to unmarshal identity [%s]", data.Identity) } - if recipient.Type != identity.SerializedIdentityType { + if recipient.Type != owner.SerializedIdentityType { return errors.Errorf("expected serialized identity type, got [%s]", recipient.Type) } err = matcher.Match(recipient.Identity) @@ -272,7 +273,7 @@ func (s *WalletService) SpentIDs(ids ...*token.ID) ([]string, error) { } func (s *WalletService) wrapWalletIdentity(id view.Identity) (view.Identity, error) { - raw, err := identity.MarshallRawOwner(&identity.RawOwner{Type: identity.SerializedIdentityType, Identity: id}) + raw, err := owner.MarshallTypedIdentity(&owner.TypedIdentity{Type: owner.SerializedIdentityType, Identity: id}) if err != nil { return nil, err } diff --git a/token/core/identity/owner.go b/token/core/identity/owner.go deleted file mode 100644 index 65bf68527..000000000 --- a/token/core/identity/owner.go +++ /dev/null @@ -1,76 +0,0 @@ -/* -Copyright IBM Corp. All Rights Reserved. - -SPDX-License-Identifier: Apache-2.0 -*/ - -package identity - -import ( - "encoding/asn1" - - "github.com/hyperledger-labs/fabric-smart-client/platform/view/view" - "github.com/pkg/errors" - - "github.com/hyperledger-labs/fabric-token-sdk/token/driver" -) - -const ( - SerializedIdentityType = "si" -) - -// RawOwner encodes an owner of an identity -type RawOwner struct { - // Type encodes the type of the owner (currently it can only be a SerializedIdentity) - Type string `protobuf:"bytes,1,opt,name=type,json=type,proto3" json:"type,omitempty"` - // Identity encodes the identity - Identity []byte `protobuf:"bytes,2,opt,name=identity,proto3" json:"identity,omitempty"` -} - -func UnmarshallRawOwner(id view.Identity) (*RawOwner, error) { - si := &RawOwner{} - _, err := asn1.Unmarshal(id, si) - if err != nil { - return nil, errors.Wrap(err, "failed to unmarshal to RawOwner") - } - return si, nil -} - -func MarshallRawOwner(o *RawOwner) (view.Identity, error) { - raw, err := asn1.Marshal(*o) - if err != nil { - return nil, errors.Wrap(err, "failed to unmarshal to RawOwner") - } - return raw, nil -} - -type DeserializeVerifierProvider interface { - DeserializeVerifier(id view.Identity) (driver.Verifier, error) -} - -// RawOwnerIdentityDeserializer takes as MSP identity and returns an ECDSA verifier -type RawOwnerIdentityDeserializer struct { - DeserializeVerifierProvider -} - -func NewRawOwnerIdentityDeserializer(dvp DeserializeVerifierProvider) *RawOwnerIdentityDeserializer { - return &RawOwnerIdentityDeserializer{ - DeserializeVerifierProvider: dvp, - } -} - -func (deserializer *RawOwnerIdentityDeserializer) DeserializeVerifier(id view.Identity) (driver.Verifier, error) { - si, err := UnmarshallRawOwner(id) - if err != nil { - return nil, errors.Wrap(err, "failed to unmarshal to RawOwner") - } - return deserializer.DeserializeVerifierProvider.DeserializeVerifier(si.Identity) -} - -func (deserializer *RawOwnerIdentityDeserializer) DeserializeSigner(raw []byte) (driver.Signer, error) { - return nil, errors.Errorf("signer deserialization not supported") -} - -func (deserializer *RawOwnerIdentityDeserializer) Info(raw []byte, auditInfo []byte) (string, error) { - return "info not supported", nil -} diff --git a/token/core/identity/provider.go b/token/core/identity/provider.go index 7b3d86dc1..f56a593c9 100644 --- a/token/core/identity/provider.go +++ b/token/core/identity/provider.go @@ -14,6 +14,7 @@ import ( "github.com/hyperledger-labs/fabric-smart-client/platform/view/services/flogging" "github.com/hyperledger-labs/fabric-smart-client/platform/view/view" "github.com/hyperledger-labs/fabric-token-sdk/token/driver" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/owner" "github.com/pkg/errors" "go.uber.org/zap/zapcore" ) @@ -158,8 +159,8 @@ func (p *Provider) GetSigner(identity view.Identity) (driver.Signer, error) { // give it a second chance - // is the identity wrapped in RawOwner? - ro, err2 := UnmarshallRawOwner(identity) + // is the identity wrapped in TypedIdentity? + ro, err2 := owner.UnmarshallTypedIdentity(identity) if err2 != nil { // No signer, err := p.tryDeserialization(identity) diff --git a/token/core/interop/htlc/info.go b/token/core/interop/htlc/info.go deleted file mode 100644 index 0ad50b5da..000000000 --- a/token/core/interop/htlc/info.go +++ /dev/null @@ -1,88 +0,0 @@ -/* -Copyright IBM Corp. All Rights Reserved. - -SPDX-License-Identifier: Apache-2.0 -*/ - -package htlc - -import ( - "encoding/json" - - "github.com/hyperledger-labs/fabric-smart-client/platform/view/view" - "github.com/hyperledger-labs/fabric-token-sdk/token/core/identity" - "github.com/hyperledger-labs/fabric-token-sdk/token/services/interop/htlc" - "github.com/pkg/errors" -) - -type AuditInfoProvider interface { - GetAuditInfo(identity view.Identity) ([]byte, error) -} - -// GetOwnerAuditInfo returns the audit info of the owner -func GetOwnerAuditInfo(raw []byte, s AuditInfoProvider) ([]byte, error) { - if len(raw) == 0 { - // this is a redeem - return nil, nil - } - - owner, err := identity.UnmarshallRawOwner(raw) - if err != nil { - return nil, errors.Wrap(err, "failed to unmarshal owner of input token") - } - if owner.Type == identity.SerializedIdentityType { - auditInfo, err := s.GetAuditInfo(raw) - if err != nil { - return nil, errors.Wrapf(err, "failed getting audit info for recipient identity [%s]", view.Identity(raw).String()) - } - return auditInfo, nil - } - - sender, recipient, err := GetScriptSenderAndRecipient(owner) - if err != nil { - return nil, errors.Wrapf(err, "failed getting script sender and recipient") - } - - auditInfo := &ScriptInfo{} - auditInfo.Sender, err = s.GetAuditInfo(sender) - if err != nil { - return nil, errors.Wrapf(err, "failed getting audit info for htlc script [%s]", view.Identity(raw).String()) - } - - auditInfo.Recipient, err = s.GetAuditInfo(recipient) - if err != nil { - return nil, errors.Wrapf(err, "failed getting audit info for script [%s]", view.Identity(raw).String()) - } - raw, err = json.Marshal(auditInfo) - if err != nil { - return nil, errors.Wrapf(err, "failed marshaling audit info for script") - } - return raw, nil -} - -// 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) Unarshal(raw []byte) error { - return json.Unmarshal(raw, si) -} - -// GetScriptSenderAndRecipient returns the script's sender and recipient according to the type of the given owner -func GetScriptSenderAndRecipient(ro *identity.RawOwner) (sender, recipient view.Identity, err error) { - if ro.Type == htlc.ScriptType { - script := &htlc.Script{} - err = json.Unmarshal(ro.Identity, script) - if err != nil { - return nil, nil, errors.Wrapf(err, "failed to unmarshal htlc script") - } - return script.Sender, script.Recipient, nil - } - return nil, nil, errors.New("unknown identity type") -} diff --git a/token/core/state.go b/token/core/state.go new file mode 100644 index 000000000..514183ebb --- /dev/null +++ b/token/core/state.go @@ -0,0 +1,94 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package core + +import ( + url2 "net/url" + "sync" + + "github.com/hyperledger-labs/fabric-smart-client/platform/view" + "github.com/hyperledger-labs/fabric-token-sdk/token/driver" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/interop/fabric" + "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 +} + +type StateServiceProvider struct { + sp view.ServiceProvider + + sspsMu sync.RWMutex + ssps map[string]driver.StateServiceProvider +} + +func NewStateServiceProvider(sp view.ServiceProvider) *StateServiceProvider { + return &StateServiceProvider{ + sp: sp, + ssps: map[string]driver.StateServiceProvider{}, + } +} + +func (p *StateServiceProvider) 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 *StateServiceProvider) 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 *StateServiceProvider) URLToTMSID(url string) (driver.TMSID, error) { + return fabric.FabricURLToTMSID(url) +} + +func (p *StateServiceProvider) 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 +} diff --git a/token/core/state/fabric/driver.go b/token/core/state/fabric/driver.go new file mode 100644 index 000000000..2004bbc00 --- /dev/null +++ b/token/core/state/fabric/driver.go @@ -0,0 +1,40 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package fabric + +import ( + "sync" + + "github.com/hyperledger-labs/fabric-token-sdk/token/driver" +) + +type TokenSDKStateDriver interface { + // NewStateQueryExecutor returns a new StateQueryExecutor for the given URL + NewStateQueryExecutor(sp driver.ServiceProvider, url string) (driver.StateQueryExecutor, error) + // NewStateVerifier returns a new StateVerifier for the given url + NewStateVerifier(sp driver.ServiceProvider, url string) (driver.StateVerifier, error) +} + +var ( + driversMu sync.RWMutex + drivers = make(map[string]TokenSDKStateDriver) +) + +// 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 TokenSDKStateDriver) { + 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/core/state/fabric/ssp.go b/token/core/state/fabric/ssp.go new file mode 100644 index 000000000..e8e92f09c --- /dev/null +++ b/token/core/state/fabric/ssp.go @@ -0,0 +1,166 @@ +/* +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" + weaver2 "github.com/hyperledger-labs/fabric-smart-client/platform/fabric/services/weaver" + "github.com/hyperledger-labs/fabric-smart-client/platform/view/services/flogging" + "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/driver" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/interop/pledge" + "github.com/pkg/errors" +) + +const ( + QueryPublicParamsFunction = "queryPublicParams" +) + +var logger = flogging.MustGetLogger("token-sdk.state") + +type StateServiceProvider struct { + sp driver.ServiceProvider + mu sync.RWMutex + queryExecutors map[string]driver.StateQueryExecutor + verifiers map[string]driver.StateVerifier +} + +func NewStateServiceProvider(sp driver.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 := pledge.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) (driver.TMSID, error) { + //TODO implement me + panic("implement me") +} + +func (f *StateServiceProvider) fetchPublicParameters(url string) ([]byte, error) { + relay := weaver2.GetProvider(f.sp).Relay(fabric.GetDefaultFNS(f.sp)) + 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 driver.ServiceProvider) (driver.StateServiceProvider, error) { + return NewStateServiceProvider(sp), nil +} + +func init() { + core.RegisterSSPDriver("fabric", NewSSPDriver()) +} diff --git a/token/core/state/fabric/utils.go b/token/core/state/fabric/utils.go new file mode 100644 index 000000000..205cbff17 --- /dev/null +++ b/token/core/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/core/zkatdlog/crypto/audit/auditor.go b/token/core/zkatdlog/crypto/audit/auditor.go index ca9586f47..1c7af8fab 100644 --- a/token/core/zkatdlog/crypto/audit/auditor.go +++ b/token/core/zkatdlog/crypto/audit/auditor.go @@ -13,13 +13,15 @@ import ( math "github.com/IBM/mathlib" "github.com/hyperledger-labs/fabric-smart-client/platform/view/services/flogging" "github.com/hyperledger-labs/fabric-smart-client/platform/view/services/hash" - "github.com/hyperledger-labs/fabric-token-sdk/token/core/identity" - "github.com/hyperledger-labs/fabric-token-sdk/token/core/interop/htlc" "github.com/hyperledger-labs/fabric-token-sdk/token/core/zkatdlog/crypto/common" issue2 "github.com/hyperledger-labs/fabric-token-sdk/token/core/zkatdlog/crypto/issue" "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/driver" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/interop" + "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/owner" "github.com/pkg/errors" ) @@ -256,11 +258,11 @@ func InspectTokenOwner(des Deserializer, token *AuditableToken, index int) error if len(token.Owner.OwnerInfo) == 0 { return errors.Errorf("failed to inspect owner at index [%d]: owner info is nil", index) } - ro, err := identity.UnmarshallRawOwner(token.Token.Owner) + ro, err := owner.UnmarshallTypedIdentity(token.Token.Owner) if err != nil { return errors.Errorf("owner at index [%d] cannot be unwrapped", index) } - if ro.Type == identity.SerializedIdentityType { + if ro.Type == owner.SerializedIdentityType { matcher, err := des.GetOwnerMatcher(token.Owner.OwnerInfo) if err != nil { return errors.Errorf("failed to get owner matcher for output [%d]", index) @@ -274,41 +276,68 @@ func InspectTokenOwner(des Deserializer, token *AuditableToken, index int) error } func inspectTokenOwnerOfScript(des Deserializer, token *AuditableToken, index int) error { - owner, err := identity.UnmarshallRawOwner(token.Token.Owner) + identity, err := owner.UnmarshallTypedIdentity(token.Token.Owner) if err != nil { return errors.Errorf("input owner at index [%d] cannot be unmarshalled", index) } - scriptInf := &htlc.ScriptInfo{} + scriptInf := &interop.ScriptInfo{} if err := json.Unmarshal(token.Owner.OwnerInfo, scriptInf); err != nil { return errors.Wrapf(err, "failed to unmarshal script info") } - scriptSender, scriptRecipient, err := htlc.GetScriptSenderAndRecipient(owner) + scriptSender, scriptRecipient, scriptIssuer, err := interop.GetScriptSenderAndRecipient(identity) 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 unmarshal audit info from script sender [%s]", string(scriptInf.Sender)) - } - ro, err := identity.UnmarshallRawOwner(scriptSender) - if err != nil { - return errors.Wrapf(err, "failed to retrieve raw owner from sender in 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)) - } + if identity.Type == htlc.ScriptType { + sender, err := des.GetOwnerMatcher(scriptInf.Sender) + if err != nil { + return errors.Wrapf(err, "failed to get owner matcher from htlc script sender [%s]", string(scriptInf.Sender)) + } + ro, err := owner.UnmarshallTypedIdentity(scriptSender) + if err != nil { + return errors.Wrapf(err, "failed to retrieve raw owner from sender in htlc 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)) + } - recipient, err := des.GetOwnerMatcher(scriptInf.Recipient) - if err != nil { - return errors.Wrapf(err, "failed to unmarshal audit info from script recipient [%s]", string(scriptInf.Recipient)) - } - ro, err = identity.UnmarshallRawOwner(scriptRecipient) - if err != nil { - return errors.Wrapf(err, "failed to retrieve raw owner from recipien in script") + recipient, err := des.GetOwnerMatcher(scriptInf.Recipient) + if err != nil { + return errors.Wrapf(err, "failed to unmarshal audit info from script recipient [%s]", string(scriptInf.Recipient)) + } + ro, err = owner.UnmarshallTypedIdentity(scriptRecipient) + if err != nil { + return errors.Wrapf(err, "failed to retrieve raw owner from recipien in script") + } + if err := recipient.Match(ro.Identity); err != nil { + return errors.Wrapf(err, "token at index [%d] does not match the provided opening [%s]", index, string(scriptInf.Recipient)) + } } - if err := recipient.Match(ro.Identity); err != nil { - return errors.Wrapf(err, "token at index [%d] does not match the provided opening [%s]", index, string(scriptInf.Recipient)) + + if identity.Type == pledge.ScriptType { + 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 := owner.UnmarshallTypedIdentity(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 := owner.UnmarshallTypedIdentity(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 diff --git a/token/core/zkatdlog/crypto/audit/auditor_test.go b/token/core/zkatdlog/crypto/audit/auditor_test.go index d843f8b5f..d59b0ab55 100644 --- a/token/core/zkatdlog/crypto/audit/auditor_test.go +++ b/token/core/zkatdlog/crypto/audit/auditor_test.go @@ -18,7 +18,6 @@ import ( "github.com/hyperledger-labs/fabric-smart-client/platform/view/services/kvs" registry2 "github.com/hyperledger-labs/fabric-smart-client/platform/view/services/registry" "github.com/hyperledger-labs/fabric-smart-client/platform/view/view" - "github.com/hyperledger-labs/fabric-token-sdk/token/core/identity" "github.com/hyperledger-labs/fabric-token-sdk/token/core/identity/msp/idemix" "github.com/hyperledger-labs/fabric-token-sdk/token/core/zkatdlog/crypto" "github.com/hyperledger-labs/fabric-token-sdk/token/core/zkatdlog/crypto/audit" @@ -27,6 +26,7 @@ import ( "github.com/hyperledger-labs/fabric-token-sdk/token/core/zkatdlog/crypto/token" transfer2 "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/owner" msp2 "github.com/hyperledger/fabric/msp" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" @@ -272,7 +272,7 @@ func getIdemixInfo(dir string) (view.Identity, *idemix2.AuditInfo) { err = auditInfo.Match(id) Expect(err).NotTo(HaveOccurred()) - id, err = identity.MarshallRawOwner(&identity.RawOwner{Identity: id, Type: identity.SerializedIdentityType}) + id, err = owner.MarshallTypedIdentity(&owner.TypedIdentity{Identity: id, Type: owner.SerializedIdentityType}) Expect(err).NotTo(HaveOccurred()) return id, auditInfo diff --git a/token/core/zkatdlog/crypto/issue/issue.go b/token/core/zkatdlog/crypto/issue/issue.go index d25bfc8f4..b5731ce34 100644 --- a/token/core/zkatdlog/crypto/issue/issue.go +++ b/token/core/zkatdlog/crypto/issue/issue.go @@ -14,9 +14,17 @@ import ( rp "github.com/hyperledger-labs/fabric-token-sdk/token/core/zkatdlog/crypto/range" "github.com/hyperledger-labs/fabric-token-sdk/token/core/zkatdlog/crypto/token" "github.com/hyperledger-labs/fabric-token-sdk/token/driver" + token2 "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 *token2.ID + // OriginNetwork is the network where the pledge took place + OriginNetwork string +} + // IssueAction specifies an issue of one or more tokens type IssueAction struct { // Identity of issuer diff --git a/token/core/zkatdlog/crypto/validator/validator.go b/token/core/zkatdlog/crypto/validator/validator.go index 4a25d4da6..adf058092 100644 --- a/token/core/zkatdlog/crypto/validator/validator.go +++ b/token/core/zkatdlog/crypto/validator/validator.go @@ -33,6 +33,7 @@ func New(pp *crypto.PublicParams, deserializer driver.Deserializer, extraValidat TransferSignatureValidate, TransferZKProofValidate, TransferHTLCValidate, + TransferPledgeValidate, } transferValidators = append(transferValidators, extraValidators...) return &Validator{ 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 000000000..946f45912 --- /dev/null +++ b/token/core/zkatdlog/crypto/validator/validator_pledge.go @@ -0,0 +1,136 @@ +/* +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/interop/pledge" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/owner" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/vault/keys" + "github.com/pkg/errors" +) + +func TransferPledgeValidate(ctx *Context) error { + for _, in := range ctx.InputTokens { + identity, err := owner.UnmarshallTypedIdentity(in.Owner) + if err != nil { + return errors.Wrap(err, "failed to unmarshal owner of input token") + } + if identity.Type == pledge.ScriptType { + if len(ctx.InputTokens) != 1 || len(ctx.Action.GetOutputs()) != 1 { + return errors.Errorf("invalid transfer action: a pledge script only transfers the ownership of a token") + } + out := ctx.Action.GetOutputs()[0].(*token.Token) + sender, err := owner.UnmarshallTypedIdentity(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.Action) + if err != nil { + return errors.Wrap(err, "failed constructing metadata key") + } + + if out.IsRedeem() { + redeemKey := pledge.RedeemPledgeKey + key + v, ok := ctx.Action.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.Action.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.Action.GetOutputs() { + out, ok := o.(*token.Token) + if !ok { + return errors.Errorf("invalid output") + } + if out.IsRedeem() { + continue + } + owner, err := owner.UnmarshallTypedIdentity(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.Action.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_test.go b/token/core/zkatdlog/crypto/validator/validator_test.go index 15e1716e7..17e65e839 100644 --- a/token/core/zkatdlog/crypto/validator/validator_test.go +++ b/token/core/zkatdlog/crypto/validator/validator_test.go @@ -19,7 +19,6 @@ import ( "github.com/hyperledger-labs/fabric-smart-client/platform/view/services/kvs" registry2 "github.com/hyperledger-labs/fabric-smart-client/platform/view/services/registry" "github.com/hyperledger-labs/fabric-smart-client/platform/view/view" - "github.com/hyperledger-labs/fabric-token-sdk/token/core/identity" "github.com/hyperledger-labs/fabric-token-sdk/token/core/identity/msp/idemix" "github.com/hyperledger-labs/fabric-token-sdk/token/core/zkatdlog/crypto" "github.com/hyperledger-labs/fabric-token-sdk/token/core/zkatdlog/crypto/audit" @@ -32,6 +31,7 @@ import ( "github.com/hyperledger-labs/fabric-token-sdk/token/core/zkatdlog/crypto/validator/mock" zkatdlog "github.com/hyperledger-labs/fabric-token-sdk/token/core/zkatdlog/nogh" "github.com/hyperledger-labs/fabric-token-sdk/token/driver" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/owner" msp2 "github.com/hyperledger/fabric/msp" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" @@ -411,7 +411,7 @@ func getIdemixInfo(dir string) (view.Identity, *idemix2.AuditInfo, driver.Signin signer, err := p.DeserializeSigningIdentity(id) Expect(err).NotTo(HaveOccurred()) - id, err = identity.MarshallRawOwner(&identity.RawOwner{Identity: id, Type: identity.SerializedIdentityType}) + id, err = owner.MarshallTypedIdentity(&owner.TypedIdentity{Identity: id, Type: owner.SerializedIdentityType}) Expect(err).NotTo(HaveOccurred()) return id, auditInfo, signer diff --git a/token/core/zkatdlog/crypto/validator/validator_transfer.go b/token/core/zkatdlog/crypto/validator/validator_transfer.go index d3fabe49d..891dcdde4 100644 --- a/token/core/zkatdlog/crypto/validator/validator_transfer.go +++ b/token/core/zkatdlog/crypto/validator/validator_transfer.go @@ -12,13 +12,12 @@ import ( math "github.com/IBM/mathlib" "github.com/hyperledger-labs/fabric-smart-client/platform/view/view" - "github.com/hyperledger-labs/fabric-token-sdk/token/core/identity" - htlc2 "github.com/hyperledger-labs/fabric-token-sdk/token/core/interop/htlc" "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/core/zkatdlog/crypto/transfer" "github.com/hyperledger-labs/fabric-token-sdk/token/driver" "github.com/hyperledger-labs/fabric-token-sdk/token/services/interop/htlc" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/owner" "github.com/pkg/errors" ) @@ -101,7 +100,7 @@ func TransferHTLCValidate(ctx *Context) error { now := time.Now() for i, in := range ctx.InputTokens { - owner, err := identity.UnmarshallRawOwner(in.Owner) + owner, err := owner.UnmarshallTypedIdentity(in.Owner) if err != nil { return errors.Wrap(err, "failed to unmarshal owner of input token") } @@ -113,18 +112,18 @@ func TransferHTLCValidate(ctx *Context) error { out := ctx.Action.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.Action, script, op, sigma) + metadataKey, err := htlc.MetadataClaimKeyCheck(ctx.Action, 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) } } @@ -138,7 +137,7 @@ func TransferHTLCValidate(ctx *Context) error { if out.IsRedeem() { continue } - owner, err := identity.UnmarshallRawOwner(out.Owner) + owner, err := owner.UnmarshallTypedIdentity(out.Owner) if err != nil { return err } @@ -151,7 +150,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.Action, script) + metadataKey, err := htlc.MetadataLockKeyCheck(ctx.Action, script) if err != nil { return errors.WithMessagef(err, "failed to check htlc metadata") } diff --git a/token/core/zkatdlog/nogh/deserializer.go b/token/core/zkatdlog/nogh/deserializer.go index d51ee78d0..8fde50f56 100644 --- a/token/core/zkatdlog/nogh/deserializer.go +++ b/token/core/zkatdlog/nogh/deserializer.go @@ -14,12 +14,12 @@ import ( idemix2 "github.com/hyperledger-labs/fabric-smart-client/platform/fabric/core/generic/msp/idemix" "github.com/hyperledger-labs/fabric-smart-client/platform/view/view" - "github.com/hyperledger-labs/fabric-token-sdk/token/core/identity" "github.com/hyperledger-labs/fabric-token-sdk/token/core/identity/msp/idemix" "github.com/hyperledger-labs/fabric-token-sdk/token/core/identity/msp/x509" - "github.com/hyperledger-labs/fabric-token-sdk/token/core/interop/htlc" "github.com/hyperledger-labs/fabric-token-sdk/token/core/zkatdlog/crypto" "github.com/hyperledger-labs/fabric-token-sdk/token/driver" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/interop" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/owner" "github.com/pkg/errors" ) @@ -56,7 +56,7 @@ func NewDeserializer(pp *crypto.PublicParams) (*Deserializer, error) { return &Deserializer{ auditorDeserializer: &x509.MSPIdentityDeserializer{}, issuerDeserializer: &x509.MSPIdentityDeserializer{}, - ownerDeserializer: htlc.NewDeserializer(identity.NewRawOwnerIdentityDeserializer(idemixDes)), + ownerDeserializer: interop.NewDeserializer(owner.NewTypedIdentityDeserializer(idemixDes)), auditDeserializer: idemixDes, }, nil } @@ -155,7 +155,7 @@ func (e *EnrollmentService) getAuditInfo(auditInfo []byte) (*idemix2.AuditInfo, } // Try to unmarshal it as ScriptInfo - si := &htlc.ScriptInfo{} + si := &interop.ScriptInfo{} err := json.Unmarshal(auditInfo, si) if err == nil && (len(si.Sender) != 0 || len(si.Recipient) != 0) { if len(si.Recipient) != 0 { diff --git a/token/core/zkatdlog/nogh/driver/driver.go b/token/core/zkatdlog/nogh/driver/driver.go index af0878240..b6e46a5ab 100644 --- a/token/core/zkatdlog/nogh/driver/driver.go +++ b/token/core/zkatdlog/nogh/driver/driver.go @@ -10,6 +10,8 @@ import ( "time" math "github.com/IBM/mathlib" + fabric2 "github.com/hyperledger-labs/fabric-smart-client/platform/fabric" + weaver2 "github.com/hyperledger-labs/fabric-smart-client/platform/fabric/services/weaver" "github.com/hyperledger-labs/fabric-smart-client/platform/view" "github.com/hyperledger-labs/fabric-smart-client/platform/view/services/kvs" "github.com/hyperledger-labs/fabric-token-sdk/token" @@ -18,16 +20,34 @@ import ( "github.com/hyperledger-labs/fabric-token-sdk/token/core/identity" "github.com/hyperledger-labs/fabric-token-sdk/token/core/identity/msp" "github.com/hyperledger-labs/fabric-token-sdk/token/core/identity/msp/common" + "github.com/hyperledger-labs/fabric-token-sdk/token/core/state/fabric" "github.com/hyperledger-labs/fabric-token-sdk/token/core/zkatdlog/crypto" "github.com/hyperledger-labs/fabric-token-sdk/token/core/zkatdlog/crypto/ppm" "github.com/hyperledger-labs/fabric-token-sdk/token/core/zkatdlog/crypto/validator" zkatdlog "github.com/hyperledger-labs/fabric-token-sdk/token/core/zkatdlog/nogh" + fabric3 "github.com/hyperledger-labs/fabric-token-sdk/token/core/zkatdlog/nogh/driver/state/fabric" "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/network" "github.com/pkg/errors" ) -type Driver struct { +type Driver struct{} + +func (d *Driver) NewStateQueryExecutor(sp driver.ServiceProvider, url string) (driver.StateQueryExecutor, error) { + return fabric3.NewStateQueryExecutor(weaver2.GetProvider(sp), url, fabric2.GetDefaultFNS(sp)) +} + +func (d *Driver) NewStateVerifier(sp driver.ServiceProvider, url string) (driver.StateVerifier, error) { + return fabric3.NewStateVerifier( + weaver2.GetProvider(sp), + pledge.Vault(sp), + func(id string) *fabric2.NetworkService { + return fabric2.GetFabricNetworkService(sp, id) + }, + url, + fabric2.GetDefaultFNS(sp), + ) } func (d *Driver) PublicParametersFromBytes(params []byte) (driver.PublicParameters, error) { @@ -240,5 +260,7 @@ func (d *Driver) NewWalletService(sp view.ServiceProvider, networkID string, cha } func init() { - core.Register(crypto.DLogPublicParameters, &Driver{}) + d := &Driver{} + core.Register(crypto.DLogPublicParameters, d) + fabric.RegisterStateDriver(crypto.DLogPublicParameters, d) } diff --git a/token/core/zkatdlog/nogh/driver/state/fabric/state.go b/token/core/zkatdlog/nogh/driver/state/fabric/state.go new file mode 100644 index 000000000..95a31de21 --- /dev/null +++ b/token/core/zkatdlog/nogh/driver/state/fabric/state.go @@ -0,0 +1,331 @@ +/* +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-smart-client/platform/fabric" + weaver2 "github.com/hyperledger-labs/fabric-smart-client/platform/fabric/services/weaver" + "github.com/hyperledger-labs/fabric-smart-client/platform/view/services/flogging" + fabric2 "github.com/hyperledger-labs/fabric-token-sdk/token/core/state/fabric" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/interop/pledge" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/network/fabric/tcc" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/vault/keys" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/vault/translator" + "github.com/hyperledger-labs/fabric-token-sdk/token/token" + "github.com/pkg/errors" +) + +var logger = flogging.MustGetLogger("token-sdk.driver.zkatdlog") + +type RelayProvider interface { + Relay(fns *fabric.NetworkService) *weaver2.Relay +} + +type PledgeVault interface { + PledgeByTokenID(tokenID *token.ID) ([]*pledge.Info, error) +} + +type GetFabricNetworkServiceFunc = func(string) *fabric.NetworkService + +type StateQueryExecutor struct { + RelayProvider RelayProvider + TargetNetworkURL string + RelaySelector *fabric.NetworkService +} + +func NewStateQueryExecutor(RelayProvider RelayProvider, targetNetworkURL string, relaySelector *fabric.NetworkService) (*StateQueryExecutor, error) { + if err := fabric2.CheckFabricScheme(targetNetworkURL); err != nil { + return nil, err + } + return &StateQueryExecutor{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) + 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(pledge.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) + + 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(pledge.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) + + 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(pledge.TokenExistsError, "%s", err) + default: + return nil, err + } + } + + return res.Proof() +} + +type StateVerifier struct { + RelayProvider RelayProvider + NetworkURL string + RelaySelector *fabric.NetworkService + PledgeVault PledgeVault + GetFabricNetworkService GetFabricNetworkServiceFunc +} + +func NewStateVerifier(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{ + 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 := pledge.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 + logger.Debugf("verify proof of existence for token id [%s]", tokenID) + pledges, err := v.PledgeVault.PledgeByTokenID(tokenID) + if err != nil { + 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 { + 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] + 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 := pledge.FabricURLToTMSID(origin) + if err != nil { + return errors.Wrapf(err, "failed to parse network url") + } + relay := v.RelayProvider.Relay(v.GetFabricNetworkService(tokenOriginNetworkTMSID.Network)) + 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 := pledge.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 != pledge.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 := pledge.FabricURLToTMSID(origin) + if err != nil { + return errors.Wrapf(err, "failed to parse network url") + } + relay := v.RelayProvider.Relay(v.GetFabricNetworkService(tokenOriginNetworkTMSID.Network)) + 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 := pledge.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 != pledge.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 +} diff --git a/token/core/zkatdlog/nogh/issuer.go b/token/core/zkatdlog/nogh/issuer.go index ad92e942f..d7d7a75c9 100644 --- a/token/core/zkatdlog/nogh/issuer.go +++ b/token/core/zkatdlog/nogh/issuer.go @@ -7,11 +7,15 @@ SPDX-License-Identifier: Apache-2.0 package nogh import ( + "encoding/json" + + "github.com/hyperledger-labs/fabric-smart-client/platform/view/services/hash" "github.com/hyperledger-labs/fabric-smart-client/platform/view/view" "github.com/hyperledger-labs/fabric-token-sdk/token/core/zkatdlog/crypto/common" "github.com/hyperledger-labs/fabric-token-sdk/token/core/zkatdlog/crypto/issue" "github.com/hyperledger-labs/fabric-token-sdk/token/core/zkatdlog/crypto/issue/nonanonym" "github.com/hyperledger-labs/fabric-token-sdk/token/driver" + token2 "github.com/hyperledger-labs/fabric-token-sdk/token/token" "github.com/pkg/errors" ) @@ -64,6 +68,12 @@ func (s *Service) Issue(issuerIdentity view.Identity, tokenType string, values [ return nil, nil, err } + md, err := getIssueActionMetadata(opts) + if err != nil { + return nil, nil, errors.Wrapf(err, "failed getting issue action metadata") + } + issue.Metadata = md + meta := &driver.IssueMetadata{ Issuer: issuerSerializedIdentity, TokenInfo: outputMetadataRaw, @@ -71,6 +81,34 @@ func (s *Service) Issue(issuerIdentity view.Identity, tokenType string, values [ return issue, meta, err } +func getIssueActionMetadata(opts *driver.IssueOptions) (map[string][]byte, error) { + var metadata *issue.Metadata + 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 = &issue.Metadata{ + OriginTokenID: tokenID.(*token2.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) + } + return map[string][]byte{key: marshalled, key + "proof_of_claim": proof}, nil + } + return nil, nil +} + // VerifyIssue checks if the outputs of an IssueAction match the passed metadata func (s *Service) VerifyIssue(ia driver.IssueAction, outputsMetadata [][]byte) error { if ia == nil { diff --git a/token/core/zkatdlog/nogh/logger.go b/token/core/zkatdlog/nogh/logger.go index f5b1ed463..faed0412c 100644 --- a/token/core/zkatdlog/nogh/logger.go +++ b/token/core/zkatdlog/nogh/logger.go @@ -3,6 +3,7 @@ Copyright IBM Corp. All Rights Reserved. SPDX-License-Identifier: Apache-2.0 */ + package nogh import "github.com/hyperledger-labs/fabric-smart-client/platform/view/services/flogging" diff --git a/token/core/zkatdlog/nogh/sender.go b/token/core/zkatdlog/nogh/sender.go index f37bac4b3..a17e863e1 100644 --- a/token/core/zkatdlog/nogh/sender.go +++ b/token/core/zkatdlog/nogh/sender.go @@ -10,11 +10,13 @@ import ( math "github.com/IBM/mathlib" "github.com/hyperledger-labs/fabric-smart-client/platform/view/view" "github.com/hyperledger-labs/fabric-token-sdk/token/core/common" - "github.com/hyperledger-labs/fabric-token-sdk/token/core/identity" - "github.com/hyperledger-labs/fabric-token-sdk/token/core/interop/htlc" "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/driver" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/interop" + "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/owner" token3 "github.com/hyperledger-labs/fabric-token-sdk/token/token" "github.com/pkg/errors" ) @@ -65,19 +67,27 @@ func (s *Service) Transfer(txID string, wallet driver.OwnerWallet, ids []*token3 ownerIdentities = append(ownerIdentities, output.Owner.Raw) continue } - owner, err := identity.UnmarshallRawOwner(output.Owner.Raw) + identity, err := owner.UnmarshallTypedIdentity(output.Owner.Raw) if err != nil { return nil, nil, errors.Wrap(err, "failed to unmarshal owner of the output token") } - if owner.Type == identity.SerializedIdentityType { + if identity.Type == owner.SerializedIdentityType { ownerIdentities = append(ownerIdentities, output.Owner.Raw) continue } - _, recipient, err := htlc.GetScriptSenderAndRecipient(owner) + _, recipient, issuer, err := interop.GetScriptSenderAndRecipient(identity) if err != nil { return nil, nil, errors.Wrap(err, "failed getting script sender and recipient") } - ownerIdentities = append(ownerIdentities, recipient) + if identity.Type == htlc.ScriptType { + ownerIdentities = append(ownerIdentities, recipient) + continue + } + if identity.Type == pledge.ScriptType { + ownerIdentities = append(ownerIdentities, issuer) + continue + } + return nil, nil, errors.Errorf("owner's type not recognized [%s]", identity.Type) } // produce zkatdlog transfer action // return for each output its information in the clear @@ -101,7 +111,7 @@ func (s *Service) Transfer(txID string, wallet driver.OwnerWallet, ids []*token3 // audit info for receivers var receiverAuditInfos [][]byte for _, output := range outputTokens { - auditInfo, err := htlc.GetOwnerAuditInfo(output.Owner.Raw, s) + auditInfo, err := interop.GetOwnerAuditInfo(output.Owner.Raw, s) if err != nil { return nil, nil, errors.Wrapf(err, "failed getting audit info for recipient identity [%s]", view.Identity(output.Owner.Raw).String()) } @@ -111,7 +121,7 @@ func (s *Service) Transfer(txID string, wallet driver.OwnerWallet, ids []*token3 // audit info for senders var senderAuditInfos [][]byte for i, t := range tokens { - auditInfo, err := htlc.GetOwnerAuditInfo(t.Owner, s) + auditInfo, err := interop.GetOwnerAuditInfo(t.Owner, s) if err != nil { return nil, nil, errors.Wrapf(err, "failed getting audit info for sender identity [%s]", view.Identity(t.Owner).String()) } diff --git a/token/core/zkatdlog/nogh/wallet.go b/token/core/zkatdlog/nogh/wallet.go index ab336868a..d0c66ce8e 100644 --- a/token/core/zkatdlog/nogh/wallet.go +++ b/token/core/zkatdlog/nogh/wallet.go @@ -15,6 +15,7 @@ import ( "github.com/hyperledger-labs/fabric-token-sdk/token/core/identity/msp/idemix" "github.com/hyperledger-labs/fabric-token-sdk/token/driver" "github.com/hyperledger-labs/fabric-token-sdk/token/driver/config" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/owner" "github.com/hyperledger-labs/fabric-token-sdk/token/services/vault/keys" "github.com/hyperledger-labs/fabric-token-sdk/token/token" "github.com/pkg/errors" @@ -102,11 +103,11 @@ func (s *WalletService) RegisterRecipientIdentity(data *driver.RecipientData) er } // match identity and audit info - recipient, err := identity.UnmarshallRawOwner(data.Identity) + recipient, err := owner.UnmarshallTypedIdentity(data.Identity) if err != nil { return errors.Wrapf(err, "failed to unmarshal identity [%s]", data.Identity) } - if recipient.Type != identity.SerializedIdentityType { + if recipient.Type != owner.SerializedIdentityType { return errors.Errorf("expected serialized identity type, got [%s]", recipient.Type) } err = matcher.Match(recipient.Identity) @@ -267,7 +268,7 @@ func (s *WalletService) SpentIDs(ids ...*token.ID) ([]string, error) { } func (s *WalletService) wrapWalletIdentity(id view.Identity) (view.Identity, error) { - raw, err := identity.MarshallRawOwner(&identity.RawOwner{Type: identity.SerializedIdentityType, Identity: id}) + raw, err := owner.MarshallTypedIdentity(&owner.TypedIdentity{Type: owner.SerializedIdentityType, Identity: id}) if err != nil { return nil, err } diff --git a/token/driver/state.go b/token/driver/state.go new file mode 100644 index 000000000..958a43d1b --- /dev/null +++ b/token/driver/state.go @@ -0,0 +1,59 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package driver + +import ( + "time" + + "github.com/hyperledger-labs/fabric-token-sdk/token/token" +) + +// 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) +} + +// 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) (TMSID, error) +} + +// SSPDriver models a driver fatcory for state-related services +type SSPDriver interface { + // New returns an instance of a state service provider + New(sp ServiceProvider) (StateServiceProvider, error) +} diff --git a/token/driver/tms.go b/token/driver/tms.go index 2ee6ede0c..767190804 100644 --- a/token/driver/tms.go +++ b/token/driver/tms.go @@ -6,7 +6,27 @@ SPDX-License-Identifier: Apache-2.0 package driver -import "github.com/hyperledger-labs/fabric-token-sdk/token/driver/config" +import ( + "fmt" + + "github.com/hyperledger-labs/fabric-token-sdk/token/driver/config" +) + +// 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 { diff --git a/token/sdk/sdk.go b/token/sdk/sdk.go index 6ba948f4e..bbd6ba0b4 100644 --- a/token/sdk/sdk.go +++ b/token/sdk/sdk.go @@ -26,6 +26,7 @@ import ( "github.com/hyperledger-labs/fabric-token-sdk/token/services/auditor" _ "github.com/hyperledger-labs/fabric-token-sdk/token/services/certifier/dummy" _ "github.com/hyperledger-labs/fabric-token-sdk/token/services/certifier/interactive" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/interop/pledge" "github.com/hyperledger-labs/fabric-token-sdk/token/services/network" _ "github.com/hyperledger-labs/fabric-token-sdk/token/services/network/fabric/driver" "github.com/hyperledger-labs/fabric-token-sdk/token/services/network/orion" @@ -108,6 +109,11 @@ func (p *SDK) Install() error { p.ownerManager = owner.NewManager(p.registry, kvs.GetService(p.registry)) assert.NoError(p.registry.RegisterService(p.ownerManager)) + // State Service Provider + provider, err := pledge.NewProvider(tms2.NewStateServiceProvider(p.registry)) + assert.NoError(err, "failed instantiating interoperability prover provider") + assert.NoError(p.registry.RegisterService(provider), "failed registering interoperability prover provider") + enabled, err := orion.IsCustodian(view2.GetConfigService(p.registry)) assert.NoError(err, "failed to get custodian status") logger.Infof("Orion Custodian enabled: %t", enabled) diff --git a/token/sdk/tms/tms.go b/token/sdk/tms/tms.go index 6621348bb..f1487c278 100644 --- a/token/sdk/tms/tms.go +++ b/token/sdk/tms/tms.go @@ -17,6 +17,7 @@ import ( "github.com/hyperledger-labs/fabric-token-sdk/token/driver" network2 "github.com/hyperledger-labs/fabric-token-sdk/token/sdk/network" "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/network" fabric2 "github.com/hyperledger-labs/fabric-token-sdk/token/services/network/fabric" orion2 "github.com/hyperledger-labs/fabric-token-sdk/token/services/network/orion" @@ -71,7 +72,7 @@ func (p *PostInitializer) PostInit(tms driver.TokenManagerService, networkID, ch ons, namespace, p.sp, - network2.NewAuthorizationMultiplexer(&network2.TMSAuthorization{}, &htlc.ScriptOwnership{}), + network2.NewAuthorizationMultiplexer(&network2.TMSAuthorization{}, &htlc.ScriptOwnership{}, &pledge.ScriptOwnership{}), network2.NewIssuedMultiplexer(&network2.WalletIssued{}), tokenStore, ), @@ -105,7 +106,7 @@ func (p *PostInitializer) PostInit(tms driver.TokenManagerService, networkID, ch n, namespace, p.sp, - network2.NewAuthorizationMultiplexer(&network2.TMSAuthorization{}, &htlc.ScriptOwnership{}), + network2.NewAuthorizationMultiplexer(&network2.TMSAuthorization{}, &htlc.ScriptOwnership{}, &pledge.ScriptOwnership{}), network2.NewIssuedMultiplexer(&network2.WalletIssued{}), tokenStore, ), diff --git a/token/services/interop/audit.go b/token/services/interop/audit.go new file mode 100644 index 000000000..a409e6926 --- /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/interop/htlc" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/interop/pledge" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/owner" + "github.com/pkg/errors" +) + +type Input struct { + *token.Input + isHTLC bool + isPledge bool +} + +func ToInput(i *token.Input) (*Input, error) { + owner, err := owner.UnmarshallTypedIdentity(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 := owner.UnmarshallTypedIdentity(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 := owner.UnmarshallTypedIdentity(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 := owner.UnmarshallTypedIdentity(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 := owner.UnmarshallTypedIdentity(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 := owner.UnmarshallTypedIdentity(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/core/interop/htlc/deserializer.go b/token/services/interop/deserializer.go similarity index 54% rename from token/core/interop/htlc/deserializer.go rename to token/services/interop/deserializer.go index 51c6a24d1..f1638d511 100644 --- a/token/core/interop/htlc/deserializer.go +++ b/token/services/interop/deserializer.go @@ -4,15 +4,16 @@ Copyright IBM Corp. All Rights Reserved. SPDX-License-Identifier: Apache-2.0 */ -package htlc +package interop import ( "encoding/json" "github.com/hyperledger-labs/fabric-smart-client/platform/view/view" - "github.com/hyperledger-labs/fabric-token-sdk/token/core/identity" "github.com/hyperledger-labs/fabric-token-sdk/token/driver" "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/owner" "github.com/pkg/errors" ) @@ -29,24 +30,47 @@ func NewDeserializer(ownerDeserializer VerifierDES) *Deserializer { } func (d *Deserializer) DeserializeVerifier(id view.Identity) (driver.Verifier, error) { - si, err := identity.UnmarshallRawOwner(id) + si, err := owner.UnmarshallTypedIdentity(id) if err != nil { - return nil, errors.Wrap(err, "failed to unmarshal RawOwner") + return nil, errors.Wrap(err, "failed to unmarshal TypedIdentity") } - if si.Type == identity.SerializedIdentityType { + + switch t := si.Type; t { + case owner.SerializedIdentityType: return d.OwnerDeserializer.DeserializeVerifier(id) - } - if si.Type == htlc.ScriptType { + case pledge.ScriptType: + return d.getPledgeVerifier(si.Identity) + case htlc.ScriptType: return d.getHTLCVerifier(si.Identity) + default: + return nil, errors.Errorf("failed to deserialize TypedIdentity: Unknown owner type %s", t) + } +} + +func (d *Deserializer) getPledgeVerifier(raw []byte) (driver.Verifier, error) { + script := &pledge.Script{} + err := json.Unmarshal(raw, script) + if err != nil { + return nil, errors.Errorf("failed to unmarshal TypedIdentity as a pledge script") } - return nil, errors.Errorf("failed to deserialize RawOwner: Unknown owner type %s", si.Type) + v := &pledge.Verifier{} + v.Sender, err = d.OwnerDeserializer.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 = d.OwnerDeserializer.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 (d *Deserializer) getHTLCVerifier(raw []byte) (driver.Verifier, error) { script := &htlc.Script{} err := json.Unmarshal(raw, script) if err != nil { - return nil, errors.Errorf("failed to unmarshal RawOwner as an htlc script") + return nil, errors.Errorf("failed to unmarshal TypedIdentity as an htlc script") } v := &htlc.Verifier{} v.Sender, err = d.OwnerDeserializer.DeserializeVerifier(script.Sender) diff --git a/token/services/interop/fabric/url.go b/token/services/interop/fabric/url.go new file mode 100644 index 000000000..5553bea49 --- /dev/null +++ b/token/services/interop/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/driver" + "github.com/pkg/errors" +) + +func FabricURL(tms driver.TMSID) string { + return fmt.Sprintf("fabric://%s.%s.%s/", tms.Network, tms.Channel, tms.Namespace) +} + +func FabricURLToTMSID(url string) (driver.TMSID, error) { + u, err := url2.Parse(url) + if err != nil { + return driver.TMSID{}, errors.Wrapf(err, "failed parsing url") + } + if u.Scheme != "fabric" { + return driver.TMSID{}, errors.Errorf("invalid scheme, expected fabric, got [%s]", u.Scheme) + } + + res := strings.Split(u.Host, ".") + if len(res) != 3 { + return driver.TMSID{}, errors.Errorf("invalid host, expected 3 components, found [%d,%v]", len(res), res) + } + return driver.TMSID{ + Network: res[0], Channel: res[1], Namespace: res[2], + }, nil +} diff --git a/token/services/interop/htlc/audit.go b/token/services/interop/htlc/audit.go deleted file mode 100644 index f599d6f66..000000000 --- a/token/services/interop/htlc/audit.go +++ /dev/null @@ -1,89 +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/core/identity" - "github.com/pkg/errors" -) - -type Input struct { - *token.Input - isHTLC bool -} - -func ToInput(i *token.Input) (*Input, error) { - owner, err := identity.UnmarshallRawOwner(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.UnmarshallRawOwner(i.Owner) - if err != nil { - return nil, errors.Wrapf(err, "failed to unmarshal owner") - } - 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.UnmarshallRawOwner(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.UnmarshallRawOwner(o.Owner) - if err != nil { - return nil, errors.Wrapf(err, "failed to unmarshal owner") - } - 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/interop/htlc/finality.go b/token/services/interop/htlc/finality.go index 9566d7e6c..f7a72fbc8 100644 --- a/token/services/interop/htlc/finality.go +++ b/token/services/interop/htlc/finality.go @@ -7,8 +7,6 @@ SPDX-License-Identifier: Apache-2.0 package htlc import ( - "time" - "github.com/hyperledger-labs/fabric-smart-client/platform/view/view" "github.com/hyperledger-labs/fabric-token-sdk/token/services/ttx" ) @@ -17,8 +15,3 @@ import ( func NewFinalityView(tx *Transaction) view.View { return ttx.NewFinalityView(tx.Transaction) } - -// NewFinalityWithTimeoutView returns an instance of the ttx FinalityView with timeout -func NewFinalityWithTimeoutView(tx *Transaction, timeout time.Duration) view.View { - return ttx.NewFinalityWithTimeoutView(tx.Transaction, timeout) -} diff --git a/token/services/interop/htlc/logger.go b/token/services/interop/htlc/logger.go index 880d2451b..2e0fe2695 100644 --- a/token/services/interop/htlc/logger.go +++ b/token/services/interop/htlc/logger.go @@ -8,4 +8,4 @@ package htlc import "github.com/hyperledger-labs/fabric-smart-client/platform/view/services/flogging" -var logger = flogging.MustGetLogger("token-sdk.htlc") +var logger = flogging.MustGetLogger("token-sdk.services.htlc") diff --git a/token/services/interop/htlc/ordering.go b/token/services/interop/htlc/ordering.go index 4d7baf07a..63e72199b 100644 --- a/token/services/interop/htlc/ordering.go +++ b/token/services/interop/htlc/ordering.go @@ -7,8 +7,6 @@ SPDX-License-Identifier: Apache-2.0 package htlc import ( - "time" - "github.com/hyperledger-labs/fabric-smart-client/platform/view/view" "github.com/hyperledger-labs/fabric-token-sdk/token/services/ttx" ) @@ -17,8 +15,3 @@ import ( func NewOrderingAndFinalityView(tx *Transaction) view.View { return ttx.NewOrderingAndFinalityView(tx.Transaction) } - -// NewOrderingAndFinalityWithTimeoutView returns a new instance of the ttx orderingAndFinalityWithTimeoutView struct -func NewOrderingAndFinalityWithTimeoutView(tx *Transaction, timeout time.Duration) view.View { - return ttx.NewOrderingAndFinalityWithTimeoutView(tx.Transaction, timeout) -} diff --git a/token/services/interop/htlc/script.go b/token/services/interop/htlc/script.go index 5d9060581..d448aa51c 100644 --- a/token/services/interop/htlc/script.go +++ b/token/services/interop/htlc/script.go @@ -14,12 +14,14 @@ import ( "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/core/identity" "github.com/hyperledger-labs/fabric-token-sdk/token/services/interop/encoding" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/owner" token3 "github.com/hyperledger-labs/fabric-token-sdk/token/token" "github.com/pkg/errors" ) +const ScriptType = "htlc" // htlc script + // HashInfo contains the information regarding the hashing type HashInfo struct { Hash []byte @@ -99,7 +101,7 @@ func (s *ScriptOwnership) AmIAnAuditor(tms *token.ManagementService) bool { // IsMine returns true if one is either a sender or a recipient of an htlc script func (s *ScriptOwnership) IsMine(tms *token.ManagementService, tok *token3.Token) ([]string, bool) { - owner, err := identity.UnmarshallRawOwner(tok.Owner.Raw) + owner, err := owner.UnmarshallTypedIdentity(tok.Owner.Raw) if err != nil { logger.Debugf("Is Mine [%s,%s,%s]? No, failed unmarshalling [%s]", view.Identity(tok.Owner.Raw), tok.Type, tok.Quantity, err) return nil, false diff --git a/token/services/interop/htlc/stream.go b/token/services/interop/htlc/stream.go index de795404e..4acfc7733 100644 --- a/token/services/interop/htlc/stream.go +++ b/token/services/interop/htlc/stream.go @@ -9,8 +9,9 @@ package htlc import ( "encoding/json" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/owner" + "github.com/hyperledger-labs/fabric-token-sdk/token" - "github.com/hyperledger-labs/fabric-token-sdk/token/core/identity" ) // OutputStream models a stream over a set of outputs @@ -31,7 +32,7 @@ func (o *OutputStream) Filter(f func(t *token.Output) bool) *OutputStream { // ByScript filters the OutputStream to only include outputs that are owned by an htlc script func (o *OutputStream) ByScript() *OutputStream { return o.Filter(func(t *token.Output) bool { - owner, err := identity.UnmarshallRawOwner(t.Owner) + owner, err := owner.UnmarshallTypedIdentity(t.Owner) if err != nil { return false } @@ -46,7 +47,7 @@ func (o *OutputStream) ByScript() *OutputStream { // ScriptAt returns an htlc script that is the owner of the output at the passed index of the OutputStream func (o *OutputStream) ScriptAt(i int) *Script { tok := o.OutputStream.At(i) - owner, err := identity.UnmarshallRawOwner(tok.Owner) + owner, err := owner.UnmarshallTypedIdentity(tok.Owner) if err != nil { logger.Debugf("failed unmarshalling raw owner [%s]: [%s]", tok, err) return nil diff --git a/token/services/interop/htlc/transaction.go b/token/services/interop/htlc/transaction.go index e5c4cdb0a..9ee00d58c 100644 --- a/token/services/interop/htlc/transaction.go +++ b/token/services/interop/htlc/transaction.go @@ -16,17 +16,14 @@ 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" - "github.com/hyperledger-labs/fabric-token-sdk/token/core/identity" "github.com/hyperledger-labs/fabric-token-sdk/token/services/interop/encoding" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/owner" "github.com/hyperledger-labs/fabric-token-sdk/token/services/ttx" token2 "github.com/hyperledger-labs/fabric-token-sdk/token/token" "github.com/pkg/errors" ) -const ( - ScriptType = "htlc" // htlc script - defaultDeadlineOffset = time.Hour -) +const defaultDeadlineOffset = time.Hour // WithHash sets a hash attribute to be used to customize the transfer command func WithHash(hash []byte) token.TransferOption { @@ -76,17 +73,6 @@ type Transaction struct { *ttx.Transaction } -// NewTransaction returns a new token transaction customized with the passed opts that will be signed by the passed signer -func NewTransaction(sp view.Context, signer view.Identity, opts ...ttx.TxOption) (*Transaction, error) { - tx, err := ttx.NewTransaction(sp, signer, opts...) - if err != nil { - return nil, err - } - return &Transaction{ - Transaction: tx, - }, nil -} - // NewAnonymousTransaction returns a new anonymous token transaction customized with the passed opts func NewAnonymousTransaction(sp view.Context, opts ...ttx.TxOption) (*Transaction, error) { tx, err := ttx.NewAnonymousTransaction(sp, opts...) @@ -191,7 +177,7 @@ func (t *Transaction) Reclaim(wallet *token.OwnerWallet, tok *token2.UnspentToke if err != nil { return errors.Wrapf(err, "failed to convert quantity [%s]", tok.Quantity) } - owner, err := identity.UnmarshallRawOwner(tok.Owner.Raw) + owner, err := owner.UnmarshallTypedIdentity(tok.Owner.Raw) if err != nil { return err } @@ -201,7 +187,7 @@ func (t *Transaction) Reclaim(wallet *token.OwnerWallet, tok *token2.UnspentToke script := &Script{} err = json.Unmarshal(owner.Identity, script) if err != nil { - return errors.Errorf("failed to unmarshal RawOwner as an htlc script") + return errors.Errorf("failed to unmarshal TypedIdentity as an htlc script") } // Register the signer for the reclaim @@ -241,7 +227,7 @@ func (t *Transaction) Claim(wallet *token.OwnerWallet, tok *token2.UnspentToken, return errors.Wrapf(err, "failed to convert quantity [%s]", tok.Quantity) } - owner, err := identity.UnmarshallRawOwner(tok.Owner.Raw) + owner, err := owner.UnmarshallTypedIdentity(tok.Owner.Raw) if err != nil { return err } @@ -250,7 +236,7 @@ func (t *Transaction) Claim(wallet *token.OwnerWallet, tok *token2.UnspentToken, return errors.New("invalid owner type, expected htlc script") } if err := json.Unmarshal(owner.Identity, script); err != nil { - return errors.New("failed to unmarshal RawOwner as an htlc script") + return errors.New("failed to unmarshal TypedIdentity as an htlc script") } image, err := script.HashInfo.Image(preImage) @@ -345,11 +331,11 @@ func (t *Transaction) recipientAsScript(sender, recipient view.Identity, deadlin if err != nil { return nil, nil, nil, err } - ro := &identity.RawOwner{ + ro := &owner.TypedIdentity{ Type: ScriptType, Identity: rawScript, } - raw, err := identity.MarshallRawOwner(ro) + raw, err := owner.MarshallTypedIdentity(ro) if err != nil { return nil, nil, nil, err } diff --git a/token/core/interop/htlc/validator.go b/token/services/interop/htlc/validator.go similarity index 80% rename from token/core/interop/htlc/validator.go rename to token/services/interop/htlc/validator.go index 97d021764..5ae0da674 100644 --- a/token/core/interop/htlc/validator.go +++ b/token/services/interop/htlc/validator.go @@ -11,8 +11,7 @@ import ( "encoding/json" "time" - "github.com/hyperledger-labs/fabric-token-sdk/token/core/identity" - "github.com/hyperledger-labs/fabric-token-sdk/token/services/interop/htlc" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/owner" "github.com/pkg/errors" ) @@ -29,12 +28,12 @@ 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) { - sender, err := identity.UnmarshallRawOwner(senderRawOwner) +func VerifyOwner(senderRawOwner []byte, outRawOwner []byte, now time.Time) (*Script, OperationType, error) { + sender, err := owner.UnmarshallTypedIdentity(senderRawOwner) if err != nil { return nil, None, err } - script := &htlc.Script{} + script := &Script{} err = json.Unmarshal(sender.Identity, script) if err != nil { return nil, None, err @@ -56,14 +55,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)) } @@ -81,7 +80,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") @@ -94,17 +93,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/htlc/wallet.go b/token/services/interop/htlc/wallet.go index 1dbdd1943..525ba5f5d 100644 --- a/token/services/interop/htlc/wallet.go +++ b/token/services/interop/htlc/wallet.go @@ -12,9 +12,9 @@ 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" - "github.com/hyperledger-labs/fabric-token-sdk/token/core/identity" "github.com/hyperledger-labs/fabric-token-sdk/token/driver" "github.com/hyperledger-labs/fabric-token-sdk/token/services/network" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/owner" "github.com/hyperledger-labs/fabric-token-sdk/token/services/ttx" "github.com/hyperledger-labs/fabric-token-sdk/token/services/vault" token2 "github.com/hyperledger-labs/fabric-token-sdk/token/token" @@ -350,7 +350,7 @@ func (f *FilteredIterator) Next() (*token2.UnspentToken, error) { logger.Debugf("no more tokens!") return nil, nil } - owner, err := identity.UnmarshallRawOwner(tok.Owner.Raw) + owner, err := owner.UnmarshallTypedIdentity(tok.Owner.Raw) if err != nil { logger.Debugf("Is Mine [%s,%s,%s]? No, failed unmarshalling [%s]", view.Identity(tok.Owner.Raw), tok.Type, tok.Quantity, err) continue diff --git a/token/services/interop/info.go b/token/services/interop/info.go new file mode 100644 index 000000000..535dd77f8 --- /dev/null +++ b/token/services/interop/info.go @@ -0,0 +1,117 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package interop + +import ( + "encoding/json" + + "github.com/hyperledger-labs/fabric-smart-client/platform/view/view" + "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/owner" + "github.com/pkg/errors" +) + +type AuditInfoProvider interface { + GetAuditInfo(identity view.Identity) ([]byte, error) +} + +// GetOwnerAuditInfo returns the audit info of the owner +func GetOwnerAuditInfo(raw []byte, s AuditInfoProvider) ([]byte, error) { + if len(raw) == 0 { + // this is a redeem + return nil, nil + } + + identity, err := owner.UnmarshallTypedIdentity(raw) + if err != nil { + return nil, errors.Wrap(err, "failed to unmarshal owner") + } + if identity.Type == owner.SerializedIdentityType { + auditInfo, err := s.GetAuditInfo(raw) + if err != nil { + return nil, errors.Wrapf(err, "failed getting audit info for identity [%s]", view.Identity(raw).String()) + } + return auditInfo, nil + } + + sender, recipient, issuer, err := GetScriptSenderAndRecipient(identity) + if err != nil { + return nil, errors.Wrapf(err, "failed getting script sender and recipient") + } + + auditInfo := &ScriptInfo{} + + if identity.Type == htlc.ScriptType { + auditInfo.Sender, err = s.GetAuditInfo(sender) + if err != nil { + return nil, errors.Wrapf(err, "failed getting audit info for sender of htlc script [%s]", view.Identity(raw).String()) + } + + auditInfo.Recipient, err = s.GetAuditInfo(recipient) + if err != nil { + return nil, errors.Wrapf(err, "failed getting audit info for recipient of htlc script [%s]", view.Identity(raw).String()) + } + } + + if identity.Type == pledge.ScriptType { + auditInfo.Sender, err = s.GetAuditInfo(sender) + if err != nil { + return nil, errors.Wrapf(err, "failed getting audit info for sender of pledge script [%s]", view.Identity(raw).String()) + } + + 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 = s.GetAuditInfo(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()) + } + } + + // 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 = s.GetAuditInfo(issuer) + if err != nil { + return nil, errors.Wrapf(err, "failed getting audit info for issuer of pledge script [%s]", view.Identity(raw).String()) + } + } + + raw, err = json.Marshal(auditInfo) + if err != nil { + return nil, errors.Wrapf(err, "failed marshaling audit info for script") + } + return raw, nil +} + +// ScriptInfo includes info about the sender and the recipient +type ScriptInfo struct { + Sender []byte + Recipient []byte +} + +// GetScriptSenderAndRecipient returns the script's sender, recipient, and issuer, according to the type of the given owner +func GetScriptSenderAndRecipient(ro *owner.TypedIdentity) (sender, recipient, issuer view.Identity, err error) { + if ro.Type == htlc.ScriptType { + script := &htlc.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, nil, nil + } + if ro.Type == pledge.ScriptType { + script := &pledge.Script{} + err = json.Unmarshal(ro.Identity, script) + if err != nil { + return nil, nil, nil, errors.Wrapf(err, "failed to unmarshal pledge script") + } + return script.Sender, script.Recipient, script.Issuer, nil + } + return nil, nil, nil, errors.Errorf("owner's type not recognized [%s]", ro.Type) +} diff --git a/token/services/interop/pledge/accept.go b/token/services/interop/pledge/accept.go new file mode 100644 index 000000000..db6f7e999 --- /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 000000000..c3714ebb1 --- /dev/null +++ b/token/services/interop/pledge/approve.go @@ -0,0 +1,274 @@ +/* +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/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 := GetStateServiceProvider(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 000000000..ef5bd5ed8 --- /dev/null +++ b/token/services/interop/pledge/claim.go @@ -0,0 +1,255 @@ +/* +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/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().OwnerWalletByIdentity(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.CompileTxOption(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 := GetStateServiceProvider(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/distribute.go b/token/services/interop/pledge/distribute.go new file mode 100644 index 000000000..c80d4566a --- /dev/null +++ b/token/services/interop/pledge/distribute.go @@ -0,0 +1,161 @@ +/* +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) + + tokenType := output.Type + amount := output.Quantity.ToBigInt().Uint64() + + 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: tokenType, + Amount: amount, + 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 000000000..2ee411170 --- /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 000000000..eb340d73c --- /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 000000000..2b7d8287f --- /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 000000000..c3bf2f2a9 --- /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 000000000..109ec5d25 --- /dev/null +++ b/token/services/interop/pledge/pledge.go @@ -0,0 +1,93 @@ +/* +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/owner" + "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 := &owner.TypedIdentity{ + Type: ScriptType, + Identity: rawScript, + } + return owner.MarshallTypedIdentity(ro) +} + +// 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/provider.go b/token/services/interop/pledge/provider.go new file mode 100644 index 000000000..4f24ee802 --- /dev/null +++ b/token/services/interop/pledge/provider.go @@ -0,0 +1,122 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package pledge + +import ( + "time" + + token2 "github.com/hyperledger-labs/fabric-token-sdk/token" + "github.com/hyperledger-labs/fabric-token-sdk/token/driver" + "github.com/hyperledger-labs/fabric-token-sdk/token/token" + "github.com/pkg/errors" +) + +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") +) + +// 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) +} + +// StateQueryExecutor models a prover of token related states +type StateQueryExecutor struct { + p driver.StateQueryExecutor +} + +// Exist returns a proof that the passed token exists in the network this query executor targets +func (p *StateQueryExecutor) Exist(tokenID *token.ID) ([]byte, error) { + return p.p.Exist(tokenID) +} + +// DoesNotExist returns a proof that the passed token, originated in the given network, does not exist +// in the network this query executor targets +func (p *StateQueryExecutor) DoesNotExist(tokenID *token.ID, origin string, deadline time.Time) ([]byte, error) { + return p.p.DoesNotExist(tokenID, origin, deadline) +} + +// 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) { + return p.p.ExistsWithMetadata(tokenID, origin) +} + +// StateVerifier is used to verify proofs related to the state of tokens in a target network +type StateVerifier struct { + v driver.StateVerifier +} + +// VerifyProofExistence verifies that a proof of existence of the passed token in the target network is valid +func (v *StateVerifier) VerifyProofExistence(proof []byte, tokenID *token.ID, metadata []byte) error { + err := v.v.VerifyProofExistence(proof, tokenID, metadata) + logger.Debugf("verify proof of existence for token id [%s] with [%s]", tokenID, err) + if err != nil { + return errors.WithMessagef(err, "failed to verify proof of existence") + } + return err +} + +// VerifyProofNonExistence verifies that a proof of non-existence of the given token, +// originated in the given network, in the target network is valid +func (v *StateVerifier) VerifyProofNonExistence(proof []byte, tokenID *token.ID, origin string, deadline time.Time) error { + return v.v.VerifyProofNonExistence(proof, tokenID, origin, deadline) +} + +// 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(proof []byte, tokenID *token.ID, origin string) error { + return v.v.VerifyProofTokenWithMetadataExistence(proof, tokenID, origin) +} + +// StateServiceProvider manages state-related services +type StateServiceProvider struct { + p driver.StateServiceProvider +} + +func NewProvider(p driver.StateServiceProvider) (*StateServiceProvider, error) { + return &StateServiceProvider{p: p}, nil +} + +// QueryExecutor returns an instance of a query executor to requests proofs from the network identified by the passed url +func (p *StateServiceProvider) QueryExecutor(url string) (*StateQueryExecutor, error) { + prover, err := p.p.QueryExecutor(url) + if err != nil { + return nil, errors.WithMessagef(err, "failed getting prover for url [%s]", url) + } + return &StateQueryExecutor{ + p: prover, + }, nil +} + +// Verifier returns an instance of a verifier of proofs generated by the network identified by the passed url +func (p *StateServiceProvider) Verifier(url string) (*StateVerifier, error) { + verifier, err := p.p.Verifier(url) + if err != nil { + return nil, errors.WithMessagef(err, "failed getting verifier for url [%s]", url) + } + return &StateVerifier{ + v: verifier, + }, nil +} + +func (p *StateServiceProvider) URLToTMSID(url string) (token2.TMSID, error) { + return p.p.URLToTMSID(url) +} + +// GetStateServiceProvider returns an instance of a state service provider +func GetStateServiceProvider(sp ServiceProvider) (*StateServiceProvider, error) { + s, err := sp.GetService(&StateServiceProvider{}) + if err != nil { + return nil, errors.Wrap(err, "failed getting state service provider") + } + return s.(*StateServiceProvider), nil +} diff --git a/token/services/interop/pledge/recipients.go b/token/services/interop/pledge/recipients.go new file mode 100644 index 000000000..9455b2066 --- /dev/null +++ b/token/services/interop/pledge/recipients.go @@ -0,0 +1,178 @@ +/* +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/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().OwnerWalletByIdentity(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.Identity, recipientData.AuditInfo, recipientData.Metadata); 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 := GetStateServiceProvider(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, err + } + auditInfo, err := w.GetAuditInfo(recipientIdentity) + if err != nil { + return nil, err + } + metadata, err := w.GetTokenMetadata(recipientIdentity) + if err != nil { + return nil, err + } + + // Step 3: send the public key back to the invoker + err = session.Send(&RecipientData{ + Identity: recipientIdentity, + AuditInfo: auditInfo, + Metadata: 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 000000000..b71a20895 --- /dev/null +++ b/token/services/interop/pledge/reclaim.go @@ -0,0 +1,91 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package pledge + +import ( + "encoding/json" + "fmt" + + 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" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/owner" + 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 := owner.UnmarshallTypedIdentity(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 := view2.GetEndpointService(t.SP).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 000000000..a2629586a --- /dev/null +++ b/token/services/interop/pledge/redeem.go @@ -0,0 +1,88 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package pledge + +import ( + "encoding/json" + "fmt" + + view2 "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/owner" + 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 := owner.UnmarshallTypedIdentity(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 := view2.GetEndpointService(t.SP).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 000000000..e82729e93 --- /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, 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 000000000..8b88f4817 --- /dev/null +++ b/token/services/interop/pledge/script.go @@ -0,0 +1,48 @@ +/* +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() 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.Deadline.Before(time.Now()) { + return errors.New("invalid pledge script: deadline already elapsed") + } + 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 000000000..b51bd1d1a --- /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/driver" + "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 driver.Signer + // Issuer corresponds to the issuer in the origin network + // this is used during redeem + Issuer driver.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 driver.Verifier + // Issuer is the issuer in the pledge script + Issuer driver.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 000000000..f43b32f46 --- /dev/null +++ b/token/services/interop/pledge/state.go @@ -0,0 +1,206 @@ +/* +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" + 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 := GetStateServiceProvider(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 := GetStateServiceProvider(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 := GetStateServiceProvider(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 := GetStateServiceProvider(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 := GetStateServiceProvider(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, err + } + proof, ok := boxed.([]byte) + if !ok { + return nil, errors.Errorf("failed to collect proof of token existence") + } + + // verify proof before returning it + ssp, err := GetStateServiceProvider(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 000000000..b7a3cc51c --- /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 000000000..e4443f52e --- /dev/null +++ b/token/services/interop/pledge/stream.go @@ -0,0 +1,70 @@ +/* +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/view" + "github.com/hyperledger-labs/fabric-token-sdk/token" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/owner" +) + +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 := owner.UnmarshallTypedIdentity(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 := owner.UnmarshallTypedIdentity(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 000000000..4fed25720 --- /dev/null +++ b/token/services/interop/pledge/transaction.go @@ -0,0 +1,48 @@ +/* +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" +) + +// Transaction holds a ttx transaction +type Transaction struct { + *ttx.Transaction +} + +// NewAnonymousTransaction returns a new anonymous token transaction customized with the passed opts +func NewAnonymousTransaction(sp view.Context, opts ...ttx.TxOption) (*Transaction, error) { + tx, err := ttx.NewAnonymousTransaction(sp, opts...) + if err != nil { + return nil, err + } + return &Transaction{ + Transaction: tx, + }, 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, + }, 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/url.go b/token/services/interop/pledge/url.go new file mode 100644 index 000000000..7012c9fd4 --- /dev/null +++ b/token/services/interop/pledge/url.go @@ -0,0 +1,38 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package pledge + +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/pledge/wallet.go b/token/services/interop/pledge/wallet.go new file mode 100644 index 000000000..a91b530ca --- /dev/null +++ b/token/services/interop/pledge/wallet.go @@ -0,0 +1,206 @@ +/* +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/network" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/owner" + 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 { + 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, + } +} + +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, + } +} + +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", len(unspentTokens.Tokens)) + + var res []*token2.UnspentToken + var scripts []*Script + for _, tok := range unspentTokens.Tokens { + if tok.Id.String() == tokenID.String() { + owner, err := owner.UnmarshallTypedIdentity(tok.Owner.Raw) + if err != nil { + return nil, nil, errors.Wrapf(err, "failed to unmarshal owner") + } + 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 := owner.UnmarshallTypedIdentity(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 + for _, beneficiary := range []struct { + identity view2.Identity + desc string + }{ + { + identity: script.Sender, + desc: "sender", + }, { + identity: script.Recipient, + desc: "recipient", + }, { + identity: script.Issuer, + desc: "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().OwnerWalletByIdentity(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) + return []string{wallet.ID()}, true + } + } + + logger.Debugf("Is Mine [%s,%s,%s]? No", view2.Identity(tok.Owner.Raw), tok.Type, tok.Quantity) + + return nil, false +} diff --git a/token/services/network/driver/network.go b/token/services/network/driver/network.go index 3c54fc6f7..ca3df423a 100644 --- a/token/services/network/driver/network.go +++ b/token/services/network/driver/network.go @@ -117,4 +117,7 @@ type Network interface { // ProcessNamespace indicates to the commit pipeline to process all transaction in the passed namespace ProcessNamespace(namespace string) error + + // InteropURL returns the interoperability url for this network and the passed namespace + InteropURL(namespace string) string } diff --git a/token/services/network/fabric/network.go b/token/services/network/fabric/network.go index 04de64131..133013c5f 100644 --- a/token/services/network/fabric/network.go +++ b/token/services/network/fabric/network.go @@ -20,6 +20,7 @@ import ( view2 "github.com/hyperledger-labs/fabric-smart-client/platform/view" "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/network" "github.com/hyperledger-labs/fabric-token-sdk/token/services/network/driver" "github.com/hyperledger-labs/fabric-token-sdk/token/services/network/processor" @@ -432,3 +433,11 @@ func (n *Network) ProcessNamespace(namespace string) error { } return nil } + +func (n *Network) InteropURL(namespace string) string { + return pledge.FabricURL(token2.TMSID{ + Network: n.Name(), + Channel: n.Channel(), + Namespace: namespace, + }) +} diff --git a/token/services/network/fabric/tcc/tcc.go b/token/services/network/fabric/tcc/tcc.go index 0303739f5..fb4cc6970 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-smart-client/platform/view/services/flogging" "github.com/hyperledger-labs/fabric-token-sdk/token" @@ -28,8 +29,12 @@ var logger = flogging.MustGetLogger("token-sdk.tcc") const ( InvokeFunction = "invoke" QueryPublicParamsFunction = "queryPublicParams" - QueryTokensFunctions = "queryTokens" - AreTokensSpent = "areTokensSpent" + + QueryTokensFunctions = "queryTokens" + AreTokensSpent = "areTokensSpent" + ProofOfTokenExistenceQuery = "proof_of_token_existence" + ProofOfTokenNonExistenceQuery = "proof_of_token_non_existence" + ProofOfTokenMetadataExistenceQuery = "proof_of_token_metadata_existence" PublicParamsPathVarEnv = "PUBLIC_PARAMS_FILE_PATH" ) @@ -58,6 +63,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 @@ -127,6 +143,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)) } @@ -298,3 +329,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 30bca97f9..b597b59e9 100644 --- a/token/services/network/network.go +++ b/token/services/network/network.go @@ -467,6 +467,10 @@ func (n *Network) ProcessNamespace(namespace string) error { return n.n.ProcessNamespace(namespace) } +func (n *Network) InteropURL(namespace string) string { + return n.n.InteropURL(namespace) +} + // Provider returns an instance of network provider type Provider struct { sp view2.ServiceProvider diff --git a/token/services/network/orion/network.go b/token/services/network/orion/network.go index 1114ec95e..d06e663f3 100644 --- a/token/services/network/orion/network.go +++ b/token/services/network/orion/network.go @@ -258,6 +258,10 @@ func (n *Network) ProcessNamespace(namespace string) error { return nil } +func (n *Network) InteropURL(namespace string) string { + panic("not supported") +} + type nv struct { v *orion.Vault tokenVault *vault.Vault diff --git a/token/services/owner/identity.go b/token/services/owner/identity.go new file mode 100644 index 000000000..338993a5c --- /dev/null +++ b/token/services/owner/identity.go @@ -0,0 +1,75 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package owner + +import ( + "encoding/asn1" + + "github.com/hyperledger-labs/fabric-smart-client/platform/view/view" + "github.com/hyperledger-labs/fabric-token-sdk/token/driver" + "github.com/pkg/errors" +) + +const ( + SerializedIdentityType = "si" +) + +// TypedIdentity encodes an owner of an identity +type TypedIdentity struct { + // Type encodes the type of the owner (currently it can only be a SerializedIdentity) + Type string `protobuf:"bytes,1,opt,name=type,json=type,proto3" json:"type,omitempty"` + // Identity encodes the identity + Identity []byte `protobuf:"bytes,2,opt,name=identity,proto3" json:"identity,omitempty"` +} + +func UnmarshallTypedIdentity(id view.Identity) (*TypedIdentity, error) { + si := &TypedIdentity{} + _, err := asn1.Unmarshal(id, si) + if err != nil { + return nil, errors.Wrap(err, "failed to unmarshal to TypedIdentity") + } + return si, nil +} + +func MarshallTypedIdentity(o *TypedIdentity) (view.Identity, error) { + raw, err := asn1.Marshal(*o) + if err != nil { + return nil, errors.Wrap(err, "failed to unmarshal to TypedIdentity") + } + return raw, nil +} + +type DeserializeVerifierProvider interface { + DeserializeVerifier(id view.Identity) (driver.Verifier, error) +} + +// TypedIdentityDeserializer takes as MSP identity and returns an ECDSA verifier +type TypedIdentityDeserializer struct { + DeserializeVerifierProvider +} + +func NewTypedIdentityDeserializer(dvp DeserializeVerifierProvider) *TypedIdentityDeserializer { + return &TypedIdentityDeserializer{ + DeserializeVerifierProvider: dvp, + } +} + +func (d *TypedIdentityDeserializer) DeserializeVerifier(id view.Identity) (driver.Verifier, error) { + si, err := UnmarshallTypedIdentity(id) + if err != nil { + return nil, errors.Wrap(err, "failed to unmarshal to TypedIdentity") + } + return d.DeserializeVerifierProvider.DeserializeVerifier(si.Identity) +} + +func (d *TypedIdentityDeserializer) DeserializeSigner(raw []byte) (driver.Signer, error) { + return nil, errors.Errorf("signer deserialization not supported") +} + +func (d *TypedIdentityDeserializer) Info(raw []byte, auditInfo []byte) (string, error) { + return "info not supported", nil +} diff --git a/token/services/ttx/opts.go b/token/services/ttx/opts.go index f06cb0f2e..1ab0ab6a0 100644 --- a/token/services/ttx/opts.go +++ b/token/services/ttx/opts.go @@ -19,7 +19,15 @@ type TxOptions struct { NoTransactionVerification bool } -func compile(opts ...TxOption) (*TxOptions, error) { +func (o TxOptions) TMSID() token.TMSID { + return token.TMSID{ + Network: o.Network, + Channel: o.Channel, + Namespace: o.Namespace, + } +} + +func CompileTxOption(opts ...TxOption) (*TxOptions, error) { txOptions := &TxOptions{} for _, opt := range opts { if err := opt(txOptions); err != nil { diff --git a/token/services/ttx/transaction.go b/token/services/ttx/transaction.go index cbcef896e..54aa2e0a2 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 := CompileTxOption(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 := CompileTxOption(opts...) if err != nil { return nil, errors.WithMessage(err, "failed compiling tx options") } @@ -136,7 +136,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 := CompileTxOption(opts...) if err != nil { return nil, errors.WithMessagef(err, "failed to parse options") } diff --git a/token/services/vault/keys/keys.go b/token/services/vault/keys/keys.go index 9e99e0d6e..7e1083cac 100644 --- a/token/services/vault/keys/keys.go +++ b/token/services/vault/keys/keys.go @@ -11,6 +11,7 @@ import ( "strconv" "unicode/utf8" + "github.com/hyperledger-labs/fabric-smart-client/platform/view/services/hash" "github.com/hyperledger-labs/fabric-token-sdk/token/token" "github.com/pkg/errors" ) @@ -37,6 +38,10 @@ const ( IssueActionMetadata = "iam" TransferActionMetadata = "tam" TokenRequestMetadata = "trmd" + + ProofOfExistencePrefix = "pe" + ProofOfNonExistencePrefix = "pne" + ProofOfMetadataExistencePrefix = "pme" ) func GetTokenIdFromKey(key string) (*token.ID, error) { @@ -216,3 +221,22 @@ func GetSNFromKey(key string) (string, error) { return components[1], nil } */ + +func CreateProofOfExistenceKey(tokenId *token.ID) (string, error) { + id := hash.Hashable(tokenId.String()).String() + return CreateCompositeKey(ProofOfExistencePrefix, []string{id}) +} + +func CreateProofOfNonExistenceKey(tokenID *token.ID, origin string) (string, error) { + return CreateCompositeKey(ProofOfNonExistencePrefix, []string{ + hash.Hashable(tokenID.String()).String(), + hash.Hashable(origin).String(), + }) +} + +func CreateProofOfMetadataExistenceKey(tokenID *token.ID, origin string) (string, error) { + return CreateCompositeKey(ProofOfMetadataExistencePrefix, []string{ + hash.Hashable(tokenID.String()).String(), + hash.Hashable(origin).String(), + }) +} diff --git a/token/services/vault/translator/prover.go b/token/services/vault/translator/prover.go new file mode 100644 index 000000000..3660b28dd --- /dev/null +++ b/token/services/vault/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/vault/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/vault/translator/translator.go b/token/services/vault/translator/translator.go index 1769e26f7..665813cbc 100644 --- a/token/services/vault/translator/translator.go +++ b/token/services/vault/translator/translator.go @@ -129,7 +129,6 @@ func (w *Translator) QueryTokens(ids []*token.ID) ([][]byte, error) { bytes, err := w.RWSet.GetState(w.namespace, outputID) if err != nil { errs = append(errs, errors.Wrapf(err, "failed getting output for [%s]", outputID)) - // return nil, errors.Wrapf(err, "failed getting output for [%s]", outputID) continue } if len(bytes) == 0 { @@ -185,12 +184,12 @@ func (w *Translator) checkIssue(issue IssueAction) error { } func (w *Translator) checkTransfer(t TransferAction) error { - keys, err := t.GetInputs() + inputKeys, err := t.GetInputs() if err != nil { return errors.Wrapf(err, "invalid transfer: failed getting input IDs") } if !t.IsGraphHiding() { - for _, key := range keys { + for _, key := range inputKeys { bytes, err := w.RWSet.GetState(w.namespace, key) if err != nil { return errors.Wrapf(err, "invalid transfer: failed getting state [%s]", key) @@ -200,7 +199,7 @@ func (w *Translator) checkTransfer(t TransferAction) error { } } } else { - for _, key := range keys { + for _, key := range inputKeys { bytes, err := w.RWSet.GetState(w.namespace, key) if err != nil { return errors.Wrapf(err, "invalid transfer: failed getting state [%s]", key) diff --git a/token/tms.go b/token/tms.go index 0a3de37c5..f86fadafd 100644 --- a/token/tms.go +++ b/token/tms.go @@ -19,20 +19,7 @@ import ( var logger = flogging.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 {