Skip to content

Commit da918c6

Browse files
authored
Merge branch 'main' into fix/boolpolicy-enrollment-id
2 parents 382a0c4 + 312f321 commit da918c6

11 files changed

Lines changed: 619 additions & 11 deletions

File tree

docs/configuration.md

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -415,7 +415,24 @@ token:
415415
# Adds random jitter to break symmetry when multiple auditors retry simultaneously
416416
# Default: 0.3 (30%)
417417
jitterFactor: 0.3
418-
Security: 256
418+
419+
# auditTokensRetry controls how the auditor's early validation gate
420+
# (AuditorCheck) tolerates the read-timing race in which a referenced
421+
# token's producing transaction is still pending, so its outputs have not
422+
# yet been persisted to the token store by the asynchronous finality
423+
# listener. When a lookup misses and the producing tx is still pending,
424+
# the gate waits and retries instead of spuriously rejecting the request.
425+
auditTokensRetry:
426+
# numRetries is the number of token-lookup attempts before giving up.
427+
# There are numRetries-1 delays between attempts. Values <= 0 (or an
428+
# omitted key) keep the default; a single attempt is always made.
429+
# Default: 3
430+
numRetries: 3
431+
# retryDelay is the backoff slept between attempts, as a duration string
432+
# (e.g. 500ms, 3s). Must be > 0; an invalid or non-positive value falls
433+
# back to the default. The backoff honors context cancellation.
434+
# Default: 3s
435+
retryDelay: 3s
419436
```
420437
421438
## Minimal Configuration
@@ -821,6 +838,59 @@ Default values:
821838

822839
---
823840

841+
### Optional: token.tms.<name>.auditor.auditTokensRetry
842+
843+
Controls how the auditor's early validation gate (`AuditorCheck`) tolerates the
844+
read-timing race in which a referenced token's producing transaction is still
845+
pending, so its outputs have not yet been persisted to the token store by the
846+
asynchronous finality listener. On a missing lookup whose producing transaction is
847+
still `Pending`, the gate waits `retryDelay` and retries up to `numRetries`
848+
attempts before failing, instead of spuriously rejecting a validly-audited,
849+
quickly-chained transaction. This mirrors the tolerance already applied on the
850+
sibling `Audit()` path. Applies to both the `fabtoken` and `dlog` drivers.
851+
852+
If not specified, the default configuration is:
853+
854+
```yaml
855+
token:
856+
tms:
857+
<name>:
858+
auditor:
859+
auditTokensRetry:
860+
numRetries: 3
861+
retryDelay: 3s
862+
```
863+
864+
Default values:
865+
866+
- numRetries: 3
867+
- retryDelay: 3s
868+
869+
**Parameter Descriptions:**
870+
871+
- **numRetries**: Number of token-lookup attempts made before giving up, with
872+
`numRetries - 1` backoff delays between them (so the defaults give a ~6s grace
873+
window: 3 attempts, 2 delays of 3s). A value `<= 0`, or an omitted key, keeps the
874+
default; at least one attempt is always made. Retries are only spent while a
875+
referenced transaction is genuinely `Pending` — a hard lookup failure (or a
876+
failure to determine pending status) fails fast without consuming the budget.
877+
- **retryDelay**: Backoff slept between attempts, expressed as a Go duration string
878+
(e.g. `500ms`, `3s`). Must be `> 0`; an invalid or non-positive value logs a
879+
warning and falls back to the default. The backoff honors context cancellation,
880+
so a cancelled or timed-out request returns immediately rather than pinning a
881+
goroutine for the full grace window.
882+
883+
**Tuning Recommendations:**
884+
885+
- **For quickly-chained, high-throughput workloads** (an output spent shortly after
886+
it was audited), raise `numRetries` and/or `retryDelay` to widen the grace window
887+
and further reduce spurious rejections when finality persistence lags.
888+
- **For latency-sensitive gates**, lower `retryDelay` (e.g. `500ms`) so the audit
889+
gate fails faster when the producing transaction never becomes final, at the cost
890+
of tolerating a shorter persistence lag.
891+
892+
---
893+
824894
### Optional: token.tms.<name>.auditor.locker
825895

826896
Controls the distributed locking strategy used by the auditor to serialise
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
/*
2+
Copyright IBM Corp. All Rights Reserved.
3+
4+
SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
package common
8+
9+
import (
10+
"time"
11+
12+
"github.com/LFDT-Panurus/panurus/token/services/logging"
13+
)
14+
15+
// AuditRetryConfigKey is the per-TMS configuration key (relative to the TMS
16+
// configuration block) holding the audit-token retry/backoff settings consumed by
17+
// AuditorCheck's RetrieveAuditTokens call.
18+
const AuditRetryConfigKey = "auditor.auditTokensRetry"
19+
20+
// AuditRetryConfig holds the retry budget and backoff used by RetrieveAuditTokens
21+
// to tolerate the pending-transaction read-timing race (issue #2105).
22+
type AuditRetryConfig struct {
23+
// NumRetries is the number of ListAuditTokens attempts made before giving up.
24+
// A value <= 0 is clamped to a single attempt by RetrieveAuditTokens.
25+
NumRetries int
26+
// RetryDelay is the backoff slept between attempts (NumRetries-1 delays).
27+
RetryDelay time.Duration
28+
}
29+
30+
// DefaultAuditRetryConfig returns the audit-token retry configuration using the
31+
// package default constants.
32+
func DefaultAuditRetryConfig() AuditRetryConfig {
33+
return AuditRetryConfig{
34+
NumRetries: DefaultAuditTokensNumRetries,
35+
RetryDelay: DefaultAuditTokensRetryDelay,
36+
}
37+
}
38+
39+
// AuditConfigProvider is the minimal configuration surface needed to load the
40+
// audit-token retry configuration. It is satisfied by driver.Configuration (the
41+
// per-TMS config) and is kept minimal so it is trivial to mock in tests.
42+
type AuditConfigProvider interface {
43+
// IsSet checks whether a configuration key is defined.
44+
IsSet(key string) bool
45+
// UnmarshalKey decodes the configuration value associated with a key into rawVal.
46+
UnmarshalKey(key string, rawVal any) error
47+
}
48+
49+
// auditRetryConfigRaw is the yaml-facing shape of AuditRetryConfigKey. RetryDelay
50+
// is decoded as a duration string (e.g. "3s") so operators can express it in
51+
// human-readable units.
52+
type auditRetryConfigRaw struct {
53+
NumRetries int `yaml:"numRetries"`
54+
RetryDelay string `yaml:"retryDelay"`
55+
}
56+
57+
// LoadAuditRetryConfig loads the audit-token retry configuration from the passed
58+
// provider, overlaying any valid configured value onto DefaultAuditRetryConfig().
59+
// A missing key, an unmarshal failure, or an individually invalid field leaves the
60+
// corresponding default in place (a warning is logged for invalid values), so this
61+
// function never fails: the audit gate always has a usable configuration.
62+
func LoadAuditRetryConfig(cp AuditConfigProvider) AuditRetryConfig {
63+
cfg := DefaultAuditRetryConfig()
64+
65+
if cp == nil || !cp.IsSet(AuditRetryConfigKey) {
66+
return cfg
67+
}
68+
69+
var raw auditRetryConfigRaw
70+
if err := cp.UnmarshalKey(AuditRetryConfigKey, &raw); err != nil {
71+
logging.MustGetLogger().Warnf("failed to unmarshal audit-token retry configuration [%s], using defaults: %v", AuditRetryConfigKey, err)
72+
73+
return cfg
74+
}
75+
76+
// Apply the retry count if valid. A value <= 0 keeps the default; disabling
77+
// retries entirely is intentionally not expressible here, matching the
78+
// "at least one attempt" clamp in RetrieveAuditTokens.
79+
if raw.NumRetries > 0 {
80+
cfg.NumRetries = raw.NumRetries
81+
}
82+
83+
// Apply the retry delay if valid.
84+
if raw.RetryDelay != "" {
85+
if duration, err := time.ParseDuration(raw.RetryDelay); err == nil && duration > 0 {
86+
cfg.RetryDelay = duration
87+
} else {
88+
logging.MustGetLogger().Warnf("invalid retryDelay value [%s] for key [%s], using default", raw.RetryDelay, AuditRetryConfigKey)
89+
}
90+
}
91+
92+
return cfg
93+
}
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
/*
2+
Copyright IBM Corp. All Rights Reserved.
3+
4+
SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
package common
8+
9+
import (
10+
"testing"
11+
"time"
12+
13+
"github.com/hyperledger-labs/fabric-smart-client/pkg/utils/errors"
14+
"github.com/stretchr/testify/assert"
15+
)
16+
17+
// stubAuditConfigProvider is a minimal AuditConfigProvider whose IsSet result and
18+
// UnmarshalKey behavior are supplied per test.
19+
type stubAuditConfigProvider struct {
20+
set bool
21+
unmarshal func(key string, rawVal any) error
22+
lastKey string
23+
unmarshaled bool
24+
}
25+
26+
func (s *stubAuditConfigProvider) IsSet(string) bool { return s.set }
27+
28+
func (s *stubAuditConfigProvider) UnmarshalKey(key string, rawVal any) error {
29+
s.lastKey = key
30+
s.unmarshaled = true
31+
if s.unmarshal == nil {
32+
return nil
33+
}
34+
35+
return s.unmarshal(key, rawVal)
36+
}
37+
38+
func TestDefaultAuditRetryConfig(t *testing.T) {
39+
cfg := DefaultAuditRetryConfig()
40+
assert.Equal(t, DefaultAuditTokensNumRetries, cfg.NumRetries)
41+
assert.Equal(t, DefaultAuditTokensRetryDelay, cfg.RetryDelay)
42+
}
43+
44+
func TestLoadAuditRetryConfig(t *testing.T) {
45+
t.Run("NilProviderReturnsDefaults", func(t *testing.T) {
46+
cfg := LoadAuditRetryConfig(nil)
47+
assert.Equal(t, DefaultAuditRetryConfig(), cfg)
48+
})
49+
50+
t.Run("KeyNotSetReturnsDefaults", func(t *testing.T) {
51+
cp := &stubAuditConfigProvider{set: false}
52+
cfg := LoadAuditRetryConfig(cp)
53+
assert.Equal(t, DefaultAuditRetryConfig(), cfg)
54+
assert.False(t, cp.unmarshaled, "UnmarshalKey must not be called when the key is unset")
55+
})
56+
57+
t.Run("UsesConfiguredKey", func(t *testing.T) {
58+
cp := &stubAuditConfigProvider{set: true}
59+
LoadAuditRetryConfig(cp)
60+
assert.Equal(t, AuditRetryConfigKey, cp.lastKey)
61+
})
62+
63+
t.Run("OverridesBothFields", func(t *testing.T) {
64+
cp := &stubAuditConfigProvider{set: true, unmarshal: func(_ string, rawVal any) error {
65+
raw := rawVal.(*auditRetryConfigRaw)
66+
raw.NumRetries = 7
67+
raw.RetryDelay = "500ms"
68+
69+
return nil
70+
}}
71+
cfg := LoadAuditRetryConfig(cp)
72+
assert.Equal(t, 7, cfg.NumRetries)
73+
assert.Equal(t, 500*time.Millisecond, cfg.RetryDelay)
74+
})
75+
76+
t.Run("UnmarshalErrorReturnsDefaults", func(t *testing.T) {
77+
cp := &stubAuditConfigProvider{set: true, unmarshal: func(_ string, _ any) error {
78+
return errors.New("boom")
79+
}}
80+
cfg := LoadAuditRetryConfig(cp)
81+
assert.Equal(t, DefaultAuditRetryConfig(), cfg)
82+
})
83+
84+
t.Run("NonPositiveNumRetriesKeepsDefault", func(t *testing.T) {
85+
cp := &stubAuditConfigProvider{set: true, unmarshal: func(_ string, rawVal any) error {
86+
raw := rawVal.(*auditRetryConfigRaw)
87+
raw.NumRetries = 0
88+
raw.RetryDelay = "2s"
89+
90+
return nil
91+
}}
92+
cfg := LoadAuditRetryConfig(cp)
93+
assert.Equal(t, DefaultAuditTokensNumRetries, cfg.NumRetries)
94+
assert.Equal(t, 2*time.Second, cfg.RetryDelay)
95+
})
96+
97+
t.Run("InvalidRetryDelayKeepsDefault", func(t *testing.T) {
98+
cp := &stubAuditConfigProvider{set: true, unmarshal: func(_ string, rawVal any) error {
99+
raw := rawVal.(*auditRetryConfigRaw)
100+
raw.NumRetries = 5
101+
raw.RetryDelay = "not-a-duration"
102+
103+
return nil
104+
}}
105+
cfg := LoadAuditRetryConfig(cp)
106+
assert.Equal(t, 5, cfg.NumRetries)
107+
assert.Equal(t, DefaultAuditTokensRetryDelay, cfg.RetryDelay)
108+
})
109+
110+
t.Run("NonPositiveRetryDelayKeepsDefault", func(t *testing.T) {
111+
cp := &stubAuditConfigProvider{set: true, unmarshal: func(_ string, rawVal any) error {
112+
raw := rawVal.(*auditRetryConfigRaw)
113+
raw.RetryDelay = "0s"
114+
115+
return nil
116+
}}
117+
cfg := LoadAuditRetryConfig(cp)
118+
assert.Equal(t, DefaultAuditTokensRetryDelay, cfg.RetryDelay)
119+
})
120+
121+
t.Run("EmptyRetryDelayKeepsDefault", func(t *testing.T) {
122+
cp := &stubAuditConfigProvider{set: true, unmarshal: func(_ string, rawVal any) error {
123+
raw := rawVal.(*auditRetryConfigRaw)
124+
raw.NumRetries = 4
125+
126+
return nil
127+
}}
128+
cfg := LoadAuditRetryConfig(cp)
129+
assert.Equal(t, 4, cfg.NumRetries)
130+
assert.Equal(t, DefaultAuditTokensRetryDelay, cfg.RetryDelay)
131+
})
132+
}

0 commit comments

Comments
 (0)