Skip to content

Commit b1fc703

Browse files
authored
Merge pull request #2213 from lavanet/feat/cross-validation-selection-refactor
refactor(cross-validation)!: change from min,max,rate to agreementThreshold and maxParticipants
2 parents 7fe7354 + ee5e246 commit b1fc703

20 files changed

+1913
-807
lines changed

protocol/chainlib/protocol_message.go

Lines changed: 42 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -110,63 +110,56 @@ func NewProtocolMessage(chainMessage ChainMessage, directiveHeaders map[string]s
110110
}
111111
}
112112

113-
const (
114-
DEFAULT_CROSS_VALIDATION_RATE = 0.66
115-
DEFAULT_CROSS_VALIDATION_MAX = 5
116-
DEFAULT_CROSS_VALIDATION_MIN = 2
117-
)
118-
119-
func (bpm *BaseProtocolMessage) GetCrossValidationParameters() (common.CrossValidationParams, error) {
113+
// GetCrossValidationParameters parses cross-validation headers from the request.
114+
// Returns (params, headersPresent, error).
115+
// - headersPresent is true if any cross-validation header was found (used by state machine to set Selection)
116+
// - error is returned if headers are present but invalid
117+
func (bpm *BaseProtocolMessage) GetCrossValidationParameters() (common.CrossValidationParams, bool, error) {
118+
var maxParticipants, agreementThreshold int
120119
var err error
121-
enabled := false
122-
var crossValidationRate float64
123-
var crossValidationMax int
124-
var crossValidationMin int
125-
126-
crossValidationRateString, ok := bpm.directiveHeaders[common.CROSS_VALIDATION_HEADER_RATE]
127-
enabled = enabled || ok
128-
if !ok {
129-
crossValidationRate = DEFAULT_CROSS_VALIDATION_RATE
130-
} else {
131-
crossValidationRate, err = strconv.ParseFloat(crossValidationRateString, 64)
132-
if err != nil || crossValidationRate < 0 || crossValidationRate > 1 {
133-
return common.CrossValidationParams{}, errors.New("invalid cross-validation rate")
134-
}
120+
121+
// Check if max-participants header is present
122+
maxParticipantsStr, maxPresent := bpm.directiveHeaders[common.CROSS_VALIDATION_HEADER_MAX_PARTICIPANTS]
123+
// Check if agreement-threshold header is present
124+
agreementThresholdStr, thresholdPresent := bpm.directiveHeaders[common.CROSS_VALIDATION_HEADER_AGREEMENT_THRESHOLD]
125+
126+
// If no cross-validation headers are present, return defaults with headersPresent=false
127+
if !maxPresent && !thresholdPresent {
128+
return common.DefaultCrossValidationParams, false, nil
135129
}
136130

137-
crossValidationMaxRateString, ok := bpm.directiveHeaders[common.CROSS_VALIDATION_HEADER_MAX]
138-
enabled = enabled || ok
139-
if !ok {
140-
crossValidationMax = DEFAULT_CROSS_VALIDATION_MAX
141-
} else {
142-
crossValidationMax, err = strconv.Atoi(crossValidationMaxRateString)
143-
if err != nil || crossValidationMax < 0 {
144-
return common.CrossValidationParams{}, errors.New("invalid cross-validation max")
131+
// At least one header is present - parse and validate both
132+
if maxPresent {
133+
maxParticipants, err = strconv.Atoi(maxParticipantsStr)
134+
if err != nil || maxParticipants < 1 {
135+
return common.CrossValidationParams{}, true, errors.New("invalid cross-validation max-participants: must be a positive integer")
145136
}
137+
} else {
138+
return common.CrossValidationParams{}, true, errors.New("cross-validation max-participants header is required when using cross-validation")
146139
}
147140

148-
crossValidationMinRateString, ok := bpm.directiveHeaders[common.CROSS_VALIDATION_HEADER_MIN]
149-
enabled = enabled || ok
150-
if !ok {
151-
crossValidationMin = DEFAULT_CROSS_VALIDATION_MIN
152-
} else {
153-
crossValidationMin, err = strconv.Atoi(crossValidationMinRateString)
154-
if err != nil || crossValidationMin < 0 {
155-
return common.CrossValidationParams{}, errors.New("invalid cross-validation min")
141+
if thresholdPresent {
142+
agreementThreshold, err = strconv.Atoi(agreementThresholdStr)
143+
if err != nil || agreementThreshold < 1 {
144+
return common.CrossValidationParams{}, true, errors.New("invalid cross-validation agreement-threshold: must be a positive integer")
156145
}
146+
} else {
147+
return common.CrossValidationParams{}, true, errors.New("cross-validation agreement-threshold header is required when using cross-validation")
157148
}
158149

159-
if crossValidationMin > crossValidationMax {
160-
return common.CrossValidationParams{}, errors.New("cross-validation min is greater than cross-validation max")
150+
// Validate that agreementThreshold <= maxParticipants
151+
if agreementThreshold > maxParticipants {
152+
return common.CrossValidationParams{}, true, errors.New("cross-validation agreement-threshold cannot be greater than max-participants")
161153
}
162154

163-
if enabled {
164-
utils.LavaFormatInfo("CrossValidation parameters", utils.LogAttr("crossValidationRate", crossValidationRate), utils.LogAttr("crossValidationMax", crossValidationMax), utils.LogAttr("crossValidationMin", crossValidationMin))
165-
return common.CrossValidationParams{Rate: crossValidationRate, Max: crossValidationMax, Min: crossValidationMin}, nil
166-
} else {
167-
utils.LavaFormatInfo("CrossValidation parameters not enabled")
168-
return common.CrossValidationParams{Rate: 1, Max: 1, Min: 1}, nil
169-
}
155+
utils.LavaFormatInfo("CrossValidation parameters parsed",
156+
utils.LogAttr("maxParticipants", maxParticipants),
157+
utils.LogAttr("agreementThreshold", agreementThreshold))
158+
159+
return common.CrossValidationParams{
160+
MaxParticipants: maxParticipants,
161+
AgreementThreshold: agreementThreshold,
162+
}, true, nil
170163
}
171164

172165
type ProtocolMessage interface {
@@ -178,5 +171,7 @@ type ProtocolMessage interface {
178171
GetUserData() common.UserData
179172
IsDefaultApi() bool
180173
UpdateEarliestAndValidateExtensionRules(extensionParser *extensionslib.ExtensionParser, earliestBlockHashRequested int64, addon string, seenBlock int64) bool
181-
GetCrossValidationParameters() (common.CrossValidationParams, error)
174+
// GetCrossValidationParameters returns (params, headersPresent, error)
175+
// headersPresent indicates if cross-validation headers were found (used to set Selection type)
176+
GetCrossValidationParameters() (common.CrossValidationParams, bool, error)
182177
}

protocol/chainlib/protocol_message_test.go

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@ package chainlib
33
import (
44
"testing"
55

6+
"github.com/lavanet/lava/v5/protocol/common"
67
"github.com/lavanet/lava/v5/x/spec/types"
78
"github.com/stretchr/testify/assert"
9+
"github.com/stretchr/testify/require"
810
)
911

1012
func TestIsDefaultApi(t *testing.T) {
@@ -53,3 +55,197 @@ func TestIsDefaultApi(t *testing.T) {
5355
assert.False(t, baseProtocolMessage.IsDefaultApi())
5456
})
5557
}
58+
59+
func TestGetCrossValidationParameters(t *testing.T) {
60+
t.Run("no headers present - returns defaults with headersPresent=false", func(t *testing.T) {
61+
bpm := &BaseProtocolMessage{
62+
directiveHeaders: map[string]string{},
63+
}
64+
65+
params, headersPresent, err := bpm.GetCrossValidationParameters()
66+
require.NoError(t, err)
67+
assert.False(t, headersPresent)
68+
assert.Equal(t, common.DefaultCrossValidationParams, params)
69+
})
70+
71+
t.Run("nil headers - returns defaults with headersPresent=false", func(t *testing.T) {
72+
bpm := &BaseProtocolMessage{
73+
directiveHeaders: nil,
74+
}
75+
76+
params, headersPresent, err := bpm.GetCrossValidationParameters()
77+
require.NoError(t, err)
78+
assert.False(t, headersPresent)
79+
assert.Equal(t, common.DefaultCrossValidationParams, params)
80+
})
81+
82+
t.Run("valid headers - parses correctly", func(t *testing.T) {
83+
bpm := &BaseProtocolMessage{
84+
directiveHeaders: map[string]string{
85+
common.CROSS_VALIDATION_HEADER_MAX_PARTICIPANTS: "5",
86+
common.CROSS_VALIDATION_HEADER_AGREEMENT_THRESHOLD: "3",
87+
},
88+
}
89+
90+
params, headersPresent, err := bpm.GetCrossValidationParameters()
91+
require.NoError(t, err)
92+
assert.True(t, headersPresent)
93+
assert.Equal(t, 5, params.MaxParticipants)
94+
assert.Equal(t, 3, params.AgreementThreshold)
95+
})
96+
97+
t.Run("threshold equals max - valid", func(t *testing.T) {
98+
bpm := &BaseProtocolMessage{
99+
directiveHeaders: map[string]string{
100+
common.CROSS_VALIDATION_HEADER_MAX_PARTICIPANTS: "3",
101+
common.CROSS_VALIDATION_HEADER_AGREEMENT_THRESHOLD: "3",
102+
},
103+
}
104+
105+
params, headersPresent, err := bpm.GetCrossValidationParameters()
106+
require.NoError(t, err)
107+
assert.True(t, headersPresent)
108+
assert.Equal(t, 3, params.MaxParticipants)
109+
assert.Equal(t, 3, params.AgreementThreshold)
110+
})
111+
112+
t.Run("only max-participants header - error", func(t *testing.T) {
113+
bpm := &BaseProtocolMessage{
114+
directiveHeaders: map[string]string{
115+
common.CROSS_VALIDATION_HEADER_MAX_PARTICIPANTS: "5",
116+
},
117+
}
118+
119+
_, headersPresent, err := bpm.GetCrossValidationParameters()
120+
require.Error(t, err)
121+
assert.True(t, headersPresent)
122+
assert.Contains(t, err.Error(), "agreement-threshold header is required")
123+
})
124+
125+
t.Run("only agreement-threshold header - error", func(t *testing.T) {
126+
bpm := &BaseProtocolMessage{
127+
directiveHeaders: map[string]string{
128+
common.CROSS_VALIDATION_HEADER_AGREEMENT_THRESHOLD: "3",
129+
},
130+
}
131+
132+
_, headersPresent, err := bpm.GetCrossValidationParameters()
133+
require.Error(t, err)
134+
assert.True(t, headersPresent)
135+
assert.Contains(t, err.Error(), "max-participants header is required")
136+
})
137+
138+
t.Run("threshold greater than max - error", func(t *testing.T) {
139+
bpm := &BaseProtocolMessage{
140+
directiveHeaders: map[string]string{
141+
common.CROSS_VALIDATION_HEADER_MAX_PARTICIPANTS: "3",
142+
common.CROSS_VALIDATION_HEADER_AGREEMENT_THRESHOLD: "5",
143+
},
144+
}
145+
146+
_, headersPresent, err := bpm.GetCrossValidationParameters()
147+
require.Error(t, err)
148+
assert.True(t, headersPresent)
149+
assert.Contains(t, err.Error(), "cannot be greater than max-participants")
150+
})
151+
152+
t.Run("invalid max-participants (not a number) - error", func(t *testing.T) {
153+
bpm := &BaseProtocolMessage{
154+
directiveHeaders: map[string]string{
155+
common.CROSS_VALIDATION_HEADER_MAX_PARTICIPANTS: "invalid",
156+
common.CROSS_VALIDATION_HEADER_AGREEMENT_THRESHOLD: "3",
157+
},
158+
}
159+
160+
_, headersPresent, err := bpm.GetCrossValidationParameters()
161+
require.Error(t, err)
162+
assert.True(t, headersPresent)
163+
assert.Contains(t, err.Error(), "invalid cross-validation max-participants")
164+
})
165+
166+
t.Run("invalid agreement-threshold (not a number) - error", func(t *testing.T) {
167+
bpm := &BaseProtocolMessage{
168+
directiveHeaders: map[string]string{
169+
common.CROSS_VALIDATION_HEADER_MAX_PARTICIPANTS: "5",
170+
common.CROSS_VALIDATION_HEADER_AGREEMENT_THRESHOLD: "invalid",
171+
},
172+
}
173+
174+
_, headersPresent, err := bpm.GetCrossValidationParameters()
175+
require.Error(t, err)
176+
assert.True(t, headersPresent)
177+
assert.Contains(t, err.Error(), "invalid cross-validation agreement-threshold")
178+
})
179+
180+
t.Run("max-participants zero - error", func(t *testing.T) {
181+
bpm := &BaseProtocolMessage{
182+
directiveHeaders: map[string]string{
183+
common.CROSS_VALIDATION_HEADER_MAX_PARTICIPANTS: "0",
184+
common.CROSS_VALIDATION_HEADER_AGREEMENT_THRESHOLD: "0",
185+
},
186+
}
187+
188+
_, headersPresent, err := bpm.GetCrossValidationParameters()
189+
require.Error(t, err)
190+
assert.True(t, headersPresent)
191+
assert.Contains(t, err.Error(), "must be a positive integer")
192+
})
193+
194+
t.Run("negative max-participants - error", func(t *testing.T) {
195+
bpm := &BaseProtocolMessage{
196+
directiveHeaders: map[string]string{
197+
common.CROSS_VALIDATION_HEADER_MAX_PARTICIPANTS: "-1",
198+
common.CROSS_VALIDATION_HEADER_AGREEMENT_THRESHOLD: "1",
199+
},
200+
}
201+
202+
_, headersPresent, err := bpm.GetCrossValidationParameters()
203+
require.Error(t, err)
204+
assert.True(t, headersPresent)
205+
assert.Contains(t, err.Error(), "must be a positive integer")
206+
})
207+
208+
t.Run("agreement-threshold zero - error", func(t *testing.T) {
209+
bpm := &BaseProtocolMessage{
210+
directiveHeaders: map[string]string{
211+
common.CROSS_VALIDATION_HEADER_MAX_PARTICIPANTS: "5",
212+
common.CROSS_VALIDATION_HEADER_AGREEMENT_THRESHOLD: "0",
213+
},
214+
}
215+
216+
_, headersPresent, err := bpm.GetCrossValidationParameters()
217+
require.Error(t, err)
218+
assert.True(t, headersPresent)
219+
assert.Contains(t, err.Error(), "must be a positive integer")
220+
})
221+
222+
t.Run("minimum valid values (1, 1) - valid", func(t *testing.T) {
223+
bpm := &BaseProtocolMessage{
224+
directiveHeaders: map[string]string{
225+
common.CROSS_VALIDATION_HEADER_MAX_PARTICIPANTS: "1",
226+
common.CROSS_VALIDATION_HEADER_AGREEMENT_THRESHOLD: "1",
227+
},
228+
}
229+
230+
params, headersPresent, err := bpm.GetCrossValidationParameters()
231+
require.NoError(t, err)
232+
assert.True(t, headersPresent)
233+
assert.Equal(t, 1, params.MaxParticipants)
234+
assert.Equal(t, 1, params.AgreementThreshold)
235+
})
236+
237+
t.Run("large valid values - valid", func(t *testing.T) {
238+
bpm := &BaseProtocolMessage{
239+
directiveHeaders: map[string]string{
240+
common.CROSS_VALIDATION_HEADER_MAX_PARTICIPANTS: "100",
241+
common.CROSS_VALIDATION_HEADER_AGREEMENT_THRESHOLD: "51",
242+
},
243+
}
244+
245+
params, headersPresent, err := bpm.GetCrossValidationParameters()
246+
require.NoError(t, err)
247+
assert.True(t, headersPresent)
248+
assert.Equal(t, 100, params.MaxParticipants)
249+
assert.Equal(t, 51, params.AgreementThreshold)
250+
})
251+
}

protocol/common/endpoints.go

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -35,32 +35,32 @@ const (
3535
LAVAP_VERSION_HEADER_NAME = "Lavap-Version"
3636
LAVA_CONSUMER_PROCESS_GUID = "lava-consumer-process-guid"
3737
// these headers need to be lowercase
38-
BLOCK_PROVIDERS_ADDRESSES_HEADER_NAME = "lava-providers-block"
39-
RELAY_TIMEOUT_HEADER_NAME = "lava-relay-timeout"
40-
EXTENSION_OVERRIDE_HEADER_NAME = "lava-extension"
41-
FORCE_CACHE_REFRESH_HEADER_NAME = "lava-force-cache-refresh"
42-
LAVA_DEBUG_RELAY = "lava-debug-relay"
43-
LAVA_LB_UNIQUE_ID_HEADER = "lava-lb-unique-id"
44-
STICKINESS_HEADER_NAME = "lava-stickiness"
45-
CROSS_VALIDATION_HEADER_RATE = "lava-cross-validation-rate"
46-
CROSS_VALIDATION_HEADER_MAX = "lava-cross-validation-max"
47-
CROSS_VALIDATION_HEADER_MIN = "lava-cross-validation-min"
48-
CROSS_VALIDATION_ALL_PROVIDERS_HEADER_NAME = "lava-cross-validation-all-providers"
38+
BLOCK_PROVIDERS_ADDRESSES_HEADER_NAME = "lava-providers-block"
39+
RELAY_TIMEOUT_HEADER_NAME = "lava-relay-timeout"
40+
EXTENSION_OVERRIDE_HEADER_NAME = "lava-extension"
41+
FORCE_CACHE_REFRESH_HEADER_NAME = "lava-force-cache-refresh"
42+
LAVA_DEBUG_RELAY = "lava-debug-relay"
43+
LAVA_LB_UNIQUE_ID_HEADER = "lava-lb-unique-id"
44+
STICKINESS_HEADER_NAME = "lava-stickiness"
45+
CROSS_VALIDATION_HEADER_MAX_PARTICIPANTS = "lava-cross-validation-max-participants"
46+
CROSS_VALIDATION_HEADER_AGREEMENT_THRESHOLD = "lava-cross-validation-agreement-threshold"
47+
CROSS_VALIDATION_ALL_PROVIDERS_HEADER_NAME = "lava-cross-validation-all-providers"
48+
CROSS_VALIDATION_STATUS_HEADER_NAME = "lava-cross-validation-status"
49+
CROSS_VALIDATION_AGREEING_PROVIDERS_HEADER = "lava-cross-validation-agreeing-providers"
4950
// send http request to /lava/health to see if the process is up - (ret code 200)
5051
DEFAULT_HEALTH_PATH = "/lava/health"
5152
MAXIMUM_ALLOWED_TIMEOUT_EXTEND_MULTIPLIER_BY_THE_CONSUMER = 4
5253
)
5354

5455
var SPECIAL_LAVA_DIRECTIVE_HEADERS = map[string]struct{}{
55-
BLOCK_PROVIDERS_ADDRESSES_HEADER_NAME: {},
56-
RELAY_TIMEOUT_HEADER_NAME: {},
57-
EXTENSION_OVERRIDE_HEADER_NAME: {},
58-
FORCE_CACHE_REFRESH_HEADER_NAME: {},
59-
LAVA_DEBUG_RELAY: {},
60-
STICKINESS_HEADER_NAME: {},
61-
CROSS_VALIDATION_HEADER_RATE: {},
62-
CROSS_VALIDATION_HEADER_MAX: {},
63-
CROSS_VALIDATION_HEADER_MIN: {},
56+
BLOCK_PROVIDERS_ADDRESSES_HEADER_NAME: {},
57+
RELAY_TIMEOUT_HEADER_NAME: {},
58+
EXTENSION_OVERRIDE_HEADER_NAME: {},
59+
FORCE_CACHE_REFRESH_HEADER_NAME: {},
60+
LAVA_DEBUG_RELAY: {},
61+
STICKINESS_HEADER_NAME: {},
62+
CROSS_VALIDATION_HEADER_MAX_PARTICIPANTS: {},
63+
CROSS_VALIDATION_HEADER_AGREEMENT_THRESHOLD: {},
6464
}
6565

6666
type UserData struct {

protocol/common/types.go

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,15 @@
11
package common
22

33
// CrossValidationParams holds the cross-validation configuration parameters
4+
// Note: Whether cross-validation is enabled is determined by the Selection type (CrossValidation),
5+
// not by these parameters. These parameters only store the values when cross-validation is active.
46
type CrossValidationParams struct {
5-
Rate float64
6-
Max int
7-
Min int
7+
MaxParticipants int // Maximum number of providers to query
8+
AgreementThreshold int // Number of matching responses needed for consensus
89
}
910

11+
// DefaultCrossValidationParams are used when cross-validation is not enabled (Selection != CrossValidation)
1012
var DefaultCrossValidationParams = CrossValidationParams{
11-
Rate: 1,
12-
Max: 1,
13-
Min: 1,
14-
}
15-
16-
func (cvp CrossValidationParams) Enabled() bool {
17-
return !(cvp.Rate == DefaultCrossValidationParams.Rate && cvp.Max == DefaultCrossValidationParams.Max && cvp.Min == DefaultCrossValidationParams.Min)
13+
MaxParticipants: 1,
14+
AgreementThreshold: 1,
1815
}

0 commit comments

Comments
 (0)