Skip to content

HashEscrow: separate hash-based escrow identity flow (#1657)#1678

Open
Flamki wants to merge 19 commits into
LFDT-Panurus:mainfrom
Flamki:feature/issue-1657-hash-based-escrow
Open

HashEscrow: separate hash-based escrow identity flow (#1657)#1678
Flamki wants to merge 19 commits into
LFDT-Panurus:mainfrom
Flamki:feature/issue-1657-hash-based-escrow

Conversation

@Flamki

@Flamki Flamki commented May 6, 2026

Copy link
Copy Markdown
Contributor

Summary

This PR reworks #1657 by introducing a separate hashescrow identity flow instead of extending HTLC behavior.

The implementation was adjusted after review feedback to keep semantics clear:

  • HTLC behavior stays unchanged.
  • Hash-based escrow is handled by a dedicated identity type and validation path.

Design

  • New identity type: hashescrow.
  • Claim requires:
    • a valid preimage for the committed hash, and
    • a valid signature from one of the two parties encoded in the script (sender or recipient).
  • No timeout/deadline logic is used in the hashescrow path.

This keeps escrow non-custodial: escrow controls preimage disclosure, but does not own locked tokens.

What Changed

  • Added new interop package:

    • token/services/interop/hashescrow
    • script model, signer/verifier, lock/claim helpers, metadata keys.
  • Added new identity package:

    • token/services/identity/interop/hashescrow
    • typed deserializer, audit deserializer, owner/metadata validation helpers.
  • Added identity mappings:

    • token/driver/wallet.go
    • token/services/identity/typed.go
    • token/services/identity/marshal/marshal.go
  • Wired hashescrow into both token drivers (fabtoken and zkatdlog):

    • typed verifier registration
    • audit deserializer registration
    • authorization multiplexer registration
  • Extended transfer validators (fabtoken and zkatdlog) to validate hashescrow spend/lock semantics and metadata.

Tests

Added unit coverage for:

  • hashescrow signer/verifier behavior
  • hashescrow script and transaction helpers
  • hashescrow identity deserializer and validator
  • marshal compatibility for the new identity string mapping

Validation

I validated this change locally with:

  • go test -count=1 ./token/services/interop/hashescrow ./token/services/identity/interop/hashescrow ./token/core/fabtoken/v1/driver ./token/core/zkatdlog/nogh/v1/driver ./token/core/fabtoken/v1/validator ./token/core/zkatdlog/nogh/v1/validator ./token/services/identity/marshal
  • golangci-lint run --timeout=30m

Notes

  • Rebased on latest main and resolved the token/driver/wallet.go conflict.
  • This PR now reflects the separate-identity direction suggested in review.

Fixes #1657

@Flamki Flamki changed the title HTLC: add optional hash-based escrow claim mode (#1657) HTLC: optional escrow-style sender claim mode (#1657) May 6, 2026
@Flamki Flamki marked this pull request as draft May 6, 2026 17:18
@Flamki Flamki mentioned this pull request May 6, 2026
@adecaro

adecaro commented May 7, 2026

Copy link
Copy Markdown
Contributor

Hi @Flamki , thanks for submitting this PR and for your effort 🙏

I think it is confusing adding that flag to the HTLC-based identity because it introduces a new path that is outside the pure scope of the HTLC.

I think @KElkhiyaoui had in mind something much simpler that doesn't even name Alice or Bob but just the hash. Who has the pre-image can spend the token. @KElkhiyaoui , please, can you clarify this?

@Flamki

Flamki commented May 7, 2026

Copy link
Copy Markdown
Contributor Author

Hi @Flamki , thanks for submitting this PR and for your effort 🙏

I think it is confusing adding that flag to the HTLC-based identity because it introduces a new path that is outside the pure scope of the HTLC.

I think @KElkhiyaoui had in mind something much simpler that doesn't even name Alice or Bob but just the hash. Who has the pre-image can spend the token. @KElkhiyaoui , please, can you clarify this?

Thanks for the review. I'm currently re-evaluating the approach and exploring a direction that better aligns with the intended HTLC semantics. I'll refine the design based on the feedback and update the PR accordingly.

@adecaro

adecaro commented May 7, 2026

Copy link
Copy Markdown
Contributor

@Flamki , if it is of help, this PR introduced a new identity in the system: #1597

@Flamki Flamki changed the title HTLC: optional escrow-style sender claim mode (#1657) HashEscrow: separate hash-based escrow identity flow (#1657) May 7, 2026
@Flamki Flamki force-pushed the feature/issue-1657-hash-based-escrow branch 2 times, most recently from 9a7fae1 to 0431933 Compare May 7, 2026 17:19
@Flamki

Flamki commented May 9, 2026

Copy link
Copy Markdown
Contributor Author

Implemented and pushed an update aligned with the latest direction.

What changed in this push:

  • HashEscrow now uses two hashes/two preimages in the script:
    • recipient hash -> token goes to recipient
    • sender hash -> token goes back to sender
  • Timeout/deadline is not used in HashEscrow.
  • Claim path is preimage-driven:
    • spend submission can be relayed by anyone
    • recipient is resolved from preimage and enforced during validation
  • Lock metadata now includes both hash lock entries.
  • Claim metadata/key checks and validators were updated accordingly in both fabtoken and zkatdlog paths.
  • Tests were updated/added for script/signer/transaction/deserializer/validator behavior.

Local validation:

  • go test -count=1 ./token/services/interop/hashescrow ./token/services/identity/interop/hashescrow ./token/core/fabtoken/v1/validator ./token/core/zkatdlog/nogh/v1/validator ./token/core/fabtoken/v1/driver ./token/core/zkatdlog/nogh/v1/driver ./token/services/identity/marshal
  • golangci-lint run --timeout=30m

Both passed locally.

@Flamki Flamki force-pushed the feature/issue-1657-hash-based-escrow branch from 8c64f69 to a359faf Compare May 9, 2026 19:53
@Flamki Flamki marked this pull request as ready for review May 9, 2026 19:53
@adecaro adecaro force-pushed the feature/issue-1657-hash-based-escrow branch from a359faf to 56749b4 Compare May 12, 2026 11:18
@adecaro adecaro requested review from KElkhiyaoui and adecaro May 12, 2026 11:20
@adecaro adecaro self-assigned this May 12, 2026
@adecaro adecaro added this to the Q2/26 milestone May 12, 2026
@adecaro

adecaro commented May 12, 2026

Copy link
Copy Markdown
Contributor

Hi @Flamki , thanks for this big effort. I'll ask @KElkhiyaoui to also review. I would already ask to enhance the integration test to exercise the new identity as done for HTLC. Thanks much 🙏

Signed-off-by: Flamki <9833ayush@gmail.com>
@Flamki

Flamki commented May 17, 2026

Copy link
Copy Markdown
Contributor Author

@adecaro @KElkhiyaoui
I added integration coverage for the new hashescrow identity.

The new flow covers:

  • locking with the hashescrow identity
  • claiming with the recipient preimage
  • rejecting a repeated claim
  • claiming back to the sender with the sender preimage

I also added the small integration view/helpers needed to exercise this path in the same style as the existing HTLC tests.

Local checks passed:

  • go test -count=1 ./token/services/interop/hashescrow ./integration/token/interop ./integration/token/interop/views ./integration/token/interop/views/hashescrow
  • golangci-lint run --timeout=30m ./token/services/interop/hashescrow/... ./integration/token/interop/...

@adecaro

adecaro commented May 18, 2026

Copy link
Copy Markdown
Contributor

Hi @KElkhiyaoui , I need a review from you here to make sure the PR matches what you had in mind. Thanks much 🙏

@Flamki , thanks for your effort 🙏

senderWallet := hashescrow.GetWallet(context, hv.Wallet, token.WithTMSID(hv.TMSID))
assert.NotNil(senderWallet, "sender wallet [%s] not found", hv.Wallet)

preImages, err := tx.LockWithPreImages(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The idea is that the preimage are not known at time of lock. The sender and the recipient are given hashes from the escrow through side channels. If the escrow is trusted, we can assume for now that the hashes are correctly computed. We can also imagine that the escrow provides proofs that the hashes are well computed.
The next step could be to have a view where the sender and the recipient are provided with the hshaes.
So the lock method should just take the hashes directly and not compute them within.

ctx.CountMetadataKey(metadataKey)
}
}
if owner.Type == hashescrow.ScriptType {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of having this code under TransferHTLCValidate, you can create a new dedicated method that we add to the validator at time of instantiation.

ctx.CountMetadataKey(metadataKey)
}
}
if owner.Type == hashescrow.ScriptType {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be better to create a dedicated method to validate HashEscrow scripts.

if err != nil {
return nil, err
}
if sender.Type != ScriptType {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe instead of calling ScriptType, we call it HashEscrow, so that it's clear what it refers to?

if len(claim.Preimage) == 0 {
return "", nil, errors.New("expected a valid claim preImage")
}
resolvedOwner, image, err := script.ResolveRecipientForPreImage(claim.Preimage)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we mean hash?

if options.Attributes != nil {
// backward compatibility
boxed, ok := options.Attributes["hashescrow.hash"]
if ok {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need this entry?

preImages := &PreImages{}
var err error

recipientHashInfo, recipientPreImage, err := buildHashInfo(recipientHash, hashFunc, hashEncoding)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As mentioned before, the computation of the hashes will precede the call of lock view.

}

// MetadataClaimKeyCheck validates claim metadata and returns the matched metadata key.
func MetadataClaimKeyCheck(action Action, script *hashescrow.Script, sig []byte) (string, []byte, error) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe this method should be called ClaimMetadataCheck. The current name suggests that we are checking the key.

if len(claim.Preimage) == 0 {
return "", nil, errors.New("expected a valid claim preImage")
}
resolvedOwner, image, err := script.ResolveRecipientForPreImage(claim.Preimage)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be better to separate duties. That is the method checking the metadata takes as input the pre-image and the hash directly and checks that the value matches. Resolving recipients could be directly used in validator transfer.


// check metadata
sigma := ctx.Signatures[i]
metadataKey, resolvedOwner, err := hashescrow2.MetadataClaimKeyCheck(ctx.TransferAction, script, sigma)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For better readability, I would recommend that we use a method dedicated to resolving the identity o the recipient and returning the pre-image.

@adecaro

adecaro commented May 27, 2026

Copy link
Copy Markdown
Contributor

Hi @Flamki , could you address @KElkhiyaoui 's comments. Many thanks for the effort 🙏

Flamki added 3 commits May 28, 2026 23:23
Signed-off-by: Flamki <111645429+Flamki@users.noreply.github.com>
Signed-off-by: Flamki <111645429+Flamki@users.noreply.github.com>
Signed-off-by: Flamki <111645429+Flamki@users.noreply.github.com>
Flamki added 12 commits May 29, 2026 03:13
Signed-off-by: Flamki <111645429+Flamki@users.noreply.github.com>
Signed-off-by: Flamki <111645429+Flamki@users.noreply.github.com>
Signed-off-by: Flamki <111645429+Flamki@users.noreply.github.com>
Signed-off-by: Flamki <111645429+Flamki@users.noreply.github.com>
Signed-off-by: Flamki <111645429+Flamki@users.noreply.github.com>
Signed-off-by: Flamki <111645429+Flamki@users.noreply.github.com>
Signed-off-by: Flamki <111645429+Flamki@users.noreply.github.com>
Signed-off-by: Flamki <111645429+Flamki@users.noreply.github.com>
Signed-off-by: Flamki <111645429+Flamki@users.noreply.github.com>
Signed-off-by: Flamki <111645429+Flamki@users.noreply.github.com>
Signed-off-by: Flamki <111645429+Flamki@users.noreply.github.com>
Signed-off-by: Flamki <111645429+Flamki@users.noreply.github.com>
@adecaro adecaro modified the milestones: Q2/26, Q3/26 Jun 2, 2026
continue
}
if len(ctx.InputTokens) != 1 || len(ctx.TransferAction.GetOutputs()) != 1 {
return ErrInvalidHTLCAction

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return ErrInvalidHTLCAction
return ErrInvalidHashEscrowAction


out, ok := ctx.TransferAction.GetOutputs()[0].(*token.Token)
if !ok || out == nil {
return ErrHTLCOutputNotFound

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return ErrHTLCOutputNotFound
return ErrHashEscrowOutputNotFound

return &AuditDeserializer{AuditInfoDeserializer: auditInfoDeserializer}
}

func (a *AuditDeserializer) DeserializeAuditInfo(ctx context.Context, identity driver.Identity, raw []byte) (idriver.AuditInfo, error) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I expect this method to return the auditInfo for the script (i.e., ScriptInfo).

return nil, errors.New("owner of output token does not correspond to sender or recipient in hash escrow request")
}

func ClaimFromSignature(sig []byte) (*hashescrow.ClaimSignature, error) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can add methods Serialize and Deserialize to ClaimSignature.


// 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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of owner we can use Recipient. Maybe to avoid confusion between recipient who gets the token by the end of claim and recipient in the script, we call it claimant.

@AkramBitar

Copy link
Copy Markdown
Contributor

Hello @Flamki,

First, I would like to thank you for your efforts on this PR. I really appreciate your contribution.

Second, I was wondering if you have had a chance to address @KElkhiyaoui’s comments?

Regards,
Akram

@adecaro

adecaro commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Hi @Flamki , if it is okay for you, I'm gonna fork your branch (so keeping the history) and continue the work.

Would that be okay for you?

Thanks a lot for this initial effort 🙏

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Hash-based Escrow

4 participants