diff --git a/token/core/common/validator.go b/token/core/common/validator.go index 207594d0f1..f2e8199a90 100644 --- a/token/core/common/validator.go +++ b/token/core/common/validator.go @@ -9,12 +9,38 @@ package common import ( "context" "encoding/json" + "time" "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/logging" ) +// validationTimeKey is the unexported context key used to carry a deterministic +// reference timestamp into the token-request validator (e.g. the Fabric proposal +// timestamp from stub.GetTxTimestamp()). +type validationTimeKey struct{} + +// WithValidationTime returns a child context that carries t as the reference +// time for HTLC deadline evaluation. Call this from the chaincode path so that +// all endorsing peers use the same client-supplied proposal timestamp instead of +// each peer's local wall clock. +func WithValidationTime(ctx context.Context, t time.Time) context.Context { + return context.WithValue(ctx, validationTimeKey{}, t) +} + +// validationTimeFromContext extracts the reference time injected by +// WithValidationTime. If none was set it falls back to time.Now(), which +// preserves the existing behaviour for non-chaincode callers (local FSC +// validation, unit tests, etc.). +func validationTimeFromContext(ctx context.Context) time.Time { + if t, ok := ctx.Value(validationTimeKey{}).(time.Time); ok && !t.IsZero() { + return t + } + + return time.Now() +} + // MetadataCounterID defines the type for metadata counter identifiers. type MetadataCounterID = string @@ -40,6 +66,11 @@ type Context[P driver.PublicParameters, T driver.Input, TA driver.TransferAction Ledger driver.Ledger MetadataCounter map[MetadataCounterID]int Attributes driver.ValidationAttributes + // Now is the reference time used for HTLC deadline evaluation. + // It is populated by VerifyTransfer from the Go context so that the + // chaincode path can supply the deterministic Fabric proposal timestamp + // (stub.GetTxTimestamp()) instead of each peer's local wall clock. + Now time.Time } // CountMetadataKey increments the counter for the passed metadata key. @@ -285,6 +316,7 @@ func (v *Validator[P, T, TA, IA, DS]) VerifyTransfer( SignatureProvider: signatureProvider, MetadataCounter: map[MetadataCounterID]int{}, Attributes: attributes, + Now: validationTimeFromContext(ctx), } for _, v := range v.TransferValidators { if err := v(ctx, context); err != nil { diff --git a/token/core/fabtoken/v1/validator/validator_transfer.go b/token/core/fabtoken/v1/validator/validator_transfer.go index 943938aa1c..0a54943080 100644 --- a/token/core/fabtoken/v1/validator/validator_transfer.go +++ b/token/core/fabtoken/v1/validator/validator_transfer.go @@ -140,7 +140,10 @@ func TransferBalanceValidate(c context.Context, ctx *Context) error { // TransferHTLCValidate checks the validity of the HTLC scripts, if any func TransferHTLCValidate(c context.Context, ctx *Context) error { - now := time.Now() + now := ctx.Now + if now.IsZero() { + now = time.Now() + } for i, in := range ctx.InputTokens { owner, err := identity.UnmarshalTypedIdentity(in.GetOwner()) diff --git a/token/core/zkatdlog/nogh/v1/validator/validator_transfer.go b/token/core/zkatdlog/nogh/v1/validator/validator_transfer.go index 54bb95a6ad..6e3746ab5c 100644 --- a/token/core/zkatdlog/nogh/v1/validator/validator_transfer.go +++ b/token/core/zkatdlog/nogh/v1/validator/validator_transfer.go @@ -148,7 +148,10 @@ func TransferZKProofValidate(c context.Context, ctx *Context) error { // TransferHTLCValidate validates the HTLC scripts in the transfer action. // It ensures that HTLC scripts only transfer ownership of a single token and that the script conditions are met. func TransferHTLCValidate(c context.Context, ctx *Context) error { - now := time.Now() + now := ctx.Now + if now.IsZero() { + now = time.Now() + } for i, in := range ctx.InputTokens { owner, err := identity.UnmarshalTypedIdentity(in.Owner) diff --git a/token/services/network/fabric/tcc/tcc.go b/token/services/network/fabric/tcc/tcc.go index 6f8be29572..d16df5ef94 100644 --- a/token/services/network/fabric/tcc/tcc.go +++ b/token/services/network/fabric/tcc/tcc.go @@ -231,9 +231,20 @@ func (cc *TokenChaincode) ProcessRequest(raw []byte, stub shim.ChaincodeStubInte return shim.Error(err.Error()) } + // Derive a deterministic reference time from the Fabric proposal timestamp. + // stub.GetTxTimestamp() returns the timestamp embedded by the client in the + // signed proposal; it is identical for all endorsing peers processing the + // same transaction, making HTLC deadline evaluation deterministic across the + // endorsing set regardless of local wall-clock skew. + ts, err := stub.GetTxTimestamp() + if err != nil { + return shim.Error("failed to get tx timestamp: " + err.Error()) + } + ctx := common.WithValidationTime(context.Background(), ts.AsTime()) + // Verify actions, attributes, err := validator.UnmarshallAndVerifyWithMetadata( - context.Background(), + ctx, &ledger{stub: stub, keyTranslator: &keys.Translator{}}, token.RequestAnchor(stub.GetTxID()), raw, @@ -244,7 +255,6 @@ func (cc *TokenChaincode) ProcessRequest(raw []byte, stub shim.ChaincodeStubInte // Write w := translator.New(stub.GetTxID(), translator.NewRWSetWrapper(&rwsWrapper{stub: stub}, "", stub.GetTxID()), &keys.Translator{}) - ctx := context.Background() for _, action := range actions { err = w.Write(ctx, action) if err != nil {