From 0f38be9fee216ca0e4741acd93cefda208a33c5a Mon Sep 17 00:00:00 2001 From: Flamki <9833ayush@gmail.com> Date: Thu, 7 May 2026 22:49:12 +0530 Subject: [PATCH 01/18] feat(hashescrow): add separate hash-based escrow identity flow (#1657) Signed-off-by: Flamki <9833ayush@gmail.com> --- token/core/fabtoken/v1/driver/deserializer.go | 4 + token/core/fabtoken/v1/driver/driver.go | 2 + .../v1/validator/validator_transfer.go | 51 +++ .../zkatdlog/nogh/v1/driver/deserializer.go | 4 + token/core/zkatdlog/nogh/v1/driver/driver.go | 2 + .../nogh/v1/validator/validator_transfer.go | 42 +++ token/driver/wallet.go | 2 + .../interop/hashescrow/deserializer.go | 178 ++++++++++ .../interop/hashescrow/deserializer_test.go | 171 ++++++++++ .../identity/interop/hashescrow/info.go | 29 ++ .../identity/interop/hashescrow/validator.go | 93 +++++ .../interop/hashescrow/validator_test.go | 116 +++++++ token/services/identity/marshal/marshal.go | 3 + .../services/identity/marshal/marshal_test.go | 6 + token/services/identity/typed.go | 2 + token/services/interop/hashescrow/keys.go | 29 ++ token/services/interop/hashescrow/logger.go | 11 + token/services/interop/hashescrow/script.go | 132 +++++++ .../interop/hashescrow/script_test.go | 63 ++++ token/services/interop/hashescrow/signer.go | 112 ++++++ .../interop/hashescrow/signer_test.go | 145 ++++++++ .../interop/hashescrow/transaction.go | 323 ++++++++++++++++++ .../interop/hashescrow/transaction_test.go | 66 ++++ 23 files changed, 1586 insertions(+) create mode 100644 token/services/identity/interop/hashescrow/deserializer.go create mode 100644 token/services/identity/interop/hashescrow/deserializer_test.go create mode 100644 token/services/identity/interop/hashescrow/info.go create mode 100644 token/services/identity/interop/hashescrow/validator.go create mode 100644 token/services/identity/interop/hashescrow/validator_test.go create mode 100644 token/services/interop/hashescrow/keys.go create mode 100644 token/services/interop/hashescrow/logger.go create mode 100644 token/services/interop/hashescrow/script.go create mode 100644 token/services/interop/hashescrow/script_test.go create mode 100644 token/services/interop/hashescrow/signer.go create mode 100644 token/services/interop/hashescrow/signer_test.go create mode 100644 token/services/interop/hashescrow/transaction.go create mode 100644 token/services/interop/hashescrow/transaction_test.go diff --git a/token/core/fabtoken/v1/driver/deserializer.go b/token/core/fabtoken/v1/driver/deserializer.go index ff8e4b5b1d..04e1c7c5a4 100644 --- a/token/core/fabtoken/v1/driver/deserializer.go +++ b/token/core/fabtoken/v1/driver/deserializer.go @@ -13,9 +13,11 @@ import ( "github.com/hyperledger-labs/fabric-token-sdk/token/driver" "github.com/hyperledger-labs/fabric-token-sdk/token/services/identity/boolpolicy" "github.com/hyperledger-labs/fabric-token-sdk/token/services/identity/deserializer" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/identity/interop/hashescrow" "github.com/hyperledger-labs/fabric-token-sdk/token/services/identity/interop/htlc" "github.com/hyperledger-labs/fabric-token-sdk/token/services/identity/multisig" "github.com/hyperledger-labs/fabric-token-sdk/token/services/identity/x509" + hashescrow2 "github.com/hyperledger-labs/fabric-token-sdk/token/services/interop/hashescrow" htlc2 "github.com/hyperledger-labs/fabric-token-sdk/token/services/interop/htlc" ) @@ -29,6 +31,7 @@ func NewDeserializer() *Deserializer { des := deserializer.NewTypedVerifierDeserializerMultiplex() des.AddTypedVerifierDeserializer(x509.IdentityType, deserializer.NewTypedIdentityVerifierDeserializer(&x509.IdentityDeserializer{}, &x509.AuditMatcherDeserializer{})) des.AddTypedVerifierDeserializer(htlc2.ScriptType, htlc.NewTypedIdentityDeserializer(des)) + des.AddTypedVerifierDeserializer(hashescrow2.ScriptType, hashescrow.NewTypedIdentityDeserializer(des)) des.AddTypedVerifierDeserializer(multisig.Multisig, multisig.NewTypedIdentityDeserializer(des, des)) des.AddTypedVerifierDeserializer(boolpolicy.Policy, boolpolicy.NewTypedIdentityDeserializer(des, des)) @@ -51,6 +54,7 @@ func NewEIDRHDeserializer() *EIDRHDeserializer { d := deserializer.NewEIDRHDeserializer() d.AddDeserializer(x509.IdentityType, &x509.AuditInfoDeserializer{}) d.AddDeserializer(htlc2.ScriptType, htlc.NewAuditDeserializer(d)) + d.AddDeserializer(hashescrow2.ScriptType, hashescrow.NewAuditDeserializer(d)) d.AddDeserializer(multisig.Multisig, &multisig.AuditInfoDeserializer{}) d.AddDeserializer(boolpolicy.Policy, &boolpolicy.AuditInfoDeserializer{}) diff --git a/token/core/fabtoken/v1/driver/driver.go b/token/core/fabtoken/v1/driver/driver.go index 6e7bceaeea..25276304e9 100644 --- a/token/core/fabtoken/v1/driver/driver.go +++ b/token/core/fabtoken/v1/driver/driver.go @@ -16,6 +16,7 @@ import ( v1setup "github.com/hyperledger-labs/fabric-token-sdk/token/core/fabtoken/v1/setup" "github.com/hyperledger-labs/fabric-token-sdk/token/core/fabtoken/v1/validator" "github.com/hyperledger-labs/fabric-token-sdk/token/driver" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/interop/hashescrow" "github.com/hyperledger-labs/fabric-token-sdk/token/services/interop/htlc" "github.com/hyperledger-labs/fabric-token-sdk/token/services/logging" "github.com/hyperledger-labs/fabric-token-sdk/token/services/ttx/boolpolicy" @@ -146,6 +147,7 @@ func (d *Driver) NewTokenService(tmsID driver.TMSID, publicParams []byte) (drive authorization := common.NewAuthorizationMultiplexer( common.NewTMSAuthorization(logger, publicParamsManager.PublicParams(), ws), htlc.NewScriptAuth(ws), + hashescrow.NewScriptAuth(ws), multisig.NewEscrowAuth(ws), boolpolicy.NewEscrowAuth(ws), ) diff --git a/token/core/fabtoken/v1/validator/validator_transfer.go b/token/core/fabtoken/v1/validator/validator_transfer.go index 943938aa1c..ff0885b9d5 100644 --- a/token/core/fabtoken/v1/validator/validator_transfer.go +++ b/token/core/fabtoken/v1/validator/validator_transfer.go @@ -15,7 +15,9 @@ import ( "github.com/hyperledger-labs/fabric-token-sdk/token/core/fabtoken/v1/actions" "github.com/hyperledger-labs/fabric-token-sdk/token/driver" "github.com/hyperledger-labs/fabric-token-sdk/token/services/identity" + hashescrow2 "github.com/hyperledger-labs/fabric-token-sdk/token/services/identity/interop/hashescrow" htlc2 "github.com/hyperledger-labs/fabric-token-sdk/token/services/identity/interop/htlc" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/interop/hashescrow" "github.com/hyperledger-labs/fabric-token-sdk/token/services/interop/htlc" "github.com/hyperledger-labs/fabric-token-sdk/token/token" ) @@ -183,6 +185,38 @@ func TransferHTLCValidate(c context.Context, ctx *Context) error { ctx.CountMetadataKey(metadataKey) } } + if owner.Type == hashescrow.ScriptType { + if len(ctx.TransferAction.GetOutputs()) != 1 { + return errors.New("invalid transfer action: a hash escrow script only transfers the ownership of a token") + } + + // check type and quantity + output := ctx.TransferAction.GetOutputs()[0].(*actions.Output) + tok := output + if ctx.InputTokens[0].Type != tok.Type { + return errors.New("invalid transfer action: type of input does not match type of output") + } + if ctx.InputTokens[0].Quantity != tok.Quantity { + return errors.New("invalid transfer action: quantity of input does not match quantity of output") + } + if output.IsRedeem() { + return errors.New("invalid transfer action: the output corresponding to a hash escrow spending should not be a redeem") + } + + // check owner field + script, err := hashescrow2.VerifyOwner(ctx.InputTokens[0].GetOwner(), tok.Owner) + if err != nil { + return errors.Wrap(err, "failed to verify transfer from hash escrow script") + } + + // check metadata + sigma := ctx.Signatures[i] + metadataKey, err := hashescrow2.MetadataClaimKeyCheck(ctx.TransferAction, script, sigma) + if err != nil { + return errors.WithMessagef(err, "failed to check hash escrow metadata") + } + ctx.CountMetadataKey(metadataKey) + } } for _, o := range ctx.TransferAction.GetOutputs() { @@ -214,6 +248,23 @@ func TransferHTLCValidate(c context.Context, ctx *Context) error { } ctx.CountMetadataKey(metadataKey) + continue + } + if owner.Type == hashescrow.ScriptType { + script := &hashescrow.Script{} + err = json.Unmarshal(owner.Identity, script) + if err != nil { + return err + } + if err := script.Validate(); err != nil { + return errors.WithMessagef(err, "hash escrow script invalid") + } + metadataKey, err := hashescrow2.MetadataLockKeyCheck(ctx.TransferAction, script) + if err != nil { + return errors.WithMessagef(err, "failed to check hash escrow metadata") + } + ctx.CountMetadataKey(metadataKey) + continue } } diff --git a/token/core/zkatdlog/nogh/v1/driver/deserializer.go b/token/core/zkatdlog/nogh/v1/driver/deserializer.go index 566e34a2ae..f3444eb169 100644 --- a/token/core/zkatdlog/nogh/v1/driver/deserializer.go +++ b/token/core/zkatdlog/nogh/v1/driver/deserializer.go @@ -16,9 +16,11 @@ import ( "github.com/hyperledger-labs/fabric-token-sdk/token/services/identity/deserializer" "github.com/hyperledger-labs/fabric-token-sdk/token/services/identity/idemix" "github.com/hyperledger-labs/fabric-token-sdk/token/services/identity/idemixnym" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/identity/interop/hashescrow" "github.com/hyperledger-labs/fabric-token-sdk/token/services/identity/interop/htlc" "github.com/hyperledger-labs/fabric-token-sdk/token/services/identity/multisig" "github.com/hyperledger-labs/fabric-token-sdk/token/services/identity/x509" + hashescrow2 "github.com/hyperledger-labs/fabric-token-sdk/token/services/interop/hashescrow" htlc2 "github.com/hyperledger-labs/fabric-token-sdk/token/services/interop/htlc" ) @@ -46,6 +48,7 @@ func NewDeserializer(pp *v1.PublicParams) (*Deserializer, error) { } des.AddTypedVerifierDeserializer(x509.IdentityType, deserializer.NewTypedIdentityVerifierDeserializer(&x509.IdentityDeserializer{}, &x509.AuditMatcherDeserializer{})) des.AddTypedVerifierDeserializer(htlc2.ScriptType, htlc.NewTypedIdentityDeserializer(des)) + des.AddTypedVerifierDeserializer(hashescrow2.ScriptType, hashescrow.NewTypedIdentityDeserializer(des)) des.AddTypedVerifierDeserializer(multisig.Multisig, multisig.NewTypedIdentityDeserializer(des, des)) des.AddTypedVerifierDeserializer(boolpolicy.Policy, boolpolicy.NewTypedIdentityDeserializer(des, des)) @@ -93,6 +96,7 @@ func NewEIDRHDeserializer() *EIDRHDeserializer { d.AddDeserializer(idemixnym.IdentityType, &idemixnym.AuditInfoDeserializer{}) d.AddDeserializer(x509.IdentityType, &x509.AuditInfoDeserializer{}) d.AddDeserializer(htlc2.ScriptType, htlc.NewAuditDeserializer(d)) + d.AddDeserializer(hashescrow2.ScriptType, hashescrow.NewAuditDeserializer(d)) d.AddDeserializer(multisig.Multisig, &multisig.AuditInfoDeserializer{}) d.AddDeserializer(boolpolicy.Policy, &boolpolicy.AuditInfoDeserializer{}) diff --git a/token/core/zkatdlog/nogh/v1/driver/driver.go b/token/core/zkatdlog/nogh/v1/driver/driver.go index a980f27b22..e669e91a19 100644 --- a/token/core/zkatdlog/nogh/v1/driver/driver.go +++ b/token/core/zkatdlog/nogh/v1/driver/driver.go @@ -18,6 +18,7 @@ import ( v1token "github.com/hyperledger-labs/fabric-token-sdk/token/core/zkatdlog/nogh/v1/token" "github.com/hyperledger-labs/fabric-token-sdk/token/core/zkatdlog/nogh/v1/validator" "github.com/hyperledger-labs/fabric-token-sdk/token/driver" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/interop/hashescrow" "github.com/hyperledger-labs/fabric-token-sdk/token/services/interop/htlc" "github.com/hyperledger-labs/fabric-token-sdk/token/services/logging" "github.com/hyperledger-labs/fabric-token-sdk/token/services/ttx/boolpolicy" @@ -150,6 +151,7 @@ func (d *Driver) NewTokenService(tmsID driver.TMSID, publicParams []byte) (drive authorization := common.NewAuthorizationMultiplexer( common.NewTMSAuthorization(logger, ppm.PublicParams(), ws), htlc.NewScriptAuth(ws), + hashescrow.NewScriptAuth(ws), multisig.NewEscrowAuth(ws), boolpolicy.NewEscrowAuth(ws), ) diff --git a/token/core/zkatdlog/nogh/v1/validator/validator_transfer.go b/token/core/zkatdlog/nogh/v1/validator/validator_transfer.go index 54bb95a6ad..ac4ffb419f 100644 --- a/token/core/zkatdlog/nogh/v1/validator/validator_transfer.go +++ b/token/core/zkatdlog/nogh/v1/validator/validator_transfer.go @@ -17,7 +17,9 @@ import ( "github.com/hyperledger-labs/fabric-token-sdk/token/core/zkatdlog/nogh/v1/transfer" "github.com/hyperledger-labs/fabric-token-sdk/token/driver" "github.com/hyperledger-labs/fabric-token-sdk/token/services/identity" + hashescrow2 "github.com/hyperledger-labs/fabric-token-sdk/token/services/identity/interop/hashescrow" htlc2 "github.com/hyperledger-labs/fabric-token-sdk/token/services/identity/interop/htlc" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/interop/hashescrow" "github.com/hyperledger-labs/fabric-token-sdk/token/services/interop/htlc" token2 "github.com/hyperledger-labs/fabric-token-sdk/token/token" ) @@ -182,6 +184,29 @@ func TransferHTLCValidate(c context.Context, ctx *Context) error { ctx.CountMetadataKey(metadataKey) } } + if owner.Type == hashescrow.ScriptType { + // Hash escrow script must be a 1-to-1 transfer of ownership + if len(ctx.InputTokens) != 1 || len(ctx.TransferAction.GetOutputs()) != 1 { + return ErrInvalidHTLCAction + } + + out, ok := ctx.TransferAction.GetOutputs()[0].(*token.Token) + if !ok || out == nil { + return ErrHTLCOutputNotFound + } + + script, err := hashescrow2.VerifyOwner(ctx.InputTokens[0].Owner, out.Owner) + if err != nil { + return errors.Wrap(err, "failed to verify transfer from hash escrow script") + } + + sigma := ctx.Signatures[i] + metadataKey, err := hashescrow2.MetadataClaimKeyCheck(ctx.TransferAction, script, sigma) + if err != nil { + return errors.WithMessagef(err, "failed to check hash escrow metadata") + } + ctx.CountMetadataKey(metadataKey) + } } for _, o := range ctx.TransferAction.Outputs { @@ -207,6 +232,23 @@ func TransferHTLCValidate(c context.Context, ctx *Context) error { } ctx.CountMetadataKey(metadataKey) + continue + } + if owner.Type == hashescrow.ScriptType { + script := &hashescrow.Script{} + err = script.FromBytes(owner.Identity) + if err != nil { + return err + } + if err := script.Validate(); err != nil { + return errors.WithMessagef(err, "hash escrow script invalid") + } + metadataKey, err := hashescrow2.MetadataLockKeyCheck(ctx.TransferAction, script) + if err != nil { + return errors.WithMessagef(err, "failed to check hash escrow metadata") + } + ctx.CountMetadataKey(metadataKey) + continue } } diff --git a/token/driver/wallet.go b/token/driver/wallet.go index f92c46efd7..067ba641ff 100644 --- a/token/driver/wallet.go +++ b/token/driver/wallet.go @@ -207,6 +207,7 @@ const ( HTLCScriptIdentityType IdentityType = 4 MultiSigIdentityType IdentityType = 5 PolicyIdentityType IdentityType = 6 + HashEscrowIdentityType IdentityType = 7 ) // IdentityTypeString identifies the type of identity as a string @@ -219,6 +220,7 @@ const ( HTLCScriptIdentityTypeString IdentityTypeString = "htlc" MultiSigIdentityTypeString IdentityTypeString = "multisig" PolicyIdentityTypeString IdentityTypeString = "policy" + HashEscrowIdentityTypeString IdentityTypeString = "hashescrow" ) // Authorization checks the relationship between a token and different wallet types (owner, issuer, auditor). diff --git a/token/services/identity/interop/hashescrow/deserializer.go b/token/services/identity/interop/hashescrow/deserializer.go new file mode 100644 index 0000000000..29c5ae54c1 --- /dev/null +++ b/token/services/identity/interop/hashescrow/deserializer.go @@ -0,0 +1,178 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package hashescrow + +import ( + "context" + + "github.com/hyperledger-labs/fabric-smart-client/pkg/utils/errors" + "github.com/hyperledger-labs/fabric-token-sdk/token/core/common/encoding/json" + "github.com/hyperledger-labs/fabric-token-sdk/token/driver" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/identity" + idriver "github.com/hyperledger-labs/fabric-token-sdk/token/services/identity/driver" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/interop/hashescrow" +) + +const ( + ScriptType = hashescrow.ScriptType + ScriptTypeString = hashescrow.ScriptTypeString +) + +//go:generate counterfeiter -o mock/deserializer.go -fake-name Deserializer . Deserializer +type Deserializer interface { + DeserializeVerifier(ctx context.Context, id driver.Identity) (driver.Verifier, error) + MatchIdentity(ctx context.Context, id driver.Identity, ai []byte) error +} + +type TypedIdentityDeserializer struct { + deserializer Deserializer +} + +func NewTypedIdentityDeserializer(deserializer Deserializer) *TypedIdentityDeserializer { + return &TypedIdentityDeserializer{deserializer: deserializer} +} + +func (t *TypedIdentityDeserializer) DeserializeVerifier(ctx context.Context, typ identity.Type, raw []byte) (driver.Verifier, error) { + if typ != ScriptType { + return nil, errors.Errorf("cannot deserializer type [%s], expected [%s]", typ, ScriptType) + } + + script := &hashescrow.Script{} + err := json.Unmarshal(raw, script) + if err != nil { + return nil, errors.Errorf("failed to unmarshal TypedIdentity as a hash escrow script") + } + v := &hashescrow.Verifier{} + v.Sender, err = t.deserializer.DeserializeVerifier(ctx, script.Sender) + if err != nil { + return nil, errors.Errorf("failed to unmarshal the identity of the sender in the hash escrow script") + } + v.Recipient, err = t.deserializer.DeserializeVerifier(ctx, script.Recipient) + if err != nil { + return nil, errors.Errorf("failed to unmarshal the identity of the recipient in the hash escrow script") + } + v.HashInfo.Hash = script.HashInfo.Hash + v.HashInfo.HashFunc = script.HashInfo.HashFunc + v.HashInfo.HashEncoding = script.HashInfo.HashEncoding + + return v, nil +} + +func (t *TypedIdentityDeserializer) Recipients(id driver.Identity, typ identity.Type, raw []byte) ([]driver.Identity, error) { + if typ != ScriptType { + return nil, errors.New("unknown identity type") + } + script := &hashescrow.Script{} + err := json.Unmarshal(raw, script) + if err != nil { + return nil, errors.Wrapf(err, "failed to unmarshal hash escrow script") + } + + return []driver.Identity{script.Recipient}, nil +} + +func (t *TypedIdentityDeserializer) GetAuditInfo(ctx context.Context, id driver.Identity, typ identity.Type, raw []byte, p driver.AuditInfoProvider) ([]byte, error) { + if typ != ScriptType { + return nil, errors.Errorf("invalid type, got [%s], expected [%s]", typ, ScriptType) + } + script := &hashescrow.Script{} + if err := json.Unmarshal(raw, script); err != nil { + return nil, errors.Wrapf(err, "failed to unmarshal hash escrow script") + } + + auditInfo := &ScriptInfo{} + var err error + auditInfo.Sender, err = p.GetAuditInfo(ctx, script.Sender) + if err != nil { + return nil, errors.Wrapf(err, "failed getting audit info for hash escrow script [%s]", id.String()) + } + auditInfo.Recipient, err = p.GetAuditInfo(ctx, script.Recipient) + if err != nil { + return nil, errors.Wrapf(err, "failed getting audit info for hash escrow script [%s]", id.String()) + } + + auditInfoRaw, err := json.Marshal(auditInfo) + if err != nil { + return nil, errors.Wrapf(err, "failed marshaling audit info for hash escrow script") + } + + return auditInfoRaw, nil +} + +func (t *TypedIdentityDeserializer) GetAuditInfoMatcher(ctx context.Context, owner driver.Identity, auditInfo []byte) (driver.Matcher, error) { + return &AuditInfoMatcher{ + AuditInfo: auditInfo, + Deserializer: t.deserializer, + }, nil +} + +type AuditDeserializer struct { + AuditInfoDeserializer idriver.AuditInfoDeserializer +} + +func NewAuditDeserializer(auditInfoDeserializer idriver.AuditInfoDeserializer) *AuditDeserializer { + return &AuditDeserializer{AuditInfoDeserializer: auditInfoDeserializer} +} + +func (a *AuditDeserializer) DeserializeAuditInfo(ctx context.Context, identity driver.Identity, raw []byte) (idriver.AuditInfo, error) { + script := &hashescrow.Script{} + err := json.Unmarshal(identity, script) + if err != nil { + return nil, errors.Wrapf(err, "failed to unmarshal hash escrow identity") + } + + si := &ScriptInfo{} + err = json.Unmarshal(raw, si) + if err != nil || (len(si.Sender) == 0 && len(si.Recipient) == 0) { + return nil, errors.Errorf("invalid audit info, failed unmarshal [%s][%d][%d]", string(raw), len(si.Sender), len(si.Recipient)) + } + if len(si.Recipient) == 0 { + return nil, errors.Errorf("no recipient defined") + } + ai, err := a.AuditInfoDeserializer.DeserializeAuditInfo(ctx, script.Recipient, si.Recipient) + if err != nil { + return nil, errors.Wrapf(err, "failed unmarshalling audit info [%s]", raw) + } + + return ai, nil +} + +type AuditInfoMatcher struct { + AuditInfo []byte + Deserializer Deserializer +} + +func (a *AuditInfoMatcher) Match(ctx context.Context, id []byte) error { + scriptInf := &ScriptInfo{} + if err := json.Unmarshal(a.AuditInfo, scriptInf); err != nil { + return errors.Wrapf(err, "failed to unmarshal script info") + } + scriptSender, scriptRecipient, err := GetScriptSenderAndRecipient(id) + if err != nil { + return errors.Wrap(err, "failed getting script sender and recipient") + } + err = a.Deserializer.MatchIdentity(ctx, scriptSender, scriptInf.Sender) + if err != nil { + return errors.Wrapf(err, "failed matching sender identity [%s]", scriptSender.String()) + } + err = a.Deserializer.MatchIdentity(ctx, scriptRecipient, scriptInf.Recipient) + if err != nil { + return errors.Wrapf(err, "failed matching recipient identity [%s]", scriptRecipient.String()) + } + + return nil +} + +func GetScriptSenderAndRecipient(id []byte) (sender, recipient driver.Identity, err error) { + script := &hashescrow.Script{} + err = json.Unmarshal(id, script) + if err != nil { + return nil, nil, errors.Wrapf(err, "failed to unmarshal hash escrow script") + } + + return script.Sender, script.Recipient, nil +} diff --git a/token/services/identity/interop/hashescrow/deserializer_test.go b/token/services/identity/interop/hashescrow/deserializer_test.go new file mode 100644 index 0000000000..edc6695ed6 --- /dev/null +++ b/token/services/identity/interop/hashescrow/deserializer_test.go @@ -0,0 +1,171 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package hashescrow_test + +import ( + "context" + "encoding/json" + "errors" + "testing" + + "github.com/hyperledger-labs/fabric-token-sdk/token/driver" + driverMock "github.com/hyperledger-labs/fabric-token-sdk/token/driver/mock" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/identity" + identityDriverMock "github.com/hyperledger-labs/fabric-token-sdk/token/services/identity/driver/mock" + ihe "github.com/hyperledger-labs/fabric-token-sdk/token/services/identity/interop/hashescrow" + he "github.com/hyperledger-labs/fabric-token-sdk/token/services/interop/hashescrow" + "github.com/stretchr/testify/require" +) + +type stubDeserializer struct { + deserializeVerifier func(ctx context.Context, id driver.Identity) (driver.Verifier, error) + matchIdentity func(ctx context.Context, id driver.Identity, ai []byte) error +} + +func (s *stubDeserializer) DeserializeVerifier(ctx context.Context, id driver.Identity) (driver.Verifier, error) { + if s.deserializeVerifier != nil { + return s.deserializeVerifier(ctx, id) + } + + return &driverMock.Verifier{}, nil +} + +func (s *stubDeserializer) MatchIdentity(ctx context.Context, id driver.Identity, ai []byte) error { + if s.matchIdentity != nil { + return s.matchIdentity(ctx, id, ai) + } + + return nil +} + +type fakeAuditInfo struct{} + +func (f *fakeAuditInfo) EnrollmentID() string { return "e" } +func (f *fakeAuditInfo) RevocationHandle() string { return "r" } + +func mkScript(t *testing.T, sender, recipient []byte) []byte { + t.Helper() + s := &he.Script{ + Sender: sender, + Recipient: recipient, + HashInfo: he.HashInfo{Hash: []byte("h")}, + } + raw, err := json.Marshal(s) + require.NoError(t, err) + + return raw +} + +func TestGetScriptSenderAndRecipient(t *testing.T) { + raw := mkScript(t, []byte("s"), []byte("r")) + sender, recipient, err := ihe.GetScriptSenderAndRecipient(raw) + require.NoError(t, err) + require.Equal(t, identity.Identity("s"), sender) + require.Equal(t, identity.Identity("r"), recipient) + + _, _, err = ihe.GetScriptSenderAndRecipient([]byte("bad-json")) + require.Error(t, err) +} + +func TestTypedIdentityDeserializerDeserializeVerifier(t *testing.T) { + d := ihe.NewTypedIdentityDeserializer(&stubDeserializer{}) + ctx := t.Context() + + _, err := d.DeserializeVerifier(ctx, identity.Type(999), []byte{}) + require.Error(t, err) + + _, err = d.DeserializeVerifier(ctx, he.ScriptType, []byte("bad-json")) + require.Error(t, err) + + stub := &stubDeserializer{ + deserializeVerifier: func(context.Context, driver.Identity) (driver.Verifier, error) { + return nil, errors.New("nope") + }, + } + d = ihe.NewTypedIdentityDeserializer(stub) + _, err = d.DeserializeVerifier(ctx, he.ScriptType, mkScript(t, []byte("s"), []byte("r"))) + require.Error(t, err) + + d = ihe.NewTypedIdentityDeserializer(&stubDeserializer{}) + v, err := d.DeserializeVerifier(ctx, he.ScriptType, mkScript(t, []byte("s"), []byte("r"))) + require.NoError(t, err) + require.NotNil(t, v) +} + +func TestTypedIdentityDeserializerRecipientsAndAuditInfo(t *testing.T) { + d := ihe.NewTypedIdentityDeserializer(&stubDeserializer{}) + ctx := t.Context() + raw := mkScript(t, []byte("s"), []byte("r")) + + _, err := d.Recipients(nil, identity.Type(999), raw) + require.Error(t, err) + + ids, err := d.Recipients(nil, he.ScriptType, raw) + require.NoError(t, err) + require.Len(t, ids, 1) + require.Equal(t, identity.Identity("r"), ids[0]) + + p := &driverMock.AuditInfoProvider{} + p.GetAuditInfoReturnsOnCall(0, nil, errors.New("nope")) + _, err = d.GetAuditInfo(ctx, []byte("id"), he.ScriptType, raw, p) + require.Error(t, err) + + p = &driverMock.AuditInfoProvider{} + p.GetAuditInfoReturnsOnCall(0, []byte("sa"), nil) + p.GetAuditInfoReturnsOnCall(1, []byte("ra"), nil) + ai, err := d.GetAuditInfo(ctx, []byte("id"), he.ScriptType, raw, p) + require.NoError(t, err) + + var out ihe.ScriptInfo + require.NoError(t, json.Unmarshal(ai, &out)) + require.Equal(t, []byte("sa"), out.Sender) + require.Equal(t, []byte("ra"), out.Recipient) +} + +func TestAuditDeserializerAndMatcher(t *testing.T) { + ctx := t.Context() + aid := &identityDriverMock.AuditInfoDeserializer{} + ad := ihe.NewAuditDeserializer(aid) + + scriptRaw, err := json.Marshal(&he.Script{Recipient: []byte("r")}) + require.NoError(t, err) + + _, err = ad.DeserializeAuditInfo(ctx, scriptRaw, []byte("bad-json")) + require.Error(t, err) + + siRaw, err := json.Marshal(&ihe.ScriptInfo{Sender: []byte("s")}) + require.NoError(t, err) + _, err = ad.DeserializeAuditInfo(ctx, scriptRaw, siRaw) + require.Error(t, err) + + siRaw, err = json.Marshal(&ihe.ScriptInfo{Recipient: []byte("ra")}) + require.NoError(t, err) + aid.DeserializeAuditInfoReturns(&fakeAuditInfo{}, nil) + ai, err := ad.DeserializeAuditInfo(ctx, scriptRaw, siRaw) + require.NoError(t, err) + require.NotNil(t, ai) + + matcher := &ihe.AuditInfoMatcher{ + AuditInfo: []byte("bad-json"), + Deserializer: &stubDeserializer{ + matchIdentity: func(context.Context, driver.Identity, []byte) error { return nil }, + }, + } + require.Error(t, matcher.Match(ctx, []byte("id"))) + + matcher.AuditInfo, err = json.Marshal(&ihe.ScriptInfo{Sender: []byte("sa"), Recipient: []byte("ra")}) + require.NoError(t, err) + require.Error(t, matcher.Match(ctx, []byte("bad-script"))) + + matcher.Deserializer = &stubDeserializer{ + matchIdentity: func(context.Context, driver.Identity, []byte) error { return errors.New("nope") }, + } + require.Error(t, matcher.Match(ctx, mkScript(t, []byte("s"), []byte("r")))) + + matcher.Deserializer = &stubDeserializer{} + require.NoError(t, matcher.Match(ctx, mkScript(t, []byte("s"), []byte("r")))) +} diff --git a/token/services/identity/interop/hashescrow/info.go b/token/services/identity/interop/hashescrow/info.go new file mode 100644 index 0000000000..5662ed3dd0 --- /dev/null +++ b/token/services/identity/interop/hashescrow/info.go @@ -0,0 +1,29 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package hashescrow + +import ( + "context" + + "github.com/hyperledger-labs/fabric-token-sdk/token/driver" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/interop/hashescrow" +) + +type Script = hashescrow.Script + +// AuditInfoProvider provides access to audit information for a given identity. +// +//go:generate counterfeiter -o mock/aip.go -fake-name AuditInfoProvider . AuditInfoProvider +type AuditInfoProvider interface { + GetAuditInfo(ctx context.Context, identity driver.Identity) ([]byte, error) +} + +// ScriptInfo contains sender and recipient audit info. +type ScriptInfo struct { + Sender []byte + Recipient []byte +} diff --git a/token/services/identity/interop/hashescrow/validator.go b/token/services/identity/interop/hashescrow/validator.go new file mode 100644 index 0000000000..ec7de4e056 --- /dev/null +++ b/token/services/identity/interop/hashescrow/validator.go @@ -0,0 +1,93 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package hashescrow + +import ( + "bytes" + "encoding/json" + + "github.com/hyperledger-labs/fabric-smart-client/pkg/utils/errors" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/identity" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/interop/hashescrow" +) + +//go:generate counterfeiter -o mock/action.go -fake-name Action . Action +type Action interface { + GetMetadata() map[string][]byte +} + +// VerifyOwner validates that the output owner is either sender or recipient as encoded in the hash escrow script. +func VerifyOwner(senderRawOwner []byte, outRawOwner []byte) (*hashescrow.Script, error) { + if len(outRawOwner) == 0 { + return nil, errors.Errorf("the output owner must be set") + } + sender, err := identity.UnmarshalTypedIdentity(senderRawOwner) + if err != nil { + return nil, err + } + if sender.Type != ScriptType { + return nil, errors.Errorf("invalid identity type, expected [%s], got [%s]", ScriptType, sender.Type) + } + script := &hashescrow.Script{} + err = json.Unmarshal(sender.Identity, script) + if err != nil { + return nil, err + } + if script.Recipient.Equal(outRawOwner) || script.Sender.Equal(outRawOwner) { + return script, nil + } + + return nil, errors.New("owner of output token does not correspond to sender or recipient in hash escrow request") +} + +// MetadataClaimKeyCheck validates claim metadata and returns the matched metadata key. +func MetadataClaimKeyCheck(action Action, script *hashescrow.Script, sig []byte) (string, error) { + claim := &hashescrow.ClaimSignature{} + if err := json.Unmarshal(sig, claim); err != nil { + return "", errors.Wrapf(err, "failed unmarshalling claim signature [%s]", string(sig)) + } + if len(claim.Preimage) == 0 || len(claim.ClaimantSignature) == 0 { + return "", errors.New("expected a valid claim preImage and claimant signature") + } + + metadata := action.GetMetadata() + if len(metadata) == 0 { + return "", errors.New("cannot find hash escrow pre-image, no metadata") + } + image, err := script.HashInfo.Image(claim.Preimage) + if err != nil { + return "", errors.Wrapf(err, "failed to compute image of [%x]", claim.Preimage) + } + key := hashescrow.ClaimKey(image) + value, ok := metadata[key] + if !ok { + return "", errors.New("cannot find hash escrow pre-image, missing metadata entry") + } + if !bytes.Equal(value, claim.Preimage) { + return "", errors.Errorf("invalid action, cannot match hash escrow pre-image with metadata [%x]!=[%x]", value, claim.Preimage) + } + + return key, nil +} + +// MetadataLockKeyCheck validates lock metadata and returns the lock metadata key. +func MetadataLockKeyCheck(action Action, script *hashescrow.Script) (string, error) { + metadata := action.GetMetadata() + if len(metadata) == 0 { + return "", errors.New("cannot find hash escrow lock, no metadata") + } + key := hashescrow.LockKey(script.HashInfo.Hash) + value, ok := metadata[key] + if !ok { + return "", errors.New("cannot find hash escrow lock, missing metadata entry") + } + if !bytes.Equal(value, hashescrow.LockValue(script.HashInfo.Hash)) { + return "", errors.Errorf("invalid action, cannot match hash escrow lock with metadata [%x]!=[%x]", value, script.HashInfo.Hash) + } + + return key, nil +} diff --git a/token/services/identity/interop/hashescrow/validator_test.go b/token/services/identity/interop/hashescrow/validator_test.go new file mode 100644 index 0000000000..1d06e636bf --- /dev/null +++ b/token/services/identity/interop/hashescrow/validator_test.go @@ -0,0 +1,116 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package hashescrow_test + +import ( + "crypto" + "encoding/json" + "testing" + + "github.com/hyperledger-labs/fabric-token-sdk/token/services/identity" + ihe "github.com/hyperledger-labs/fabric-token-sdk/token/services/identity/interop/hashescrow" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/interop/encoding" + he "github.com/hyperledger-labs/fabric-token-sdk/token/services/interop/hashescrow" + "github.com/stretchr/testify/require" +) + +type actionStub struct { + md map[string][]byte +} + +func (a *actionStub) GetMetadata() map[string][]byte { + return a.md +} + +func wrapScript(t *testing.T, sender, recipient []byte, hash []byte) ([]byte, *he.Script) { + t.Helper() + script := &he.Script{ + Sender: sender, + Recipient: recipient, + HashInfo: he.HashInfo{ + Hash: hash, + HashFunc: crypto.SHA256, + HashEncoding: encoding.Base64, + }, + } + rawScript, err := json.Marshal(script) + require.NoError(t, err) + rawOwner, err := identity.WrapWithType(he.ScriptType, rawScript) + require.NoError(t, err) + + return rawOwner, script +} + +func TestVerifyOwner(t *testing.T) { + rawOwner, _ := wrapScript(t, []byte("s"), []byte("r"), []byte("h")) + + script, err := ihe.VerifyOwner(rawOwner, []byte("r")) + require.NoError(t, err) + require.Equal(t, identity.Identity("s"), script.Sender) + require.Equal(t, identity.Identity("r"), script.Recipient) + + script, err = ihe.VerifyOwner(rawOwner, []byte("s")) + require.NoError(t, err) + require.Equal(t, identity.Identity("s"), script.Sender) + + _, err = ihe.VerifyOwner(rawOwner, []byte("x")) + require.Error(t, err) + + _, err = ihe.VerifyOwner([]byte("bad"), []byte("r")) + require.Error(t, err) + + typed := identity.TypedIdentity{Type: identity.Type(99), Identity: []byte("x")} + b, err := typed.Bytes() + require.NoError(t, err) + _, err = ihe.VerifyOwner(b, []byte("r")) + require.Error(t, err) +} + +func TestMetadataClaimKeyCheck(t *testing.T) { + _, script := wrapScript(t, []byte("s"), []byte("r"), []byte("h")) + + preimage := []byte("pre") + image, err := script.HashInfo.Image(preimage) + require.NoError(t, err) + key := he.ClaimKey(image) + + sigRaw, err := json.Marshal(&he.ClaimSignature{ + ClaimantSignature: []byte("sig"), + Preimage: preimage, + }) + require.NoError(t, err) + + act := &actionStub{md: map[string][]byte{key: preimage}} + got, err := ihe.MetadataClaimKeyCheck(act, script, sigRaw) + require.NoError(t, err) + require.Equal(t, key, got) + + act = &actionStub{md: map[string][]byte{}} + _, err = ihe.MetadataClaimKeyCheck(act, script, sigRaw) + require.Error(t, err) + + _, err = ihe.MetadataClaimKeyCheck(act, script, []byte("bad-json")) + require.Error(t, err) +} + +func TestMetadataLockKeyCheck(t *testing.T) { + _, script := wrapScript(t, []byte("s"), []byte("r"), []byte("h")) + key := he.LockKey(script.HashInfo.Hash) + + act := &actionStub{md: map[string][]byte{key: he.LockValue(script.HashInfo.Hash)}} + got, err := ihe.MetadataLockKeyCheck(act, script) + require.NoError(t, err) + require.Equal(t, key, got) + + act = &actionStub{md: map[string][]byte{}} + _, err = ihe.MetadataLockKeyCheck(act, script) + require.Error(t, err) + + act = &actionStub{md: map[string][]byte{key: []byte("bad")}} + _, err = ihe.MetadataLockKeyCheck(act, script) + require.Error(t, err) +} diff --git a/token/services/identity/marshal/marshal.go b/token/services/identity/marshal/marshal.go index 3f9bf52ce8..e8037dfd22 100644 --- a/token/services/identity/marshal/marshal.go +++ b/token/services/identity/marshal/marshal.go @@ -131,6 +131,9 @@ func DecodeIdentity(b []byte) (Result, error) { case driver.HTLCScriptIdentityTypeString: r.Int32 = driver.HTLCScriptIdentityType r.IsInt = true + case driver.HashEscrowIdentityTypeString: + r.Int32 = driver.HashEscrowIdentityType + r.IsInt = true } } diff --git a/token/services/identity/marshal/marshal_test.go b/token/services/identity/marshal/marshal_test.go index 10eb8462fc..cd0349a114 100644 --- a/token/services/identity/marshal/marshal_test.go +++ b/token/services/identity/marshal/marshal_test.go @@ -18,6 +18,7 @@ import ( "github.com/hyperledger-labs/fabric-token-sdk/token/services/identity/marshal" "github.com/hyperledger-labs/fabric-token-sdk/token/services/identity/multisig" "github.com/hyperledger-labs/fabric-token-sdk/token/services/identity/x509" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/interop/hashescrow" "github.com/hyperledger-labs/fabric-token-sdk/token/services/interop/htlc" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -540,6 +541,11 @@ func TestStringCompatibility(t *testing.T) { str: htlc.ScriptTypeString, identityType: htlc.ScriptType, }, + { + name: "hashescrow", + str: hashescrow.ScriptTypeString, + identityType: hashescrow.ScriptType, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/token/services/identity/typed.go b/token/services/identity/typed.go index df5e35ff71..c8568e611b 100644 --- a/token/services/identity/typed.go +++ b/token/services/identity/typed.go @@ -73,6 +73,8 @@ func TypeToString(t driver.IdentityType) string { return tdriver.MultiSigIdentityTypeString case tdriver.HTLCScriptIdentityType: return tdriver.HTLCScriptIdentityTypeString + case tdriver.HashEscrowIdentityType: + return tdriver.HashEscrowIdentityTypeString default: return fmt.Sprintf("Type (%d)", t) } diff --git a/token/services/interop/hashescrow/keys.go b/token/services/interop/hashescrow/keys.go new file mode 100644 index 0000000000..4da24c0a9e --- /dev/null +++ b/token/services/interop/hashescrow/keys.go @@ -0,0 +1,29 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package hashescrow + +import "encoding/hex" + +const ( + ClaimPreImage = "hashescrow.cpi" + LockHash = "hashescrow.lh" +) + +// ClaimKey returns the claim key for the passed byte array. +func ClaimKey(v []byte) string { + return ClaimPreImage + hex.EncodeToString(v) +} + +// LockKey returns the lock key for the passed byte array. +func LockKey(v []byte) string { + return LockHash + hex.EncodeToString(v) +} + +// LockValue returns the encoding of the value for a lock key. +func LockValue(v []byte) []byte { + return []byte(hex.EncodeToString(v)) +} diff --git a/token/services/interop/hashescrow/logger.go b/token/services/interop/hashescrow/logger.go new file mode 100644 index 0000000000..880aac0af8 --- /dev/null +++ b/token/services/interop/hashescrow/logger.go @@ -0,0 +1,11 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package hashescrow + +import "github.com/hyperledger-labs/fabric-token-sdk/token/services/logging" + +var logger = logging.MustGetLogger() diff --git a/token/services/interop/hashescrow/script.go b/token/services/interop/hashescrow/script.go new file mode 100644 index 0000000000..8cebac9be4 --- /dev/null +++ b/token/services/interop/hashescrow/script.go @@ -0,0 +1,132 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package hashescrow + +import ( + "context" + "encoding/json" + + "github.com/hyperledger-labs/fabric-smart-client/pkg/utils/errors" + "github.com/hyperledger-labs/fabric-smart-client/platform/view/view" + "github.com/hyperledger-labs/fabric-token-sdk/token/driver" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/identity" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/interop/htlc" + token3 "github.com/hyperledger-labs/fabric-token-sdk/token/token" +) + +// HashInfo contains hash configuration used by hash-based escrow scripts. +// Reuse the HTLC hash representation to keep hashing semantics identical. +type HashInfo = htlc.HashInfo + +// Script contains the details of a hash-based escrow lock. +// Either sender or recipient can claim by presenting a valid preimage and signature. +type Script struct { + Sender view.Identity + Recipient view.Identity + HashInfo HashInfo +} + +// Validate performs the following checks: +// - sender must be set +// - recipient must be set +// - hash info must be valid +func (s *Script) Validate() error { + if s.Sender.IsNone() { + return errors.New("sender not set") + } + if s.Recipient.IsNone() { + return errors.New("recipient not set") + } + if err := s.HashInfo.Validate(); err != nil { + return err + } + + return nil +} + +func (s *Script) FromBytes(raw []byte) error { + return json.Unmarshal(raw, s) +} + +// ScriptAuth implements token ownership checks for hash-based escrow scripts. +type ScriptAuth struct { + WalletService driver.WalletService +} + +func NewScriptAuth(walletService driver.WalletService) *ScriptAuth { + return &ScriptAuth{WalletService: walletService} +} + +// AmIAnAuditor returns false for script ownership. +func (s *ScriptAuth) AmIAnAuditor() bool { + return false +} + +// IsMine returns true if either the sender or recipient belongs to one of our owner wallets. +func (s *ScriptAuth) IsMine(ctx context.Context, tok *token3.Token) (string, []string, bool) { + owner, err := identity.UnmarshalTypedIdentity(tok.Owner) + if err != nil { + logger.DebugfContext(ctx, "Is Mine [%s,%s,%s]? No, failed unmarshalling [%s]", view.Identity(tok.Owner), tok.Type, tok.Quantity, err) + + return "", nil, false + } + if owner.Type != ScriptType { + logger.DebugfContext(ctx, "Is Mine [%s,%s,%s]? No, owner type is [%s] instead of [%s]", view.Identity(tok.Owner), tok.Type, tok.Quantity, owner.Type, ScriptType) + + return "", nil, false + } + script := &Script{} + if err := json.Unmarshal(owner.Identity, script); err != nil { + logger.DebugfContext(ctx, "Is Mine [%s,%s,%s]? No, failed unmarshalling [%s]", view.Identity(tok.Owner), tok.Type, tok.Quantity, err) + + return "", nil, false + } + if script.Sender.IsNone() || script.Recipient.IsNone() { + logger.DebugfContext(ctx, "Is Mine [%s,%s,%s]? No, invalid content [%v]", view.Identity(tok.Owner), tok.Type, tok.Quantity, script) + + return "", nil, false + } + + var ids []string + + logger.DebugfContext(ctx, "Is Mine [%s,%s,%s] as sender?", view.Identity(tok.Owner), tok.Type, tok.Quantity) + if wallet, err := s.WalletService.OwnerWallet(ctx, script.Sender); err == nil { + ids = append(ids, senderWallet(wallet)) + } + + logger.DebugfContext(ctx, "Is Mine [%s,%s,%s] as recipient?", view.Identity(tok.Owner), tok.Type, tok.Quantity) + if wallet, err := s.WalletService.OwnerWallet(ctx, script.Recipient); err == nil { + ids = append(ids, recipientWallet(wallet)) + } + + return "", ids, len(ids) != 0 +} + +func (s *ScriptAuth) Issued(ctx context.Context, issuer driver.Identity, tok *token3.Token) bool { + return false +} + +func (s *ScriptAuth) OwnerType(raw []byte) (driver.IdentityType, []byte, error) { + owner, err := identity.UnmarshalTypedIdentity(raw) + if err != nil { + return driver.ZeroIdentityType, nil, err + } + + return owner.Type, owner.Identity, nil +} + +type ownerWallet interface { + ID() string +} + +func senderWallet(w ownerWallet) string { + return "hashescrow.sender" + w.ID() +} + +func recipientWallet(w ownerWallet) string { + return "hashescrow.recipient" + w.ID() +} diff --git a/token/services/interop/hashescrow/script_test.go b/token/services/interop/hashescrow/script_test.go new file mode 100644 index 0000000000..5f3a318583 --- /dev/null +++ b/token/services/interop/hashescrow/script_test.go @@ -0,0 +1,63 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package hashescrow + +import ( + "crypto" + "encoding/json" + "testing" + + "github.com/hyperledger-labs/fabric-token-sdk/token/services/interop/encoding" + "github.com/stretchr/testify/require" +) + +func TestScriptValidate(t *testing.T) { + s := &Script{} + require.EqualError(t, s.Validate(), "sender not set") + + s.Sender = []byte("sender") + require.EqualError(t, s.Validate(), "recipient not set") + + s.Recipient = []byte("recipient") + s.HashInfo = HashInfo{Hash: []byte("h")} + require.Error(t, s.Validate()) + + s.HashInfo = HashInfo{Hash: []byte("h"), HashFunc: crypto.SHA256, HashEncoding: encoding.Base64} + require.NoError(t, s.Validate()) +} + +func TestScriptFromBytes(t *testing.T) { + raw, err := json.Marshal(&Script{ + Sender: []byte("sender"), + Recipient: []byte("recipient"), + HashInfo: HashInfo{ + Hash: []byte("h"), + HashFunc: crypto.SHA256, + HashEncoding: encoding.Base64, + }, + }) + require.NoError(t, err) + + s := &Script{} + require.NoError(t, s.FromBytes(raw)) + require.Equal(t, []byte("sender"), []byte(s.Sender)) + require.Equal(t, []byte("recipient"), []byte(s.Recipient)) + require.Equal(t, []byte("h"), s.HashInfo.Hash) + + require.Error(t, s.FromBytes([]byte("bad-json"))) +} + +func TestKeys(t *testing.T) { + k := ClaimKey([]byte{0x01, 0x02}) + require.Equal(t, "hashescrow.cpi0102", k) + + k = LockKey([]byte{0x0a}) + require.Equal(t, "hashescrow.lh0a", k) + + v := LockValue([]byte{0x0a, 0x0b}) + require.Equal(t, []byte("0a0b"), v) +} diff --git a/token/services/interop/hashescrow/signer.go b/token/services/interop/hashescrow/signer.go new file mode 100644 index 0000000000..5a31df6e9d --- /dev/null +++ b/token/services/interop/hashescrow/signer.go @@ -0,0 +1,112 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package hashescrow + +import ( + "encoding/json" + + "github.com/hyperledger-labs/fabric-smart-client/pkg/utils/errors" + "github.com/hyperledger-labs/fabric-token-sdk/token/driver" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/interop/htlc" +) + +// ClaimSignature carries the preimage and the claimant signature. +type ClaimSignature struct { + ClaimantSignature []byte + Preimage []byte +} + +// ClaimSigner signs token request bytes and embeds the preimage. +type ClaimSigner struct { + Claimant driver.Signer + Preimage []byte +} + +// Sign returns a signature of the claimant over token request and preimage. +func (cs *ClaimSigner) Sign(tokenRequestAndTxID []byte) ([]byte, error) { + msg := concatTokenRequestTxIDPreimage(tokenRequestAndTxID, cs.Preimage) + sigma, err := cs.Claimant.Sign(msg) + if err != nil { + return nil, err + } + + claimSignature := ClaimSignature{ + Preimage: cs.Preimage, + ClaimantSignature: sigma, + } + + return json.Marshal(claimSignature) +} + +func concatTokenRequestTxIDPreimage(tokenRequestAndTxID []byte, preImage []byte) []byte { + var msg []byte + msg = append(msg, tokenRequestAndTxID...) + msg = append(msg, preImage...) + + return msg +} + +// ClaimVerifier checks preimage validity and claimant signature. +type ClaimVerifier struct { + Claimant driver.Verifier + HashInfo HashInfo +} + +func (cv *ClaimVerifier) Verify(tokenRequestAndTxID, claimSignature []byte) error { + sig := &ClaimSignature{} + err := json.Unmarshal(claimSignature, sig) + if err != nil { + return errors.Wrapf(err, "failed to unmarshal claim signature") + } + + msg := concatTokenRequestTxIDPreimage(tokenRequestAndTxID, sig.Preimage) + if err := cv.Claimant.Verify(msg, sig.ClaimantSignature); err != nil { + return errors.WithMessagef(err, "failed to verify claimant signature") + } + + image, err := (&htlc.HashInfo{ + Hash: cv.HashInfo.Hash, + HashFunc: cv.HashInfo.HashFunc, + HashEncoding: cv.HashInfo.HashEncoding, + }).Image(sig.Preimage) + if err != nil { + return err + } + if err := (&htlc.HashInfo{Hash: cv.HashInfo.Hash}).Compare(image); err != nil { + return errors.Errorf("hash mismatch: %s", err) + } + + return nil +} + +// Verifier validates claims for hash-based escrow scripts. +// A valid claim is signed by either recipient or sender and carries a valid preimage. +type Verifier struct { + Recipient driver.Verifier + Sender driver.Verifier + HashInfo HashInfo +} + +func (v *Verifier) Verify(msg []byte, sigma []byte) error { + recipientClaimVerifier := &ClaimVerifier{ + Claimant: v.Recipient, + HashInfo: v.HashInfo, + } + if err := recipientClaimVerifier.Verify(msg, sigma); err == nil { + return nil + } + + senderClaimVerifier := &ClaimVerifier{ + Claimant: v.Sender, + HashInfo: v.HashInfo, + } + if err := senderClaimVerifier.Verify(msg, sigma); err != nil { + return errors.WithMessagef(err, "failed verifying hash escrow claim signature") + } + + return nil +} diff --git a/token/services/interop/hashescrow/signer_test.go b/token/services/interop/hashescrow/signer_test.go new file mode 100644 index 0000000000..5c328b56ed --- /dev/null +++ b/token/services/interop/hashescrow/signer_test.go @@ -0,0 +1,145 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package hashescrow_test + +import ( + "crypto" + "encoding/json" + "errors" + "testing" + + "github.com/hyperledger-labs/fabric-token-sdk/token/driver/mock" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/interop/encoding" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/interop/hashescrow" + "github.com/stretchr/testify/require" +) + +func validClaimSigBytes(t *testing.T, preimage, claimantSig []byte) []byte { + t.Helper() + raw, err := json.Marshal(hashescrow.ClaimSignature{Preimage: preimage, ClaimantSignature: claimantSig}) + require.NoError(t, err) + + return raw +} + +func claimHashInfo(preimage []byte) hashescrow.HashInfo { + h := crypto.SHA256.New() + _, _ = h.Write(preimage) + + return hashescrow.HashInfo{ + Hash: []byte(encoding.Base64.New().EncodeToString(h.Sum(nil))), + HashFunc: crypto.SHA256, + HashEncoding: encoding.Base64, + } +} + +func TestClaimSignerSign(t *testing.T) { + preimage := []byte("secret") + msg := []byte("txrequest") + expectedSig := []byte("claimant-sig") + + signer := &mock.Signer{} + signer.SignReturns(expectedSig, nil) + + cs := &hashescrow.ClaimSigner{Claimant: signer, Preimage: preimage} + raw, err := cs.Sign(msg) + require.NoError(t, err) + + var claimSig hashescrow.ClaimSignature + require.NoError(t, json.Unmarshal(raw, &claimSig)) + require.Equal(t, preimage, claimSig.Preimage) + require.Equal(t, expectedSig, claimSig.ClaimantSignature) + require.Equal(t, 1, signer.SignCallCount()) + require.Equal(t, append(msg, preimage...), signer.SignArgsForCall(0)) +} + +func TestClaimSignerSignError(t *testing.T) { + signer := &mock.Signer{} + signer.SignReturns(nil, errors.New("sign failed")) + + cs := &hashescrow.ClaimSigner{Claimant: signer, Preimage: []byte("p")} + _, err := cs.Sign([]byte("req")) + require.EqualError(t, err, "sign failed") +} + +func TestClaimVerifierVerify(t *testing.T) { + preimage := []byte("secret") + msg := []byte("txrequest") + + verifier := &mock.Verifier{} + verifier.VerifyReturns(nil) + + cv := &hashescrow.ClaimVerifier{Claimant: verifier, HashInfo: claimHashInfo(preimage)} + require.NoError(t, cv.Verify(msg, validClaimSigBytes(t, preimage, []byte("sig")))) + vmsg, _ := verifier.VerifyArgsForCall(0) + require.Equal(t, append(msg, preimage...), vmsg) +} + +func TestClaimVerifierErrors(t *testing.T) { + cv := &hashescrow.ClaimVerifier{Claimant: &mock.Verifier{}, HashInfo: claimHashInfo([]byte("p"))} + require.Error(t, cv.Verify([]byte("req"), []byte("bad-json"))) + + verifier := &mock.Verifier{} + verifier.VerifyReturns(errors.New("bad sig")) + cv = &hashescrow.ClaimVerifier{Claimant: verifier, HashInfo: claimHashInfo([]byte("p"))} + err := cv.Verify([]byte("req"), validClaimSigBytes(t, []byte("p"), []byte("sig"))) + require.Error(t, err) + require.Contains(t, err.Error(), "failed to verify claimant signature") + + verifier = &mock.Verifier{} + verifier.VerifyReturns(nil) + cv = &hashescrow.ClaimVerifier{ + Claimant: verifier, + HashInfo: claimHashInfo([]byte("different-preimage")), + } + err = cv.Verify([]byte("req"), validClaimSigBytes(t, []byte("p"), []byte("sig"))) + require.Error(t, err) + require.Contains(t, err.Error(), "hash mismatch") +} + +func TestVerifierVerify(t *testing.T) { + preimage := []byte("secret") + msg := []byte("txrequest") + sigma := validClaimSigBytes(t, preimage, []byte("sig")) + + recipientVerifier := &mock.Verifier{} + recipientVerifier.VerifyReturns(nil) + senderVerifier := &mock.Verifier{} + v := &hashescrow.Verifier{ + Recipient: recipientVerifier, + Sender: senderVerifier, + HashInfo: claimHashInfo(preimage), + } + require.NoError(t, v.Verify(msg, sigma)) + require.Equal(t, 1, recipientVerifier.VerifyCallCount()) + require.Equal(t, 0, senderVerifier.VerifyCallCount()) + + recipientVerifier = &mock.Verifier{} + recipientVerifier.VerifyReturns(errors.New("bad recipient sig")) + senderVerifier = &mock.Verifier{} + senderVerifier.VerifyReturns(nil) + v = &hashescrow.Verifier{ + Recipient: recipientVerifier, + Sender: senderVerifier, + HashInfo: claimHashInfo(preimage), + } + require.NoError(t, v.Verify(msg, sigma)) + require.Equal(t, 1, senderVerifier.VerifyCallCount()) + + recipientVerifier = &mock.Verifier{} + recipientVerifier.VerifyReturns(errors.New("bad recipient sig")) + senderVerifier = &mock.Verifier{} + senderVerifier.VerifyReturns(errors.New("bad sender sig")) + v = &hashescrow.Verifier{ + Recipient: recipientVerifier, + Sender: senderVerifier, + HashInfo: claimHashInfo(preimage), + } + err := v.Verify(msg, sigma) + require.Error(t, err) + require.Contains(t, err.Error(), "failed verifying hash escrow claim signature") +} diff --git a/token/services/interop/hashescrow/transaction.go b/token/services/interop/hashescrow/transaction.go new file mode 100644 index 0000000000..373dfb8482 --- /dev/null +++ b/token/services/interop/hashescrow/transaction.go @@ -0,0 +1,323 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package hashescrow + +import ( + "context" + "crypto" + "crypto/rand" + "encoding/base64" + "encoding/json" + + "github.com/hyperledger-labs/fabric-smart-client/pkg/utils/errors" + "github.com/hyperledger-labs/fabric-smart-client/platform/view/services/endpoint" + "github.com/hyperledger-labs/fabric-smart-client/platform/view/view" + "github.com/hyperledger-labs/fabric-token-sdk/token" + "github.com/hyperledger-labs/fabric-token-sdk/token/driver" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/identity" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/interop/encoding" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/ttx" + token2 "github.com/hyperledger-labs/fabric-token-sdk/token/token" +) + +const ( + ScriptType = driver.HashEscrowIdentityType + ScriptTypeString = driver.HashEscrowIdentityTypeString +) + +// WithHash sets a hash attribute to customize the lock command. +func WithHash(hash []byte) token.TransferOption { + return func(o *token.TransferOptions) error { + if o.Attributes == nil { + o.Attributes = map[interface{}]interface{}{} + } + o.Attributes["hashescrow.hash"] = hash + + return nil + } +} + +// WithHashFunc sets a hash function attribute to customize the lock command. +func WithHashFunc(hashFunc crypto.Hash) token.TransferOption { + return func(o *token.TransferOptions) error { + if o.Attributes == nil { + o.Attributes = map[interface{}]interface{}{} + } + o.Attributes["hashescrow.hashFunc"] = hashFunc + + return nil + } +} + +// WithHashEncoding sets a hash encoding attribute to customize the lock command. +func WithHashEncoding(enc encoding.Encoding) token.TransferOption { + return func(o *token.TransferOptions) error { + if o.Attributes == nil { + o.Attributes = map[interface{}]interface{}{} + } + o.Attributes["hashescrow.hashEncoding"] = enc + + return nil + } +} + +func compileTransferOptions(opts ...token.TransferOption) (*token.TransferOptions, error) { + txOptions := &token.TransferOptions{} + for _, opt := range opts { + if err := opt(txOptions); err != nil { + return nil, err + } + } + + return txOptions, nil +} + +type Binder interface { + Bind(ctx context.Context, longTerm view.Identity, ephemeral ...view.Identity) error +} + +// Transaction holds a ttx transaction. +type Transaction struct { + *ttx.Transaction + Binder Binder +} + +func NewTransaction(sp view.Context, signer view.Identity, opts ...ttx.TxOption) (*Transaction, error) { + tx, err := ttx.NewTransaction(sp, signer, opts...) + if err != nil { + return nil, err + } + + return &Transaction{Transaction: tx, Binder: endpoint.GetService(sp)}, nil +} + +func NewAnonymousTransaction(sp view.Context, opts ...ttx.TxOption) (*Transaction, error) { + tx, err := ttx.NewAnonymousTransaction(sp, opts...) + if err != nil { + return nil, err + } + + return &Transaction{Transaction: tx, Binder: endpoint.GetService(sp)}, nil +} + +func NewTransactionFromBytes(ctx view.Context, network, channel string, raw []byte) (*Transaction, error) { + tx, err := ttx.NewTransactionFromBytes(ctx, raw) + if err != nil { + return nil, err + } + + return &Transaction{Transaction: tx, Binder: endpoint.GetService(ctx)}, nil +} + +// Lock appends a hash-based escrow lock action to the token request. +// This lock has no timeout semantics. +func (t *Transaction) Lock(ctx context.Context, wallet *token.OwnerWallet, sender view.Identity, typ token2.Type, value uint64, recipient view.Identity, opts ...token.TransferOption) ([]byte, error) { + options, err := compileTransferOptions(opts...) + if err != nil { + return nil, err + } + if recipient.IsNone() { + return nil, errors.Errorf("must specify a recipient") + } + if sender == nil { + sender, err = wallet.GetRecipientIdentity(ctx) + if err != nil { + return nil, errors.WithMessagef(err, "failed getting sender identity") + } + } + + var hash []byte + hashFunc := crypto.SHA256 + var hashEncoding encoding.Encoding + if options.Attributes != nil { + boxed, ok := options.Attributes["hashescrow.hash"] + if ok { + hash, ok = boxed.([]byte) + if !ok { + return nil, errors.Errorf("expected hashescrow.hash attribute to be []byte, got [%T]", boxed) + } + } + boxed, ok = options.Attributes["hashescrow.hashFunc"] + if ok { + hashFunc, ok = boxed.(crypto.Hash) + if !ok { + return nil, errors.Errorf("expected hashescrow.hashFunc attribute to be crypto.Hash, got [%T]", boxed) + } + if hashFunc == 0 { + hashFunc = crypto.SHA256 + } + } + boxed, ok = options.Attributes["hashescrow.hashEncoding"] + if ok { + hashEncoding, ok = boxed.(encoding.Encoding) + if !ok { + return nil, errors.Errorf("expected hashescrow.hashEncoding attribute to be Encoding, got [%T]", boxed) + } + } + } + + scriptID, preImage, script, err := t.recipientAsScript(sender, recipient, hash, hashFunc, hashEncoding) + if err != nil { + return nil, err + } + _, err = t.TokenRequest.Transfer( + t.Context, + wallet, + typ, + []uint64{value}, + []view.Identity{scriptID}, + append(opts, token.WithTransferMetadata(LockKey(script.HashInfo.Hash), LockValue(script.HashInfo.Hash)))..., + ) + if err != nil { + return nil, err + } + + return preImage, nil +} + +// Claim appends a claim action to the token request. +// A claim can be performed by either party encoded in the script. +func (t *Transaction) Claim(wallet *token.OwnerWallet, tok *token2.UnspentToken, preImage []byte, opts ...token.TransferOption) error { + if len(preImage) == 0 { + return errors.New("preImage is nil") + } + + q, err := token2.ToQuantity(tok.Quantity, t.TokenRequest.TokenService.PublicParametersManager().PublicParameters().Precision()) + if err != nil { + return errors.Wrapf(err, "failed to convert quantity [%s]", tok.Quantity) + } + owner, err := identity.UnmarshalTypedIdentity(tok.Owner) + if err != nil { + return err + } + if owner.Type != ScriptType { + return errors.New("invalid owner type, expected hash escrow script") + } + script := &Script{} + if err := json.Unmarshal(owner.Identity, script); err != nil { + return errors.New("failed to unmarshal TypedIdentity as a hash escrow script") + } + + claimIdentity := script.Recipient + if !wallet.Contains(t.Context, script.Recipient) { + if wallet.Contains(t.Context, script.Sender) { + claimIdentity = script.Sender + } else { + return errors.New("passed wallet does not contain sender nor recipient identity for this script") + } + } + + image, err := script.HashInfo.Image(preImage) + if err != nil { + return errors.Wrapf(err, "failed to compute image of [%x]", preImage) + } + if err := script.HashInfo.Compare(image); err != nil { + return errors.Wrap(err, "passed preImage does not match script hash") + } + + sigService := t.TokenService().SigService() + claimSigner, err := sigService.GetSigner(t.Context, claimIdentity) + if err != nil { + return err + } + claimVerifier, err := sigService.OwnerVerifier(t.Context, claimIdentity) + if err != nil { + return err + } + if err := sigService.RegisterEphemeralSigner( + t.Context, + tok.Owner, + &ClaimSigner{ + Claimant: claimSigner, + Preimage: preImage, + }, + &ClaimVerifier{ + Claimant: claimVerifier, + HashInfo: script.HashInfo, + }, + ); err != nil { + return err + } + + if err := t.Binder.Bind(t.Context, claimIdentity, tok.Owner); err != nil { + return err + } + + return t.Transfer( + wallet, + tok.Type, + []uint64{q.ToBigInt().Uint64()}, + []view.Identity{claimIdentity}, + append(opts, token.WithTokenIDs(&tok.Id), token.WithTransferMetadata(ClaimKey(image), preImage))..., + ) +} + +func (t *Transaction) recipientAsScript(sender, recipient view.Identity, h []byte, hashFunc crypto.Hash, hashEncoding encoding.Encoding) (view.Identity, []byte, *Script, error) { + var preImage []byte + var err error + if len(h) == 0 { + preImage, err = CreateNonce() + if err != nil { + return nil, nil, nil, err + } + hash := hashFunc.New() + if _, err := hash.Write(preImage); err != nil { + return nil, nil, nil, err + } + h = hash.Sum(nil) + if hashEncoding != 0 { + enc := hashEncoding.New() + if enc == nil { + return nil, nil, nil, errors.New("hashEncoding.New() returned nil") + } + h = []byte(enc.EncodeToString(h)) + } + } + + logger.Debugf("pair (pre-image, hash) = (%s,%s)", base64.StdEncoding.EncodeToString(preImage), base64.StdEncoding.EncodeToString(h)) + + script := &Script{ + HashInfo: HashInfo{ + Hash: h, + HashFunc: hashFunc, + HashEncoding: hashEncoding, + }, + Recipient: recipient, + Sender: sender, + } + rawScript, err := json.Marshal(script) + if err != nil { + return nil, nil, nil, err + } + ro := &identity.TypedIdentity{ + Type: ScriptType, + Identity: rawScript, + } + raw, err := ro.Bytes() + if err != nil { + return nil, nil, nil, err + } + + return raw, preImage, script, nil +} + +// CreateNonce generates a nonce. +func CreateNonce() ([]byte, error) { + nonce, err := getRandomNonce() + + return nonce, errors.WithMessagef(err, "error generating random nonce") +} + +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/hashescrow/transaction_test.go b/token/services/interop/hashescrow/transaction_test.go new file mode 100644 index 0000000000..4f3a28601a --- /dev/null +++ b/token/services/interop/hashescrow/transaction_test.go @@ -0,0 +1,66 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package hashescrow + +import ( + "crypto" + "testing" + + "github.com/hyperledger-labs/fabric-token-sdk/token/services/interop/encoding" + "github.com/stretchr/testify/require" +) + +func TestCompileTransferOptions(t *testing.T) { + opts, err := compileTransferOptions( + WithHash([]byte("h")), + WithHashFunc(crypto.SHA512), + WithHashEncoding(encoding.Hex), + ) + require.NoError(t, err) + require.NotNil(t, opts.Attributes) + require.Equal(t, []byte("h"), opts.Attributes["hashescrow.hash"]) + require.Equal(t, crypto.SHA512, opts.Attributes["hashescrow.hashFunc"]) + require.Equal(t, encoding.Hex, opts.Attributes["hashescrow.hashEncoding"]) +} + +func TestRecipientAsScript(t *testing.T) { + tx := &Transaction{} + sender := []byte("sender") + recipient := []byte("recipient") + + raw, preimage, script, err := tx.recipientAsScript(sender, recipient, []byte("custom-hash"), crypto.SHA256, encoding.Base64) + require.NoError(t, err) + require.Empty(t, preimage) + require.NotNil(t, raw) + require.Equal(t, sender, []byte(script.Sender)) + require.Equal(t, recipient, []byte(script.Recipient)) + require.Equal(t, []byte("custom-hash"), script.HashInfo.Hash) + + raw, preimage, script, err = tx.recipientAsScript(sender, recipient, nil, crypto.SHA256, encoding.Base64) + require.NoError(t, err) + require.NotNil(t, raw) + require.NotEmpty(t, preimage) + require.NotEmpty(t, script.HashInfo.Hash) +} + +func TestRecipientAsScriptBadEncoding(t *testing.T) { + tx := &Transaction{} + _, _, _, err := tx.recipientAsScript( + []byte("sender"), + []byte("recipient"), + nil, + crypto.SHA256, + encoding.Encoding(999), + ) + require.EqualError(t, err, "hashEncoding.New() returned nil") +} + +func TestCreateNonce(t *testing.T) { + nonce, err := CreateNonce() + require.NoError(t, err) + require.Len(t, nonce, 24) +} From 56749b4e097c78b068c66c5778e0644576aca6de Mon Sep 17 00:00:00 2001 From: Flamki <9833ayush@gmail.com> Date: Sat, 9 May 2026 18:26:59 +0530 Subject: [PATCH 02/18] feat(hashescrow): resolve recipient by preimage with dual hashes Signed-off-by: Flamki <9833ayush@gmail.com> --- .../v1/validator/validator_transfer.go | 11 +- .../nogh/v1/validator/validator_transfer.go | 11 +- .../interop/hashescrow/deserializer.go | 16 +- .../interop/hashescrow/deserializer_test.go | 7 +- .../identity/interop/hashescrow/validator.go | 48 +++-- .../interop/hashescrow/validator_test.go | 39 ++-- token/services/interop/hashescrow/script.go | 39 +++- .../interop/hashescrow/script_test.go | 53 +++++- token/services/interop/hashescrow/signer.go | 78 ++------ .../interop/hashescrow/signer_test.go | 154 +++++---------- .../interop/hashescrow/transaction.go | 180 ++++++++++++------ .../interop/hashescrow/transaction_test.go | 25 ++- 12 files changed, 371 insertions(+), 290 deletions(-) diff --git a/token/core/fabtoken/v1/validator/validator_transfer.go b/token/core/fabtoken/v1/validator/validator_transfer.go index ff0885b9d5..0f13ee1363 100644 --- a/token/core/fabtoken/v1/validator/validator_transfer.go +++ b/token/core/fabtoken/v1/validator/validator_transfer.go @@ -211,10 +211,13 @@ func TransferHTLCValidate(c context.Context, ctx *Context) error { // check metadata sigma := ctx.Signatures[i] - metadataKey, err := hashescrow2.MetadataClaimKeyCheck(ctx.TransferAction, script, sigma) + metadataKey, resolvedOwner, err := hashescrow2.MetadataClaimKeyCheck(ctx.TransferAction, script, sigma) if err != nil { return errors.WithMessagef(err, "failed to check hash escrow metadata") } + if !identity.Identity(resolvedOwner).Equal(tok.Owner) { + return errors.New("invalid transfer action: output owner does not match hash escrow recipient resolved from preimage") + } ctx.CountMetadataKey(metadataKey) } } @@ -259,11 +262,13 @@ func TransferHTLCValidate(c context.Context, ctx *Context) error { if err := script.Validate(); err != nil { return errors.WithMessagef(err, "hash escrow script invalid") } - metadataKey, err := hashescrow2.MetadataLockKeyCheck(ctx.TransferAction, script) + metadataKeys, err := hashescrow2.MetadataLockKeyCheck(ctx.TransferAction, script) if err != nil { return errors.WithMessagef(err, "failed to check hash escrow metadata") } - ctx.CountMetadataKey(metadataKey) + for _, metadataKey := range metadataKeys { + ctx.CountMetadataKey(metadataKey) + } continue } diff --git a/token/core/zkatdlog/nogh/v1/validator/validator_transfer.go b/token/core/zkatdlog/nogh/v1/validator/validator_transfer.go index ac4ffb419f..6421c34b59 100644 --- a/token/core/zkatdlog/nogh/v1/validator/validator_transfer.go +++ b/token/core/zkatdlog/nogh/v1/validator/validator_transfer.go @@ -201,10 +201,13 @@ func TransferHTLCValidate(c context.Context, ctx *Context) error { } sigma := ctx.Signatures[i] - metadataKey, err := hashescrow2.MetadataClaimKeyCheck(ctx.TransferAction, script, sigma) + metadataKey, resolvedOwner, err := hashescrow2.MetadataClaimKeyCheck(ctx.TransferAction, script, sigma) if err != nil { return errors.WithMessagef(err, "failed to check hash escrow metadata") } + if !identity.Identity(resolvedOwner).Equal(out.Owner) { + return errors.New("invalid transfer action: output owner does not match hash escrow recipient resolved from preimage") + } ctx.CountMetadataKey(metadataKey) } } @@ -243,11 +246,13 @@ func TransferHTLCValidate(c context.Context, ctx *Context) error { if err := script.Validate(); err != nil { return errors.WithMessagef(err, "hash escrow script invalid") } - metadataKey, err := hashescrow2.MetadataLockKeyCheck(ctx.TransferAction, script) + metadataKeys, err := hashescrow2.MetadataLockKeyCheck(ctx.TransferAction, script) if err != nil { return errors.WithMessagef(err, "failed to check hash escrow metadata") } - ctx.CountMetadataKey(metadataKey) + for _, metadataKey := range metadataKeys { + ctx.CountMetadataKey(metadataKey) + } continue } diff --git a/token/services/identity/interop/hashescrow/deserializer.go b/token/services/identity/interop/hashescrow/deserializer.go index 29c5ae54c1..9c9f95ccfd 100644 --- a/token/services/identity/interop/hashescrow/deserializer.go +++ b/token/services/identity/interop/hashescrow/deserializer.go @@ -46,18 +46,14 @@ func (t *TypedIdentityDeserializer) DeserializeVerifier(ctx context.Context, typ if err != nil { return nil, errors.Errorf("failed to unmarshal TypedIdentity as a hash escrow script") } - v := &hashescrow.Verifier{} - v.Sender, err = t.deserializer.DeserializeVerifier(ctx, script.Sender) - if err != nil { - return nil, errors.Errorf("failed to unmarshal the identity of the sender in the hash escrow script") + // Ensure sender and recipient identities are still syntactically valid and deserializable. + if _, err = t.deserializer.DeserializeVerifier(ctx, script.Sender); err != nil { + return nil, errors.Errorf("failed to deserialize the identity of the sender in the hash escrow script") } - v.Recipient, err = t.deserializer.DeserializeVerifier(ctx, script.Recipient) - if err != nil { - return nil, errors.Errorf("failed to unmarshal the identity of the recipient in the hash escrow script") + if _, err = t.deserializer.DeserializeVerifier(ctx, script.Recipient); err != nil { + return nil, errors.Errorf("failed to deserialize the identity of the recipient in the hash escrow script") } - v.HashInfo.Hash = script.HashInfo.Hash - v.HashInfo.HashFunc = script.HashInfo.HashFunc - v.HashInfo.HashEncoding = script.HashInfo.HashEncoding + v := &hashescrow.Verifier{Script: script} return v, nil } diff --git a/token/services/identity/interop/hashescrow/deserializer_test.go b/token/services/identity/interop/hashescrow/deserializer_test.go index edc6695ed6..f856dd69ef 100644 --- a/token/services/identity/interop/hashescrow/deserializer_test.go +++ b/token/services/identity/interop/hashescrow/deserializer_test.go @@ -52,7 +52,12 @@ func mkScript(t *testing.T, sender, recipient []byte) []byte { s := &he.Script{ Sender: sender, Recipient: recipient, - HashInfo: he.HashInfo{Hash: []byte("h")}, + RecipientHashInfo: he.HashInfo{ + Hash: []byte("rh"), + }, + SenderHashInfo: he.HashInfo{ + Hash: []byte("sh"), + }, } raw, err := json.Marshal(s) require.NoError(t, err) diff --git a/token/services/identity/interop/hashescrow/validator.go b/token/services/identity/interop/hashescrow/validator.go index ec7de4e056..d261126bf6 100644 --- a/token/services/identity/interop/hashescrow/validator.go +++ b/token/services/identity/interop/hashescrow/validator.go @@ -45,49 +45,57 @@ func VerifyOwner(senderRawOwner []byte, outRawOwner []byte) (*hashescrow.Script, } // MetadataClaimKeyCheck validates claim metadata and returns the matched metadata key. -func MetadataClaimKeyCheck(action Action, script *hashescrow.Script, sig []byte) (string, error) { +func MetadataClaimKeyCheck(action Action, script *hashescrow.Script, sig []byte) (string, []byte, error) { claim := &hashescrow.ClaimSignature{} if err := json.Unmarshal(sig, claim); err != nil { - return "", errors.Wrapf(err, "failed unmarshalling claim signature [%s]", string(sig)) + return "", nil, errors.Wrapf(err, "failed unmarshalling claim signature [%s]", string(sig)) } - if len(claim.Preimage) == 0 || len(claim.ClaimantSignature) == 0 { - return "", errors.New("expected a valid claim preImage and claimant signature") + if len(claim.Preimage) == 0 { + return "", nil, errors.New("expected a valid claim preImage") + } + resolvedOwner, image, err := script.ResolveRecipientForPreImage(claim.Preimage) + if err != nil { + return "", nil, errors.Wrap(err, "failed resolving recipient from preimage") } metadata := action.GetMetadata() if len(metadata) == 0 { - return "", errors.New("cannot find hash escrow pre-image, no metadata") - } - image, err := script.HashInfo.Image(claim.Preimage) - if err != nil { - return "", errors.Wrapf(err, "failed to compute image of [%x]", claim.Preimage) + return "", nil, errors.New("cannot find hash escrow pre-image, no metadata") } key := hashescrow.ClaimKey(image) value, ok := metadata[key] if !ok { - return "", errors.New("cannot find hash escrow pre-image, missing metadata entry") + return "", nil, errors.New("cannot find hash escrow pre-image, missing metadata entry") } if !bytes.Equal(value, claim.Preimage) { - return "", errors.Errorf("invalid action, cannot match hash escrow pre-image with metadata [%x]!=[%x]", value, claim.Preimage) + return "", nil, errors.Errorf("invalid action, cannot match hash escrow pre-image with metadata [%x]!=[%x]", value, claim.Preimage) } - return key, nil + return key, resolvedOwner, nil } // MetadataLockKeyCheck validates lock metadata and returns the lock metadata key. -func MetadataLockKeyCheck(action Action, script *hashescrow.Script) (string, error) { +func MetadataLockKeyCheck(action Action, script *hashescrow.Script) ([]string, error) { metadata := action.GetMetadata() if len(metadata) == 0 { - return "", errors.New("cannot find hash escrow lock, no metadata") + return nil, errors.New("cannot find hash escrow lock, no metadata") } - key := hashescrow.LockKey(script.HashInfo.Hash) - value, ok := metadata[key] + recipientKey := hashescrow.LockKey(script.RecipientHashInfo.Hash) + recipientValue, ok := metadata[recipientKey] + if !ok { + return nil, errors.New("cannot find recipient hash escrow lock, missing metadata entry") + } + if !bytes.Equal(recipientValue, hashescrow.LockValue(script.RecipientHashInfo.Hash)) { + return nil, errors.Errorf("invalid action, cannot match recipient hash escrow lock with metadata [%x]!=[%x]", recipientValue, script.RecipientHashInfo.Hash) + } + senderKey := hashescrow.LockKey(script.SenderHashInfo.Hash) + senderValue, ok := metadata[senderKey] if !ok { - return "", errors.New("cannot find hash escrow lock, missing metadata entry") + return nil, errors.New("cannot find sender hash escrow lock, missing metadata entry") } - if !bytes.Equal(value, hashescrow.LockValue(script.HashInfo.Hash)) { - return "", errors.Errorf("invalid action, cannot match hash escrow lock with metadata [%x]!=[%x]", value, script.HashInfo.Hash) + if !bytes.Equal(senderValue, hashescrow.LockValue(script.SenderHashInfo.Hash)) { + return nil, errors.Errorf("invalid action, cannot match sender hash escrow lock with metadata [%x]!=[%x]", senderValue, script.SenderHashInfo.Hash) } - return key, nil + return []string{recipientKey, senderKey}, nil } diff --git a/token/services/identity/interop/hashescrow/validator_test.go b/token/services/identity/interop/hashescrow/validator_test.go index 1d06e636bf..74a7633e74 100644 --- a/token/services/identity/interop/hashescrow/validator_test.go +++ b/token/services/identity/interop/hashescrow/validator_test.go @@ -28,11 +28,19 @@ func (a *actionStub) GetMetadata() map[string][]byte { func wrapScript(t *testing.T, sender, recipient []byte, hash []byte) ([]byte, *he.Script) { t.Helper() + recipientHash := append([]byte{}, hash...) + senderHash := append([]byte{}, hash...) + senderHash = append(senderHash, byte('2')) script := &he.Script{ Sender: sender, Recipient: recipient, - HashInfo: he.HashInfo{ - Hash: hash, + RecipientHashInfo: he.HashInfo{ + Hash: recipientHash, + HashFunc: crypto.SHA256, + HashEncoding: encoding.Base64, + }, + SenderHashInfo: he.HashInfo{ + Hash: senderHash, HashFunc: crypto.SHA256, HashEncoding: encoding.Base64, }, @@ -74,43 +82,50 @@ func TestMetadataClaimKeyCheck(t *testing.T) { _, script := wrapScript(t, []byte("s"), []byte("r"), []byte("h")) preimage := []byte("pre") - image, err := script.HashInfo.Image(preimage) + image, err := script.RecipientHashInfo.Image(preimage) require.NoError(t, err) + script.RecipientHashInfo.Hash = image key := he.ClaimKey(image) sigRaw, err := json.Marshal(&he.ClaimSignature{ - ClaimantSignature: []byte("sig"), - Preimage: preimage, + Preimage: preimage, }) require.NoError(t, err) act := &actionStub{md: map[string][]byte{key: preimage}} - got, err := ihe.MetadataClaimKeyCheck(act, script, sigRaw) + got, _, err := ihe.MetadataClaimKeyCheck(act, script, sigRaw) require.NoError(t, err) require.Equal(t, key, got) act = &actionStub{md: map[string][]byte{}} - _, err = ihe.MetadataClaimKeyCheck(act, script, sigRaw) + _, _, err = ihe.MetadataClaimKeyCheck(act, script, sigRaw) require.Error(t, err) - _, err = ihe.MetadataClaimKeyCheck(act, script, []byte("bad-json")) + _, _, err = ihe.MetadataClaimKeyCheck(act, script, []byte("bad-json")) require.Error(t, err) } func TestMetadataLockKeyCheck(t *testing.T) { _, script := wrapScript(t, []byte("s"), []byte("r"), []byte("h")) - key := he.LockKey(script.HashInfo.Hash) + recipientKey := he.LockKey(script.RecipientHashInfo.Hash) + senderKey := he.LockKey(script.SenderHashInfo.Hash) - act := &actionStub{md: map[string][]byte{key: he.LockValue(script.HashInfo.Hash)}} + act := &actionStub{md: map[string][]byte{ + recipientKey: he.LockValue(script.RecipientHashInfo.Hash), + senderKey: he.LockValue(script.SenderHashInfo.Hash), + }} got, err := ihe.MetadataLockKeyCheck(act, script) require.NoError(t, err) - require.Equal(t, key, got) + require.ElementsMatch(t, []string{recipientKey, senderKey}, got) act = &actionStub{md: map[string][]byte{}} _, err = ihe.MetadataLockKeyCheck(act, script) require.Error(t, err) - act = &actionStub{md: map[string][]byte{key: []byte("bad")}} + act = &actionStub{md: map[string][]byte{ + recipientKey: []byte("bad"), + senderKey: he.LockValue(script.SenderHashInfo.Hash), + }} _, err = ihe.MetadataLockKeyCheck(act, script) require.Error(t, err) } diff --git a/token/services/interop/hashescrow/script.go b/token/services/interop/hashescrow/script.go index 8cebac9be4..37e3c027b2 100644 --- a/token/services/interop/hashescrow/script.go +++ b/token/services/interop/hashescrow/script.go @@ -25,15 +25,16 @@ type HashInfo = htlc.HashInfo // Script contains the details of a hash-based escrow lock. // Either sender or recipient can claim by presenting a valid preimage and signature. type Script struct { - Sender view.Identity - Recipient view.Identity - HashInfo HashInfo + Sender view.Identity + Recipient view.Identity + RecipientHashInfo HashInfo + SenderHashInfo HashInfo } // Validate performs the following checks: // - sender must be set // - recipient must be set -// - hash info must be valid +// - both recipient and sender hash info must be valid func (s *Script) Validate() error { if s.Sender.IsNone() { return errors.New("sender not set") @@ -41,13 +42,39 @@ func (s *Script) Validate() error { if s.Recipient.IsNone() { return errors.New("recipient not set") } - if err := s.HashInfo.Validate(); err != nil { - return err + if err := s.RecipientHashInfo.Validate(); err != nil { + return errors.WithMessage(err, "recipient hash info invalid") + } + if err := s.SenderHashInfo.Validate(); err != nil { + return errors.WithMessage(err, "sender hash info invalid") } return nil } +// ResolveRecipientForPreImage resolves the output recipient from the provided pre-image. +// If the pre-image matches the recipient hash, recipient receives the token. +// If the pre-image matches the sender hash, sender receives the token. +func (s *Script) ResolveRecipientForPreImage(preImage []byte) (view.Identity, []byte, error) { + recipientImage, err := s.RecipientHashInfo.Image(preImage) + if err != nil { + return nil, nil, errors.WithMessage(err, "failed computing recipient hash image") + } + if err := s.RecipientHashInfo.Compare(recipientImage); err == nil { + return s.Recipient, recipientImage, nil + } + + senderImage, err := s.SenderHashInfo.Image(preImage) + if err != nil { + return nil, nil, errors.WithMessage(err, "failed computing sender hash image") + } + if err := s.SenderHashInfo.Compare(senderImage); err == nil { + return s.Sender, senderImage, nil + } + + return nil, nil, errors.New("preimage does not match any hashescrow hash") +} + func (s *Script) FromBytes(raw []byte) error { return json.Unmarshal(raw, s) } diff --git a/token/services/interop/hashescrow/script_test.go b/token/services/interop/hashescrow/script_test.go index 5f3a318583..379734a9e9 100644 --- a/token/services/interop/hashescrow/script_test.go +++ b/token/services/interop/hashescrow/script_test.go @@ -23,10 +23,11 @@ func TestScriptValidate(t *testing.T) { require.EqualError(t, s.Validate(), "recipient not set") s.Recipient = []byte("recipient") - s.HashInfo = HashInfo{Hash: []byte("h")} + s.RecipientHashInfo = HashInfo{Hash: []byte("h")} require.Error(t, s.Validate()) - s.HashInfo = HashInfo{Hash: []byte("h"), HashFunc: crypto.SHA256, HashEncoding: encoding.Base64} + s.RecipientHashInfo = HashInfo{Hash: []byte("h"), HashFunc: crypto.SHA256, HashEncoding: encoding.Base64} + s.SenderHashInfo = HashInfo{Hash: []byte("h2"), HashFunc: crypto.SHA256, HashEncoding: encoding.Base64} require.NoError(t, s.Validate()) } @@ -34,11 +35,16 @@ func TestScriptFromBytes(t *testing.T) { raw, err := json.Marshal(&Script{ Sender: []byte("sender"), Recipient: []byte("recipient"), - HashInfo: HashInfo{ + RecipientHashInfo: HashInfo{ Hash: []byte("h"), HashFunc: crypto.SHA256, HashEncoding: encoding.Base64, }, + SenderHashInfo: HashInfo{ + Hash: []byte("h2"), + HashFunc: crypto.SHA256, + HashEncoding: encoding.Base64, + }, }) require.NoError(t, err) @@ -46,11 +52,50 @@ func TestScriptFromBytes(t *testing.T) { require.NoError(t, s.FromBytes(raw)) require.Equal(t, []byte("sender"), []byte(s.Sender)) require.Equal(t, []byte("recipient"), []byte(s.Recipient)) - require.Equal(t, []byte("h"), s.HashInfo.Hash) + require.Equal(t, []byte("h"), s.RecipientHashInfo.Hash) + require.Equal(t, []byte("h2"), s.SenderHashInfo.Hash) require.Error(t, s.FromBytes([]byte("bad-json"))) } +func TestResolveRecipientForPreImage(t *testing.T) { + s := &Script{ + Sender: []byte("sender"), + Recipient: []byte("recipient"), + RecipientHashInfo: HashInfo{ + HashFunc: crypto.SHA256, + HashEncoding: encoding.Base64, + }, + SenderHashInfo: HashInfo{ + HashFunc: crypto.SHA256, + HashEncoding: encoding.Base64, + }, + } + + recipientPreImage := []byte("recipient-secret") + recipientImage, err := s.RecipientHashInfo.Image(recipientPreImage) + require.NoError(t, err) + s.RecipientHashInfo.Hash = recipientImage + + senderPreImage := []byte("sender-secret") + senderImage, err := s.SenderHashInfo.Image(senderPreImage) + require.NoError(t, err) + s.SenderHashInfo.Hash = senderImage + + owner, image, err := s.ResolveRecipientForPreImage(recipientPreImage) + require.NoError(t, err) + require.Equal(t, []byte("recipient"), []byte(owner)) + require.Equal(t, recipientImage, image) + + owner, image, err = s.ResolveRecipientForPreImage(senderPreImage) + require.NoError(t, err) + require.Equal(t, []byte("sender"), []byte(owner)) + require.Equal(t, senderImage, image) + + _, _, err = s.ResolveRecipientForPreImage([]byte("wrong")) + require.Error(t, err) +} + func TestKeys(t *testing.T) { k := ClaimKey([]byte{0x01, 0x02}) require.Equal(t, "hashescrow.cpi0102", k) diff --git a/token/services/interop/hashescrow/signer.go b/token/services/interop/hashescrow/signer.go index 5a31df6e9d..b5587124a0 100644 --- a/token/services/interop/hashescrow/signer.go +++ b/token/services/interop/hashescrow/signer.go @@ -10,102 +10,64 @@ import ( "encoding/json" "github.com/hyperledger-labs/fabric-smart-client/pkg/utils/errors" - "github.com/hyperledger-labs/fabric-token-sdk/token/driver" - "github.com/hyperledger-labs/fabric-token-sdk/token/services/interop/htlc" ) -// ClaimSignature carries the preimage and the claimant signature. +// ClaimSignature carries the preimage used to unlock a hash escrow script. type ClaimSignature struct { - ClaimantSignature []byte - Preimage []byte + Preimage []byte } -// ClaimSigner signs token request bytes and embeds the preimage. +// ClaimSigner embeds the preimage in the signature payload. +// No claimant signature is required: anyone can submit if they know a valid preimage. type ClaimSigner struct { - Claimant driver.Signer Preimage []byte } -// Sign returns a signature of the claimant over token request and preimage. +// Sign returns a serialized claim signature containing the preimage. func (cs *ClaimSigner) Sign(tokenRequestAndTxID []byte) ([]byte, error) { - msg := concatTokenRequestTxIDPreimage(tokenRequestAndTxID, cs.Preimage) - sigma, err := cs.Claimant.Sign(msg) - if err != nil { - return nil, err - } - claimSignature := ClaimSignature{ - Preimage: cs.Preimage, - ClaimantSignature: sigma, + Preimage: cs.Preimage, } return json.Marshal(claimSignature) } -func concatTokenRequestTxIDPreimage(tokenRequestAndTxID []byte, preImage []byte) []byte { - var msg []byte - msg = append(msg, tokenRequestAndTxID...) - msg = append(msg, preImage...) - - return msg -} - -// ClaimVerifier checks preimage validity and claimant signature. +// ClaimVerifier checks that the preimage unlocks one of the two script hashes. type ClaimVerifier struct { - Claimant driver.Verifier - HashInfo HashInfo + Script *Script } func (cv *ClaimVerifier) Verify(tokenRequestAndTxID, claimSignature []byte) error { + _ = tokenRequestAndTxID sig := &ClaimSignature{} err := json.Unmarshal(claimSignature, sig) if err != nil { return errors.Wrapf(err, "failed to unmarshal claim signature") } - msg := concatTokenRequestTxIDPreimage(tokenRequestAndTxID, sig.Preimage) - if err := cv.Claimant.Verify(msg, sig.ClaimantSignature); err != nil { - return errors.WithMessagef(err, "failed to verify claimant signature") + if len(sig.Preimage) == 0 { + return errors.New("invalid claim signature, empty preimage") } - - image, err := (&htlc.HashInfo{ - Hash: cv.HashInfo.Hash, - HashFunc: cv.HashInfo.HashFunc, - HashEncoding: cv.HashInfo.HashEncoding, - }).Image(sig.Preimage) - if err != nil { - return err + if cv.Script == nil { + return errors.New("invalid claim verifier, script is nil") } - if err := (&htlc.HashInfo{Hash: cv.HashInfo.Hash}).Compare(image); err != nil { - return errors.Errorf("hash mismatch: %s", err) + if _, _, err = cv.Script.ResolveRecipientForPreImage(sig.Preimage); err != nil { + return errors.WithMessage(err, "preimage does not unlock hash escrow script") } return nil } // Verifier validates claims for hash-based escrow scripts. -// A valid claim is signed by either recipient or sender and carries a valid preimage. +// A valid claim carries a preimage that resolves to either recipient or sender. type Verifier struct { - Recipient driver.Verifier - Sender driver.Verifier - HashInfo HashInfo + Script *Script } func (v *Verifier) Verify(msg []byte, sigma []byte) error { - recipientClaimVerifier := &ClaimVerifier{ - Claimant: v.Recipient, - HashInfo: v.HashInfo, - } - if err := recipientClaimVerifier.Verify(msg, sigma); err == nil { - return nil - } - - senderClaimVerifier := &ClaimVerifier{ - Claimant: v.Sender, - HashInfo: v.HashInfo, - } - if err := senderClaimVerifier.Verify(msg, sigma); err != nil { - return errors.WithMessagef(err, "failed verifying hash escrow claim signature") + claimVerifier := &ClaimVerifier{Script: v.Script} + if err := claimVerifier.Verify(msg, sigma); err != nil { + return errors.WithMessage(err, "failed verifying hash escrow claim signature") } return nil diff --git a/token/services/interop/hashescrow/signer_test.go b/token/services/interop/hashescrow/signer_test.go index 5c328b56ed..e1f10cf0b8 100644 --- a/token/services/interop/hashescrow/signer_test.go +++ b/token/services/interop/hashescrow/signer_test.go @@ -9,137 +9,83 @@ package hashescrow_test import ( "crypto" "encoding/json" - "errors" "testing" - "github.com/hyperledger-labs/fabric-token-sdk/token/driver/mock" "github.com/hyperledger-labs/fabric-token-sdk/token/services/interop/encoding" "github.com/hyperledger-labs/fabric-token-sdk/token/services/interop/hashescrow" "github.com/stretchr/testify/require" ) -func validClaimSigBytes(t *testing.T, preimage, claimantSig []byte) []byte { +func mkScriptForClaims(t *testing.T) (*hashescrow.Script, []byte, []byte) { t.Helper() - raw, err := json.Marshal(hashescrow.ClaimSignature{Preimage: preimage, ClaimantSignature: claimantSig}) - require.NoError(t, err) - - return raw -} - -func claimHashInfo(preimage []byte) hashescrow.HashInfo { - h := crypto.SHA256.New() - _, _ = h.Write(preimage) - return hashescrow.HashInfo{ - Hash: []byte(encoding.Base64.New().EncodeToString(h.Sum(nil))), - HashFunc: crypto.SHA256, - HashEncoding: encoding.Base64, + s := &hashescrow.Script{ + Sender: []byte("sender"), + Recipient: []byte("recipient"), + RecipientHashInfo: hashescrow.HashInfo{ + HashFunc: crypto.SHA256, + HashEncoding: encoding.Base64, + }, + SenderHashInfo: hashescrow.HashInfo{ + HashFunc: crypto.SHA256, + HashEncoding: encoding.Base64, + }, } -} - -func TestClaimSignerSign(t *testing.T) { - preimage := []byte("secret") - msg := []byte("txrequest") - expectedSig := []byte("claimant-sig") - signer := &mock.Signer{} - signer.SignReturns(expectedSig, nil) + recipientPreImage := []byte("recipient-preimage") + recipientImage, err := s.RecipientHashInfo.Image(recipientPreImage) + require.NoError(t, err) + s.RecipientHashInfo.Hash = recipientImage - cs := &hashescrow.ClaimSigner{Claimant: signer, Preimage: preimage} - raw, err := cs.Sign(msg) + senderPreImage := []byte("sender-preimage") + senderImage, err := s.SenderHashInfo.Image(senderPreImage) require.NoError(t, err) + s.SenderHashInfo.Hash = senderImage - var claimSig hashescrow.ClaimSignature - require.NoError(t, json.Unmarshal(raw, &claimSig)) - require.Equal(t, preimage, claimSig.Preimage) - require.Equal(t, expectedSig, claimSig.ClaimantSignature) - require.Equal(t, 1, signer.SignCallCount()) - require.Equal(t, append(msg, preimage...), signer.SignArgsForCall(0)) + return s, recipientPreImage, senderPreImage } -func TestClaimSignerSignError(t *testing.T) { - signer := &mock.Signer{} - signer.SignReturns(nil, errors.New("sign failed")) +func TestClaimSignerSign(t *testing.T) { + cs := &hashescrow.ClaimSigner{Preimage: []byte("pre")} + raw, err := cs.Sign([]byte("ignored-msg")) + require.NoError(t, err) - cs := &hashescrow.ClaimSigner{Claimant: signer, Preimage: []byte("p")} - _, err := cs.Sign([]byte("req")) - require.EqualError(t, err, "sign failed") + var sig hashescrow.ClaimSignature + require.NoError(t, json.Unmarshal(raw, &sig)) + require.Equal(t, []byte("pre"), sig.Preimage) } func TestClaimVerifierVerify(t *testing.T) { - preimage := []byte("secret") - msg := []byte("txrequest") + s, recipientPreImage, senderPreImage := mkScriptForClaims(t) + cv := &hashescrow.ClaimVerifier{Script: s} - verifier := &mock.Verifier{} - verifier.VerifyReturns(nil) - - cv := &hashescrow.ClaimVerifier{Claimant: verifier, HashInfo: claimHashInfo(preimage)} - require.NoError(t, cv.Verify(msg, validClaimSigBytes(t, preimage, []byte("sig")))) - vmsg, _ := verifier.VerifyArgsForCall(0) - require.Equal(t, append(msg, preimage...), vmsg) -} + recipientRaw, err := json.Marshal(&hashescrow.ClaimSignature{Preimage: recipientPreImage}) + require.NoError(t, err) + require.NoError(t, cv.Verify([]byte("msg"), recipientRaw)) -func TestClaimVerifierErrors(t *testing.T) { - cv := &hashescrow.ClaimVerifier{Claimant: &mock.Verifier{}, HashInfo: claimHashInfo([]byte("p"))} - require.Error(t, cv.Verify([]byte("req"), []byte("bad-json"))) + senderRaw, err := json.Marshal(&hashescrow.ClaimSignature{Preimage: senderPreImage}) + require.NoError(t, err) + require.NoError(t, cv.Verify([]byte("msg"), senderRaw)) - verifier := &mock.Verifier{} - verifier.VerifyReturns(errors.New("bad sig")) - cv = &hashescrow.ClaimVerifier{Claimant: verifier, HashInfo: claimHashInfo([]byte("p"))} - err := cv.Verify([]byte("req"), validClaimSigBytes(t, []byte("p"), []byte("sig"))) - require.Error(t, err) - require.Contains(t, err.Error(), "failed to verify claimant signature") + wrongRaw, err := json.Marshal(&hashescrow.ClaimSignature{Preimage: []byte("wrong")}) + require.NoError(t, err) + require.Error(t, cv.Verify([]byte("msg"), wrongRaw)) - verifier = &mock.Verifier{} - verifier.VerifyReturns(nil) - cv = &hashescrow.ClaimVerifier{ - Claimant: verifier, - HashInfo: claimHashInfo([]byte("different-preimage")), - } - err = cv.Verify([]byte("req"), validClaimSigBytes(t, []byte("p"), []byte("sig"))) - require.Error(t, err) - require.Contains(t, err.Error(), "hash mismatch") + require.Error(t, cv.Verify([]byte("msg"), []byte("bad-json"))) + require.Error(t, (&hashescrow.ClaimVerifier{}).Verify([]byte("msg"), recipientRaw)) } func TestVerifierVerify(t *testing.T) { - preimage := []byte("secret") - msg := []byte("txrequest") - sigma := validClaimSigBytes(t, preimage, []byte("sig")) - - recipientVerifier := &mock.Verifier{} - recipientVerifier.VerifyReturns(nil) - senderVerifier := &mock.Verifier{} - v := &hashescrow.Verifier{ - Recipient: recipientVerifier, - Sender: senderVerifier, - HashInfo: claimHashInfo(preimage), - } - require.NoError(t, v.Verify(msg, sigma)) - require.Equal(t, 1, recipientVerifier.VerifyCallCount()) - require.Equal(t, 0, senderVerifier.VerifyCallCount()) - - recipientVerifier = &mock.Verifier{} - recipientVerifier.VerifyReturns(errors.New("bad recipient sig")) - senderVerifier = &mock.Verifier{} - senderVerifier.VerifyReturns(nil) - v = &hashescrow.Verifier{ - Recipient: recipientVerifier, - Sender: senderVerifier, - HashInfo: claimHashInfo(preimage), - } - require.NoError(t, v.Verify(msg, sigma)) - require.Equal(t, 1, senderVerifier.VerifyCallCount()) - - recipientVerifier = &mock.Verifier{} - recipientVerifier.VerifyReturns(errors.New("bad recipient sig")) - senderVerifier = &mock.Verifier{} - senderVerifier.VerifyReturns(errors.New("bad sender sig")) - v = &hashescrow.Verifier{ - Recipient: recipientVerifier, - Sender: senderVerifier, - HashInfo: claimHashInfo(preimage), - } - err := v.Verify(msg, sigma) + s, recipientPreImage, _ := mkScriptForClaims(t) + v := &hashescrow.Verifier{Script: s} + + sigma, err := json.Marshal(&hashescrow.ClaimSignature{Preimage: recipientPreImage}) + require.NoError(t, err) + require.NoError(t, v.Verify([]byte("msg"), sigma)) + + badSigma, err := json.Marshal(&hashescrow.ClaimSignature{Preimage: []byte("wrong")}) + require.NoError(t, err) + err = v.Verify([]byte("msg"), badSigma) require.Error(t, err) require.Contains(t, err.Error(), "failed verifying hash escrow claim signature") } diff --git a/token/services/interop/hashescrow/transaction.go b/token/services/interop/hashescrow/transaction.go index 373dfb8482..a3395857ea 100644 --- a/token/services/interop/hashescrow/transaction.go +++ b/token/services/interop/hashescrow/transaction.go @@ -30,12 +30,30 @@ const ( ) // WithHash sets a hash attribute to customize the lock command. +// This is kept for backward compatibility and maps to recipient hash. func WithHash(hash []byte) token.TransferOption { + return WithRecipientHash(hash) +} + +// WithRecipientHash sets recipient-claim hash attribute. +func WithRecipientHash(hash []byte) token.TransferOption { return func(o *token.TransferOptions) error { if o.Attributes == nil { o.Attributes = map[interface{}]interface{}{} } - o.Attributes["hashescrow.hash"] = hash + o.Attributes["hashescrow.recipientHash"] = hash + + return nil + } +} + +// WithSenderHash sets sender-return hash attribute. +func WithSenderHash(hash []byte) token.TransferOption { + return func(o *token.TransferOptions) error { + if o.Attributes == nil { + o.Attributes = map[interface{}]interface{}{} + } + o.Attributes["hashescrow.senderHash"] = hash return nil } @@ -86,6 +104,13 @@ type Transaction struct { Binder Binder } +// PreImages carries the generated preimages for recipient and sender paths. +// If a hash is externally provided for a path, the corresponding preimage is nil. +type PreImages struct { + Recipient []byte + Sender []byte +} + func NewTransaction(sp view.Context, signer view.Identity, opts ...ttx.TxOption) (*Transaction, error) { tx, err := ttx.NewTransaction(sp, signer, opts...) if err != nil { @@ -116,6 +141,16 @@ func NewTransactionFromBytes(ctx view.Context, network, channel string, raw []by // Lock appends a hash-based escrow lock action to the token request. // This lock has no timeout semantics. func (t *Transaction) Lock(ctx context.Context, wallet *token.OwnerWallet, sender view.Identity, typ token2.Type, value uint64, recipient view.Identity, opts ...token.TransferOption) ([]byte, error) { + preImages, err := t.LockWithPreImages(ctx, wallet, sender, typ, value, recipient, opts...) + if err != nil { + return nil, err + } + + return preImages.Recipient, nil +} + +// LockWithPreImages appends a hash-based escrow lock action and returns both preimages. +func (t *Transaction) LockWithPreImages(ctx context.Context, wallet *token.OwnerWallet, sender view.Identity, typ token2.Type, value uint64, recipient view.Identity, opts ...token.TransferOption) (*PreImages, error) { options, err := compileTransferOptions(opts...) if err != nil { return nil, err @@ -130,17 +165,33 @@ func (t *Transaction) Lock(ctx context.Context, wallet *token.OwnerWallet, sende } } - var hash []byte + var recipientHash []byte + var senderHash []byte hashFunc := crypto.SHA256 var hashEncoding encoding.Encoding if options.Attributes != nil { + // backward compatibility boxed, ok := options.Attributes["hashescrow.hash"] if ok { - hash, ok = boxed.([]byte) + recipientHash, ok = boxed.([]byte) if !ok { return nil, errors.Errorf("expected hashescrow.hash attribute to be []byte, got [%T]", boxed) } } + boxed, ok = options.Attributes["hashescrow.recipientHash"] + if ok { + recipientHash, ok = boxed.([]byte) + if !ok { + return nil, errors.Errorf("expected hashescrow.recipientHash attribute to be []byte, got [%T]", boxed) + } + } + boxed, ok = options.Attributes["hashescrow.senderHash"] + if ok { + senderHash, ok = boxed.([]byte) + if !ok { + return nil, errors.Errorf("expected hashescrow.senderHash attribute to be []byte, got [%T]", boxed) + } + } boxed, ok = options.Attributes["hashescrow.hashFunc"] if ok { hashFunc, ok = boxed.(crypto.Hash) @@ -160,27 +211,32 @@ func (t *Transaction) Lock(ctx context.Context, wallet *token.OwnerWallet, sende } } - scriptID, preImage, script, err := t.recipientAsScript(sender, recipient, hash, hashFunc, hashEncoding) + scriptID, preImages, script, err := t.recipientAsScript(sender, recipient, recipientHash, senderHash, hashFunc, hashEncoding) if err != nil { return nil, err } + + transferOpts := append(opts, + token.WithTransferMetadata(LockKey(script.RecipientHashInfo.Hash), LockValue(script.RecipientHashInfo.Hash)), + token.WithTransferMetadata(LockKey(script.SenderHashInfo.Hash), LockValue(script.SenderHashInfo.Hash)), + ) _, err = t.TokenRequest.Transfer( t.Context, wallet, typ, []uint64{value}, []view.Identity{scriptID}, - append(opts, token.WithTransferMetadata(LockKey(script.HashInfo.Hash), LockValue(script.HashInfo.Hash)))..., + transferOpts..., ) if err != nil { return nil, err } - return preImage, nil + return preImages, nil } // Claim appends a claim action to the token request. -// A claim can be performed by either party encoded in the script. +// Spending can be submitted by anyone; recipient is resolved from the preimage. func (t *Transaction) Claim(wallet *token.OwnerWallet, tok *token2.UnspentToken, preImage []byte, opts ...token.TransferOption) error { if len(preImage) == 0 { return errors.New("preImage is nil") @@ -202,42 +258,20 @@ func (t *Transaction) Claim(wallet *token.OwnerWallet, tok *token2.UnspentToken, return errors.New("failed to unmarshal TypedIdentity as a hash escrow script") } - claimIdentity := script.Recipient - if !wallet.Contains(t.Context, script.Recipient) { - if wallet.Contains(t.Context, script.Sender) { - claimIdentity = script.Sender - } else { - return errors.New("passed wallet does not contain sender nor recipient identity for this script") - } - } - - image, err := script.HashInfo.Image(preImage) + claimIdentity, image, err := script.ResolveRecipientForPreImage(preImage) if err != nil { - return errors.Wrapf(err, "failed to compute image of [%x]", preImage) - } - if err := script.HashInfo.Compare(image); err != nil { - return errors.Wrap(err, "passed preImage does not match script hash") + return errors.Wrap(err, "passed preImage does not match script hashes") } sigService := t.TokenService().SigService() - claimSigner, err := sigService.GetSigner(t.Context, claimIdentity) - if err != nil { - return err - } - claimVerifier, err := sigService.OwnerVerifier(t.Context, claimIdentity) - if err != nil { - return err - } if err := sigService.RegisterEphemeralSigner( t.Context, tok.Owner, &ClaimSigner{ - Claimant: claimSigner, Preimage: preImage, }, &ClaimVerifier{ - Claimant: claimVerifier, - HashInfo: script.HashInfo, + Script: script, }, ); err != nil { return err @@ -256,38 +290,29 @@ func (t *Transaction) Claim(wallet *token.OwnerWallet, tok *token2.UnspentToken, ) } -func (t *Transaction) recipientAsScript(sender, recipient view.Identity, h []byte, hashFunc crypto.Hash, hashEncoding encoding.Encoding) (view.Identity, []byte, *Script, error) { - var preImage []byte +func (t *Transaction) recipientAsScript(sender, recipient view.Identity, recipientHash []byte, senderHash []byte, hashFunc crypto.Hash, hashEncoding encoding.Encoding) (view.Identity, *PreImages, *Script, error) { + preImages := &PreImages{} var err error - if len(h) == 0 { - preImage, err = CreateNonce() - if err != nil { - return nil, nil, nil, err - } - hash := hashFunc.New() - if _, err := hash.Write(preImage); err != nil { - return nil, nil, nil, err - } - h = hash.Sum(nil) - if hashEncoding != 0 { - enc := hashEncoding.New() - if enc == nil { - return nil, nil, nil, errors.New("hashEncoding.New() returned nil") - } - h = []byte(enc.EncodeToString(h)) - } + + recipientHashInfo, recipientPreImage, err := buildHashInfo(recipientHash, hashFunc, hashEncoding) + if err != nil { + return nil, nil, nil, errors.WithMessage(err, "failed preparing recipient hash info") } + senderHashInfo, senderPreImage, err := buildHashInfo(senderHash, hashFunc, hashEncoding) + if err != nil { + return nil, nil, nil, errors.WithMessage(err, "failed preparing sender hash info") + } + preImages.Recipient = recipientPreImage + preImages.Sender = senderPreImage - logger.Debugf("pair (pre-image, hash) = (%s,%s)", base64.StdEncoding.EncodeToString(preImage), base64.StdEncoding.EncodeToString(h)) + logger.Debugf("recipient pair (pre-image, hash) = (%s,%s)", base64.StdEncoding.EncodeToString(preImages.Recipient), base64.StdEncoding.EncodeToString(recipientHashInfo.Hash)) + logger.Debugf("sender pair (pre-image, hash) = (%s,%s)", base64.StdEncoding.EncodeToString(preImages.Sender), base64.StdEncoding.EncodeToString(senderHashInfo.Hash)) script := &Script{ - HashInfo: HashInfo{ - Hash: h, - HashFunc: hashFunc, - HashEncoding: hashEncoding, - }, - Recipient: recipient, - Sender: sender, + RecipientHashInfo: recipientHashInfo, + SenderHashInfo: senderHashInfo, + Recipient: recipient, + Sender: sender, } rawScript, err := json.Marshal(script) if err != nil { @@ -302,7 +327,42 @@ func (t *Transaction) recipientAsScript(sender, recipient view.Identity, h []byt return nil, nil, nil, err } - return raw, preImage, script, nil + return raw, preImages, script, nil +} + +func buildHashInfo(hash []byte, hashFunc crypto.Hash, hashEncoding encoding.Encoding) (HashInfo, []byte, error) { + var preImage []byte + h := hash + + if hashFunc == 0 { + hashFunc = crypto.SHA256 + } + + if len(h) == 0 { + var err error + preImage, err = CreateNonce() + if err != nil { + return HashInfo{}, nil, err + } + digest := hashFunc.New() + if _, err := digest.Write(preImage); err != nil { + return HashInfo{}, nil, err + } + h = digest.Sum(nil) + if hashEncoding != 0 { + enc := hashEncoding.New() + if enc == nil { + return HashInfo{}, nil, errors.New("hashEncoding.New() returned nil") + } + h = []byte(enc.EncodeToString(h)) + } + } + + return HashInfo{ + Hash: h, + HashFunc: hashFunc, + HashEncoding: hashEncoding, + }, preImage, nil } // CreateNonce generates a nonce. diff --git a/token/services/interop/hashescrow/transaction_test.go b/token/services/interop/hashescrow/transaction_test.go index 4f3a28601a..543824f271 100644 --- a/token/services/interop/hashescrow/transaction_test.go +++ b/token/services/interop/hashescrow/transaction_test.go @@ -16,13 +16,15 @@ import ( func TestCompileTransferOptions(t *testing.T) { opts, err := compileTransferOptions( - WithHash([]byte("h")), + WithRecipientHash([]byte("h")), + WithSenderHash([]byte("h2")), WithHashFunc(crypto.SHA512), WithHashEncoding(encoding.Hex), ) require.NoError(t, err) require.NotNil(t, opts.Attributes) - require.Equal(t, []byte("h"), opts.Attributes["hashescrow.hash"]) + require.Equal(t, []byte("h"), opts.Attributes["hashescrow.recipientHash"]) + require.Equal(t, []byte("h2"), opts.Attributes["hashescrow.senderHash"]) require.Equal(t, crypto.SHA512, opts.Attributes["hashescrow.hashFunc"]) require.Equal(t, encoding.Hex, opts.Attributes["hashescrow.hashEncoding"]) } @@ -32,19 +34,23 @@ func TestRecipientAsScript(t *testing.T) { sender := []byte("sender") recipient := []byte("recipient") - raw, preimage, script, err := tx.recipientAsScript(sender, recipient, []byte("custom-hash"), crypto.SHA256, encoding.Base64) + raw, preimages, script, err := tx.recipientAsScript(sender, recipient, []byte("recipient-hash"), []byte("sender-hash"), crypto.SHA256, encoding.Base64) require.NoError(t, err) - require.Empty(t, preimage) + require.Empty(t, preimages.Recipient) + require.Empty(t, preimages.Sender) require.NotNil(t, raw) require.Equal(t, sender, []byte(script.Sender)) require.Equal(t, recipient, []byte(script.Recipient)) - require.Equal(t, []byte("custom-hash"), script.HashInfo.Hash) + require.Equal(t, []byte("recipient-hash"), script.RecipientHashInfo.Hash) + require.Equal(t, []byte("sender-hash"), script.SenderHashInfo.Hash) - raw, preimage, script, err = tx.recipientAsScript(sender, recipient, nil, crypto.SHA256, encoding.Base64) + raw, preimages, script, err = tx.recipientAsScript(sender, recipient, nil, nil, crypto.SHA256, encoding.Base64) require.NoError(t, err) require.NotNil(t, raw) - require.NotEmpty(t, preimage) - require.NotEmpty(t, script.HashInfo.Hash) + require.NotEmpty(t, preimages.Recipient) + require.NotEmpty(t, preimages.Sender) + require.NotEmpty(t, script.RecipientHashInfo.Hash) + require.NotEmpty(t, script.SenderHashInfo.Hash) } func TestRecipientAsScriptBadEncoding(t *testing.T) { @@ -53,10 +59,11 @@ func TestRecipientAsScriptBadEncoding(t *testing.T) { []byte("sender"), []byte("recipient"), nil, + nil, crypto.SHA256, encoding.Encoding(999), ) - require.EqualError(t, err, "hashEncoding.New() returned nil") + require.EqualError(t, err, "failed preparing recipient hash info: hashEncoding.New() returned nil") } func TestCreateNonce(t *testing.T) { From f6ebaae7284ef12c4df80d64c28ec46e6d782558 Mon Sep 17 00:00:00 2001 From: Flamki <9833ayush@gmail.com> Date: Sun, 17 May 2026 11:40:01 +0530 Subject: [PATCH 03/18] test(hashescrow): add interop coverage Signed-off-by: Flamki <9833ayush@gmail.com> --- integration/token/interop/support.go | 89 +++++++++ integration/token/interop/tests.go | 38 ++++ integration/token/interop/topology.go | 7 + integration/token/interop/views/auditor.go | 39 ++-- .../token/interop/views/hashescrow/claim.go | 81 +++++++++ .../token/interop/views/hashescrow/lock.go | 170 ++++++++++++++++++ token/services/interop/hashescrow/audit.go | 99 ++++++++++ token/services/interop/hashescrow/wallet.go | 94 ++++++++++ .../interop/hashescrow/wallet_filter.go | 72 ++++++++ 9 files changed, 677 insertions(+), 12 deletions(-) create mode 100644 integration/token/interop/views/hashescrow/claim.go create mode 100644 integration/token/interop/views/hashescrow/lock.go create mode 100644 token/services/interop/hashescrow/audit.go create mode 100644 token/services/interop/hashescrow/wallet.go create mode 100644 token/services/interop/hashescrow/wallet_filter.go diff --git a/integration/token/interop/support.go b/integration/token/interop/support.go index 9e3008a107..45f0b17435 100644 --- a/integration/token/interop/support.go +++ b/integration/token/interop/support.go @@ -21,6 +21,7 @@ import ( common2 "github.com/hyperledger-labs/fabric-token-sdk/integration/token/common" "github.com/hyperledger-labs/fabric-token-sdk/integration/token/fungible/views" views2 "github.com/hyperledger-labs/fabric-token-sdk/integration/token/interop/views" + hashescrowviews "github.com/hyperledger-labs/fabric-token-sdk/integration/token/interop/views/hashescrow" "github.com/hyperledger-labs/fabric-token-sdk/integration/token/interop/views/htlc" "github.com/hyperledger-labs/fabric-token-sdk/token" token2 "github.com/hyperledger-labs/fabric-token-sdk/token/token" @@ -320,6 +321,61 @@ func HTLCLock(network *integration.Infrastructure, tmsID token.TMSID, id *token3 } } +func HashEscrowLock(network *integration.Infrastructure, tmsID token.TMSID, id *token3.NodeReference, wallet string, typ token2.Type, amount uint64, receiver *token3.NodeReference, auditor *token3.NodeReference, recipientHash []byte, senderHash []byte, hashFunc crypto.Hash, errorMsgs ...string) (string, []byte, []byte, []byte, []byte) { + result, err := network.Client(id.ReplicaName()).CallView("hashescrow.lock", common.JSONMarshall(&hashescrowviews.Lock{ + TMSID: tmsID, + Wallet: wallet, + Type: typ, + Amount: amount, + Recipient: network.Identity(receiver.Id()), + RecipientHash: recipientHash, + SenderHash: senderHash, + HashFunc: hashFunc, + })) + if len(errorMsgs) == 0 { + gomega.Expect(err).NotTo(gomega.HaveOccurred()) + lockResult := &hashescrowviews.LockInfo{} + common.JSONUnmarshal(result.([]byte), lockResult) + + common2.CheckFinality(network, receiver, lockResult.TxID, &tmsID, false) + common2.CheckFinality(network, auditor, lockResult.TxID, &tmsID, false) + + if len(recipientHash) == 0 { + gomega.Expect(lockResult.RecipientPreImage).NotTo(gomega.BeNil()) + } + if len(senderHash) == 0 { + gomega.Expect(lockResult.SenderPreImage).NotTo(gomega.BeNil()) + } + gomega.Expect(lockResult.RecipientHash).NotTo(gomega.BeNil()) + gomega.Expect(lockResult.SenderHash).NotTo(gomega.BeNil()) + if len(recipientHash) != 0 { + gomega.Expect(lockResult.RecipientHash).To(gomega.BeEquivalentTo(recipientHash)) + } + if len(senderHash) != 0 { + gomega.Expect(lockResult.SenderHash).To(gomega.BeEquivalentTo(senderHash)) + } + + return lockResult.TxID, lockResult.RecipientPreImage, lockResult.SenderPreImage, lockResult.RecipientHash, lockResult.SenderHash + } + + gomega.Expect(err).To(gomega.HaveOccurred()) + for _, msg := range errorMsgs { + gomega.Expect(err.Error()).To(gomega.ContainSubstring(msg)) + } + time.Sleep(5 * time.Second) + + errMsg := err.Error() + fmt.Printf("Got error message [%s]\n", errMsg) + txID := "" + index := strings.Index(err.Error(), "<<<[") + if index != -1 { + txID = errMsg[index+4 : index+strings.Index(err.Error()[index:], "]>>>")] + } + fmt.Printf("Got error message, extracted tx id [%s]\n", txID) + + return txID, nil, nil, nil, nil +} + func HTLCReclaimAll(network *integration.Infrastructure, id *token3.NodeReference, wallet string, errorMsgs ...string) { txID, err := network.Client(id.ReplicaName()).CallView("htlc.reclaimAll", common.JSONMarshall(&htlc.ReclaimAll{ Wallet: wallet, @@ -404,6 +460,39 @@ func htlcClaim(network *integration.Infrastructure, tmsID token.TMSID, id *token } } +func hashEscrowClaim(network *integration.Infrastructure, tmsID token.TMSID, id *token3.NodeReference, wallet string, preImage []byte, auditor *token3.NodeReference, errorMsgs ...string) string { + txIDBoxed, err := network.Client(id.ReplicaName()).CallView("hashescrow.claim", common.JSONMarshall(&hashescrowviews.Claim{ + TMSID: tmsID, + Wallet: wallet, + PreImage: preImage, + })) + if len(errorMsgs) == 0 { + gomega.Expect(err).NotTo(gomega.HaveOccurred()) + txID := common.JSONUnmarshalString(txIDBoxed) + common2.CheckFinality(network, id, txID, &tmsID, false) + common2.CheckFinality(network, auditor, txID, &tmsID, false) + + return txID + } + + gomega.Expect(err).To(gomega.HaveOccurred()) + for _, msg := range errorMsgs { + gomega.Expect(err.Error()).To(gomega.ContainSubstring(msg)) + } + time.Sleep(5 * time.Second) + + errMsg := err.Error() + fmt.Printf("Got error message [%s]\n", errMsg) + txID := "" + index := strings.Index(err.Error(), "<<<[") + if index != -1 { + txID = errMsg[index+4 : index+strings.Index(err.Error()[index:], "]>>>")] + } + fmt.Printf("Got error message, extracted tx id [%s]\n", txID) + + return txID +} + func fastExchange(network *integration.Infrastructure, id *token3.NodeReference, recipient *token3.NodeReference, tmsID1 token.TMSID, typ1 token2.Type, amount1 uint64, tmsID2 token.TMSID, typ2 token2.Type, amount2 uint64, deadline time.Duration) { _, err := network.Client(id.ReplicaName()).CallView("htlc.fastExchange", common.JSONMarshall(&htlc.FastExchange{ Recipient: network.Identity(recipient.Id()), diff --git a/integration/token/interop/tests.go b/integration/token/interop/tests.go index 366383fa2d..6090a7898b 100644 --- a/integration/token/interop/tests.go +++ b/integration/token/interop/tests.go @@ -25,6 +25,7 @@ import ( const ( EUR = token3.Type("EUR") USD = token3.Type("USD") + GBP = token3.Type("GBP") ) func TestHTLCSingleNetwork(network *integration.Infrastructure, sel *token2.ReplicaSelector) { @@ -143,6 +144,43 @@ func TestHTLCSingleNetwork(network *integration.Infrastructure, sel *token2.Repl IDs := ListVaultUnspentTokens(network, defaultTMSID, name) CheckIfExistsInVault(network, defaultTMSID, auditor, IDs) } + + TestHashEscrowSingleNetwork(network, sel) +} + +func TestHashEscrowSingleNetwork(network *integration.Infrastructure, sel *token2.ReplicaSelector) { + alice := sel.Get("alice") + bob := sel.Get("bob") + auditor := sel.Get("auditor") + + defaultTMSID := token.TMSID{} + + IssueCash(network, "", GBP, 20, alice, auditor) + CheckBalance(network, alice, "", GBP, 20) + CheckBalance(network, bob, "", GBP, 0) + + _, recipientPreImage, _, _, _ := HashEscrowLock(network, defaultTMSID, alice, "", GBP, 10, bob, auditor, nil, nil, crypto.SHA3_256) + CheckBalance(network, alice, "", GBP, 10) + CheckBalance(network, bob, "", GBP, 0) + + hashEscrowClaim(network, defaultTMSID, bob, "", recipientPreImage, auditor) + CheckBalance(network, alice, "", GBP, 10) + CheckBalance(network, bob, "", GBP, 10) + + hashEscrowClaim(network, defaultTMSID, bob, "", recipientPreImage, auditor, "expected only one hash escrow script to match") + CheckBalance(network, alice, "", GBP, 10) + CheckBalance(network, bob, "", GBP, 10) + + _, _, senderPreImage, _, _ := HashEscrowLock(network, defaultTMSID, alice, "", GBP, 7, bob, auditor, nil, nil, crypto.SHA3_256) + CheckBalance(network, alice, "", GBP, 3) + CheckBalance(network, bob, "", GBP, 10) + + hashEscrowClaim(network, defaultTMSID, bob, "", senderPreImage, auditor) + gomega.Eventually(CheckBalanceReturnError).WithArguments(network, alice, "", GBP, uint64(10)).WithTimeout(2 * time.Minute).WithPolling(5 * time.Second).Should(gomega.Succeed()) + CheckBalance(network, bob, "", GBP, 10) + + CheckOwnerStore(network, defaultTMSID, nil, alice, bob) + CheckAuditorStore(network, defaultTMSID, auditor, "", nil) } func TestHTLCTwoNetworks(network *integration.Infrastructure, sel *token2.ReplicaSelector) { diff --git a/integration/token/interop/topology.go b/integration/token/interop/topology.go index 53391686ef..21c3af4533 100644 --- a/integration/token/interop/topology.go +++ b/integration/token/interop/topology.go @@ -19,6 +19,7 @@ import ( views3 "github.com/hyperledger-labs/fabric-token-sdk/integration/token/common/views" "github.com/hyperledger-labs/fabric-token-sdk/integration/token/fungible/views" views2 "github.com/hyperledger-labs/fabric-token-sdk/integration/token/interop/views" + "github.com/hyperledger-labs/fabric-token-sdk/integration/token/interop/views/hashescrow" "github.com/hyperledger-labs/fabric-token-sdk/integration/token/interop/views/htlc" ) @@ -245,6 +246,9 @@ func addAlice(fscTopology *fsc.Topology) *node.Node { RegisterViewFactory("htlc.lock", &htlc.LockViewFactory{}). RegisterViewFactory("htlc.reclaimAll", &htlc.ReclaimAllViewFactory{}). RegisterViewFactory("htlc.fastExchange", &htlc.FastExchangeInitiatorViewFactory{}). + RegisterResponder(&hashescrow.LockAcceptView{}, &hashescrow.LockView{}). + RegisterViewFactory("hashescrow.lock", &hashescrow.LockViewFactory{}). + RegisterViewFactory("hashescrow.claim", &hashescrow.ClaimViewFactory{}). RegisterViewFactory("TxFinality", &views3.TxFinalityViewFactory{}) } @@ -264,5 +268,8 @@ func addBob(fscTopology *fsc.Topology) *node.Node { RegisterResponder(&htlc.LockAcceptView{}, &htlc.LockView{}). RegisterResponder(&htlc.FastExchangeResponderView{}, &htlc.FastExchangeInitiatorView{}). RegisterViewFactory("htlc.claim", &htlc.ClaimViewFactory{}). + RegisterResponder(&hashescrow.LockAcceptView{}, &hashescrow.LockView{}). + RegisterViewFactory("hashescrow.lock", &hashescrow.LockViewFactory{}). + RegisterViewFactory("hashescrow.claim", &hashescrow.ClaimViewFactory{}). RegisterViewFactory("TxFinality", &views3.TxFinalityViewFactory{}) } diff --git a/integration/token/interop/views/auditor.go b/integration/token/interop/views/auditor.go index aad4f553c2..e878b29b63 100644 --- a/integration/token/interop/views/auditor.go +++ b/integration/token/interop/views/auditor.go @@ -14,6 +14,7 @@ import ( "github.com/hyperledger-labs/fabric-smart-client/platform/common/utils/assert" "github.com/hyperledger-labs/fabric-smart-client/platform/view/view" "github.com/hyperledger-labs/fabric-token-sdk/token" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/interop/hashescrow" "github.com/hyperledger-labs/fabric-token-sdk/token/services/interop/htlc" "github.com/hyperledger-labs/fabric-token-sdk/token/services/ttx" ) @@ -68,26 +69,40 @@ func (a *AuditView) Call(context view.Context) (interface{}, error) { for i := range inputs.Count() { input, err := htlc.ToInput(inputs.At(i)) assert.NoError(err, "cannot get htlc input wrapper") - if !input.IsHTLC() { - continue + if input.IsHTLC() { + // check script details, for example make sure the hash is set + script, err := input.Script() + assert.NoError(err, "cannot get htlc script from input") + assert.True(len(script.HashInfo.Hash) > 0, "hash is not set") + } + + hashEscrowInput, err := hashescrow.ToInput(inputs.At(i)) + assert.NoError(err, "cannot get hash escrow input wrapper") + if hashEscrowInput.IsHashEscrow() { + hashEscrowScript, err := hashEscrowInput.Script() + assert.NoError(err, "cannot get hash escrow script from input") + assert.NoError(hashEscrowScript.Validate(), "hash escrow script is not valid") } - // check script details, for example make sure the hash is set - script, err := input.Script() - assert.NoError(err, "cannot get htlc script from input") - assert.True(len(script.HashInfo.Hash) > 0, "hash is not set") } now := time.Now() for i := range outputs.Count() { output, err := htlc.ToOutput(outputs.At(i)) assert.NoError(err, "cannot get htlc output wrapper") - if !output.IsHTLC() { - continue + if output.IsHTLC() { + // check script details + script, err := output.Script() + assert.NoError(err, "cannot get htlc script from output") + assert.NoError(script.Validate(now), "script is not valid") + } + + hashEscrowOutput, err := hashescrow.ToOutput(outputs.At(i)) + assert.NoError(err, "cannot get hash escrow output wrapper") + if hashEscrowOutput.IsHashEscrow() { + hashEscrowScript, err := hashEscrowOutput.Script() + assert.NoError(err, "cannot get hash escrow script from output") + assert.NoError(hashEscrowScript.Validate(), "hash escrow script is not valid") } - // check script details - script, err := output.Script() - assert.NoError(err, "cannot get htlc script from output") - assert.NoError(script.Validate(now), "script is not valid") } return context.RunView(ttx.NewAuditApproveView(w, tx)) diff --git a/integration/token/interop/views/hashescrow/claim.go b/integration/token/interop/views/hashescrow/claim.go new file mode 100644 index 0000000000..00e0148424 --- /dev/null +++ b/integration/token/interop/views/hashescrow/claim.go @@ -0,0 +1,81 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package hashescrow + +import ( + "encoding/json" + + "github.com/hyperledger-labs/fabric-smart-client/pkg/utils/errors" + "github.com/hyperledger-labs/fabric-smart-client/platform/common/utils/assert" + "github.com/hyperledger-labs/fabric-smart-client/platform/view/services/id" + "github.com/hyperledger-labs/fabric-smart-client/platform/view/view" + "github.com/hyperledger-labs/fabric-token-sdk/token" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/interop/hashescrow" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/ttx" +) + +type Claim struct { + TMSID token.TMSID + Wallet string + PreImage []byte +} + +type ClaimView struct { + *Claim +} + +func (r *ClaimView) Call(ctx view.Context) (res interface{}, err error) { + var tx *hashescrow.Transaction + defer func() { + if e := recover(); e != nil { + txID := "none" + if tx != nil { + txID = tx.ID() + } + if err == nil { + err = errors.Errorf("<<<[%s]>>>: %s", txID, e) + } else { + err = errors.Errorf("<<<[%s]>>>: %s", txID, err) + } + } + }() + + claimWallet := hashescrow.GetWallet(ctx, r.Wallet, token.WithTMSID(r.TMSID)) + assert.NotNil(claimWallet, "wallet [%s] not found", r.Wallet) + + matched, err := hashescrow.Wallet(claimWallet).ListByPreImage(ctx.Context(), r.PreImage) + assert.NoError(err, "failed looking up hash escrow script") + assert.True(matched.Count() == 1, "expected only one hash escrow script to match [%s], got [%d]", view.Identity(r.PreImage), matched.Count()) + + idProvider, err := id.GetProvider(ctx) + assert.NoError(err, "failed getting id provider") + tx, err = hashescrow.NewAnonymousTransaction( + ctx, + ttx.WithAuditor(idProvider.Identity("auditor")), + ttx.WithTMSID(r.TMSID), + ) + assert.NoError(err, "failed to create a hash escrow transaction") + assert.NoError(tx.Claim(claimWallet, matched.At(0), r.PreImage), "failed adding a hash escrow claim for [%s]", matched.At(0).Id) + + _, err = ctx.RunView(ttx.NewCollectEndorsementsView(tx.Transaction)) + assert.NoError(err, "failed to collect endorsements on hash escrow transaction") + + _, err = ctx.RunView(ttx.NewOrderingAndFinalityView(tx.Transaction)) + assert.NoError(err, "failed to commit hash escrow transaction") + + return tx.ID(), nil +} + +type ClaimViewFactory struct{} + +func (p *ClaimViewFactory) NewView(in []byte) (view.View, error) { + f := &ClaimView{Claim: &Claim{}} + err := json.Unmarshal(in, f.Claim) + assert.NoError(err, "failed unmarshalling input") + + return f, nil +} diff --git a/integration/token/interop/views/hashescrow/lock.go b/integration/token/interop/views/hashescrow/lock.go new file mode 100644 index 0000000000..5869b7649e --- /dev/null +++ b/integration/token/interop/views/hashescrow/lock.go @@ -0,0 +1,170 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package hashescrow + +import ( + "crypto" + "encoding/json" + + "github.com/hyperledger-labs/fabric-smart-client/pkg/utils/errors" + "github.com/hyperledger-labs/fabric-smart-client/platform/common/utils/assert" + "github.com/hyperledger-labs/fabric-smart-client/platform/view/services/id" + "github.com/hyperledger-labs/fabric-smart-client/platform/view/view" + "github.com/hyperledger-labs/fabric-token-sdk/token" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/interop/encoding" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/interop/hashescrow" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/ttx" + token2 "github.com/hyperledger-labs/fabric-token-sdk/token/token" +) + +type Lock struct { + TMSID token.TMSID + Wallet string + Type token2.Type + Amount uint64 + Recipient view.Identity + RecipientHash []byte + SenderHash []byte + HashFunc crypto.Hash +} + +type LockInfo struct { + TxID string + RecipientPreImage []byte + SenderPreImage []byte + RecipientHash []byte + SenderHash []byte +} + +type LockView struct { + *Lock +} + +func (hv *LockView) Call(context view.Context) (res interface{}, err error) { + var tx *hashescrow.Transaction + defer func() { + if e := recover(); e != nil { + txID := "none" + if tx != nil { + txID = tx.ID() + } + if err == nil { + err = errors.Errorf("<<<[%s]>>>: %s", txID, e) + } else { + err = errors.Errorf("<<<[%s]>>>: %s", txID, err) + } + } + }() + + me, recipient, err := ttx.ExchangeRecipientIdentities(context, "", hv.Recipient, token.WithTMSID(hv.TMSID)) + assert.NoError(err, "failed getting recipient identity") + + idProvider, err := id.GetProvider(context) + assert.NoError(err, "failed getting id provider") + tx, err = hashescrow.NewAnonymousTransaction( + context, + ttx.WithAuditor(idProvider.Identity("auditor")), + ttx.WithTMSID(hv.TMSID), + ) + assert.NoError(err, "failed creating a hash escrow transaction") + + senderWallet := hashescrow.GetWallet(context, hv.Wallet, token.WithTMSID(hv.TMSID)) + assert.NotNil(senderWallet, "sender wallet [%s] not found", hv.Wallet) + + preImages, err := tx.LockWithPreImages( + context.Context(), + senderWallet, + me, + hv.Type, + hv.Amount, + recipient, + hashescrow.WithRecipientHash(hv.RecipientHash), + hashescrow.WithSenderHash(hv.SenderHash), + hashescrow.WithHashFunc(hv.HashFunc), + hashescrow.WithHashEncoding(encoding.None), + ) + assert.NoError(err, "failed adding a hash escrow lock operation") + + _, err = context.RunView(ttx.NewCollectEndorsementsView(tx.Transaction)) + assert.NoError(err, "failed to collect endorsements for hash escrow transaction") + + _, err = context.RunView(ttx.NewOrderingAndFinalityView(tx.Transaction)) + assert.NoError(err, "failed to commit hash escrow transaction") + + outputs, err := tx.Outputs() + assert.NoError(err, "failed getting outputs") + var script *hashescrow.Script + for i := range outputs.Count() { + output, err := hashescrow.ToOutput(outputs.At(i)) + assert.NoError(err, "cannot get hash escrow output wrapper") + if !output.IsHashEscrow() { + continue + } + script, err = output.Script() + assert.NoError(err, "cannot get hash escrow script from output") + + break + } + assert.NotNil(script, "expected a hash escrow script output") + + return &LockInfo{ + TxID: tx.ID(), + RecipientPreImage: preImages.Recipient, + SenderPreImage: preImages.Sender, + RecipientHash: script.RecipientHashInfo.Hash, + SenderHash: script.SenderHashInfo.Hash, + }, nil +} + +type LockViewFactory struct{} + +func (p *LockViewFactory) NewView(in []byte) (view.View, error) { + f := &LockView{Lock: &Lock{}} + err := json.Unmarshal(in, f.Lock) + assert.NoError(err, "failed unmarshalling input") + + return f, nil +} + +type LockAcceptView struct{} + +func (h *LockAcceptView) Call(context view.Context) (interface{}, error) { + me, sender, err := ttx.RespondExchangeRecipientIdentities(context) + assert.NoError(err, "failed to respond to identity request") + + tx, err := ttx.ReceiveTransaction(context) + assert.NoError(err, "failed to receive tokens") + + outputs, err := tx.Outputs() + assert.NoError(err, "failed getting outputs") + assert.True(outputs.Count() >= 1, "expected at least one output, got [%d]", outputs.Count()) + + var script *hashescrow.Script + for i := range outputs.Count() { + output, err := hashescrow.ToOutput(outputs.At(i)) + assert.NoError(err, "cannot get hash escrow output wrapper") + if !output.IsHashEscrow() { + continue + } + script, err = output.Script() + assert.NoError(err, "cannot get hash escrow script from output") + + break + } + assert.NotNil(script, "expected a hash escrow script") + assert.NoError(script.Validate(), "script is not valid") + assert.True(me.Equal(script.Recipient), "expected me as recipient of the script") + assert.True(sender.Equal(script.Sender), "expected sender as sender of the script") + + _, err = context.RunView(ttx.NewAcceptView(tx)) + assert.NoError(err, "failed to accept new tokens") + + _, err = context.RunView(ttx.NewFinalityView(tx)) + assert.NoError(err, "new tokens were not committed") + + return tx, nil +} diff --git a/token/services/interop/hashescrow/audit.go b/token/services/interop/hashescrow/audit.go new file mode 100644 index 0000000000..91490a6167 --- /dev/null +++ b/token/services/interop/hashescrow/audit.go @@ -0,0 +1,99 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package hashescrow + +import ( + "encoding/json" + + "github.com/hyperledger-labs/fabric-smart-client/pkg/utils/errors" + "github.com/hyperledger-labs/fabric-token-sdk/token" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/identity" +) + +type Input struct { + *token.Input + isHashEscrow bool +} + +func ToInput(i *token.Input) (*Input, error) { + owner, err := identity.UnmarshalTypedIdentity(i.Owner) + if err != nil { + return nil, errors.Wrapf(err, "failed to unmarshal owner") + } + + return &Input{ + Input: i, + isHashEscrow: owner.Type == ScriptType, + }, nil +} + +func (i *Input) IsHashEscrow() bool { + return i.isHashEscrow +} + +func (i *Input) Script() (*Script, error) { + if !i.isHashEscrow { + return nil, errors.New("this input does not refer to a hash escrow script") + } + + owner, err := identity.UnmarshalTypedIdentity(i.Owner) + if err != nil { + return nil, errors.Wrapf(err, "failed to unmarshal owner") + } + if owner.Type != ScriptType { + return nil, errors.Errorf("invalid identity type, expected [%s], got [%s]", ScriptType, owner.Type) + } + script := &Script{} + err = json.Unmarshal(owner.Identity, script) + if err != nil { + return nil, errors.Wrapf(err, "failed to unmarshal hash escrow script") + } + + return script, nil +} + +type Output struct { + *token.Output + isHashEscrow bool +} + +func ToOutput(i *token.Output) (*Output, error) { + owner, err := identity.UnmarshalTypedIdentity(i.Owner) + if err != nil { + return nil, errors.Wrapf(err, "failed to unmarshal owner") + } + + return &Output{ + Output: i, + isHashEscrow: owner.Type == ScriptType, + }, nil +} + +func (o *Output) IsHashEscrow() bool { + return o.isHashEscrow +} + +func (o *Output) Script() (*Script, error) { + if !o.isHashEscrow { + return nil, errors.New("this output does not refer to a hash escrow script") + } + + owner, err := identity.UnmarshalTypedIdentity(o.Owner) + if err != nil { + return nil, errors.Wrapf(err, "failed to unmarshal owner") + } + if owner.Type != ScriptType { + return nil, errors.Errorf("invalid identity type, expected [%s], got [%s]", ScriptType, owner.Type) + } + script := &Script{} + err = json.Unmarshal(owner.Identity, script) + if err != nil { + return nil, errors.Wrapf(err, "failed to unmarshal hash escrow script") + } + + return script, nil +} diff --git a/token/services/interop/hashescrow/wallet.go b/token/services/interop/hashescrow/wallet.go new file mode 100644 index 0000000000..317666e638 --- /dev/null +++ b/token/services/interop/hashescrow/wallet.go @@ -0,0 +1,94 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package hashescrow + +import ( + "context" + + "github.com/hyperledger-labs/fabric-smart-client/pkg/utils/errors" + "github.com/hyperledger-labs/fabric-smart-client/platform/common/utils/collections/iterators" + "github.com/hyperledger-labs/fabric-smart-client/platform/view/view" + "github.com/hyperledger-labs/fabric-token-sdk/token" + "github.com/hyperledger-labs/fabric-token-sdk/token/driver" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/ttx" + token2 "github.com/hyperledger-labs/fabric-token-sdk/token/token" +) + +type QueryEngine interface { + UnspentTokensIteratorBy(ctx context.Context, id string, tokenType token2.Type) (driver.UnspentTokensIterator, error) +} + +type OwnerWallet struct { + wallet *token.OwnerWallet + queryEngine QueryEngine +} + +func (w *OwnerWallet) ListByPreImage(ctx context.Context, preImage []byte, opts ...token.ListTokensOption) (*token2.UnspentTokens, error) { + compiledOpts, err := token.CompileListTokensOption(opts...) + if err != nil { + return nil, errors.Wrapf(err, "failed to compile options") + } + + tokensByID := map[string]*token2.UnspentToken{} + for _, sender := range []bool{false, true} { + tokens, err := w.filter(ctx, compiledOpts.TokenType, sender, (&PreImageSelector{preImage: preImage}).Filter) + if err != nil { + return nil, err + } + for _, tok := range tokens.Tokens { + tokensByID[tok.Id.String()] = tok + } + } + + tokens := make([]*token2.UnspentToken, 0, len(tokensByID)) + for _, tok := range tokensByID { + tokens = append(tokens, tok) + } + + return &token2.UnspentTokens{Tokens: tokens}, nil +} + +func (w *OwnerWallet) filter(ctx context.Context, tokenType token2.Type, sender bool, selector SelectFunction) (*token2.UnspentTokens, error) { + it, err := w.filterIterator(ctx, tokenType, sender, selector) + if err != nil { + return nil, errors.Wrap(err, "token selection failed") + } + tokens, err := iterators.ReadAllPointers(it) + if err != nil { + return nil, err + } + + return &token2.UnspentTokens{Tokens: tokens}, nil +} + +func (w *OwnerWallet) filterIterator(ctx context.Context, tokenType token2.Type, sender bool, selector SelectFunction) (iterators.Iterator[*token2.UnspentToken], error) { + walletID := recipientWallet(w.wallet) + if sender { + walletID = senderWallet(w.wallet) + } + it, err := w.queryEngine.UnspentTokensIteratorBy(ctx, walletID, tokenType) + if err != nil { + return nil, errors.WithMessagef(err, "failed to get iterator over unspent tokens") + } + + return iterators.Filter(it, IsScript(selector)), nil +} + +func GetWallet(context view.Context, id string, opts ...token.ServiceOption) *token.OwnerWallet { + return ttx.GetWallet(context, id, opts...) +} + +func Wallet(wallet *token.OwnerWallet) *OwnerWallet { + if wallet == nil { + return nil + } + + return &OwnerWallet{ + wallet: wallet, + queryEngine: wallet.TMS().Vault().NewQueryEngine(), + } +} diff --git a/token/services/interop/hashescrow/wallet_filter.go b/token/services/interop/hashescrow/wallet_filter.go new file mode 100644 index 0000000000..7199c031c9 --- /dev/null +++ b/token/services/interop/hashescrow/wallet_filter.go @@ -0,0 +1,72 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package hashescrow + +import ( + "encoding/json" + + "github.com/hyperledger-labs/fabric-smart-client/platform/common/utils/collections/iterators" + "github.com/hyperledger-labs/fabric-smart-client/platform/view/view" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/identity" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/logging" + "github.com/hyperledger-labs/fabric-token-sdk/token/token" +) + +type SelectFunction = func(*token.UnspentToken, *Script) (bool, error) + +type PreImageSelector struct { + preImage []byte +} + +func (f *PreImageSelector) Filter(tok *token.UnspentToken, script *Script) (bool, error) { + logger.Debugf("token [%s,%s,%s,%s] contains a hash escrow script? Yes", tok.Id, view.Identity(tok.Owner).UniqueID(), tok.Type, tok.Quantity) + + _, image, err := script.ResolveRecipientForPreImage(f.preImage) + if err != nil { + logger.Debugf("hash escrow script does not match pre-image [%s]: [%s]", logging.Base64(f.preImage), err) + + return false, nil + } + logger.Debugf("hash escrow script matches pre-image [%s] with image [%s]", logging.Base64(f.preImage), logging.Base64(image)) + + return true, nil +} + +func IsScript(selector SelectFunction) iterators.Predicate[*token.UnspentToken] { + return func(tok *token.UnspentToken) bool { + owner, err := identity.UnmarshalTypedIdentity(tok.Owner) + if err != nil { + logger.Debugf("Is Mine [%s,%s,%s]? No, failed unmarshalling [%s]", view.Identity(tok.Owner), tok.Type, tok.Quantity, err) + + return false + } + if owner.Type != ScriptType { + return false + } + + script := &Script{} + if err := json.Unmarshal(owner.Identity, script); err != nil { + logger.Debugf("token [%s,%s,%s,%s] contains a hash escrow script? No", tok.Id, view.Identity(tok.Owner).UniqueID(), tok.Type, tok.Quantity) + + return false + } + if err := script.Validate(); err != nil { + logger.Debugf("token [%s,%s,%s,%s] contains an invalid hash escrow script: [%s]", tok.Id, view.Identity(tok.Owner).UniqueID(), tok.Type, tok.Quantity, err) + + return false + } + + pickItem, err := selector(tok, script) + if err != nil { + logger.Errorf("failed to select (token,script)[%v:%v] pair: %w", tok, script, err) + + return false + } + + return pickItem + } +} From 39505eef585213056e3cb2429d1686840fdf20d9 Mon Sep 17 00:00:00 2001 From: Flamki <111645429+Flamki@users.noreply.github.com> Date: Thu, 28 May 2026 23:23:30 +0530 Subject: [PATCH 04/18] fix(hashescrow): address review feedback Signed-off-by: Flamki <111645429+Flamki@users.noreply.github.com> --- integration/token/interop/support.go | 42 +++-- .../token/interop/views/hashescrow/lock.go | 20 +-- token/core/fabtoken/v1/validator/validator.go | 1 + .../v1/validator/validator_transfer.go | 132 ++++++++------ .../zkatdlog/nogh/v1/validator/validator.go | 1 + .../nogh/v1/validator/validator_transfer.go | 110 +++++++----- .../identity/interop/hashescrow/validator.go | 75 ++++---- .../interop/hashescrow/validator_test.go | 45 +++-- token/services/interop/hashescrow/audit.go | 12 +- token/services/interop/hashescrow/keys.go | 67 ++++++-- token/services/interop/hashescrow/script.go | 36 ++-- .../interop/hashescrow/script_test.go | 16 +- token/services/interop/hashescrow/signer.go | 2 +- .../interop/hashescrow/transaction.go | 162 ++++++------------ .../interop/hashescrow/transaction_test.go | 28 +-- .../interop/hashescrow/wallet_filter.go | 6 +- 16 files changed, 412 insertions(+), 343 deletions(-) diff --git a/integration/token/interop/support.go b/integration/token/interop/support.go index 45f0b17435..cd3d89fbf9 100644 --- a/integration/token/interop/support.go +++ b/integration/token/interop/support.go @@ -8,6 +8,7 @@ package interop import ( "crypto" + "crypto/rand" "fmt" "strconv" "strings" @@ -322,6 +323,31 @@ func HTLCLock(network *integration.Infrastructure, tmsID token.TMSID, id *token3 } func HashEscrowLock(network *integration.Infrastructure, tmsID token.TMSID, id *token3.NodeReference, wallet string, typ token2.Type, amount uint64, receiver *token3.NodeReference, auditor *token3.NodeReference, recipientHash []byte, senderHash []byte, hashFunc crypto.Hash, errorMsgs ...string) (string, []byte, []byte, []byte, []byte) { + if hashFunc == 0 { + hashFunc = crypto.SHA256 + } + + var recipientPreImage []byte + var senderPreImage []byte + if len(recipientHash) == 0 { + recipientPreImage = make([]byte, 24) + _, err := rand.Read(recipientPreImage) + gomega.Expect(err).NotTo(gomega.HaveOccurred()) + h := hashFunc.New() + _, err = h.Write(recipientPreImage) + gomega.Expect(err).NotTo(gomega.HaveOccurred()) + recipientHash = h.Sum(nil) + } + if len(senderHash) == 0 { + senderPreImage = make([]byte, 24) + _, err := rand.Read(senderPreImage) + gomega.Expect(err).NotTo(gomega.HaveOccurred()) + h := hashFunc.New() + _, err = h.Write(senderPreImage) + gomega.Expect(err).NotTo(gomega.HaveOccurred()) + senderHash = h.Sum(nil) + } + result, err := network.Client(id.ReplicaName()).CallView("hashescrow.lock", common.JSONMarshall(&hashescrowviews.Lock{ TMSID: tmsID, Wallet: wallet, @@ -340,22 +366,12 @@ func HashEscrowLock(network *integration.Infrastructure, tmsID token.TMSID, id * common2.CheckFinality(network, receiver, lockResult.TxID, &tmsID, false) common2.CheckFinality(network, auditor, lockResult.TxID, &tmsID, false) - if len(recipientHash) == 0 { - gomega.Expect(lockResult.RecipientPreImage).NotTo(gomega.BeNil()) - } - if len(senderHash) == 0 { - gomega.Expect(lockResult.SenderPreImage).NotTo(gomega.BeNil()) - } gomega.Expect(lockResult.RecipientHash).NotTo(gomega.BeNil()) gomega.Expect(lockResult.SenderHash).NotTo(gomega.BeNil()) - if len(recipientHash) != 0 { - gomega.Expect(lockResult.RecipientHash).To(gomega.BeEquivalentTo(recipientHash)) - } - if len(senderHash) != 0 { - gomega.Expect(lockResult.SenderHash).To(gomega.BeEquivalentTo(senderHash)) - } + gomega.Expect(lockResult.RecipientHash).To(gomega.BeEquivalentTo(recipientHash)) + gomega.Expect(lockResult.SenderHash).To(gomega.BeEquivalentTo(senderHash)) - return lockResult.TxID, lockResult.RecipientPreImage, lockResult.SenderPreImage, lockResult.RecipientHash, lockResult.SenderHash + return lockResult.TxID, recipientPreImage, senderPreImage, lockResult.RecipientHash, lockResult.SenderHash } gomega.Expect(err).To(gomega.HaveOccurred()) diff --git a/integration/token/interop/views/hashescrow/lock.go b/integration/token/interop/views/hashescrow/lock.go index 5869b7649e..04a60c135a 100644 --- a/integration/token/interop/views/hashescrow/lock.go +++ b/integration/token/interop/views/hashescrow/lock.go @@ -15,7 +15,6 @@ import ( "github.com/hyperledger-labs/fabric-smart-client/platform/view/services/id" "github.com/hyperledger-labs/fabric-smart-client/platform/view/view" "github.com/hyperledger-labs/fabric-token-sdk/token" - "github.com/hyperledger-labs/fabric-token-sdk/token/services/interop/encoding" "github.com/hyperledger-labs/fabric-token-sdk/token/services/interop/hashescrow" "github.com/hyperledger-labs/fabric-token-sdk/token/services/ttx" token2 "github.com/hyperledger-labs/fabric-token-sdk/token/token" @@ -33,11 +32,9 @@ type Lock struct { } type LockInfo struct { - TxID string - RecipientPreImage []byte - SenderPreImage []byte - RecipientHash []byte - SenderHash []byte + TxID string + RecipientHash []byte + SenderHash []byte } type LockView struct { @@ -75,7 +72,7 @@ func (hv *LockView) Call(context view.Context) (res interface{}, err error) { senderWallet := hashescrow.GetWallet(context, hv.Wallet, token.WithTMSID(hv.TMSID)) assert.NotNil(senderWallet, "sender wallet [%s] not found", hv.Wallet) - preImages, err := tx.LockWithPreImages( + _, err = tx.Lock( context.Context(), senderWallet, me, @@ -85,7 +82,6 @@ func (hv *LockView) Call(context view.Context) (res interface{}, err error) { hashescrow.WithRecipientHash(hv.RecipientHash), hashescrow.WithSenderHash(hv.SenderHash), hashescrow.WithHashFunc(hv.HashFunc), - hashescrow.WithHashEncoding(encoding.None), ) assert.NoError(err, "failed adding a hash escrow lock operation") @@ -112,11 +108,9 @@ func (hv *LockView) Call(context view.Context) (res interface{}, err error) { assert.NotNil(script, "expected a hash escrow script output") return &LockInfo{ - TxID: tx.ID(), - RecipientPreImage: preImages.Recipient, - SenderPreImage: preImages.Sender, - RecipientHash: script.RecipientHashInfo.Hash, - SenderHash: script.SenderHashInfo.Hash, + TxID: tx.ID(), + RecipientHash: script.RecipientHashInfo.Hash, + SenderHash: script.SenderHashInfo.Hash, }, nil } diff --git a/token/core/fabtoken/v1/validator/validator.go b/token/core/fabtoken/v1/validator/validator.go index 2f19d90ffc..fe373c1bdf 100644 --- a/token/core/fabtoken/v1/validator/validator.go +++ b/token/core/fabtoken/v1/validator/validator.go @@ -61,6 +61,7 @@ func NewValidator( TransferSignatureValidate, TransferBalanceValidate, TransferHTLCValidate, + TransferHashEscrowValidate, common.TransferApplicationDataValidate[*setup.PublicParams, *actions.Output, *actions.TransferAction, *actions.IssueAction, driver.Deserializer], } transferValidators = append(transferValidators, extraTransferValidators...) diff --git a/token/core/fabtoken/v1/validator/validator_transfer.go b/token/core/fabtoken/v1/validator/validator_transfer.go index 0f13ee1363..5113078df5 100644 --- a/token/core/fabtoken/v1/validator/validator_transfer.go +++ b/token/core/fabtoken/v1/validator/validator_transfer.go @@ -185,41 +185,6 @@ func TransferHTLCValidate(c context.Context, ctx *Context) error { ctx.CountMetadataKey(metadataKey) } } - if owner.Type == hashescrow.ScriptType { - if len(ctx.TransferAction.GetOutputs()) != 1 { - return errors.New("invalid transfer action: a hash escrow script only transfers the ownership of a token") - } - - // check type and quantity - output := ctx.TransferAction.GetOutputs()[0].(*actions.Output) - tok := output - if ctx.InputTokens[0].Type != tok.Type { - return errors.New("invalid transfer action: type of input does not match type of output") - } - if ctx.InputTokens[0].Quantity != tok.Quantity { - return errors.New("invalid transfer action: quantity of input does not match quantity of output") - } - if output.IsRedeem() { - return errors.New("invalid transfer action: the output corresponding to a hash escrow spending should not be a redeem") - } - - // check owner field - script, err := hashescrow2.VerifyOwner(ctx.InputTokens[0].GetOwner(), tok.Owner) - if err != nil { - return errors.Wrap(err, "failed to verify transfer from hash escrow script") - } - - // check metadata - sigma := ctx.Signatures[i] - metadataKey, resolvedOwner, err := hashescrow2.MetadataClaimKeyCheck(ctx.TransferAction, script, sigma) - if err != nil { - return errors.WithMessagef(err, "failed to check hash escrow metadata") - } - if !identity.Identity(resolvedOwner).Equal(tok.Owner) { - return errors.New("invalid transfer action: output owner does not match hash escrow recipient resolved from preimage") - } - ctx.CountMetadataKey(metadataKey) - } } for _, o := range ctx.TransferAction.GetOutputs() { @@ -253,25 +218,90 @@ func TransferHTLCValidate(c context.Context, ctx *Context) error { continue } - if owner.Type == hashescrow.ScriptType { - script := &hashescrow.Script{} - err = json.Unmarshal(owner.Identity, script) - if err != nil { - return err - } - if err := script.Validate(); err != nil { - return errors.WithMessagef(err, "hash escrow script invalid") - } - metadataKeys, err := hashescrow2.MetadataLockKeyCheck(ctx.TransferAction, script) - if err != nil { - return errors.WithMessagef(err, "failed to check hash escrow metadata") - } - for _, metadataKey := range metadataKeys { - ctx.CountMetadataKey(metadataKey) - } + } + + return nil +} +// TransferHashEscrowValidate validates the hash escrow scripts in a transfer action. +func TransferHashEscrowValidate(c context.Context, ctx *Context) error { + for i, in := range ctx.InputTokens { + owner, err := identity.UnmarshalTypedIdentity(in.GetOwner()) + if err != nil { + return errors.Wrap(err, "failed to unmarshal owner of input token") + } + if owner.Type != hashescrow.HashEscrow { continue } + if len(ctx.TransferAction.GetOutputs()) != 1 { + return errors.New("invalid transfer action: a hash escrow script only transfers the ownership of a token") + } + + output := ctx.TransferAction.GetOutputs()[0].(*actions.Output) + tok := output + if ctx.InputTokens[0].Type != tok.Type { + return errors.New("invalid transfer action: type of input does not match type of output") + } + if ctx.InputTokens[0].Quantity != tok.Quantity { + return errors.New("invalid transfer action: quantity of input does not match quantity of output") + } + if output.IsRedeem() { + return errors.New("invalid transfer action: the output corresponding to a hash escrow spending should not be a redeem") + } + + script, err := hashescrow2.VerifyOwner(ctx.InputTokens[0].GetOwner(), tok.Owner) + if err != nil { + return errors.Wrap(err, "failed to verify transfer from hash escrow script") + } + + sigma := ctx.Signatures[i] + claim, err := hashescrow2.ClaimFromSignature(sigma) + if err != nil { + return errors.WithMessagef(err, "failed to parse hash escrow claim signature") + } + resolvedOwner, _, claimedBy, err := hashescrow2.ResolveOwnerAndHash(script, claim.Preimage) + if err != nil { + return errors.WithMessagef(err, "failed to resolve hash escrow claim") + } + if !identity.Identity(resolvedOwner).Equal(tok.Owner) { + return errors.New("invalid transfer action: output owner does not match hash escrow recipient resolved from preimage") + } + metadataKey, err := hashescrow2.ClaimMetadataCheck(ctx.TransferAction, script, claim.Preimage, claimedBy) + if err != nil { + return errors.WithMessagef(err, "failed to check hash escrow claim metadata") + } + ctx.CountMetadataKey(metadataKey) + } + + for _, o := range ctx.TransferAction.GetOutputs() { + out, ok := o.(*actions.Output) + if !ok { + return errors.New("invalid output") + } + if out.IsRedeem() { + continue + } + + owner, err := identity.UnmarshalTypedIdentity(out.Owner) + if err != nil { + return err + } + if owner.Type != hashescrow.HashEscrow { + continue + } + script := &hashescrow.Script{} + err = json.Unmarshal(owner.Identity, script) + if err != nil { + return err + } + if err := script.Validate(); err != nil { + return errors.WithMessagef(err, "hash escrow script invalid") + } + metadataKey, err := hashescrow2.LockMetadataCheck(ctx.TransferAction, script) + if err != nil { + return errors.WithMessagef(err, "failed to check hash escrow lock metadata") + } + ctx.CountMetadataKey(metadataKey) } return nil diff --git a/token/core/zkatdlog/nogh/v1/validator/validator.go b/token/core/zkatdlog/nogh/v1/validator/validator.go index f73e2f62fc..90ab050840 100644 --- a/token/core/zkatdlog/nogh/v1/validator/validator.go +++ b/token/core/zkatdlog/nogh/v1/validator/validator.go @@ -74,6 +74,7 @@ func New( TransferUpgradeWitnessValidate, TransferZKProofValidate, TransferHTLCValidate, + TransferHashEscrowValidate, common.TransferApplicationDataValidate[*v1.PublicParams, *token.Token, *transfer.Action, *issue.Action, driver.Deserializer], } transferValidators = append(transferValidators, extraTransferValidators...) diff --git a/token/core/zkatdlog/nogh/v1/validator/validator_transfer.go b/token/core/zkatdlog/nogh/v1/validator/validator_transfer.go index 6421c34b59..6308ae00da 100644 --- a/token/core/zkatdlog/nogh/v1/validator/validator_transfer.go +++ b/token/core/zkatdlog/nogh/v1/validator/validator_transfer.go @@ -184,32 +184,6 @@ func TransferHTLCValidate(c context.Context, ctx *Context) error { ctx.CountMetadataKey(metadataKey) } } - if owner.Type == hashescrow.ScriptType { - // Hash escrow script must be a 1-to-1 transfer of ownership - if len(ctx.InputTokens) != 1 || len(ctx.TransferAction.GetOutputs()) != 1 { - return ErrInvalidHTLCAction - } - - out, ok := ctx.TransferAction.GetOutputs()[0].(*token.Token) - if !ok || out == nil { - return ErrHTLCOutputNotFound - } - - script, err := hashescrow2.VerifyOwner(ctx.InputTokens[0].Owner, out.Owner) - if err != nil { - return errors.Wrap(err, "failed to verify transfer from hash escrow script") - } - - sigma := ctx.Signatures[i] - metadataKey, resolvedOwner, err := hashescrow2.MetadataClaimKeyCheck(ctx.TransferAction, script, sigma) - if err != nil { - return errors.WithMessagef(err, "failed to check hash escrow metadata") - } - if !identity.Identity(resolvedOwner).Equal(out.Owner) { - return errors.New("invalid transfer action: output owner does not match hash escrow recipient resolved from preimage") - } - ctx.CountMetadataKey(metadataKey) - } } for _, o := range ctx.TransferAction.Outputs { @@ -237,25 +211,77 @@ func TransferHTLCValidate(c context.Context, ctx *Context) error { continue } - if owner.Type == hashescrow.ScriptType { - script := &hashescrow.Script{} - err = script.FromBytes(owner.Identity) - if err != nil { - return err - } - if err := script.Validate(); err != nil { - return errors.WithMessagef(err, "hash escrow script invalid") - } - metadataKeys, err := hashescrow2.MetadataLockKeyCheck(ctx.TransferAction, script) - if err != nil { - return errors.WithMessagef(err, "failed to check hash escrow metadata") - } - for _, metadataKey := range metadataKeys { - ctx.CountMetadataKey(metadataKey) - } + } + + return nil +} +// TransferHashEscrowValidate validates the HashEscrow scripts in the transfer action. +func TransferHashEscrowValidate(c context.Context, ctx *Context) error { + for i, in := range ctx.InputTokens { + owner, err := identity.UnmarshalTypedIdentity(in.Owner) + if err != nil { + return errors.Wrap(err, "failed to unmarshal owner of input token") + } + if owner.Type != hashescrow.HashEscrow { continue } + if len(ctx.InputTokens) != 1 || len(ctx.TransferAction.GetOutputs()) != 1 { + return ErrInvalidHTLCAction + } + + out, ok := ctx.TransferAction.GetOutputs()[0].(*token.Token) + if !ok || out == nil { + return ErrHTLCOutputNotFound + } + + script, err := hashescrow2.VerifyOwner(ctx.InputTokens[0].Owner, out.Owner) + if err != nil { + return errors.Wrap(err, "failed to verify transfer from hash escrow script") + } + sigma := ctx.Signatures[i] + claim, err := hashescrow2.ClaimFromSignature(sigma) + if err != nil { + return errors.WithMessagef(err, "failed to parse hash escrow claim signature") + } + resolvedOwner, _, claimedBy, err := hashescrow2.ResolveOwnerAndHash(script, claim.Preimage) + if err != nil { + return errors.WithMessagef(err, "failed to resolve hash escrow claim") + } + if !identity.Identity(resolvedOwner).Equal(out.Owner) { + return errors.New("invalid transfer action: output owner does not match hash escrow recipient resolved from preimage") + } + metadataKey, err := hashescrow2.ClaimMetadataCheck(ctx.TransferAction, script, claim.Preimage, claimedBy) + if err != nil { + return errors.WithMessagef(err, "failed to check hash escrow claim metadata") + } + ctx.CountMetadataKey(metadataKey) + } + + for _, o := range ctx.TransferAction.Outputs { + if o.IsRedeem() { + continue + } + owner, err := identity.UnmarshalTypedIdentity(o.Owner) + if err != nil { + return err + } + if owner.Type != hashescrow.HashEscrow { + continue + } + script := &hashescrow.Script{} + err = script.FromBytes(owner.Identity) + if err != nil { + return err + } + if err := script.Validate(); err != nil { + return errors.WithMessagef(err, "hash escrow script invalid") + } + metadataKey, err := hashescrow2.LockMetadataCheck(ctx.TransferAction, script) + if err != nil { + return errors.WithMessagef(err, "failed to check hash escrow lock metadata") + } + ctx.CountMetadataKey(metadataKey) } return nil diff --git a/token/services/identity/interop/hashescrow/validator.go b/token/services/identity/interop/hashescrow/validator.go index d261126bf6..475d6f0531 100644 --- a/token/services/identity/interop/hashescrow/validator.go +++ b/token/services/identity/interop/hashescrow/validator.go @@ -20,6 +20,8 @@ type Action interface { GetMetadata() map[string][]byte } +const HashEscrow = ScriptType + // VerifyOwner validates that the output owner is either sender or recipient as encoded in the hash escrow script. func VerifyOwner(senderRawOwner []byte, outRawOwner []byte) (*hashescrow.Script, error) { if len(outRawOwner) == 0 { @@ -29,8 +31,8 @@ func VerifyOwner(senderRawOwner []byte, outRawOwner []byte) (*hashescrow.Script, if err != nil { return nil, err } - if sender.Type != ScriptType { - return nil, errors.Errorf("invalid identity type, expected [%s], got [%s]", ScriptType, sender.Type) + if sender.Type != HashEscrow { + return nil, errors.Errorf("invalid identity type, expected [%s], got [%s]", HashEscrow, sender.Type) } script := &hashescrow.Script{} err = json.Unmarshal(sender.Identity, script) @@ -44,58 +46,73 @@ func VerifyOwner(senderRawOwner []byte, outRawOwner []byte) (*hashescrow.Script, return nil, errors.New("owner of output token does not correspond to sender or recipient in hash escrow request") } -// MetadataClaimKeyCheck validates claim metadata and returns the matched metadata key. -func MetadataClaimKeyCheck(action Action, script *hashescrow.Script, sig []byte) (string, []byte, error) { +func ClaimFromSignature(sig []byte) (*hashescrow.ClaimSignature, error) { claim := &hashescrow.ClaimSignature{} if err := json.Unmarshal(sig, claim); err != nil { - return "", nil, errors.Wrapf(err, "failed unmarshalling claim signature [%s]", string(sig)) + return nil, errors.Wrapf(err, "failed unmarshalling claim signature [%s]", string(sig)) } if len(claim.Preimage) == 0 { - return "", nil, errors.New("expected a valid claim preImage") + return nil, errors.New("expected a valid claim preimage") } - resolvedOwner, image, err := script.ResolveRecipientForPreImage(claim.Preimage) + + return claim, nil +} + +func ResolveOwnerAndHash(script *hashescrow.Script, preimage []byte) ([]byte, []byte, string, error) { + resolvedOwner, resolvedHash, claimedBy, err := script.ResolveOwnerAndHashForPreimage(preimage) if err != nil { - return "", nil, errors.Wrap(err, "failed resolving recipient from preimage") + return nil, nil, "", errors.Wrap(err, "failed resolving recipient from preimage") } + return resolvedOwner, resolvedHash, claimedBy, nil +} + +// ClaimMetadataCheck validates claim metadata and returns the matched metadata key. +func ClaimMetadataCheck(action Action, script *hashescrow.Script, preimage []byte, claimedBy string) (string, error) { metadata := action.GetMetadata() if len(metadata) == 0 { - return "", nil, errors.New("cannot find hash escrow pre-image, no metadata") + return "", errors.New("cannot find hash escrow unlock preimage, no metadata") } - key := hashescrow.ClaimKey(image) + key := hashescrow.ClaimKey(script.RecipientHashInfo.Hash, script.SenderHashInfo.Hash) value, ok := metadata[key] if !ok { - return "", nil, errors.New("cannot find hash escrow pre-image, missing metadata entry") + return "", errors.New("cannot find hash escrow unlock preimage, missing metadata entry") + } + claimValue := &hashescrow.ClaimMetadataValue{} + if err := json.Unmarshal(value, claimValue); err != nil { + return "", errors.Wrapf(err, "cannot unmarshal hash escrow claim metadata value [%s]", string(value)) } - if !bytes.Equal(value, claim.Preimage) { - return "", nil, errors.Errorf("invalid action, cannot match hash escrow pre-image with metadata [%x]!=[%x]", value, claim.Preimage) + if !bytes.Equal(claimValue.Preimage, preimage) { + return "", errors.Errorf("invalid action, cannot match hash escrow unlock preimage with metadata [%x]!=[%x]", claimValue.Preimage, preimage) + } + if claimValue.ClaimedBy != claimedBy { + return "", errors.Errorf("invalid action, cannot match hash escrow claimant side with metadata [%s]!=[%s]", claimValue.ClaimedBy, claimedBy) } - return key, resolvedOwner, nil + return key, nil } -// MetadataLockKeyCheck validates lock metadata and returns the lock metadata key. -func MetadataLockKeyCheck(action Action, script *hashescrow.Script) ([]string, error) { +// LockMetadataCheck validates lock metadata and returns the lock metadata key. +func LockMetadataCheck(action Action, script *hashescrow.Script) (string, error) { metadata := action.GetMetadata() if len(metadata) == 0 { - return nil, errors.New("cannot find hash escrow lock, no metadata") + return "", errors.New("cannot find hash escrow lock, no metadata") } - recipientKey := hashescrow.LockKey(script.RecipientHashInfo.Hash) - recipientValue, ok := metadata[recipientKey] + key := hashescrow.LockKey(script.RecipientHashInfo.Hash, script.SenderHashInfo.Hash) + lockValue, ok := metadata[key] if !ok { - return nil, errors.New("cannot find recipient hash escrow lock, missing metadata entry") + return "", errors.New("cannot find hash escrow lock, missing metadata entry") } - if !bytes.Equal(recipientValue, hashescrow.LockValue(script.RecipientHashInfo.Hash)) { - return nil, errors.Errorf("invalid action, cannot match recipient hash escrow lock with metadata [%x]!=[%x]", recipientValue, script.RecipientHashInfo.Hash) + lockMetadata := &hashescrow.LockMetadata{} + if err := json.Unmarshal(lockValue, lockMetadata); err != nil { + return "", errors.Wrapf(err, "cannot unmarshal hash escrow lock metadata value [%s]", string(lockValue)) } - senderKey := hashescrow.LockKey(script.SenderHashInfo.Hash) - senderValue, ok := metadata[senderKey] - if !ok { - return nil, errors.New("cannot find sender hash escrow lock, missing metadata entry") + if !bytes.Equal(lockMetadata.RecipientHash, script.RecipientHashInfo.Hash) { + return "", errors.Errorf("invalid action, cannot match recipient hash escrow lock with metadata [%x]!=[%x]", lockMetadata.RecipientHash, script.RecipientHashInfo.Hash) } - if !bytes.Equal(senderValue, hashescrow.LockValue(script.SenderHashInfo.Hash)) { - return nil, errors.Errorf("invalid action, cannot match sender hash escrow lock with metadata [%x]!=[%x]", senderValue, script.SenderHashInfo.Hash) + if !bytes.Equal(lockMetadata.SenderHash, script.SenderHashInfo.Hash) { + return "", errors.Errorf("invalid action, cannot match sender hash escrow lock with metadata [%x]!=[%x]", lockMetadata.SenderHash, script.SenderHashInfo.Hash) } - return []string{recipientKey, senderKey}, nil + return key, nil } diff --git a/token/services/identity/interop/hashescrow/validator_test.go b/token/services/identity/interop/hashescrow/validator_test.go index 74a7633e74..c42c126bce 100644 --- a/token/services/identity/interop/hashescrow/validator_test.go +++ b/token/services/identity/interop/hashescrow/validator_test.go @@ -78,54 +78,61 @@ func TestVerifyOwner(t *testing.T) { require.Error(t, err) } -func TestMetadataClaimKeyCheck(t *testing.T) { +func TestClaimMetadataCheck(t *testing.T) { _, script := wrapScript(t, []byte("s"), []byte("r"), []byte("h")) preimage := []byte("pre") - image, err := script.RecipientHashInfo.Image(preimage) + resolvedHash, err := script.RecipientHashInfo.Image(preimage) + require.NoError(t, err) + script.RecipientHashInfo.Hash = resolvedHash + key := he.ClaimKey(script.RecipientHashInfo.Hash, script.SenderHashInfo.Hash) + value, err := he.ClaimValue(preimage, "recipient") require.NoError(t, err) - script.RecipientHashInfo.Hash = image - key := he.ClaimKey(image) sigRaw, err := json.Marshal(&he.ClaimSignature{ Preimage: preimage, }) require.NoError(t, err) + claim, err := ihe.ClaimFromSignature(sigRaw) + require.NoError(t, err) + resolvedOwner, _, claimedBy, err := ihe.ResolveOwnerAndHash(script, claim.Preimage) + require.NoError(t, err) + require.Equal(t, identity.Identity("r"), identity.Identity(resolvedOwner)) + require.Equal(t, "recipient", claimedBy) - act := &actionStub{md: map[string][]byte{key: preimage}} - got, _, err := ihe.MetadataClaimKeyCheck(act, script, sigRaw) + act := &actionStub{md: map[string][]byte{key: value}} + got, err := ihe.ClaimMetadataCheck(act, script, claim.Preimage, claimedBy) require.NoError(t, err) require.Equal(t, key, got) act = &actionStub{md: map[string][]byte{}} - _, _, err = ihe.MetadataClaimKeyCheck(act, script, sigRaw) + _, err = ihe.ClaimMetadataCheck(act, script, claim.Preimage, claimedBy) require.Error(t, err) - _, _, err = ihe.MetadataClaimKeyCheck(act, script, []byte("bad-json")) + _, err = ihe.ClaimFromSignature([]byte("bad-json")) require.Error(t, err) } -func TestMetadataLockKeyCheck(t *testing.T) { +func TestLockMetadataCheck(t *testing.T) { _, script := wrapScript(t, []byte("s"), []byte("r"), []byte("h")) - recipientKey := he.LockKey(script.RecipientHashInfo.Hash) - senderKey := he.LockKey(script.SenderHashInfo.Hash) + key := he.LockKey(script.RecipientHashInfo.Hash, script.SenderHashInfo.Hash) + lockValue, err := he.LockValue(script.RecipientHashInfo.Hash, script.SenderHashInfo.Hash) + require.NoError(t, err) act := &actionStub{md: map[string][]byte{ - recipientKey: he.LockValue(script.RecipientHashInfo.Hash), - senderKey: he.LockValue(script.SenderHashInfo.Hash), + key: lockValue, }} - got, err := ihe.MetadataLockKeyCheck(act, script) + got, err := ihe.LockMetadataCheck(act, script) require.NoError(t, err) - require.ElementsMatch(t, []string{recipientKey, senderKey}, got) + require.Equal(t, key, got) act = &actionStub{md: map[string][]byte{}} - _, err = ihe.MetadataLockKeyCheck(act, script) + _, err = ihe.LockMetadataCheck(act, script) require.Error(t, err) act = &actionStub{md: map[string][]byte{ - recipientKey: []byte("bad"), - senderKey: he.LockValue(script.SenderHashInfo.Hash), + key: []byte("bad"), }} - _, err = ihe.MetadataLockKeyCheck(act, script) + _, err = ihe.LockMetadataCheck(act, script) require.Error(t, err) } diff --git a/token/services/interop/hashescrow/audit.go b/token/services/interop/hashescrow/audit.go index 91490a6167..95832829f2 100644 --- a/token/services/interop/hashescrow/audit.go +++ b/token/services/interop/hashescrow/audit.go @@ -27,7 +27,7 @@ func ToInput(i *token.Input) (*Input, error) { return &Input{ Input: i, - isHashEscrow: owner.Type == ScriptType, + isHashEscrow: owner.Type == HashEscrow, }, nil } @@ -44,8 +44,8 @@ func (i *Input) Script() (*Script, error) { if err != nil { return nil, errors.Wrapf(err, "failed to unmarshal owner") } - if owner.Type != ScriptType { - return nil, errors.Errorf("invalid identity type, expected [%s], got [%s]", ScriptType, owner.Type) + if owner.Type != HashEscrow { + return nil, errors.Errorf("invalid identity type, expected [%s], got [%s]", HashEscrow, owner.Type) } script := &Script{} err = json.Unmarshal(owner.Identity, script) @@ -69,7 +69,7 @@ func ToOutput(i *token.Output) (*Output, error) { return &Output{ Output: i, - isHashEscrow: owner.Type == ScriptType, + isHashEscrow: owner.Type == HashEscrow, }, nil } @@ -86,8 +86,8 @@ func (o *Output) Script() (*Script, error) { if err != nil { return nil, errors.Wrapf(err, "failed to unmarshal owner") } - if owner.Type != ScriptType { - return nil, errors.Errorf("invalid identity type, expected [%s], got [%s]", ScriptType, owner.Type) + if owner.Type != HashEscrow { + return nil, errors.Errorf("invalid identity type, expected [%s], got [%s]", HashEscrow, owner.Type) } script := &Script{} err = json.Unmarshal(owner.Identity, script) diff --git a/token/services/interop/hashescrow/keys.go b/token/services/interop/hashescrow/keys.go index 4da24c0a9e..eaa2350ba3 100644 --- a/token/services/interop/hashescrow/keys.go +++ b/token/services/interop/hashescrow/keys.go @@ -6,24 +6,67 @@ SPDX-License-Identifier: Apache-2.0 package hashescrow -import "encoding/hex" +import ( + "crypto/sha256" + "encoding/hex" + + "github.com/hyperledger-labs/fabric-smart-client/pkg/utils/errors" + "github.com/hyperledger-labs/fabric-token-sdk/token/core/common/encoding/json" +) const ( - ClaimPreImage = "hashescrow.cpi" - LockHash = "hashescrow.lh" + UnlockPreimage = "hashescrow.upi:" + LockHashes = "hashescrow.lh:" + ClaimMetadata = "hashescrow.cm:" ) -// ClaimKey returns the claim key for the passed byte array. -func ClaimKey(v []byte) string { - return ClaimPreImage + hex.EncodeToString(v) +type LockMetadata struct { + RecipientHash []byte `json:"recipient_hash"` + SenderHash []byte `json:"sender_hash"` +} + +type ClaimMetadataValue struct { + Preimage []byte `json:"preimage"` + ClaimedBy string `json:"claimed_by"` +} + +func aggregateHash(recipientHash, senderHash []byte) []byte { + h := sha256.New() + h.Write(recipientHash) + h.Write([]byte{':'}) + h.Write(senderHash) + + return h.Sum(nil) } -// LockKey returns the lock key for the passed byte array. -func LockKey(v []byte) string { - return LockHash + hex.EncodeToString(v) +func LockKey(recipientHash, senderHash []byte) string { + return LockHashes + hex.EncodeToString(aggregateHash(recipientHash, senderHash)) } -// LockValue returns the encoding of the value for a lock key. -func LockValue(v []byte) []byte { - return []byte(hex.EncodeToString(v)) +func ClaimKey(recipientHash, senderHash []byte) string { + return ClaimMetadata + hex.EncodeToString(aggregateHash(recipientHash, senderHash)) +} + +func LockValue(recipientHash, senderHash []byte) ([]byte, error) { + raw, err := json.Marshal(&LockMetadata{ + RecipientHash: recipientHash, + SenderHash: senderHash, + }) + if err != nil { + return nil, errors.Wrap(err, "failed to marshal hash escrow lock metadata") + } + + return raw, nil +} + +func ClaimValue(preimage []byte, claimedBy string) ([]byte, error) { + raw, err := json.Marshal(&ClaimMetadataValue{ + Preimage: preimage, + ClaimedBy: claimedBy, + }) + if err != nil { + return nil, errors.Wrap(err, "failed to marshal hash escrow claim metadata") + } + + return raw, nil } diff --git a/token/services/interop/hashescrow/script.go b/token/services/interop/hashescrow/script.go index 37e3c027b2..94f7402f01 100644 --- a/token/services/interop/hashescrow/script.go +++ b/token/services/interop/hashescrow/script.go @@ -52,27 +52,37 @@ func (s *Script) Validate() error { return nil } -// ResolveRecipientForPreImage resolves the output recipient from the provided pre-image. +// ResolveOwnerAndHashForPreimage resolves the output owner from the provided pre-image. // If the pre-image matches the recipient hash, recipient receives the token. // If the pre-image matches the sender hash, sender receives the token. -func (s *Script) ResolveRecipientForPreImage(preImage []byte) (view.Identity, []byte, error) { - recipientImage, err := s.RecipientHashInfo.Image(preImage) +func (s *Script) ResolveOwnerAndHashForPreimage(preimage []byte) (view.Identity, []byte, string, error) { + recipientHash, err := s.RecipientHashInfo.Image(preimage) if err != nil { - return nil, nil, errors.WithMessage(err, "failed computing recipient hash image") + return nil, nil, "", errors.WithMessage(err, "failed computing recipient hash") } - if err := s.RecipientHashInfo.Compare(recipientImage); err == nil { - return s.Recipient, recipientImage, nil + if err := s.RecipientHashInfo.Compare(recipientHash); err == nil { + return s.Recipient, recipientHash, "recipient", nil } - senderImage, err := s.SenderHashInfo.Image(preImage) + senderHash, err := s.SenderHashInfo.Image(preimage) if err != nil { - return nil, nil, errors.WithMessage(err, "failed computing sender hash image") + return nil, nil, "", errors.WithMessage(err, "failed computing sender hash") + } + if err := s.SenderHashInfo.Compare(senderHash); err == nil { + return s.Sender, senderHash, "sender", nil } - if err := s.SenderHashInfo.Compare(senderImage); err == nil { - return s.Sender, senderImage, nil + + return nil, nil, "", errors.New("preimage does not match any hashescrow hash") +} + +// ResolveRecipientForPreImage is kept for backward compatibility with existing call sites. +func (s *Script) ResolveRecipientForPreImage(preImage []byte) (view.Identity, []byte, error) { + owner, hash, _, err := s.ResolveOwnerAndHashForPreimage(preImage) + if err != nil { + return nil, nil, err } - return nil, nil, errors.New("preimage does not match any hashescrow hash") + return owner, hash, nil } func (s *Script) FromBytes(raw []byte) error { @@ -101,8 +111,8 @@ func (s *ScriptAuth) IsMine(ctx context.Context, tok *token3.Token) (string, []s return "", nil, false } - if owner.Type != ScriptType { - logger.DebugfContext(ctx, "Is Mine [%s,%s,%s]? No, owner type is [%s] instead of [%s]", view.Identity(tok.Owner), tok.Type, tok.Quantity, owner.Type, ScriptType) + if owner.Type != HashEscrow { + logger.DebugfContext(ctx, "Is Mine [%s,%s,%s]? No, owner type is [%s] instead of [%s]", view.Identity(tok.Owner), tok.Type, tok.Quantity, owner.Type, HashEscrow) return "", nil, false } diff --git a/token/services/interop/hashescrow/script_test.go b/token/services/interop/hashescrow/script_test.go index 379734a9e9..da26e3b160 100644 --- a/token/services/interop/hashescrow/script_test.go +++ b/token/services/interop/hashescrow/script_test.go @@ -97,12 +97,16 @@ func TestResolveRecipientForPreImage(t *testing.T) { } func TestKeys(t *testing.T) { - k := ClaimKey([]byte{0x01, 0x02}) - require.Equal(t, "hashescrow.cpi0102", k) + recipientHash := []byte{0x01, 0x02} + senderHash := []byte{0x03, 0x04} - k = LockKey([]byte{0x0a}) - require.Equal(t, "hashescrow.lh0a", k) + k := ClaimKey(recipientHash, senderHash) + require.Contains(t, k, "hashescrow.cm:") - v := LockValue([]byte{0x0a, 0x0b}) - require.Equal(t, []byte("0a0b"), v) + k = LockKey(recipientHash, senderHash) + require.Contains(t, k, "hashescrow.lh:") + + v, err := LockValue(recipientHash, senderHash) + require.NoError(t, err) + require.NotEmpty(t, v) } diff --git a/token/services/interop/hashescrow/signer.go b/token/services/interop/hashescrow/signer.go index b5587124a0..898f686594 100644 --- a/token/services/interop/hashescrow/signer.go +++ b/token/services/interop/hashescrow/signer.go @@ -51,7 +51,7 @@ func (cv *ClaimVerifier) Verify(tokenRequestAndTxID, claimSignature []byte) erro if cv.Script == nil { return errors.New("invalid claim verifier, script is nil") } - if _, _, err = cv.Script.ResolveRecipientForPreImage(sig.Preimage); err != nil { + if _, _, _, err = cv.Script.ResolveOwnerAndHashForPreimage(sig.Preimage); err != nil { return errors.WithMessage(err, "preimage does not unlock hash escrow script") } diff --git a/token/services/interop/hashescrow/transaction.go b/token/services/interop/hashescrow/transaction.go index a3395857ea..76bf909ba5 100644 --- a/token/services/interop/hashescrow/transaction.go +++ b/token/services/interop/hashescrow/transaction.go @@ -9,8 +9,6 @@ package hashescrow import ( "context" "crypto" - "crypto/rand" - "encoding/base64" "encoding/json" "github.com/hyperledger-labs/fabric-smart-client/pkg/utils/errors" @@ -27,6 +25,7 @@ import ( const ( ScriptType = driver.HashEscrowIdentityType ScriptTypeString = driver.HashEscrowIdentityTypeString + HashEscrow = ScriptType ) // WithHash sets a hash attribute to customize the lock command. @@ -104,13 +103,6 @@ type Transaction struct { Binder Binder } -// PreImages carries the generated preimages for recipient and sender paths. -// If a hash is externally provided for a path, the corresponding preimage is nil. -type PreImages struct { - Recipient []byte - Sender []byte -} - func NewTransaction(sp view.Context, signer view.Identity, opts ...ttx.TxOption) (*Transaction, error) { tx, err := ttx.NewTransaction(sp, signer, opts...) if err != nil { @@ -141,16 +133,6 @@ func NewTransactionFromBytes(ctx view.Context, network, channel string, raw []by // Lock appends a hash-based escrow lock action to the token request. // This lock has no timeout semantics. func (t *Transaction) Lock(ctx context.Context, wallet *token.OwnerWallet, sender view.Identity, typ token2.Type, value uint64, recipient view.Identity, opts ...token.TransferOption) ([]byte, error) { - preImages, err := t.LockWithPreImages(ctx, wallet, sender, typ, value, recipient, opts...) - if err != nil { - return nil, err - } - - return preImages.Recipient, nil -} - -// LockWithPreImages appends a hash-based escrow lock action and returns both preimages. -func (t *Transaction) LockWithPreImages(ctx context.Context, wallet *token.OwnerWallet, sender view.Identity, typ token2.Type, value uint64, recipient view.Identity, opts ...token.TransferOption) (*PreImages, error) { options, err := compileTransferOptions(opts...) if err != nil { return nil, err @@ -170,15 +152,7 @@ func (t *Transaction) LockWithPreImages(ctx context.Context, wallet *token.Owner hashFunc := crypto.SHA256 var hashEncoding encoding.Encoding if options.Attributes != nil { - // backward compatibility - boxed, ok := options.Attributes["hashescrow.hash"] - if ok { - recipientHash, ok = boxed.([]byte) - if !ok { - return nil, errors.Errorf("expected hashescrow.hash attribute to be []byte, got [%T]", boxed) - } - } - boxed, ok = options.Attributes["hashescrow.recipientHash"] + boxed, ok := options.Attributes["hashescrow.recipientHash"] if ok { recipientHash, ok = boxed.([]byte) if !ok { @@ -210,15 +184,27 @@ func (t *Transaction) LockWithPreImages(ctx context.Context, wallet *token.Owner } } } + if len(recipientHash) == 0 { + return nil, errors.New("must specify recipient hash") + } + if len(senderHash) == 0 { + return nil, errors.New("must specify sender hash") + } + if hashFunc == 0 { + hashFunc = crypto.SHA256 + } - scriptID, preImages, script, err := t.recipientAsScript(sender, recipient, recipientHash, senderHash, hashFunc, hashEncoding) + scriptID, script, err := t.recipientAsScript(sender, recipient, recipientHash, senderHash, hashFunc, hashEncoding) + if err != nil { + return nil, err + } + lockValue, err := LockValue(script.RecipientHashInfo.Hash, script.SenderHashInfo.Hash) if err != nil { return nil, err } transferOpts := append(opts, - token.WithTransferMetadata(LockKey(script.RecipientHashInfo.Hash), LockValue(script.RecipientHashInfo.Hash)), - token.WithTransferMetadata(LockKey(script.SenderHashInfo.Hash), LockValue(script.SenderHashInfo.Hash)), + token.WithTransferMetadata(LockKey(script.RecipientHashInfo.Hash, script.SenderHashInfo.Hash), lockValue), ) _, err = t.TokenRequest.Transfer( t.Context, @@ -232,7 +218,7 @@ func (t *Transaction) LockWithPreImages(ctx context.Context, wallet *token.Owner return nil, err } - return preImages, nil + return nil, nil } // Claim appends a claim action to the token request. @@ -250,7 +236,7 @@ func (t *Transaction) Claim(wallet *token.OwnerWallet, tok *token2.UnspentToken, if err != nil { return err } - if owner.Type != ScriptType { + if owner.Type != HashEscrow { return errors.New("invalid owner type, expected hash escrow script") } script := &Script{} @@ -258,7 +244,7 @@ func (t *Transaction) Claim(wallet *token.OwnerWallet, tok *token2.UnspentToken, return errors.New("failed to unmarshal TypedIdentity as a hash escrow script") } - claimIdentity, image, err := script.ResolveRecipientForPreImage(preImage) + claimIdentity, _, claimedBy, err := script.ResolveOwnerAndHashForPreimage(preImage) if err != nil { return errors.Wrap(err, "passed preImage does not match script hashes") } @@ -281,103 +267,53 @@ func (t *Transaction) Claim(wallet *token.OwnerWallet, tok *token2.UnspentToken, return err } + claimValue, err := ClaimValue(preImage, claimedBy) + if err != nil { + return err + } + return t.Transfer( wallet, tok.Type, []uint64{q.ToBigInt().Uint64()}, []view.Identity{claimIdentity}, - append(opts, token.WithTokenIDs(&tok.Id), token.WithTransferMetadata(ClaimKey(image), preImage))..., + append(opts, + token.WithTokenIDs(&tok.Id), + token.WithTransferMetadata(ClaimKey(script.RecipientHashInfo.Hash, script.SenderHashInfo.Hash), claimValue), + )..., ) } -func (t *Transaction) recipientAsScript(sender, recipient view.Identity, recipientHash []byte, senderHash []byte, hashFunc crypto.Hash, hashEncoding encoding.Encoding) (view.Identity, *PreImages, *Script, error) { - preImages := &PreImages{} - var err error - - recipientHashInfo, recipientPreImage, err := buildHashInfo(recipientHash, hashFunc, hashEncoding) - if err != nil { - return nil, nil, nil, errors.WithMessage(err, "failed preparing recipient hash info") - } - senderHashInfo, senderPreImage, err := buildHashInfo(senderHash, hashFunc, hashEncoding) - if err != nil { - return nil, nil, nil, errors.WithMessage(err, "failed preparing sender hash info") - } - preImages.Recipient = recipientPreImage - preImages.Sender = senderPreImage - - logger.Debugf("recipient pair (pre-image, hash) = (%s,%s)", base64.StdEncoding.EncodeToString(preImages.Recipient), base64.StdEncoding.EncodeToString(recipientHashInfo.Hash)) - logger.Debugf("sender pair (pre-image, hash) = (%s,%s)", base64.StdEncoding.EncodeToString(preImages.Sender), base64.StdEncoding.EncodeToString(senderHashInfo.Hash)) - +func (t *Transaction) recipientAsScript(sender, recipient view.Identity, recipientHash []byte, senderHash []byte, hashFunc crypto.Hash, hashEncoding encoding.Encoding) (view.Identity, *Script, error) { script := &Script{ - RecipientHashInfo: recipientHashInfo, - SenderHashInfo: senderHashInfo, - Recipient: recipient, - Sender: sender, + RecipientHashInfo: HashInfo{ + Hash: recipientHash, + HashFunc: hashFunc, + HashEncoding: hashEncoding, + }, + SenderHashInfo: HashInfo{ + Hash: senderHash, + HashFunc: hashFunc, + HashEncoding: hashEncoding, + }, + Recipient: recipient, + Sender: sender, + } + if err := script.Validate(); err != nil { + return nil, nil, err } rawScript, err := json.Marshal(script) if err != nil { - return nil, nil, nil, err + return nil, nil, err } ro := &identity.TypedIdentity{ - Type: ScriptType, + Type: HashEscrow, Identity: rawScript, } raw, err := ro.Bytes() if err != nil { - return nil, nil, nil, err - } - - return raw, preImages, script, nil -} - -func buildHashInfo(hash []byte, hashFunc crypto.Hash, hashEncoding encoding.Encoding) (HashInfo, []byte, error) { - var preImage []byte - h := hash - - if hashFunc == 0 { - hashFunc = crypto.SHA256 - } - - if len(h) == 0 { - var err error - preImage, err = CreateNonce() - if err != nil { - return HashInfo{}, nil, err - } - digest := hashFunc.New() - if _, err := digest.Write(preImage); err != nil { - return HashInfo{}, nil, err - } - h = digest.Sum(nil) - if hashEncoding != 0 { - enc := hashEncoding.New() - if enc == nil { - return HashInfo{}, nil, errors.New("hashEncoding.New() returned nil") - } - h = []byte(enc.EncodeToString(h)) - } - } - - return HashInfo{ - Hash: h, - HashFunc: hashFunc, - HashEncoding: hashEncoding, - }, preImage, nil -} - -// CreateNonce generates a nonce. -func CreateNonce() ([]byte, error) { - nonce, err := getRandomNonce() - - return nonce, errors.WithMessagef(err, "error generating random nonce") -} - -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 nil, nil, err } - return key, nil + return raw, script, nil } diff --git a/token/services/interop/hashescrow/transaction_test.go b/token/services/interop/hashescrow/transaction_test.go index 543824f271..73af79a17a 100644 --- a/token/services/interop/hashescrow/transaction_test.go +++ b/token/services/interop/hashescrow/transaction_test.go @@ -34,40 +34,24 @@ func TestRecipientAsScript(t *testing.T) { sender := []byte("sender") recipient := []byte("recipient") - raw, preimages, script, err := tx.recipientAsScript(sender, recipient, []byte("recipient-hash"), []byte("sender-hash"), crypto.SHA256, encoding.Base64) + raw, script, err := tx.recipientAsScript(sender, recipient, []byte("recipient-hash"), []byte("sender-hash"), crypto.SHA256, encoding.Base64) require.NoError(t, err) - require.Empty(t, preimages.Recipient) - require.Empty(t, preimages.Sender) require.NotNil(t, raw) require.Equal(t, sender, []byte(script.Sender)) require.Equal(t, recipient, []byte(script.Recipient)) require.Equal(t, []byte("recipient-hash"), script.RecipientHashInfo.Hash) require.Equal(t, []byte("sender-hash"), script.SenderHashInfo.Hash) - - raw, preimages, script, err = tx.recipientAsScript(sender, recipient, nil, nil, crypto.SHA256, encoding.Base64) - require.NoError(t, err) - require.NotNil(t, raw) - require.NotEmpty(t, preimages.Recipient) - require.NotEmpty(t, preimages.Sender) - require.NotEmpty(t, script.RecipientHashInfo.Hash) - require.NotEmpty(t, script.SenderHashInfo.Hash) } -func TestRecipientAsScriptBadEncoding(t *testing.T) { +func TestRecipientAsScriptBadHashInfo(t *testing.T) { tx := &Transaction{} - _, _, _, err := tx.recipientAsScript( + _, _, err := tx.recipientAsScript( []byte("sender"), []byte("recipient"), - nil, - nil, + []byte("recipient-hash"), + []byte("sender-hash"), crypto.SHA256, encoding.Encoding(999), ) - require.EqualError(t, err, "failed preparing recipient hash info: hashEncoding.New() returned nil") -} - -func TestCreateNonce(t *testing.T) { - nonce, err := CreateNonce() - require.NoError(t, err) - require.Len(t, nonce, 24) + require.Error(t, err) } diff --git a/token/services/interop/hashescrow/wallet_filter.go b/token/services/interop/hashescrow/wallet_filter.go index 7199c031c9..ed1fba9c0e 100644 --- a/token/services/interop/hashescrow/wallet_filter.go +++ b/token/services/interop/hashescrow/wallet_filter.go @@ -25,13 +25,13 @@ type PreImageSelector struct { func (f *PreImageSelector) Filter(tok *token.UnspentToken, script *Script) (bool, error) { logger.Debugf("token [%s,%s,%s,%s] contains a hash escrow script? Yes", tok.Id, view.Identity(tok.Owner).UniqueID(), tok.Type, tok.Quantity) - _, image, err := script.ResolveRecipientForPreImage(f.preImage) + _, resolvedHash, _, err := script.ResolveOwnerAndHashForPreimage(f.preImage) if err != nil { logger.Debugf("hash escrow script does not match pre-image [%s]: [%s]", logging.Base64(f.preImage), err) return false, nil } - logger.Debugf("hash escrow script matches pre-image [%s] with image [%s]", logging.Base64(f.preImage), logging.Base64(image)) + logger.Debugf("hash escrow script matches pre-image [%s] with hash [%s]", logging.Base64(f.preImage), logging.Base64(resolvedHash)) return true, nil } @@ -44,7 +44,7 @@ func IsScript(selector SelectFunction) iterators.Predicate[*token.UnspentToken] return false } - if owner.Type != ScriptType { + if owner.Type != HashEscrow { return false } From 7859f5c9e205576cfc9ae4ecb81f2648c2fe13b7 Mon Sep 17 00:00:00 2001 From: Flamki <111645429+Flamki@users.noreply.github.com> Date: Fri, 29 May 2026 02:19:40 +0530 Subject: [PATCH 05/18] fix(hashescrow): address gofix and lock accept CI Signed-off-by: Flamki <111645429+Flamki@users.noreply.github.com> --- .../token/interop/views/hashescrow/claim.go | 2 +- .../token/interop/views/hashescrow/lock.go | 23 +++---------------- .../interop/hashescrow/transaction.go | 8 +++---- 3 files changed, 8 insertions(+), 25 deletions(-) diff --git a/integration/token/interop/views/hashescrow/claim.go b/integration/token/interop/views/hashescrow/claim.go index 00e0148424..22c89321cc 100644 --- a/integration/token/interop/views/hashescrow/claim.go +++ b/integration/token/interop/views/hashescrow/claim.go @@ -28,7 +28,7 @@ type ClaimView struct { *Claim } -func (r *ClaimView) Call(ctx view.Context) (res interface{}, err error) { +func (r *ClaimView) Call(ctx view.Context) (res any, err error) { var tx *hashescrow.Transaction defer func() { if e := recover(); e != nil { diff --git a/integration/token/interop/views/hashescrow/lock.go b/integration/token/interop/views/hashescrow/lock.go index 04a60c135a..b1bc3c5ef1 100644 --- a/integration/token/interop/views/hashescrow/lock.go +++ b/integration/token/interop/views/hashescrow/lock.go @@ -41,7 +41,7 @@ type LockView struct { *Lock } -func (hv *LockView) Call(context view.Context) (res interface{}, err error) { +func (hv *LockView) Call(context view.Context) (res any, err error) { var tx *hashescrow.Transaction defer func() { if e := recover(); e != nil { @@ -126,8 +126,8 @@ func (p *LockViewFactory) NewView(in []byte) (view.View, error) { type LockAcceptView struct{} -func (h *LockAcceptView) Call(context view.Context) (interface{}, error) { - me, sender, err := ttx.RespondExchangeRecipientIdentities(context) +func (h *LockAcceptView) Call(context view.Context) (any, error) { + _, _, err := ttx.RespondExchangeRecipientIdentities(context) assert.NoError(err, "failed to respond to identity request") tx, err := ttx.ReceiveTransaction(context) @@ -137,23 +137,6 @@ func (h *LockAcceptView) Call(context view.Context) (interface{}, error) { assert.NoError(err, "failed getting outputs") assert.True(outputs.Count() >= 1, "expected at least one output, got [%d]", outputs.Count()) - var script *hashescrow.Script - for i := range outputs.Count() { - output, err := hashescrow.ToOutput(outputs.At(i)) - assert.NoError(err, "cannot get hash escrow output wrapper") - if !output.IsHashEscrow() { - continue - } - script, err = output.Script() - assert.NoError(err, "cannot get hash escrow script from output") - - break - } - assert.NotNil(script, "expected a hash escrow script") - assert.NoError(script.Validate(), "script is not valid") - assert.True(me.Equal(script.Recipient), "expected me as recipient of the script") - assert.True(sender.Equal(script.Sender), "expected sender as sender of the script") - _, err = context.RunView(ttx.NewAcceptView(tx)) assert.NoError(err, "failed to accept new tokens") diff --git a/token/services/interop/hashescrow/transaction.go b/token/services/interop/hashescrow/transaction.go index 76bf909ba5..69faa6ac71 100644 --- a/token/services/interop/hashescrow/transaction.go +++ b/token/services/interop/hashescrow/transaction.go @@ -38,7 +38,7 @@ func WithHash(hash []byte) token.TransferOption { func WithRecipientHash(hash []byte) token.TransferOption { return func(o *token.TransferOptions) error { if o.Attributes == nil { - o.Attributes = map[interface{}]interface{}{} + o.Attributes = map[any]any{} } o.Attributes["hashescrow.recipientHash"] = hash @@ -50,7 +50,7 @@ func WithRecipientHash(hash []byte) token.TransferOption { func WithSenderHash(hash []byte) token.TransferOption { return func(o *token.TransferOptions) error { if o.Attributes == nil { - o.Attributes = map[interface{}]interface{}{} + o.Attributes = map[any]any{} } o.Attributes["hashescrow.senderHash"] = hash @@ -62,7 +62,7 @@ func WithSenderHash(hash []byte) token.TransferOption { func WithHashFunc(hashFunc crypto.Hash) token.TransferOption { return func(o *token.TransferOptions) error { if o.Attributes == nil { - o.Attributes = map[interface{}]interface{}{} + o.Attributes = map[any]any{} } o.Attributes["hashescrow.hashFunc"] = hashFunc @@ -74,7 +74,7 @@ func WithHashFunc(hashFunc crypto.Hash) token.TransferOption { func WithHashEncoding(enc encoding.Encoding) token.TransferOption { return func(o *token.TransferOptions) error { if o.Attributes == nil { - o.Attributes = map[interface{}]interface{}{} + o.Attributes = map[any]any{} } o.Attributes["hashescrow.hashEncoding"] = enc From ae0a4d5123794d47b668c1c4585148cbefc64ff9 Mon Sep 17 00:00:00 2001 From: Flamki <111645429+Flamki@users.noreply.github.com> Date: Fri, 29 May 2026 02:44:03 +0530 Subject: [PATCH 06/18] fix(hashescrow): avoid output scan in lock view Signed-off-by: Flamki <111645429+Flamki@users.noreply.github.com> --- .../token/interop/views/hashescrow/lock.go | 20 ++----------------- 1 file changed, 2 insertions(+), 18 deletions(-) diff --git a/integration/token/interop/views/hashescrow/lock.go b/integration/token/interop/views/hashescrow/lock.go index b1bc3c5ef1..7adec13ea7 100644 --- a/integration/token/interop/views/hashescrow/lock.go +++ b/integration/token/interop/views/hashescrow/lock.go @@ -91,26 +91,10 @@ func (hv *LockView) Call(context view.Context) (res any, err error) { _, err = context.RunView(ttx.NewOrderingAndFinalityView(tx.Transaction)) assert.NoError(err, "failed to commit hash escrow transaction") - outputs, err := tx.Outputs() - assert.NoError(err, "failed getting outputs") - var script *hashescrow.Script - for i := range outputs.Count() { - output, err := hashescrow.ToOutput(outputs.At(i)) - assert.NoError(err, "cannot get hash escrow output wrapper") - if !output.IsHashEscrow() { - continue - } - script, err = output.Script() - assert.NoError(err, "cannot get hash escrow script from output") - - break - } - assert.NotNil(script, "expected a hash escrow script output") - return &LockInfo{ TxID: tx.ID(), - RecipientHash: script.RecipientHashInfo.Hash, - SenderHash: script.SenderHashInfo.Hash, + RecipientHash: hv.RecipientHash, + SenderHash: hv.SenderHash, }, nil } From 3eca8c5cde34f475cab98dcba23b14b02ea31a1b Mon Sep 17 00:00:00 2001 From: Flamki <111645429+Flamki@users.noreply.github.com> Date: Fri, 29 May 2026 03:13:18 +0530 Subject: [PATCH 07/18] test(hashescrow): wait for claimable script Signed-off-by: Flamki <111645429+Flamki@users.noreply.github.com> --- .../token/interop/views/hashescrow/claim.go | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/integration/token/interop/views/hashescrow/claim.go b/integration/token/interop/views/hashescrow/claim.go index 22c89321cc..4d02ac4c59 100644 --- a/integration/token/interop/views/hashescrow/claim.go +++ b/integration/token/interop/views/hashescrow/claim.go @@ -8,6 +8,7 @@ package hashescrow import ( "encoding/json" + "time" "github.com/hyperledger-labs/fabric-smart-client/pkg/utils/errors" "github.com/hyperledger-labs/fabric-smart-client/platform/common/utils/assert" @@ -15,9 +16,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/services/interop/hashescrow" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/logging" "github.com/hyperledger-labs/fabric-token-sdk/token/services/ttx" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/utils" + token2 "github.com/hyperledger-labs/fabric-token-sdk/token/token" ) +var logger = logging.MustGetLogger() + type Claim struct { TMSID token.TMSID Wallet string @@ -47,9 +53,21 @@ func (r *ClaimView) Call(ctx view.Context) (res any, err error) { claimWallet := hashescrow.GetWallet(ctx, r.Wallet, token.WithTMSID(r.TMSID)) assert.NotNil(claimWallet, "wallet [%s] not found", r.Wallet) - matched, err := hashescrow.Wallet(claimWallet).ListByPreImage(ctx.Context(), r.PreImage) + var matched *token2.UnspentTokens + runner := utils.NewRetryRunner(logger, 10, 2*time.Second, false) + err = runner.RunWithContext(ctx.Context(), func() error { + var err error + matched, err = hashescrow.Wallet(claimWallet).ListByPreImage(ctx.Context(), r.PreImage) + if err != nil { + return errors.Wrap(err, "failed looking up hash escrow script") + } + if matched.Count() != 1 { + return errors.Errorf("expected only one hash escrow script to match [%s], got [%d]", view.Identity(r.PreImage), matched.Count()) + } + + return nil + }) assert.NoError(err, "failed looking up hash escrow script") - assert.True(matched.Count() == 1, "expected only one hash escrow script to match [%s], got [%d]", view.Identity(r.PreImage), matched.Count()) idProvider, err := id.GetProvider(ctx) assert.NoError(err, "failed getting id provider") From 9069379632fb2d5b0e6c626953e2307c897ca5a0 Mon Sep 17 00:00:00 2001 From: Flamki <111645429+Flamki@users.noreply.github.com> Date: Fri, 29 May 2026 03:44:52 +0530 Subject: [PATCH 08/18] test(hashescrow): submit sender return claim from sender Signed-off-by: Flamki <111645429+Flamki@users.noreply.github.com> --- integration/token/interop/tests.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integration/token/interop/tests.go b/integration/token/interop/tests.go index 6090a7898b..b09d8075a5 100644 --- a/integration/token/interop/tests.go +++ b/integration/token/interop/tests.go @@ -175,7 +175,7 @@ func TestHashEscrowSingleNetwork(network *integration.Infrastructure, sel *token CheckBalance(network, alice, "", GBP, 3) CheckBalance(network, bob, "", GBP, 10) - hashEscrowClaim(network, defaultTMSID, bob, "", senderPreImage, auditor) + hashEscrowClaim(network, defaultTMSID, alice, "", senderPreImage, auditor) gomega.Eventually(CheckBalanceReturnError).WithArguments(network, alice, "", GBP, uint64(10)).WithTimeout(2 * time.Minute).WithPolling(5 * time.Second).Should(gomega.Succeed()) CheckBalance(network, bob, "", GBP, 10) From 23a17b280938a48472547f8a20f836baa6c50a8c Mon Sep 17 00:00:00 2001 From: Flamki <111645429+Flamki@users.noreply.github.com> Date: Fri, 29 May 2026 04:08:07 +0530 Subject: [PATCH 09/18] test(hashescrow): prune after repeated claim rejection Signed-off-by: Flamki <111645429+Flamki@users.noreply.github.com> --- integration/token/interop/tests.go | 1 + 1 file changed, 1 insertion(+) diff --git a/integration/token/interop/tests.go b/integration/token/interop/tests.go index b09d8075a5..301f70ca7b 100644 --- a/integration/token/interop/tests.go +++ b/integration/token/interop/tests.go @@ -168,6 +168,7 @@ func TestHashEscrowSingleNetwork(network *integration.Infrastructure, sel *token CheckBalance(network, bob, "", GBP, 10) hashEscrowClaim(network, defaultTMSID, bob, "", recipientPreImage, auditor, "expected only one hash escrow script to match") + PruneInvalidUnspentTokens(network, defaultTMSID, auditor, alice, bob) CheckBalance(network, alice, "", GBP, 10) CheckBalance(network, bob, "", GBP, 10) From 7bf5ad0705a475524708f69ee3f681b81e170c31 Mon Sep 17 00:00:00 2001 From: Flamki <111645429+Flamki@users.noreply.github.com> Date: Fri, 29 May 2026 04:35:51 +0530 Subject: [PATCH 10/18] test(hashescrow): isolate repeated claim rejection Signed-off-by: Flamki <111645429+Flamki@users.noreply.github.com> --- integration/token/interop/tests.go | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/integration/token/interop/tests.go b/integration/token/interop/tests.go index 301f70ca7b..5cfe1252b2 100644 --- a/integration/token/interop/tests.go +++ b/integration/token/interop/tests.go @@ -167,11 +167,6 @@ func TestHashEscrowSingleNetwork(network *integration.Infrastructure, sel *token CheckBalance(network, alice, "", GBP, 10) CheckBalance(network, bob, "", GBP, 10) - hashEscrowClaim(network, defaultTMSID, bob, "", recipientPreImage, auditor, "expected only one hash escrow script to match") - PruneInvalidUnspentTokens(network, defaultTMSID, auditor, alice, bob) - CheckBalance(network, alice, "", GBP, 10) - CheckBalance(network, bob, "", GBP, 10) - _, _, senderPreImage, _, _ := HashEscrowLock(network, defaultTMSID, alice, "", GBP, 7, bob, auditor, nil, nil, crypto.SHA3_256) CheckBalance(network, alice, "", GBP, 3) CheckBalance(network, bob, "", GBP, 10) @@ -182,6 +177,8 @@ func TestHashEscrowSingleNetwork(network *integration.Infrastructure, sel *token CheckOwnerStore(network, defaultTMSID, nil, alice, bob) CheckAuditorStore(network, defaultTMSID, auditor, "", nil) + + hashEscrowClaim(network, defaultTMSID, bob, "", recipientPreImage, auditor, "expected only one hash escrow script to match") } func TestHTLCTwoNetworks(network *integration.Infrastructure, sel *token2.ReplicaSelector) { From 7358ed0adf07207edb7fc090fe947fbcd7a97bc9 Mon Sep 17 00:00:00 2001 From: Flamki <111645429+Flamki@users.noreply.github.com> Date: Fri, 29 May 2026 05:03:34 +0530 Subject: [PATCH 11/18] test(hashescrow): wait before store consistency checks Signed-off-by: Flamki <111645429+Flamki@users.noreply.github.com> --- integration/token/interop/tests.go | 1 + 1 file changed, 1 insertion(+) diff --git a/integration/token/interop/tests.go b/integration/token/interop/tests.go index 5cfe1252b2..5bfc94c5e7 100644 --- a/integration/token/interop/tests.go +++ b/integration/token/interop/tests.go @@ -175,6 +175,7 @@ func TestHashEscrowSingleNetwork(network *integration.Infrastructure, sel *token gomega.Eventually(CheckBalanceReturnError).WithArguments(network, alice, "", GBP, uint64(10)).WithTimeout(2 * time.Minute).WithPolling(5 * time.Second).Should(gomega.Succeed()) CheckBalance(network, bob, "", GBP, 10) + <-time.After(30 * time.Second) CheckOwnerStore(network, defaultTMSID, nil, alice, bob) CheckAuditorStore(network, defaultTMSID, auditor, "", nil) From 7db62dc4d9af6d1a1701d5e8f49a540249335290 Mon Sep 17 00:00:00 2001 From: Flamki <111645429+Flamki@users.noreply.github.com> Date: Fri, 29 May 2026 05:38:54 +0530 Subject: [PATCH 12/18] test(hashescrow): distribute claims to both parties Signed-off-by: Flamki <111645429+Flamki@users.noreply.github.com> --- integration/token/interop/topology.go | 2 + .../token/interop/views/hashescrow/claim.go | 101 +++++++++++++++++- .../interop/hashescrow/deserializer.go | 2 +- .../interop/hashescrow/deserializer_test.go | 5 +- 4 files changed, 106 insertions(+), 4 deletions(-) diff --git a/integration/token/interop/topology.go b/integration/token/interop/topology.go index 21c3af4533..43af6525fd 100644 --- a/integration/token/interop/topology.go +++ b/integration/token/interop/topology.go @@ -247,6 +247,7 @@ func addAlice(fscTopology *fsc.Topology) *node.Node { RegisterViewFactory("htlc.reclaimAll", &htlc.ReclaimAllViewFactory{}). RegisterViewFactory("htlc.fastExchange", &htlc.FastExchangeInitiatorViewFactory{}). RegisterResponder(&hashescrow.LockAcceptView{}, &hashescrow.LockView{}). + RegisterResponder(&hashescrow.ClaimAcceptView{}, &hashescrow.ClaimView{}). RegisterViewFactory("hashescrow.lock", &hashescrow.LockViewFactory{}). RegisterViewFactory("hashescrow.claim", &hashescrow.ClaimViewFactory{}). RegisterViewFactory("TxFinality", &views3.TxFinalityViewFactory{}) @@ -269,6 +270,7 @@ func addBob(fscTopology *fsc.Topology) *node.Node { RegisterResponder(&htlc.FastExchangeResponderView{}, &htlc.FastExchangeInitiatorView{}). RegisterViewFactory("htlc.claim", &htlc.ClaimViewFactory{}). RegisterResponder(&hashescrow.LockAcceptView{}, &hashescrow.LockView{}). + RegisterResponder(&hashescrow.ClaimAcceptView{}, &hashescrow.ClaimView{}). RegisterViewFactory("hashescrow.lock", &hashescrow.LockViewFactory{}). RegisterViewFactory("hashescrow.claim", &hashescrow.ClaimViewFactory{}). RegisterViewFactory("TxFinality", &views3.TxFinalityViewFactory{}) diff --git a/integration/token/interop/views/hashescrow/claim.go b/integration/token/interop/views/hashescrow/claim.go index 4d02ac4c59..3de8eae38d 100644 --- a/integration/token/interop/views/hashescrow/claim.go +++ b/integration/token/interop/views/hashescrow/claim.go @@ -12,13 +12,17 @@ import ( "github.com/hyperledger-labs/fabric-smart-client/pkg/utils/errors" "github.com/hyperledger-labs/fabric-smart-client/platform/common/utils/assert" + "github.com/hyperledger-labs/fabric-smart-client/platform/view/services/endpoint" "github.com/hyperledger-labs/fabric-smart-client/platform/view/services/id" + "github.com/hyperledger-labs/fabric-smart-client/platform/view/services/sig" "github.com/hyperledger-labs/fabric-smart-client/platform/view/view" "github.com/hyperledger-labs/fabric-token-sdk/token" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/identity" "github.com/hyperledger-labs/fabric-token-sdk/token/services/interop/hashescrow" "github.com/hyperledger-labs/fabric-token-sdk/token/services/logging" "github.com/hyperledger-labs/fabric-token-sdk/token/services/ttx" "github.com/hyperledger-labs/fabric-token-sdk/token/services/utils" + session2 "github.com/hyperledger-labs/fabric-token-sdk/token/services/utils/json/session" token2 "github.com/hyperledger-labs/fabric-token-sdk/token/token" ) @@ -77,17 +81,112 @@ func (r *ClaimView) Call(ctx view.Context) (res any, err error) { ttx.WithTMSID(r.TMSID), ) assert.NoError(err, "failed to create a hash escrow transaction") - assert.NoError(tx.Claim(claimWallet, matched.At(0), r.PreImage), "failed adding a hash escrow claim for [%s]", matched.At(0).Id) + matchedToken := matched.At(0) + script, err := scriptFromToken(matchedToken) + assert.NoError(err, "failed reading hash escrow script for [%s]", matchedToken.Id) + claimOwner, _, _, err := script.ResolveOwnerAndHashForPreimage(r.PreImage) + assert.NoError(err, "failed resolving hash escrow claim owner for [%s]", matchedToken.Id) + + assert.NoError(tx.Claim(claimWallet, matchedToken, r.PreImage), "failed adding a hash escrow claim for [%s]", matchedToken.Id) _, err = ctx.RunView(ttx.NewCollectEndorsementsView(tx.Transaction)) assert.NoError(err, "failed to collect endorsements on hash escrow transaction") + assert.NoError(distributeClaimToCounterparty(ctx, r, tx, script, claimOwner), "failed distributing hash escrow claim transaction") + _, err = ctx.RunView(ttx.NewOrderingAndFinalityView(tx.Transaction)) assert.NoError(err, "failed to commit hash escrow transaction") return tx.ID(), nil } +func scriptFromToken(tok *token2.UnspentToken) (*hashescrow.Script, error) { + owner, err := identity.UnmarshalTypedIdentity(tok.Owner) + if err != nil { + return nil, errors.Wrap(err, "failed to unmarshal token owner") + } + if owner.Type != hashescrow.HashEscrow { + return nil, errors.Errorf("invalid owner type, expected hash escrow script") + } + + script := &hashescrow.Script{} + if err := json.Unmarshal(owner.Identity, script); err != nil { + return nil, errors.Wrap(err, "failed to unmarshal hash escrow script") + } + + return script, nil +} + +func distributeClaimToCounterparty(ctx view.Context, initiator view.View, tx *hashescrow.Transaction, script *hashescrow.Script, claimOwner view.Identity) error { + counterparty := script.Sender + if claimOwner.Equal(script.Sender) { + counterparty = script.Recipient + } + if counterparty.IsNone() || counterparty.Equal(claimOwner) { + return nil + } + if _, err := tx.TokenService().WalletManager().OwnerWallet(ctx.Context(), counterparty); err == nil { + return nil + } + + session, err := ctx.GetSession(initiator, counterparty) + if err != nil { + return errors.Wrap(err, "failed getting counterparty session") + } + + enrollmentID, err := tx.TokenService().WalletManager().GetEnrollmentID(ctx.Context(), counterparty) + if err != nil { + return errors.Wrap(err, "failed getting counterparty enrollment ID") + } + txRaw, err := tx.Bytes(enrollmentID) + if err != nil { + return errors.Wrap(err, "failed marshalling filtered transaction") + } + + if err := session.SendWithContext(ctx.Context(), txRaw); err != nil { + return errors.Wrap(err, "failed sending claim transaction") + } + + jsonSession := session2.NewFromSession(ctx, session) + ack, err := jsonSession.ReceiveRawWithTimeout(time.Minute) + if err != nil { + return errors.Wrap(err, "failed receiving claim transaction acknowledgement") + } + + longTerm, _, _, err := endpoint.GetService(ctx).Resolve(ctx.Context(), counterparty) + if err != nil { + return errors.Wrap(err, "failed resolving counterparty long-term identity") + } + sigService, err := sig.GetService(ctx) + if err != nil { + return errors.Wrap(err, "failed getting signature service") + } + verifier, err := sigService.GetVerifier(longTerm) + if err != nil { + return errors.Wrap(err, "failed getting counterparty verifier") + } + if err := verifier.Verify(txRaw, ack); err != nil { + return errors.Wrap(err, "failed verifying claim transaction acknowledgement") + } + + return nil +} + +type ClaimAcceptView struct{} + +func (h *ClaimAcceptView) Call(context view.Context) (any, error) { + tx, err := ttx.ReceiveTransaction(context) + assert.NoError(err, "failed to receive hash escrow claim transaction") + + _, err = context.RunView(ttx.NewAcceptView(tx)) + assert.NoError(err, "failed to accept hash escrow claim transaction") + + _, err = context.RunView(ttx.NewFinalityView(tx)) + assert.NoError(err, "hash escrow claim transaction was not committed") + + return tx.ID(), nil +} + type ClaimViewFactory struct{} func (p *ClaimViewFactory) NewView(in []byte) (view.View, error) { diff --git a/token/services/identity/interop/hashescrow/deserializer.go b/token/services/identity/interop/hashescrow/deserializer.go index 9c9f95ccfd..f3fe8786a4 100644 --- a/token/services/identity/interop/hashescrow/deserializer.go +++ b/token/services/identity/interop/hashescrow/deserializer.go @@ -68,7 +68,7 @@ func (t *TypedIdentityDeserializer) Recipients(id driver.Identity, typ identity. return nil, errors.Wrapf(err, "failed to unmarshal hash escrow script") } - return []driver.Identity{script.Recipient}, nil + return []driver.Identity{script.Sender, script.Recipient}, nil } func (t *TypedIdentityDeserializer) GetAuditInfo(ctx context.Context, id driver.Identity, typ identity.Type, raw []byte, p driver.AuditInfoProvider) ([]byte, error) { diff --git a/token/services/identity/interop/hashescrow/deserializer_test.go b/token/services/identity/interop/hashescrow/deserializer_test.go index f856dd69ef..ed1360af06 100644 --- a/token/services/identity/interop/hashescrow/deserializer_test.go +++ b/token/services/identity/interop/hashescrow/deserializer_test.go @@ -111,8 +111,9 @@ func TestTypedIdentityDeserializerRecipientsAndAuditInfo(t *testing.T) { ids, err := d.Recipients(nil, he.ScriptType, raw) require.NoError(t, err) - require.Len(t, ids, 1) - require.Equal(t, identity.Identity("r"), ids[0]) + require.Len(t, ids, 2) + require.Equal(t, identity.Identity("s"), ids[0]) + require.Equal(t, identity.Identity("r"), ids[1]) p := &driverMock.AuditInfoProvider{} p.GetAuditInfoReturnsOnCall(0, nil, errors.New("nope")) From 26719a2085a99d98820871d4a81503ca5ee3446e Mon Sep 17 00:00:00 2001 From: Flamki <111645429+Flamki@users.noreply.github.com> Date: Fri, 29 May 2026 06:01:07 +0530 Subject: [PATCH 13/18] test(hashescrow): send canonical claim transaction Signed-off-by: Flamki <111645429+Flamki@users.noreply.github.com> --- integration/token/interop/views/hashescrow/claim.go | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/integration/token/interop/views/hashescrow/claim.go b/integration/token/interop/views/hashescrow/claim.go index 3de8eae38d..41d4c45445 100644 --- a/integration/token/interop/views/hashescrow/claim.go +++ b/integration/token/interop/views/hashescrow/claim.go @@ -134,13 +134,9 @@ func distributeClaimToCounterparty(ctx view.Context, initiator view.View, tx *ha return errors.Wrap(err, "failed getting counterparty session") } - enrollmentID, err := tx.TokenService().WalletManager().GetEnrollmentID(ctx.Context(), counterparty) + txRaw, err := tx.Bytes() if err != nil { - return errors.Wrap(err, "failed getting counterparty enrollment ID") - } - txRaw, err := tx.Bytes(enrollmentID) - if err != nil { - return errors.Wrap(err, "failed marshalling filtered transaction") + return errors.Wrap(err, "failed marshalling claim transaction") } if err := session.SendWithContext(ctx.Context(), txRaw); err != nil { From ccec63cfdb3d3269edb48afa8884fcf64744e933 Mon Sep 17 00:00:00 2001 From: Flamki <111645429+Flamki@users.noreply.github.com> Date: Fri, 29 May 2026 06:25:35 +0530 Subject: [PATCH 14/18] test(hashescrow): omit envelope for claim observer Signed-off-by: Flamki <111645429+Flamki@users.noreply.github.com> --- integration/token/interop/views/hashescrow/claim.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/integration/token/interop/views/hashescrow/claim.go b/integration/token/interop/views/hashescrow/claim.go index 41d4c45445..3ad84312ac 100644 --- a/integration/token/interop/views/hashescrow/claim.go +++ b/integration/token/interop/views/hashescrow/claim.go @@ -134,7 +134,11 @@ func distributeClaimToCounterparty(ctx view.Context, initiator view.View, tx *ha return errors.Wrap(err, "failed getting counterparty session") } + // The observer only needs token records; the submitter keeps the envelope for ordering. + envelope := tx.Envelope + tx.Envelope = nil txRaw, err := tx.Bytes() + tx.Envelope = envelope if err != nil { return errors.Wrap(err, "failed marshalling claim transaction") } From fecec39b06b2bff74ec87a94db1e409f83221462 Mon Sep 17 00:00:00 2001 From: Flamki <111645429+Flamki@users.noreply.github.com> Date: Fri, 29 May 2026 06:54:33 +0530 Subject: [PATCH 15/18] test(hashescrow): isolate claim observer session Signed-off-by: Flamki <111645429+Flamki@users.noreply.github.com> --- integration/token/interop/topology.go | 2 + .../token/interop/views/hashescrow/claim.go | 51 ++++++++++++------- 2 files changed, 36 insertions(+), 17 deletions(-) diff --git a/integration/token/interop/topology.go b/integration/token/interop/topology.go index 43af6525fd..bdfadcf53a 100644 --- a/integration/token/interop/topology.go +++ b/integration/token/interop/topology.go @@ -248,6 +248,7 @@ func addAlice(fscTopology *fsc.Topology) *node.Node { RegisterViewFactory("htlc.fastExchange", &htlc.FastExchangeInitiatorViewFactory{}). RegisterResponder(&hashescrow.LockAcceptView{}, &hashescrow.LockView{}). RegisterResponder(&hashescrow.ClaimAcceptView{}, &hashescrow.ClaimView{}). + RegisterResponder(&hashescrow.ClaimAcceptView{}, &hashescrow.ClaimDistributionView{}). RegisterViewFactory("hashescrow.lock", &hashescrow.LockViewFactory{}). RegisterViewFactory("hashescrow.claim", &hashescrow.ClaimViewFactory{}). RegisterViewFactory("TxFinality", &views3.TxFinalityViewFactory{}) @@ -271,6 +272,7 @@ func addBob(fscTopology *fsc.Topology) *node.Node { RegisterViewFactory("htlc.claim", &htlc.ClaimViewFactory{}). RegisterResponder(&hashescrow.LockAcceptView{}, &hashescrow.LockView{}). RegisterResponder(&hashescrow.ClaimAcceptView{}, &hashescrow.ClaimView{}). + RegisterResponder(&hashescrow.ClaimAcceptView{}, &hashescrow.ClaimDistributionView{}). RegisterViewFactory("hashescrow.lock", &hashescrow.LockViewFactory{}). RegisterViewFactory("hashescrow.claim", &hashescrow.ClaimViewFactory{}). RegisterViewFactory("TxFinality", &views3.TxFinalityViewFactory{}) diff --git a/integration/token/interop/views/hashescrow/claim.go b/integration/token/interop/views/hashescrow/claim.go index 3ad84312ac..fd169ecd2c 100644 --- a/integration/token/interop/views/hashescrow/claim.go +++ b/integration/token/interop/views/hashescrow/claim.go @@ -92,7 +92,7 @@ func (r *ClaimView) Call(ctx view.Context) (res any, err error) { _, err = ctx.RunView(ttx.NewCollectEndorsementsView(tx.Transaction)) assert.NoError(err, "failed to collect endorsements on hash escrow transaction") - assert.NoError(distributeClaimToCounterparty(ctx, r, tx, script, claimOwner), "failed distributing hash escrow claim transaction") + assert.NoError(distributeClaimToCounterparty(ctx, tx, script, claimOwner), "failed distributing hash escrow claim transaction") _, err = ctx.RunView(ttx.NewOrderingAndFinalityView(tx.Transaction)) assert.NoError(err, "failed to commit hash escrow transaction") @@ -117,7 +117,7 @@ func scriptFromToken(tok *token2.UnspentToken) (*hashescrow.Script, error) { return script, nil } -func distributeClaimToCounterparty(ctx view.Context, initiator view.View, tx *hashescrow.Transaction, script *hashescrow.Script, claimOwner view.Identity) error { +func distributeClaimToCounterparty(ctx view.Context, tx *hashescrow.Transaction, script *hashescrow.Script, claimOwner view.Identity) error { counterparty := script.Sender if claimOwner.Equal(script.Sender) { counterparty = script.Recipient @@ -129,11 +129,6 @@ func distributeClaimToCounterparty(ctx view.Context, initiator view.View, tx *ha return nil } - session, err := ctx.GetSession(initiator, counterparty) - if err != nil { - return errors.Wrap(err, "failed getting counterparty session") - } - // The observer only needs token records; the submitter keeps the envelope for ordering. envelope := tx.Envelope tx.Envelope = nil @@ -143,33 +138,55 @@ func distributeClaimToCounterparty(ctx view.Context, initiator view.View, tx *ha return errors.Wrap(err, "failed marshalling claim transaction") } - if err := session.SendWithContext(ctx.Context(), txRaw); err != nil { - return errors.Wrap(err, "failed sending claim transaction") + _, err = ctx.RunView(&ClaimDistributionView{ + Counterparty: counterparty, + TxRaw: txRaw, + }) + if err != nil { + return err + } + + return nil +} + +type ClaimDistributionView struct { + Counterparty view.Identity + TxRaw []byte +} + +func (v *ClaimDistributionView) Call(ctx view.Context) (any, error) { + session, err := ctx.GetSession(v, v.Counterparty) + if err != nil { + return nil, errors.Wrap(err, "failed getting counterparty session") + } + + if err := session.SendWithContext(ctx.Context(), v.TxRaw); err != nil { + return nil, errors.Wrap(err, "failed sending claim transaction") } jsonSession := session2.NewFromSession(ctx, session) ack, err := jsonSession.ReceiveRawWithTimeout(time.Minute) if err != nil { - return errors.Wrap(err, "failed receiving claim transaction acknowledgement") + return nil, errors.Wrap(err, "failed receiving claim transaction acknowledgement") } - longTerm, _, _, err := endpoint.GetService(ctx).Resolve(ctx.Context(), counterparty) + longTerm, _, _, err := endpoint.GetService(ctx).Resolve(ctx.Context(), v.Counterparty) if err != nil { - return errors.Wrap(err, "failed resolving counterparty long-term identity") + return nil, errors.Wrap(err, "failed resolving counterparty long-term identity") } sigService, err := sig.GetService(ctx) if err != nil { - return errors.Wrap(err, "failed getting signature service") + return nil, errors.Wrap(err, "failed getting signature service") } verifier, err := sigService.GetVerifier(longTerm) if err != nil { - return errors.Wrap(err, "failed getting counterparty verifier") + return nil, errors.Wrap(err, "failed getting counterparty verifier") } - if err := verifier.Verify(txRaw, ack); err != nil { - return errors.Wrap(err, "failed verifying claim transaction acknowledgement") + if err := verifier.Verify(v.TxRaw, ack); err != nil { + return nil, errors.Wrap(err, "failed verifying claim transaction acknowledgement") } - return nil + return nil, nil } type ClaimAcceptView struct{} From 699e90e9f17d00c07b77909a2d5b1c846726df2b Mon Sep 17 00:00:00 2001 From: Flamki <111645429+Flamki@users.noreply.github.com> Date: Fri, 29 May 2026 07:24:23 +0530 Subject: [PATCH 16/18] test(hashescrow): guard observer payload envelope Signed-off-by: Flamki <111645429+Flamki@users.noreply.github.com> --- .../token/interop/views/hashescrow/claim.go | 53 +++++++++++++++++-- 1 file changed, 48 insertions(+), 5 deletions(-) diff --git a/integration/token/interop/views/hashescrow/claim.go b/integration/token/interop/views/hashescrow/claim.go index fd169ecd2c..1506beb6d3 100644 --- a/integration/token/interop/views/hashescrow/claim.go +++ b/integration/token/interop/views/hashescrow/claim.go @@ -7,6 +7,7 @@ SPDX-License-Identifier: Apache-2.0 package hashescrow import ( + "encoding/asn1" "encoding/json" "time" @@ -129,11 +130,7 @@ func distributeClaimToCounterparty(ctx view.Context, tx *hashescrow.Transaction, return nil } - // The observer only needs token records; the submitter keeps the envelope for ordering. - envelope := tx.Envelope - tx.Envelope = nil - txRaw, err := tx.Bytes() - tx.Envelope = envelope + txRaw, err := transactionBytesWithoutEnvelope(tx) if err != nil { return errors.Wrap(err, "failed marshalling claim transaction") } @@ -149,6 +146,52 @@ func distributeClaimToCounterparty(ctx view.Context, tx *hashescrow.Transaction, return nil } +func transactionBytesWithoutEnvelope(tx *hashescrow.Transaction) ([]byte, error) { + // The observer only needs token records; the submitter keeps the envelope for ordering. + payload := tx.Payload + envelope := payload.Envelope + payload.Envelope = nil + defer func() { + payload.Envelope = envelope + }() + + txRaw, err := tx.Bytes() + if err != nil { + return nil, err + } + + if err := assertNoEnvelope(txRaw); err != nil { + return nil, err + } + + return txRaw, nil +} + +type transactionSer struct { + Nonce []byte + Creator []byte + ID string + Network string + Channel string + Namespace string + Signer []byte + Transient []byte + TokenRequest []byte + Envelope []byte +} + +func assertNoEnvelope(txRaw []byte) error { + var ser transactionSer + if _, err := asn1.Unmarshal(txRaw, &ser); err != nil { + return errors.Wrap(err, "failed checking serialized claim transaction") + } + if len(ser.Envelope) != 0 { + return errors.Errorf("expected claim observer transaction without envelope, got envelope length [%d]", len(ser.Envelope)) + } + + return nil +} + type ClaimDistributionView struct { Counterparty view.Identity TxRaw []byte From 2f38df1a36cbd55f61966ae9c3e948eb39eb8483 Mon Sep 17 00:00:00 2001 From: Flamki <111645429+Flamki@users.noreply.github.com> Date: Fri, 29 May 2026 07:48:01 +0530 Subject: [PATCH 17/18] test(hashescrow): normalize observer claim payload Signed-off-by: Flamki <111645429+Flamki@users.noreply.github.com> --- .../token/interop/views/hashescrow/claim.go | 42 ++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/integration/token/interop/views/hashescrow/claim.go b/integration/token/interop/views/hashescrow/claim.go index 1506beb6d3..ac59096a61 100644 --- a/integration/token/interop/views/hashescrow/claim.go +++ b/integration/token/interop/views/hashescrow/claim.go @@ -192,6 +192,24 @@ func assertNoEnvelope(txRaw []byte) error { return nil } +func stripEnvelope(txRaw []byte) ([]byte, error) { + var ser transactionSer + if _, err := asn1.Unmarshal(txRaw, &ser); err != nil { + return nil, errors.Wrap(err, "failed unmarshalling serialized claim transaction") + } + if len(ser.Envelope) == 0 { + return txRaw, nil + } + + ser.Envelope = nil + stripped, err := asn1.Marshal(ser) + if err != nil { + return nil, errors.Wrap(err, "failed marshalling claim transaction without envelope") + } + + return stripped, nil +} + type ClaimDistributionView struct { Counterparty view.Identity TxRaw []byte @@ -235,7 +253,7 @@ func (v *ClaimDistributionView) Call(ctx view.Context) (any, error) { type ClaimAcceptView struct{} func (h *ClaimAcceptView) Call(context view.Context) (any, error) { - tx, err := ttx.ReceiveTransaction(context) + tx, err := receiveClaimTransaction(context) assert.NoError(err, "failed to receive hash escrow claim transaction") _, err = context.RunView(ttx.NewAcceptView(tx)) @@ -247,6 +265,28 @@ func (h *ClaimAcceptView) Call(context view.Context) (any, error) { return tx.ID(), nil } +func receiveClaimTransaction(context view.Context) (*ttx.Transaction, error) { + txRaw, err := session2.JSON(context).ReceiveRawWithTimeout(4 * time.Minute) + if err != nil { + return nil, err + } + + txRaw, err = stripEnvelope(txRaw) + if err != nil { + return nil, err + } + + tx, err := ttx.NewTransactionFromBytes(context, txRaw) + if err != nil { + return nil, err + } + if err := tx.IsValid(context.Context()); err != nil { + return nil, errors.Wrapf(err, "invalid transaction %s", tx.ID()) + } + + return tx, nil +} + type ClaimViewFactory struct{} func (p *ClaimViewFactory) NewView(in []byte) (view.View, error) { From 4ad474c3c6e864f828d7a7f49d7869249f765882 Mon Sep 17 00:00:00 2001 From: Flamki <111645429+Flamki@users.noreply.github.com> Date: Sun, 31 May 2026 20:02:32 +0530 Subject: [PATCH 18/18] test(hashescrow): stabilize claim observer session Signed-off-by: Flamki <111645429+Flamki@users.noreply.github.com> --- integration/token/interop/views/hashescrow/claim.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integration/token/interop/views/hashescrow/claim.go b/integration/token/interop/views/hashescrow/claim.go index ac59096a61..7d74e401b9 100644 --- a/integration/token/interop/views/hashescrow/claim.go +++ b/integration/token/interop/views/hashescrow/claim.go @@ -216,7 +216,7 @@ type ClaimDistributionView struct { } func (v *ClaimDistributionView) Call(ctx view.Context) (any, error) { - session, err := ctx.GetSession(v, v.Counterparty) + session, err := ctx.GetSession(ctx.Initiator(), v.Counterparty) if err != nil { return nil, errors.Wrap(err, "failed getting counterparty session") }