Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
0f38be9
feat(hashescrow): add separate hash-based escrow identity flow (#1657)
Flamki May 7, 2026
56749b4
feat(hashescrow): resolve recipient by preimage with dual hashes
Flamki May 9, 2026
f6ebaae
test(hashescrow): add interop coverage
Flamki May 17, 2026
39505ee
fix(hashescrow): address review feedback
Flamki May 28, 2026
7859f5c
fix(hashescrow): address gofix and lock accept CI
Flamki May 28, 2026
ae0a4d5
fix(hashescrow): avoid output scan in lock view
Flamki May 28, 2026
3eca8c5
test(hashescrow): wait for claimable script
Flamki May 28, 2026
9069379
test(hashescrow): submit sender return claim from sender
Flamki May 28, 2026
23a17b2
test(hashescrow): prune after repeated claim rejection
Flamki May 28, 2026
7bf5ad0
test(hashescrow): isolate repeated claim rejection
Flamki May 28, 2026
7358ed0
test(hashescrow): wait before store consistency checks
Flamki May 28, 2026
7db62dc
test(hashescrow): distribute claims to both parties
Flamki May 29, 2026
26719a2
test(hashescrow): send canonical claim transaction
Flamki May 29, 2026
ccec63c
test(hashescrow): omit envelope for claim observer
Flamki May 29, 2026
fecec39
test(hashescrow): isolate claim observer session
Flamki May 29, 2026
699e90e
test(hashescrow): guard observer payload envelope
Flamki May 29, 2026
2f38df1
test(hashescrow): normalize observer claim payload
Flamki May 29, 2026
4ad474c
test(hashescrow): stabilize claim observer session
Flamki May 31, 2026
3c02447
Merge branch 'main' into feature/issue-1657-hash-based-escrow
Flamki Jun 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 105 additions & 0 deletions integration/token/interop/support.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ package interop

import (
"crypto"
"crypto/rand"
"fmt"
"slices"
"strconv"
Expand All @@ -22,6 +23,7 @@ import (
common2 "github.com/hyperledger-labs/fabric-token-sdk/integration/token/common"
"github.com/hyperledger-labs/fabric-token-sdk/integration/token/fungible/views"
views2 "github.com/hyperledger-labs/fabric-token-sdk/integration/token/interop/views"
hashescrowviews "github.com/hyperledger-labs/fabric-token-sdk/integration/token/interop/views/hashescrow"
"github.com/hyperledger-labs/fabric-token-sdk/integration/token/interop/views/htlc"
"github.com/hyperledger-labs/fabric-token-sdk/token"
token2 "github.com/hyperledger-labs/fabric-token-sdk/token/token"
Expand Down Expand Up @@ -314,6 +316,76 @@ func HTLCLock(network *integration.Infrastructure, tmsID token.TMSID, id *token3
}
}

func HashEscrowLock(network *integration.Infrastructure, tmsID token.TMSID, id *token3.NodeReference, wallet string, typ token2.Type, amount uint64, receiver *token3.NodeReference, auditor *token3.NodeReference, recipientHash []byte, senderHash []byte, hashFunc crypto.Hash, errorMsgs ...string) (string, []byte, []byte, []byte, []byte) {
if hashFunc == 0 {
hashFunc = crypto.SHA256
}

var recipientPreImage []byte
var senderPreImage []byte
if len(recipientHash) == 0 {
recipientPreImage = make([]byte, 24)
_, err := rand.Read(recipientPreImage)
gomega.Expect(err).NotTo(gomega.HaveOccurred())
h := hashFunc.New()
_, err = h.Write(recipientPreImage)
gomega.Expect(err).NotTo(gomega.HaveOccurred())
recipientHash = h.Sum(nil)
}
if len(senderHash) == 0 {
senderPreImage = make([]byte, 24)
_, err := rand.Read(senderPreImage)
gomega.Expect(err).NotTo(gomega.HaveOccurred())
h := hashFunc.New()
_, err = h.Write(senderPreImage)
gomega.Expect(err).NotTo(gomega.HaveOccurred())
senderHash = h.Sum(nil)
}

result, err := network.Client(id.ReplicaName()).CallView("hashescrow.lock", common.JSONMarshall(&hashescrowviews.Lock{
TMSID: tmsID,
Wallet: wallet,
Type: typ,
Amount: amount,
Recipient: network.Identity(receiver.Id()),
RecipientHash: recipientHash,
SenderHash: senderHash,
HashFunc: hashFunc,
}))
if len(errorMsgs) == 0 {
gomega.Expect(err).NotTo(gomega.HaveOccurred())
lockResult := &hashescrowviews.LockInfo{}
common.JSONUnmarshal(result.([]byte), lockResult)

common2.CheckFinality(network, receiver, lockResult.TxID, &tmsID, false)
common2.CheckFinality(network, auditor, lockResult.TxID, &tmsID, false)

gomega.Expect(lockResult.RecipientHash).NotTo(gomega.BeNil())
gomega.Expect(lockResult.SenderHash).NotTo(gomega.BeNil())
gomega.Expect(lockResult.RecipientHash).To(gomega.BeEquivalentTo(recipientHash))
gomega.Expect(lockResult.SenderHash).To(gomega.BeEquivalentTo(senderHash))

return lockResult.TxID, recipientPreImage, senderPreImage, lockResult.RecipientHash, lockResult.SenderHash
}

gomega.Expect(err).To(gomega.HaveOccurred())
for _, msg := range errorMsgs {
gomega.Expect(err.Error()).To(gomega.ContainSubstring(msg))
}
time.Sleep(5 * time.Second)

errMsg := err.Error()
fmt.Printf("Got error message [%s]\n", errMsg)
txID := ""
index := strings.Index(err.Error(), "<<<[")
if index != -1 {
txID = errMsg[index+4 : index+strings.Index(err.Error()[index:], "]>>>")]
}
fmt.Printf("Got error message, extracted tx id [%s]\n", txID)

return txID, nil, nil, nil, nil
}

func HTLCReclaimAll(network *integration.Infrastructure, id *token3.NodeReference, wallet string, errorMsgs ...string) {
txID, err := network.Client(id.ReplicaName()).CallView("htlc.reclaimAll", common.JSONMarshall(&htlc.ReclaimAll{
Wallet: wallet,
Expand Down Expand Up @@ -398,6 +470,39 @@ func htlcClaim(network *integration.Infrastructure, tmsID token.TMSID, id *token
}
}

func hashEscrowClaim(network *integration.Infrastructure, tmsID token.TMSID, id *token3.NodeReference, wallet string, preImage []byte, auditor *token3.NodeReference, errorMsgs ...string) string {
txIDBoxed, err := network.Client(id.ReplicaName()).CallView("hashescrow.claim", common.JSONMarshall(&hashescrowviews.Claim{
TMSID: tmsID,
Wallet: wallet,
PreImage: preImage,
}))
if len(errorMsgs) == 0 {
gomega.Expect(err).NotTo(gomega.HaveOccurred())
txID := common.JSONUnmarshalString(txIDBoxed)
common2.CheckFinality(network, id, txID, &tmsID, false)
common2.CheckFinality(network, auditor, txID, &tmsID, false)

return txID
}

gomega.Expect(err).To(gomega.HaveOccurred())
for _, msg := range errorMsgs {
gomega.Expect(err.Error()).To(gomega.ContainSubstring(msg))
}
time.Sleep(5 * time.Second)

errMsg := err.Error()
fmt.Printf("Got error message [%s]\n", errMsg)
txID := ""
index := strings.Index(err.Error(), "<<<[")
if index != -1 {
txID = errMsg[index+4 : index+strings.Index(err.Error()[index:], "]>>>")]
}
fmt.Printf("Got error message, extracted tx id [%s]\n", txID)

return txID
}

func fastExchange(network *integration.Infrastructure, id *token3.NodeReference, recipient *token3.NodeReference, tmsID1 token.TMSID, typ1 token2.Type, amount1 uint64, tmsID2 token.TMSID, typ2 token2.Type, amount2 uint64, deadline time.Duration) {
_, err := network.Client(id.ReplicaName()).CallView("htlc.fastExchange", common.JSONMarshall(&htlc.FastExchange{
Recipient: network.Identity(recipient.Id()),
Expand Down
37 changes: 37 additions & 0 deletions integration/token/interop/tests.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -143,6 +144,42 @@ func TestHTLCSingleNetwork(network *integration.Infrastructure, sel *token2.Repl
IDs := ListVaultUnspentTokens(network, defaultTMSID, name)
CheckIfExistsInVault(network, defaultTMSID, auditor, IDs)
}

TestHashEscrowSingleNetwork(network, sel)
}

func TestHashEscrowSingleNetwork(network *integration.Infrastructure, sel *token2.ReplicaSelector) {
alice := sel.Get("alice")
bob := sel.Get("bob")
auditor := sel.Get("auditor")

defaultTMSID := token.TMSID{}

IssueCash(network, "", GBP, 20, alice, auditor)
CheckBalance(network, alice, "", GBP, 20)
CheckBalance(network, bob, "", GBP, 0)

_, recipientPreImage, _, _, _ := HashEscrowLock(network, defaultTMSID, alice, "", GBP, 10, bob, auditor, nil, nil, crypto.SHA3_256)
CheckBalance(network, alice, "", GBP, 10)
CheckBalance(network, bob, "", GBP, 0)

hashEscrowClaim(network, defaultTMSID, bob, "", recipientPreImage, auditor)
CheckBalance(network, alice, "", GBP, 10)
CheckBalance(network, bob, "", GBP, 10)

_, _, senderPreImage, _, _ := HashEscrowLock(network, defaultTMSID, alice, "", GBP, 7, bob, auditor, nil, nil, crypto.SHA3_256)
CheckBalance(network, alice, "", GBP, 3)
CheckBalance(network, bob, "", GBP, 10)

hashEscrowClaim(network, defaultTMSID, alice, "", senderPreImage, auditor)
gomega.Eventually(CheckBalanceReturnError).WithArguments(network, alice, "", GBP, uint64(10)).WithTimeout(2 * time.Minute).WithPolling(5 * time.Second).Should(gomega.Succeed())
CheckBalance(network, bob, "", GBP, 10)

<-time.After(30 * time.Second)
CheckOwnerStore(network, defaultTMSID, nil, alice, bob)
CheckAuditorStore(network, defaultTMSID, auditor, "", nil)

hashEscrowClaim(network, defaultTMSID, bob, "", recipientPreImage, auditor, "expected only one hash escrow script to match")
}

func TestHTLCTwoNetworks(network *integration.Infrastructure, sel *token2.ReplicaSelector) {
Expand Down
11 changes: 11 additions & 0 deletions integration/token/interop/topology.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down Expand Up @@ -245,6 +246,11 @@ func addAlice(fscTopology *fsc.Topology) *node.Node {
RegisterViewFactory("htlc.lock", &htlc.LockViewFactory{}).
RegisterViewFactory("htlc.reclaimAll", &htlc.ReclaimAllViewFactory{}).
RegisterViewFactory("htlc.fastExchange", &htlc.FastExchangeInitiatorViewFactory{}).
RegisterResponder(&hashescrow.LockAcceptView{}, &hashescrow.LockView{}).
RegisterResponder(&hashescrow.ClaimAcceptView{}, &hashescrow.ClaimView{}).
RegisterResponder(&hashescrow.ClaimAcceptView{}, &hashescrow.ClaimDistributionView{}).
RegisterViewFactory("hashescrow.lock", &hashescrow.LockViewFactory{}).
RegisterViewFactory("hashescrow.claim", &hashescrow.ClaimViewFactory{}).
RegisterViewFactory("TxFinality", &views3.TxFinalityViewFactory{})
}

Expand All @@ -264,5 +270,10 @@ func addBob(fscTopology *fsc.Topology) *node.Node {
RegisterResponder(&htlc.LockAcceptView{}, &htlc.LockView{}).
RegisterResponder(&htlc.FastExchangeResponderView{}, &htlc.FastExchangeInitiatorView{}).
RegisterViewFactory("htlc.claim", &htlc.ClaimViewFactory{}).
RegisterResponder(&hashescrow.LockAcceptView{}, &hashescrow.LockView{}).
RegisterResponder(&hashescrow.ClaimAcceptView{}, &hashescrow.ClaimView{}).
RegisterResponder(&hashescrow.ClaimAcceptView{}, &hashescrow.ClaimDistributionView{}).
RegisterViewFactory("hashescrow.lock", &hashescrow.LockViewFactory{}).
RegisterViewFactory("hashescrow.claim", &hashescrow.ClaimViewFactory{}).
RegisterViewFactory("TxFinality", &views3.TxFinalityViewFactory{})
}
39 changes: 27 additions & 12 deletions integration/token/interop/views/auditor.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down Expand Up @@ -68,26 +69,40 @@ func (a *AuditView) Call(context view.Context) (any, error) {
for i := range inputs.Count() {
input, err := htlc.ToInput(inputs.At(i))
assert.NoError(err, "cannot get htlc input wrapper")
if !input.IsHTLC() {
continue
if input.IsHTLC() {
// check script details, for example make sure the hash is set
script, err := input.Script()
assert.NoError(err, "cannot get htlc script from input")
assert.True(len(script.HashInfo.Hash) > 0, "hash is not set")
}

hashEscrowInput, err := hashescrow.ToInput(inputs.At(i))
assert.NoError(err, "cannot get hash escrow input wrapper")
if hashEscrowInput.IsHashEscrow() {
hashEscrowScript, err := hashEscrowInput.Script()
assert.NoError(err, "cannot get hash escrow script from input")
assert.NoError(hashEscrowScript.Validate(), "hash escrow script is not valid")
}
// check script details, for example make sure the hash is set
script, err := input.Script()
assert.NoError(err, "cannot get htlc script from input")
assert.True(len(script.HashInfo.Hash) > 0, "hash is not set")
}

now := time.Now()
for i := range outputs.Count() {
output, err := htlc.ToOutput(outputs.At(i))
assert.NoError(err, "cannot get htlc output wrapper")
if !output.IsHTLC() {
continue
if output.IsHTLC() {
// check script details
script, err := output.Script()
assert.NoError(err, "cannot get htlc script from output")
assert.NoError(script.Validate(now), "script is not valid")
}

hashEscrowOutput, err := hashescrow.ToOutput(outputs.At(i))
assert.NoError(err, "cannot get hash escrow output wrapper")
if hashEscrowOutput.IsHashEscrow() {
hashEscrowScript, err := hashEscrowOutput.Script()
assert.NoError(err, "cannot get hash escrow script from output")
assert.NoError(hashEscrowScript.Validate(), "hash escrow script is not valid")
}
// check script details
script, err := output.Script()
assert.NoError(err, "cannot get htlc script from output")
assert.NoError(script.Validate(now), "script is not valid")
}

return context.RunView(ttx.NewAuditApproveView(w, tx))
Expand Down
Loading
Loading