From cb43793a70da66100c5bef1c60c17b7c8baad39b Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Mon, 19 Oct 2026 16:45:45 +0200 Subject: [PATCH 001/127] feat(ledger): gate future Raft protocol capabilities --- docs/ops/upgrades.md | 10 + .../subsystems/consensus/README.md | 1 + .../subsystems/consensus/capabilities.md | 53 + internal/infra/capabilities/capabilities.go | 217 ++ .../capabilities/capabilities_extra_test.go | 21 + .../infra/capabilities/capabilities_test.go | 53 + internal/infra/node/node.go | 7 +- internal/infra/node/proposal.go | 18 +- internal/proto/auditpb/audit.pb.go | 7 +- internal/proto/auditpb/audit_vtproto.pb.go | 14 +- .../cluster_bootstrap.pb.go | 2 +- .../cluster_bootstrap_grpc.pb.go | 2 +- .../cluster_bootstrap_vtproto.pb.go | 2 +- internal/proto/clusterpb/cluster.pb.go | 2 +- internal/proto/clusterpb/cluster_grpc.pb.go | 2 +- .../proto/clusterpb/cluster_vtproto.pb.go | 2 +- internal/proto/commonpb/common_dethash.pb.go | 11 + internal/proto/commonpb/common_reader.pb.go | 97 + internal/proto/commonpb/common_vtproto.pb.go | 2140 ++++++++++++++++- internal/proto/eventspb/events.pb.go | 2 +- internal/proto/eventspb/events_vtproto.pb.go | 2 +- internal/proto/proposalpb/proposal.pb.go | 2 +- .../proto/proposalpb/proposal_vtproto.pb.go | 2 +- internal/proto/raftcmdpb/raft_cmd.pb.go | 18 +- .../proto/raftcmdpb/raft_cmd_dethash.pb.go | 9 + .../proto/raftcmdpb/raft_cmd_reader.pb.go | 5 + .../proto/raftcmdpb/raft_cmd_vtproto.pb.go | 436 +++- .../rafttransportpb/raft_transport.pb.go | 2 +- .../rafttransportpb/raft_transport_grpc.pb.go | 2 +- .../raft_transport_vtproto.pb.go | 26 +- internal/proto/restorepb/restore.pb.go | 2 +- internal/proto/restorepb/restore_grpc.pb.go | 2 +- .../proto/restorepb/restore_vtproto.pb.go | 14 +- internal/proto/servicepb/bucket.pb.go | 2 +- internal/proto/servicepb/bucket_grpc.pb.go | 2 +- internal/proto/servicepb/bucket_vtproto.pb.go | 308 ++- internal/proto/signaturepb/signature.pb.go | 2 +- .../proto/signaturepb/signature_vtproto.pb.go | 2 +- internal/proto/snapshotpb/snapshot.pb.go | 2 +- internal/proto/snapshotpb/snapshot_grpc.pb.go | 2 +- .../proto/snapshotpb/snapshot_vtproto.pb.go | 2 +- misc/proto/common.proto | 25 + misc/proto/raft_cmd.proto | 4 + 43 files changed, 3499 insertions(+), 37 deletions(-) create mode 100644 docs/technical/architecture/subsystems/consensus/capabilities.md create mode 100644 internal/infra/capabilities/capabilities.go create mode 100644 internal/infra/capabilities/capabilities_extra_test.go create mode 100644 internal/infra/capabilities/capabilities_test.go diff --git a/docs/ops/upgrades.md b/docs/ops/upgrades.md index 24168a4226..d65dcc7636 100644 --- a/docs/ops/upgrades.md +++ b/docs/ops/upgrades.md @@ -328,6 +328,16 @@ validation passes. For RC.1 to RC.2, follow the [maintenance-barrier rolling procedure](#account-metadata-address-validation). For RC.3 to RC.4, use the [operator-required stopped restart](#operator-required-stopped-restart). +Capability barriers are the prerequisite for any future incompatible Raft +entry, execution-plan, replay, or snapshot format. Deploy barrier-aware Ledger +servers to every voter and learner first, then arm the replicated policy. An +old leader that predates the barrier is not made safe by upgrading other nodes. +See the [capability contract](../technical/architecture/subsystems/consensus/capabilities.md) +for the `FEATURE_ONLY` and `ALL_WRITES` admission policies, membership evidence, +diagnostics, and downgrade boundary. + +Use this for RC updates after the mixed-version validation above passes: + 1. Record the current leader and voter set with `ledgerctl cluster status`. 2. Restart one follower on the target binary. 3. Wait until it reports normal, has caught up to the leader's commit index, diff --git a/docs/technical/architecture/subsystems/consensus/README.md b/docs/technical/architecture/subsystems/consensus/README.md index 30670985ca..432ce2c808 100644 --- a/docs/technical/architecture/subsystems/consensus/README.md +++ b/docs/technical/architecture/subsystems/consensus/README.md @@ -13,6 +13,7 @@ The replication and ordering layer (`internal/infra/node`, `internal/infra/trans | [hybrid-logical-clock.md](hybrid-logical-clock.md) | Monotonic HLC timestamps across leader changes and clock skew. | | [removed-member-registry.md](removed-member-registry.md) | Universal member-identity invariant, committed-only peer routing, and replicated `(nodeID, instanceID)` set that prevents a removed member from silently rejoining and being auto-promoted; replay-safe per-event admission barriers, lookup costs, removal write ordering and durable restart states. | | [controlled-shutdown.md](controlled-shutdown.md) | Raft-ordered admission cutoff, local persistence confirmation, restart behavior, rolling-upgrade capability gate, and operator completion contract. | +| [capabilities.md](capabilities.md) | Stable capability declarations, persisted activation policy and replay floor, membership evidence, and mixed-version rollout boundaries. | ## Related diff --git a/docs/technical/architecture/subsystems/consensus/capabilities.md b/docs/technical/architecture/subsystems/consensus/capabilities.md new file mode 100644 index 0000000000..df6152d634 --- /dev/null +++ b/docs/technical/architecture/subsystems/consensus/capabilities.md @@ -0,0 +1,53 @@ +# Rolling-upgrade capabilities + +Ledger's capability barrier is the protocol foundation for a future mixed-version +rolling upgrade. It is separate from the client-facing gRPC protocol revision. + +## Wire contract + +`common.ClusterPolicy` carries the replicated activation state, admission policy, +membership revision under examination, and a monotonic minimum replay protocol. +`raft.Proposal.required_capabilities` carries stable producer-owned capability +IDs. The fields are optional and therefore backward-readable by binaries that +predate this foundation. IDs are never reused; semantic dependencies must be +declared even when they do not add bytes to a proposal. + +The final serialized proposal is the source of truth. A producer must rebuild +its plan first, then validate that the declared IDs exactly match +`required_capabilities`, immediately before enqueueing the proposal. A payload +that depends on a capability but omits its declaration is invalid. + +## Admission and membership + +The pure decision contract in `internal/infra/capabilities` evaluates every +current voter and learner. A member is represented by `(node_id, instance_id)`; +an identity refresh is a new incarnation and cannot silently inherit evidence +from the old one. Reachability and support are separate: an unreachable member +blocks activation just as an incapable member does. Activation is bound to the +membership revision observed by the proposer; a changed revision requires a +fresh examination. + +While armed, `FEATURE_ONLY` rejects dependent proposals and allows unrelated +business writes. `ALL_WRITES` rejects all business writes, with only the +narrow control-plane recovery and activation path exempted by its producer +inventory. FSM apply never consults node-local capability flags. + +The replay floor is monotonic and is checked before decoding an entry or +snapshot that could require a newer protocol. This floor is intentionally not +`pkg/grpcprotocol.Version`. + +## Rollout and recovery + +Deploy barrier-aware binaries to every eligible member before arming an +incompatible feature. An old leader that does not understand the barrier cannot +be made safe by upgrading only the other nodes. Restart, snapshot install, +restore, and downgrade code must preserve the policy and floor; a downgrade +below the committed floor is refused. Membership changes racing activation are +serialized at the Raft proposal boundary and must be revalidated immediately +before `rawNode.Propose`, because `Node.Propose` queues work. + +The current package is the deterministic contract and wire foundation. Wiring +each admission, mirror, events, technical-maintenance, and backup producer to +the final-proposal validator and to the node's pre-`Propose` revalidation is a +follow-up integration step; no producer may claim support until that inventory +is complete. diff --git a/internal/infra/capabilities/capabilities.go b/internal/infra/capabilities/capabilities.go new file mode 100644 index 0000000000..698b4c6392 --- /dev/null +++ b/internal/infra/capabilities/capabilities.go @@ -0,0 +1,217 @@ +// Package capabilities contains the deterministic contract used to gate +// rolling Raft protocol changes. It deliberately has no node-local state and +// no network or storage dependency: callers provide the committed policy and +// a snapshot of membership evidence. +package capabilities + +import ( + "bytes" + "errors" + "fmt" + "slices" + + "github.com/formancehq/ledger/v3/internal/proto/commonpb" + "github.com/formancehq/ledger/v3/internal/proto/raftcmdpb" +) + +type ID uint32 + +// IDs are protocol constants. Once published, an ID is never reused. +const ( + IDExecutionPlanV1 ID = 1 + IDSemanticReplayV1 ID = 2 +) + +type AdmissionPolicy uint8 + +const ( + FeatureOnly AdmissionPolicy = iota + 1 + AllWrites +) + +type MemberRole uint8 + +const ( + Voter MemberRole = iota + 1 + Learner +) + +type Member struct { + NodeID uint64 + InstanceID []byte + Role MemberRole + Reachable bool + Capabilities []ID +} + +type State struct { + Armed bool + Policy AdmissionPolicy + MinimumReplay uint32 + MembershipRevision uint64 + Activations map[ID]bool +} + +type Blocker struct { + NodeID uint64 + Role MemberRole + Reason string +} + +type Decision struct { + Allowed bool + Reason string + Blockers []Blocker +} + +func ValidateMembers(members []Member) error { + seen := make(map[uint64][]byte, len(members)) + for _, member := range members { + if member.NodeID == 0 || (member.Role != Voter && member.Role != Learner) { + return fmt.Errorf("%w: invalid member %d", ErrInvalidState, member.NodeID) + } + identity := bytes.Clone(member.InstanceID) + if previous, ok := seen[member.NodeID]; ok && !bytes.Equal(previous, identity) { + return fmt.Errorf("%w: node %d has multiple incarnations", ErrInvalidState, member.NodeID) + } + seen[member.NodeID] = identity + } + return nil +} + +func ValidateMembershipRevision(observed, examined uint64) error { + if examined == 0 || observed != examined { + return fmt.Errorf("%w: membership revision changed from %d to %d", ErrInvalidState, examined, observed) + } + return nil +} + +func AdvanceReplayFloor(current, next uint32) (uint32, error) { + if next < current { + return current, fmt.Errorf("%w: replay protocol floor cannot decrease from %d to %d", ErrInvalidState, current, next) + } + return next, nil +} + +var ( + ErrInvalidState = errors.New("invalid capability state") + ErrInvalidDeclaration = errors.New("invalid capability declaration") + ErrMissingCapability = errors.New("proposal is missing a required capability declaration") + ErrCapabilityUnsupported = errors.New("capability is unsupported by a cluster member") +) + +func (s State) Validate() error { + if !s.Armed { + return nil + } + if s.Policy != FeatureOnly && s.Policy != AllWrites { + return fmt.Errorf("%w: admission policy %d", ErrInvalidState, s.Policy) + } + if s.MembershipRevision == 0 { + return fmt.Errorf("%w: zero membership revision", ErrInvalidState) + } + return nil +} + +func FromPolicy(policy *commonpb.ClusterPolicy) State { + s := State{Activations: make(map[ID]bool)} + if policy == nil { + return s + } + s.Armed = policy.GetCapabilityActivationArmed() + s.MinimumReplay = policy.GetMinimumReplayProtocol() + s.MembershipRevision = policy.GetCapabilityMembershipRevision() + if policy.GetCapabilityAdmissionPolicy() == commonpb.CapabilityAdmissionPolicy_CAPABILITY_ADMISSION_POLICY_ALL_WRITES { + s.Policy = AllWrites + } else if policy.GetCapabilityAdmissionPolicy() == commonpb.CapabilityAdmissionPolicy_CAPABILITY_ADMISSION_POLICY_FEATURE_ONLY { + s.Policy = FeatureOnly + } + for _, activation := range policy.GetCapabilityActivations() { + if activation != nil { + s.Activations[ID(activation.GetId())] = activation.GetActive() + } + } + return s +} + +func Normalize(ids []ID) ([]ID, error) { + result := slices.Clone(ids) + slices.Sort(result) + for i := 1; i < len(result); i++ { + if result[i] == result[i-1] { + return nil, fmt.Errorf("%w: duplicate id %d", ErrInvalidDeclaration, result[i]) + } + } + return result, nil +} + +// Evaluate is pure. Reachability is reported separately from support: a +// temporarily unreachable voter is still a blocker for activation, while a +// learner is included because it must replay every committed entry. +func Evaluate(state State, required []ID, businessWrite bool, members []Member) Decision { + if err := state.Validate(); err != nil { + return Decision{Reason: err.Error()} + } + if !state.Armed { + return Decision{Allowed: true} + } + required, err := Normalize(required) + if err != nil { + return Decision{Reason: err.Error()} + } + blockers := make([]Blocker, 0) + for _, member := range members { + for _, id := range required { + if !slices.Contains(member.Capabilities, id) { + blockers = append(blockers, Blocker{NodeID: member.NodeID, Role: member.Role, Reason: fmt.Sprintf("missing capability %d", id)}) + continue + } + if !member.Reachable { + blockers = append(blockers, Blocker{NodeID: member.NodeID, Role: member.Role, Reason: "member is unreachable"}) + } + } + } + if len(blockers) > 0 && (businessWrite && state.Policy == AllWrites || len(required) > 0) { + return Decision{Reason: ErrCapabilityUnsupported.Error(), Blockers: blockers} + } + if businessWrite && state.Policy == AllWrites && len(required) == 0 { + return Decision{Reason: "business writes are blocked while capability activation is armed", Blockers: blockers} + } + return Decision{Allowed: true, Blockers: blockers} +} + +// ValidateFinalProposal proves that the declaration belongs to the bytes that +// will be proposed. Callers must invoke it after rebuilding an execution plan. +func ValidateFinalProposal(data []byte, declared []ID) error { + proposal := new(raftcmdpb.Proposal) + if err := proposal.UnmarshalVT(data); err != nil { + return fmt.Errorf("decode final proposal: %w", err) + } + wire := make([]ID, len(proposal.GetRequiredCapabilities())) + for i, id := range proposal.GetRequiredCapabilities() { + wire[i] = ID(id) + } + wire, err := Normalize(wire) + if err != nil { + return err + } + declared, err = Normalize(declared) + if err != nil { + return err + } + if !bytes.Equal(idsBytes(wire), idsBytes(declared)) { + return fmt.Errorf("%w: declaration does not match final payload", ErrMissingCapability) + } + return nil +} + +func idsBytes(ids []ID) []byte { + result := make([]byte, len(ids)*4) + for i, id := range ids { + result[i*4] = byte(id) + result[i*4+1] = byte(id >> 8) + result[i*4+2] = byte(id >> 16) + result[i*4+3] = byte(id >> 24) + } + return result +} diff --git a/internal/infra/capabilities/capabilities_extra_test.go b/internal/infra/capabilities/capabilities_extra_test.go new file mode 100644 index 0000000000..ab876b9124 --- /dev/null +++ b/internal/infra/capabilities/capabilities_extra_test.go @@ -0,0 +1,21 @@ +package capabilities + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestMembershipRevisionAndIncarnationValidation(t *testing.T) { + require.Error(t, ValidateMembershipRevision(8, 7)) + require.Error(t, ValidateMembers([]Member{{NodeID: 4, InstanceID: []byte("a"), Role: Voter}, {NodeID: 4, InstanceID: []byte("b"), Role: Learner}})) + require.NoError(t, ValidateMembers([]Member{{NodeID: 4, InstanceID: []byte("a"), Role: Voter}, {NodeID: 5, InstanceID: []byte("b"), Role: Learner}})) +} + +func TestReplayFloorIsMonotonic(t *testing.T) { + floor, err := AdvanceReplayFloor(3, 4) + require.NoError(t, err) + require.Equal(t, uint32(4), floor) + _, err = AdvanceReplayFloor(floor, 2) + require.Error(t, err) +} diff --git a/internal/infra/capabilities/capabilities_test.go b/internal/infra/capabilities/capabilities_test.go new file mode 100644 index 0000000000..262c8f4dc6 --- /dev/null +++ b/internal/infra/capabilities/capabilities_test.go @@ -0,0 +1,53 @@ +package capabilities + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/formancehq/ledger/v3/internal/proto/commonpb" + "github.com/formancehq/ledger/v3/internal/proto/raftcmdpb" +) + +func armedState(policy AdmissionPolicy) State { + return State{Armed: true, Policy: policy, MembershipRevision: 7, Activations: map[ID]bool{IDExecutionPlanV1: false}} +} + +func TestEvaluateIncludesVotersAndLearners(t *testing.T) { + decision := Evaluate(armedState(FeatureOnly), []ID{IDExecutionPlanV1}, true, []Member{ + {NodeID: 1, Role: Voter, Reachable: true, Capabilities: []ID{IDExecutionPlanV1}}, + {NodeID: 2, Role: Learner, Reachable: true}, + }) + require.False(t, decision.Allowed) + require.Equal(t, uint64(2), decision.Blockers[0].NodeID) +} + +func TestAllWritesBlocksUnrelatedBusinessWrite(t *testing.T) { + decision := Evaluate(armedState(AllWrites), nil, true, nil) + require.False(t, decision.Allowed) + require.Contains(t, decision.Reason, "business writes") +} + +func TestFeatureOnlyAllowsUnrelatedWrite(t *testing.T) { + decision := Evaluate(armedState(FeatureOnly), nil, true, []Member{{NodeID: 1, Role: Voter, Reachable: true}}) + require.True(t, decision.Allowed) +} + +func TestValidateFinalProposalRejectsMissingDeclaration(t *testing.T) { + data, err := (&raftcmdpb.Proposal{RequiredCapabilities: []uint32{uint32(IDExecutionPlanV1)}}).MarshalVT() + require.NoError(t, err) + require.ErrorIs(t, ValidateFinalProposal(data, nil), ErrMissingCapability) +} + +func TestFromPolicyRoundTrip(t *testing.T) { + state := FromPolicy(&commonpb.ClusterPolicy{ + CapabilityActivationArmed: true, + CapabilityAdmissionPolicy: commonpb.CapabilityAdmissionPolicy_CAPABILITY_ADMISSION_POLICY_ALL_WRITES, + MinimumReplayProtocol: 4, + CapabilityMembershipRevision: 9, + CapabilityActivations: []*commonpb.CapabilityActivation{{Id: uint32(IDExecutionPlanV1), Active: true}}, + }) + require.Equal(t, AllWrites, state.Policy) + require.True(t, state.Activations[IDExecutionPlanV1]) + require.NoError(t, state.Validate()) +} diff --git a/internal/infra/node/node.go b/internal/infra/node/node.go index b19282acc6..dd8c79298c 100644 --- a/internal/infra/node/node.go +++ b/internal/infra/node/node.go @@ -26,6 +26,7 @@ import ( logging "github.com/formancehq/go-libs/v5/pkg/observe/log" "github.com/formancehq/ledger/v3/internal/domain" + "github.com/formancehq/ledger/v3/internal/infra/capabilities" "github.com/formancehq/ledger/v3/internal/infra/membership" "github.com/formancehq/ledger/v3/internal/infra/state" "github.com/formancehq/ledger/v3/internal/pkg/futures" @@ -2448,7 +2449,11 @@ func (node *Node) handleProposal(p *Proposal) { } } if err == nil { - err = node.rawNode.Propose(p.data) + if validationErr := capabilities.ValidateFinalProposal(p.data, p.declaredCapabilities); validationErr != nil { + err = validationErr + } else { + err = node.rawNode.Propose(p.data) + } } if err != nil { // The IndexTracker was optimistically incremented in Propose(). diff --git a/internal/infra/node/proposal.go b/internal/infra/node/proposal.go index 96f913fce7..90fe9cef4c 100644 --- a/internal/infra/node/proposal.go +++ b/internal/infra/node/proposal.go @@ -1,6 +1,11 @@ package node -import "github.com/formancehq/ledger/v3/internal/pkg/futures" +import ( + "slices" + + "github.com/formancehq/ledger/v3/internal/infra/capabilities" + "github.com/formancehq/ledger/v3/internal/pkg/futures" +) type Proposal struct { *futures.Future[any] @@ -12,6 +17,17 @@ type Proposal struct { membershipBarrier bool // expectedTerm fences a drain preflight against a later leadership term. expectedTerm uint64 + declaredCapabilities []capabilities.ID + +// RequireCapabilities records the producer declaration that must match the +// final serialized proposal. It is intentionally separate from the bytes so a +// plan rebuild cannot silently retain stale capability metadata. +func (p *Proposal) RequireCapabilities(ids ...capabilities.ID) { + p.declaredCapabilities = slices.Clone(ids) +} + +func (p *Proposal) DeclaredCapabilities() []capabilities.ID { + return slices.Clone(p.declaredCapabilities) } func NewProposal(commandID uint64, data []byte) *Proposal { diff --git a/internal/proto/auditpb/audit.pb.go b/internal/proto/auditpb/audit.pb.go index ffa8870b4c..898ca0222e 100644 --- a/internal/proto/auditpb/audit.pb.go +++ b/internal/proto/auditpb/audit.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.12 -// protoc v7.34.2 +// protoc v7.36.2 // source: audit.proto package auditpb @@ -47,9 +47,7 @@ type AuditEntry struct { Ledgers []string `protobuf:"bytes,8,rep,name=ledgers,proto3" json:"ledgers,omitempty"` // Batch-level integrity hash chaining audit entries. // Formula: H(key, header_payload || concat(per_item_payload) || - // - // previous_audit_hash) - // + // previous_audit_hash) // where: // - `key` is derived from the once-committed random audit secret via // domain-separated BLAKE3, independently of the operational cluster ID @@ -64,7 +62,6 @@ type AuditEntry struct { // via state.BuildPerItemPayload // - `previous_audit_hash` is `hash` of the predecessor entry, or empty for // the first entry - // // Same formula for success and failure entries. The hash binds every field // of AuditEntry and AuditItem except `hash` itself. Tampering with any // other field is detected by checker.verifyAuditHashChain. diff --git a/internal/proto/auditpb/audit_vtproto.pb.go b/internal/proto/auditpb/audit_vtproto.pb.go index f2f0abab4b..92a855946e 100644 --- a/internal/proto/auditpb/audit_vtproto.pb.go +++ b/internal/proto/auditpb/audit_vtproto.pb.go @@ -1,5 +1,5 @@ // Code generated by protoc-gen-go-vtproto. DO NOT EDIT. -// protoc-gen-go-vtproto version: (devel) +// protoc-gen-go-vtproto version: v0.6.1-0.20240319094008-0393e58bdf10 // source: audit.proto package auditpb @@ -488,6 +488,10 @@ func (m *AuditEntry_Success) MarshalToSizedBufferVT(dAtA []byte) (int, error) { i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x22 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x22 } return len(dAtA) - i, nil } @@ -507,6 +511,10 @@ func (m *AuditEntry_Failure) MarshalToSizedBufferVT(dAtA []byte) (int, error) { i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x2a + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x2a } return len(dAtA) - i, nil } @@ -744,6 +752,8 @@ func (m *AuditEntry_Success) SizeVT() (n int) { if m.Success != nil { l = m.Success.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -756,6 +766,8 @@ func (m *AuditEntry_Failure) SizeVT() (n int) { if m.Failure != nil { l = m.Failure.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } diff --git a/internal/proto/clusterbootstrappb/cluster_bootstrap.pb.go b/internal/proto/clusterbootstrappb/cluster_bootstrap.pb.go index 9677fa02bb..ea77e2c28a 100644 --- a/internal/proto/clusterbootstrappb/cluster_bootstrap.pb.go +++ b/internal/proto/clusterbootstrappb/cluster_bootstrap.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.12 -// protoc v7.34.2 +// protoc v7.36.2 // source: cluster_bootstrap.proto package clusterbootstrappb diff --git a/internal/proto/clusterbootstrappb/cluster_bootstrap_grpc.pb.go b/internal/proto/clusterbootstrappb/cluster_bootstrap_grpc.pb.go index e4fe414fd1..106cb82b83 100644 --- a/internal/proto/clusterbootstrappb/cluster_bootstrap_grpc.pb.go +++ b/internal/proto/clusterbootstrappb/cluster_bootstrap_grpc.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: // - protoc-gen-go-grpc v1.6.2 -// - protoc v7.34.2 +// - protoc v7.36.2 // source: cluster_bootstrap.proto package clusterbootstrappb diff --git a/internal/proto/clusterbootstrappb/cluster_bootstrap_vtproto.pb.go b/internal/proto/clusterbootstrappb/cluster_bootstrap_vtproto.pb.go index 7487310391..e2cb702f6a 100644 --- a/internal/proto/clusterbootstrappb/cluster_bootstrap_vtproto.pb.go +++ b/internal/proto/clusterbootstrappb/cluster_bootstrap_vtproto.pb.go @@ -1,5 +1,5 @@ // Code generated by protoc-gen-go-vtproto. DO NOT EDIT. -// protoc-gen-go-vtproto version: (devel) +// protoc-gen-go-vtproto version: v0.6.1-0.20240319094008-0393e58bdf10 // source: cluster_bootstrap.proto package clusterbootstrappb diff --git a/internal/proto/clusterpb/cluster.pb.go b/internal/proto/clusterpb/cluster.pb.go index 1de4622a87..290fa17735 100644 --- a/internal/proto/clusterpb/cluster.pb.go +++ b/internal/proto/clusterpb/cluster.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.12 -// protoc v7.34.2 +// protoc v7.36.2 // source: cluster.proto package clusterpb diff --git a/internal/proto/clusterpb/cluster_grpc.pb.go b/internal/proto/clusterpb/cluster_grpc.pb.go index ee73c7400e..16b45bd11c 100644 --- a/internal/proto/clusterpb/cluster_grpc.pb.go +++ b/internal/proto/clusterpb/cluster_grpc.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: // - protoc-gen-go-grpc v1.6.2 -// - protoc v7.34.2 +// - protoc v7.36.2 // source: cluster.proto package clusterpb diff --git a/internal/proto/clusterpb/cluster_vtproto.pb.go b/internal/proto/clusterpb/cluster_vtproto.pb.go index 08ec947843..9d4422760a 100644 --- a/internal/proto/clusterpb/cluster_vtproto.pb.go +++ b/internal/proto/clusterpb/cluster_vtproto.pb.go @@ -1,5 +1,5 @@ // Code generated by protoc-gen-go-vtproto. DO NOT EDIT. -// protoc-gen-go-vtproto version: (devel) +// protoc-gen-go-vtproto version: v0.6.1-0.20240319094008-0393e58bdf10 // source: cluster.proto package clusterpb diff --git a/internal/proto/commonpb/common_dethash.pb.go b/internal/proto/commonpb/common_dethash.pb.go index 608b704f1e..1ded8fcee8 100644 --- a/internal/proto/commonpb/common_dethash.pb.go +++ b/internal/proto/commonpb/common_dethash.pb.go @@ -4962,3 +4962,14 @@ func (m *ListOptions) MarshalDeterministicVT(dAtA []byte) []byte { } return append(dAtA, b...) } + +func (m *CapabilityActivation) MarshalDeterministicVT(dAtA []byte) []byte { + if m == nil { + return dAtA + } + b, err := m.MarshalVT() + if err != nil { + panic("MarshalDeterministicVT: " + err.Error()) + } + return append(dAtA, b...) +} diff --git a/internal/proto/commonpb/common_reader.pb.go b/internal/proto/commonpb/common_reader.pb.go index 130514c3e5..8d7082efa5 100644 --- a/internal/proto/commonpb/common_reader.pb.go +++ b/internal/proto/commonpb/common_reader.pb.go @@ -3522,6 +3522,11 @@ type ClusterPolicyReader interface { GetMetadataMaxValueBytes() uint64 GetMetadataMaxEntityBytes() uint64 GetMetadataMaxCommandBytes() uint64 + GetCapabilityActivationArmed() bool + GetCapabilityAdmissionPolicy() CapabilityAdmissionPolicy + GetMinimumReplayProtocol() uint32 + GetCapabilityMembershipRevision() uint64 + GetCapabilityActivations() CapabilityActivationListReader Mutate() *ClusterPolicy } @@ -3559,6 +3564,26 @@ func (r *clusterPolicyReadonly) GetMetadataMaxCommandBytes() uint64 { return (*ClusterPolicy)(r).GetMetadataMaxCommandBytes() } +func (r *clusterPolicyReadonly) GetCapabilityActivationArmed() bool { + return (*ClusterPolicy)(r).GetCapabilityActivationArmed() +} + +func (r *clusterPolicyReadonly) GetCapabilityAdmissionPolicy() CapabilityAdmissionPolicy { + return (*ClusterPolicy)(r).GetCapabilityAdmissionPolicy() +} + +func (r *clusterPolicyReadonly) GetMinimumReplayProtocol() uint32 { + return (*ClusterPolicy)(r).GetMinimumReplayProtocol() +} + +func (r *clusterPolicyReadonly) GetCapabilityMembershipRevision() uint64 { + return (*ClusterPolicy)(r).GetCapabilityMembershipRevision() +} + +func (r *clusterPolicyReadonly) GetCapabilityActivations() CapabilityActivationListReader { + return NewCapabilityActivationListReader((*ClusterPolicy)(r).GetCapabilityActivations()) +} + func (r *clusterPolicyReadonly) Mutate() *ClusterPolicy { return (*ClusterPolicy)(r).CloneVT() } @@ -23051,3 +23076,75 @@ func (l listOptionsListReadonly) Range(yield func(int, ListOptionsReader) bool) func NewListOptionsListReader(s []*ListOptions) ListOptionsListReader { return listOptionsListReadonly(s) } + +// CapabilityActivationReader provides read-only access to CapabilityActivation. +// Call Mutate() to obtain a mutable clone. +type CapabilityActivationReader interface { + GetId() uint32 + GetActive() bool + Mutate() *CapabilityActivation +} + +type capabilityActivationReadonly CapabilityActivation + +func (r *capabilityActivationReadonly) GetId() uint32 { + return (*CapabilityActivation)(r).GetId() +} + +func (r *capabilityActivationReadonly) GetActive() bool { + return (*CapabilityActivation)(r).GetActive() +} + +func (r *capabilityActivationReadonly) Mutate() *CapabilityActivation { + return (*CapabilityActivation)(r).CloneVT() +} + +// AsReader returns a read-only view of this CapabilityActivation. +func (m *CapabilityActivation) AsReader() CapabilityActivationReader { + if m == nil { + return nil + } + return (*capabilityActivationReadonly)(m) +} + +// Mutate returns a mutable deep clone of this CapabilityActivation. +func (m *CapabilityActivation) Mutate() *CapabilityActivation { + return m.CloneVT() +} + +// CapabilityActivationListReader provides read-only iteration over []*CapabilityActivation. +type CapabilityActivationListReader interface { + Len() int + Get(i int) CapabilityActivationReader + Range(yield func(int, CapabilityActivationReader) bool) +} + +type capabilityActivationListReadonly []*CapabilityActivation + +func (l capabilityActivationListReadonly) Len() int { return len(l) } + +func (l capabilityActivationListReadonly) Get(i int) CapabilityActivationReader { + v := l[i] + if v == nil { + return nil + } + return v.AsReader() +} + +func (l capabilityActivationListReadonly) Range(yield func(int, CapabilityActivationReader) bool) { + for i, v := range l { + var r CapabilityActivationReader + if v != nil { + r = v.AsReader() + } + if !yield(i, r) { + return + } + } +} + +// NewCapabilityActivationListReader wraps s for read-only iteration. The returned +// view aliases the underlying slice; do not mutate s afterwards. +func NewCapabilityActivationListReader(s []*CapabilityActivation) CapabilityActivationListReader { + return capabilityActivationListReadonly(s) +} diff --git a/internal/proto/commonpb/common_vtproto.pb.go b/internal/proto/commonpb/common_vtproto.pb.go index 94fb2aeb76..f1cb44a7ed 100644 --- a/internal/proto/commonpb/common_vtproto.pb.go +++ b/internal/proto/commonpb/common_vtproto.pb.go @@ -1,5 +1,5 @@ // Code generated by protoc-gen-go-vtproto. DO NOT EDIT. -// protoc-gen-go-vtproto version: (devel) +// protoc-gen-go-vtproto version: v0.6.1-0.20240319094008-0393e58bdf10 // source: common.proto package commonpb @@ -1273,6 +1273,17 @@ func (m *ClusterPolicy) CloneVT() *ClusterPolicy { r.MetadataMaxValueBytes = m.MetadataMaxValueBytes r.MetadataMaxEntityBytes = m.MetadataMaxEntityBytes r.MetadataMaxCommandBytes = m.MetadataMaxCommandBytes + r.CapabilityActivationArmed = m.CapabilityActivationArmed + r.CapabilityAdmissionPolicy = m.CapabilityAdmissionPolicy + r.MinimumReplayProtocol = m.MinimumReplayProtocol + r.CapabilityMembershipRevision = m.CapabilityMembershipRevision + if rhs := m.CapabilityActivations; rhs != nil { + tmpContainer := make([]*CapabilityActivation, len(rhs)) + for k, v := range rhs { + tmpContainer[k] = v.CloneVT() + } + r.CapabilityActivations = tmpContainer + } if len(m.unknownFields) > 0 { r.unknownFields = make([]byte, len(m.unknownFields)) copy(r.unknownFields, m.unknownFields) @@ -8968,6 +8979,24 @@ func (m *ListOptions) CloneMessageVT() proto.Message { return m.CloneVT() } +func (m *CapabilityActivation) CloneVT() *CapabilityActivation { + if m == nil { + return (*CapabilityActivation)(nil) + } + r := new(CapabilityActivation) + r.Id = m.Id + r.Active = m.Active + if len(m.unknownFields) > 0 { + r.unknownFields = make([]byte, len(m.unknownFields)) + copy(r.unknownFields, m.unknownFields) + } + return r +} + +func (m *CapabilityActivation) CloneMessageVT() proto.Message { + return m.CloneVT() +} + func (this *MethodAuthPolicy) EqualVT(that *MethodAuthPolicy) bool { if this == that { return true @@ -11084,6 +11113,35 @@ func (this *ClusterPolicy) EqualVT(that *ClusterPolicy) bool { if this.MetadataMaxCommandBytes != that.MetadataMaxCommandBytes { return false } + if this.CapabilityActivationArmed != that.CapabilityActivationArmed { + return false + } + if this.CapabilityAdmissionPolicy != that.CapabilityAdmissionPolicy { + return false + } + if this.MinimumReplayProtocol != that.MinimumReplayProtocol { + return false + } + if this.CapabilityMembershipRevision != that.CapabilityMembershipRevision { + return false + } + if len(this.CapabilityActivations) != len(that.CapabilityActivations) { + return false + } + for i, vx := range this.CapabilityActivations { + vy := that.CapabilityActivations[i] + if p, q := vx, vy; p != q { + if p == nil { + p = &CapabilityActivation{} + } + if q == nil { + q = &CapabilityActivation{} + } + if !p.EqualVT(q) { + return false + } + } + } return string(this.unknownFields) == string(that.unknownFields) } @@ -23300,6 +23358,28 @@ func (this *ListOptions) EqualMessageVT(thatMsg proto.Message) bool { } return this.EqualVT(that) } +func (this *CapabilityActivation) EqualVT(that *CapabilityActivation) bool { + if this == that { + return true + } else if this == nil || that == nil { + return false + } + if this.Id != that.Id { + return false + } + if this.Active != that.Active { + return false + } + return string(this.unknownFields) == string(that.unknownFields) +} + +func (this *CapabilityActivation) EqualMessageVT(thatMsg proto.Message) bool { + that, ok := thatMsg.(*CapabilityActivation) + if !ok { + return false + } + return this.EqualVT(that) +} func (m *MethodAuthPolicy) MarshalVT() (dAtA []byte, err error) { if m == nil { return nil, nil @@ -23563,6 +23643,10 @@ func (m *MetadataValue_NullValue) MarshalToSizedBufferVT(dAtA []byte) (int, erro i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x22 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x22 } return len(dAtA) - i, nil } @@ -24713,6 +24797,10 @@ func (m *Target_Account) MarshalToSizedBufferVT(dAtA []byte) (int, error) { i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0xa + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0xa } return len(dAtA) - i, nil } @@ -25055,6 +25143,10 @@ func (m *IndexID_Metadata) MarshalToSizedBufferVT(dAtA []byte) (int, error) { i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x22 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x22 } return len(dAtA) - i, nil } @@ -25286,6 +25378,10 @@ func (m *LogPayload_CreateLedger) MarshalToSizedBufferVT(dAtA []byte) (int, erro i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0xa + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0xa } return len(dAtA) - i, nil } @@ -25305,6 +25401,10 @@ func (m *LogPayload_DeleteLedger) MarshalToSizedBufferVT(dAtA []byte) (int, erro i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x12 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x12 } return len(dAtA) - i, nil } @@ -25324,6 +25424,10 @@ func (m *LogPayload_Apply) MarshalToSizedBufferVT(dAtA []byte) (int, error) { i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x1a + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x1a } return len(dAtA) - i, nil } @@ -25343,6 +25447,10 @@ func (m *LogPayload_RegisterSigningKey) MarshalToSizedBufferVT(dAtA []byte) (int i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x22 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x22 } return len(dAtA) - i, nil } @@ -25362,6 +25470,10 @@ func (m *LogPayload_RevokeSigningKey) MarshalToSizedBufferVT(dAtA []byte) (int, i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x2a + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x2a } return len(dAtA) - i, nil } @@ -25381,6 +25493,10 @@ func (m *LogPayload_SetSigningConfig) MarshalToSizedBufferVT(dAtA []byte) (int, i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x32 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x32 } return len(dAtA) - i, nil } @@ -25400,6 +25516,10 @@ func (m *LogPayload_AddedEventsSink) MarshalToSizedBufferVT(dAtA []byte) (int, e i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x3a + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x3a } return len(dAtA) - i, nil } @@ -25419,6 +25539,10 @@ func (m *LogPayload_RemovedEventsSink) MarshalToSizedBufferVT(dAtA []byte) (int, i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x42 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x42 } return len(dAtA) - i, nil } @@ -25438,6 +25562,10 @@ func (m *LogPayload_SetMaintenanceMode) MarshalToSizedBufferVT(dAtA []byte) (int i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x4a + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x4a } return len(dAtA) - i, nil } @@ -25457,6 +25585,10 @@ func (m *LogPayload_PromoteLedger) MarshalToSizedBufferVT(dAtA []byte) (int, err i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x52 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x52 } return len(dAtA) - i, nil } @@ -25476,6 +25608,10 @@ func (m *LogPayload_CreatedPreparedQuery) MarshalToSizedBufferVT(dAtA []byte) (i i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x5a + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x5a } return len(dAtA) - i, nil } @@ -25495,6 +25631,10 @@ func (m *LogPayload_UpdatedPreparedQuery) MarshalToSizedBufferVT(dAtA []byte) (i i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x62 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x62 } return len(dAtA) - i, nil } @@ -25514,6 +25654,10 @@ func (m *LogPayload_DeletedPreparedQuery) MarshalToSizedBufferVT(dAtA []byte) (i i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x6a + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x6a } return len(dAtA) - i, nil } @@ -25533,6 +25677,10 @@ func (m *LogPayload_SavedNumscript) MarshalToSizedBufferVT(dAtA []byte) (int, er i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x72 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x72 } return len(dAtA) - i, nil } @@ -25552,6 +25700,10 @@ func (m *LogPayload_CreatedQueryCheckpoint) MarshalToSizedBufferVT(dAtA []byte) i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x7a + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x7a } return len(dAtA) - i, nil } @@ -25573,6 +25725,12 @@ func (m *LogPayload_DeletedQueryCheckpoint) MarshalToSizedBufferVT(dAtA []byte) dAtA[i] = 0x1 i-- dAtA[i] = 0x82 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x1 + i-- + dAtA[i] = 0x82 } return len(dAtA) - i, nil } @@ -25594,6 +25752,12 @@ func (m *LogPayload_SetQueryCheckpointSchedule) MarshalToSizedBufferVT(dAtA []by dAtA[i] = 0x1 i-- dAtA[i] = 0x8a + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x1 + i-- + dAtA[i] = 0x8a } return len(dAtA) - i, nil } @@ -25615,6 +25779,12 @@ func (m *LogPayload_DeleteQueryCheckpointSchedule) MarshalToSizedBufferVT(dAtA [ dAtA[i] = 0x1 i-- dAtA[i] = 0x92 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x1 + i-- + dAtA[i] = 0x92 } return len(dAtA) - i, nil } @@ -25636,6 +25806,12 @@ func (m *LogPayload_SavedLedgerMetadata) MarshalToSizedBufferVT(dAtA []byte) (in dAtA[i] = 0x1 i-- dAtA[i] = 0x9a + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x1 + i-- + dAtA[i] = 0x9a } return len(dAtA) - i, nil } @@ -25657,6 +25833,12 @@ func (m *LogPayload_DeletedLedgerMetadata) MarshalToSizedBufferVT(dAtA []byte) ( dAtA[i] = 0x1 i-- dAtA[i] = 0xa2 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x1 + i-- + dAtA[i] = 0xa2 } return len(dAtA) - i, nil } @@ -25678,6 +25860,12 @@ func (m *LogPayload_SetClusterPolicy) MarshalToSizedBufferVT(dAtA []byte) (int, dAtA[i] = 0x1 i-- dAtA[i] = 0xaa + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x1 + i-- + dAtA[i] = 0xaa } return len(dAtA) - i, nil } @@ -26376,6 +26564,44 @@ func (m *ClusterPolicy) MarshalToSizedBufferVT(dAtA []byte) (int, error) { i -= len(m.unknownFields) copy(dAtA[i:], m.unknownFields) } + if len(m.CapabilityActivations) > 0 { + for iNdEx := len(m.CapabilityActivations) - 1; iNdEx >= 0; iNdEx-- { + size, err := m.CapabilityActivations[iNdEx].MarshalToSizedBufferVT(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) + i-- + dAtA[i] = 0x6a + } + } + if m.CapabilityMembershipRevision != 0 { + i -= 8 + binary.LittleEndian.PutUint64(dAtA[i:], uint64(m.CapabilityMembershipRevision)) + i-- + dAtA[i] = 0x61 + } + if m.MinimumReplayProtocol != 0 { + i = protohelpers.EncodeVarint(dAtA, i, uint64(m.MinimumReplayProtocol)) + i-- + dAtA[i] = 0x58 + } + if m.CapabilityAdmissionPolicy != 0 { + i = protohelpers.EncodeVarint(dAtA, i, uint64(m.CapabilityAdmissionPolicy)) + i-- + dAtA[i] = 0x50 + } + if m.CapabilityActivationArmed { + i-- + if m.CapabilityActivationArmed { + dAtA[i] = 1 + } else { + dAtA[i] = 0 + } + i-- + dAtA[i] = 0x48 + } if m.MetadataMaxCommandBytes != 0 { i -= 8 binary.LittleEndian.PutUint64(dAtA[i:], uint64(m.MetadataMaxCommandBytes)) @@ -27206,6 +27432,10 @@ func (m *SinkConfig_Nats) MarshalToSizedBufferVT(dAtA []byte) (int, error) { i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x12 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x12 } return len(dAtA) - i, nil } @@ -27225,6 +27455,10 @@ func (m *SinkConfig_Clickhouse) MarshalToSizedBufferVT(dAtA []byte) (int, error) i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x1a + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x1a } return len(dAtA) - i, nil } @@ -27244,6 +27478,10 @@ func (m *SinkConfig_Kafka) MarshalToSizedBufferVT(dAtA []byte) (int, error) { i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x22 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x22 } return len(dAtA) - i, nil } @@ -27263,6 +27501,10 @@ func (m *SinkConfig_Http) MarshalToSizedBufferVT(dAtA []byte) (int, error) { i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x2a + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x2a } return len(dAtA) - i, nil } @@ -27282,6 +27524,10 @@ func (m *SinkConfig_Databricks) MarshalToSizedBufferVT(dAtA []byte) (int, error) i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x32 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x32 } return len(dAtA) - i, nil } @@ -27724,6 +27970,10 @@ func (m *DatabricksSinkConfig_OauthM2M) MarshalToSizedBufferVT(dAtA []byte) (int i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x42 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x42 } return len(dAtA) - i, nil } @@ -28210,6 +28460,10 @@ func (m *LedgerLogPayload_CreatedTransaction) MarshalToSizedBufferVT(dAtA []byte i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0xa + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0xa } return len(dAtA) - i, nil } @@ -28229,6 +28483,10 @@ func (m *LedgerLogPayload_RevertedTransaction) MarshalToSizedBufferVT(dAtA []byt i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x12 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x12 } return len(dAtA) - i, nil } @@ -28248,6 +28506,10 @@ func (m *LedgerLogPayload_SavedMetadata) MarshalToSizedBufferVT(dAtA []byte) (in i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x1a + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x1a } return len(dAtA) - i, nil } @@ -28267,6 +28529,10 @@ func (m *LedgerLogPayload_DeletedMetadata) MarshalToSizedBufferVT(dAtA []byte) ( i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x22 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x22 } return len(dAtA) - i, nil } @@ -28286,6 +28552,10 @@ func (m *LedgerLogPayload_SetMetadataFieldType) MarshalToSizedBufferVT(dAtA []by i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x2a + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x2a } return len(dAtA) - i, nil } @@ -28305,6 +28575,10 @@ func (m *LedgerLogPayload_RemovedMetadataFieldType) MarshalToSizedBufferVT(dAtA i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x32 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x32 } return len(dAtA) - i, nil } @@ -28324,6 +28598,10 @@ func (m *LedgerLogPayload_FillGap) MarshalToSizedBufferVT(dAtA []byte) (int, err i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x3a + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x3a } return len(dAtA) - i, nil } @@ -28343,6 +28621,10 @@ func (m *LedgerLogPayload_CreateIndex) MarshalToSizedBufferVT(dAtA []byte) (int, i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x42 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x42 } return len(dAtA) - i, nil } @@ -28362,6 +28644,10 @@ func (m *LedgerLogPayload_DropIndex) MarshalToSizedBufferVT(dAtA []byte) (int, e i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x4a + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x4a } return len(dAtA) - i, nil } @@ -28381,6 +28667,10 @@ func (m *LedgerLogPayload_AddedAccountType) MarshalToSizedBufferVT(dAtA []byte) i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x52 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x52 } return len(dAtA) - i, nil } @@ -28400,6 +28690,10 @@ func (m *LedgerLogPayload_RemovedAccountType) MarshalToSizedBufferVT(dAtA []byte i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x5a + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x5a } return len(dAtA) - i, nil } @@ -28419,6 +28713,10 @@ func (m *LedgerLogPayload_UpdatedDefaultEnforcementMode) MarshalToSizedBufferVT( i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x62 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x62 } return len(dAtA) - i, nil } @@ -28438,6 +28736,10 @@ func (m *LedgerLogPayload_OrderSkipped) MarshalToSizedBufferVT(dAtA []byte) (int i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x6a + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x6a } return len(dAtA) - i, nil } @@ -28499,6 +28801,10 @@ func (m *FailureDetails_AccountAddressEmpty) MarshalToSizedBufferVT(dAtA []byte) i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0xa + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0xa } return len(dAtA) - i, nil } @@ -28518,6 +28824,10 @@ func (m *FailureDetails_AccountAddressEmptySegment) MarshalToSizedBufferVT(dAtA i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x12 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x12 } return len(dAtA) - i, nil } @@ -28537,6 +28847,10 @@ func (m *FailureDetails_AccountAddressInvalidChar) MarshalToSizedBufferVT(dAtA [ i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x1a + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x1a } return len(dAtA) - i, nil } @@ -28556,6 +28870,10 @@ func (m *FailureDetails_AccountAddressTooLong) MarshalToSizedBufferVT(dAtA []byt i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x22 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x22 } return len(dAtA) - i, nil } @@ -28575,6 +28893,10 @@ func (m *FailureDetails_AccountNotMatchingType) MarshalToSizedBufferVT(dAtA []by i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x2a + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x2a } return len(dAtA) - i, nil } @@ -28594,6 +28916,10 @@ func (m *FailureDetails_AccountTypeAlreadyExists) MarshalToSizedBufferVT(dAtA [] i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x32 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x32 } return len(dAtA) - i, nil } @@ -28613,6 +28939,10 @@ func (m *FailureDetails_AccountTypeConflict) MarshalToSizedBufferVT(dAtA []byte) i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x3a + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x3a } return len(dAtA) - i, nil } @@ -28632,6 +28962,10 @@ func (m *FailureDetails_AccountTypeHasAccounts) MarshalToSizedBufferVT(dAtA []by i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x42 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x42 } return len(dAtA) - i, nil } @@ -28651,6 +28985,10 @@ func (m *FailureDetails_AccountTypeNameTooLong) MarshalToSizedBufferVT(dAtA []by i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x4a + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x4a } return len(dAtA) - i, nil } @@ -28670,6 +29008,10 @@ func (m *FailureDetails_AccountTypeNotFound) MarshalToSizedBufferVT(dAtA []byte) i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x52 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x52 } return len(dAtA) - i, nil } @@ -28689,6 +29031,10 @@ func (m *FailureDetails_AccountTypePatternTooLong) MarshalToSizedBufferVT(dAtA [ i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x5a + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x5a } return len(dAtA) - i, nil } @@ -28708,6 +29054,10 @@ func (m *FailureDetails_AssetInvalid) MarshalToSizedBufferVT(dAtA []byte) (int, i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x62 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x62 } return len(dAtA) - i, nil } @@ -28727,6 +29077,10 @@ func (m *FailureDetails_BalanceNotFound) MarshalToSizedBufferVT(dAtA []byte) (in i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x6a + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x6a } return len(dAtA) - i, nil } @@ -28746,6 +29100,10 @@ func (m *FailureDetails_CheckpointIdRequired) MarshalToSizedBufferVT(dAtA []byte i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x72 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x72 } return len(dAtA) - i, nil } @@ -28765,6 +29123,10 @@ func (m *FailureDetails_CheckpointLimitReached) MarshalToSizedBufferVT(dAtA []by i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x7a + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x7a } return len(dAtA) - i, nil } @@ -28786,6 +29148,12 @@ func (m *FailureDetails_CheckpointNotFound) MarshalToSizedBufferVT(dAtA []byte) dAtA[i] = 0x1 i-- dAtA[i] = 0x82 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x1 + i-- + dAtA[i] = 0x82 } return len(dAtA) - i, nil } @@ -28807,6 +29175,12 @@ func (m *FailureDetails_ColorInvalid) MarshalToSizedBufferVT(dAtA []byte) (int, dAtA[i] = 0x1 i-- dAtA[i] = 0x8a + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x1 + i-- + dAtA[i] = 0x8a } return len(dAtA) - i, nil } @@ -28828,6 +29202,12 @@ func (m *FailureDetails_ColorTooLong) MarshalToSizedBufferVT(dAtA []byte) (int, dAtA[i] = 0x1 i-- dAtA[i] = 0x92 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x1 + i-- + dAtA[i] = 0x92 } return len(dAtA) - i, nil } @@ -28849,6 +29229,12 @@ func (m *FailureDetails_EmptyTransaction) MarshalToSizedBufferVT(dAtA []byte) (i dAtA[i] = 0x1 i-- dAtA[i] = 0x9a + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x1 + i-- + dAtA[i] = 0x9a } return len(dAtA) - i, nil } @@ -28870,6 +29256,12 @@ func (m *FailureDetails_IdempotencyKeyConflict) MarshalToSizedBufferVT(dAtA []by dAtA[i] = 0x1 i-- dAtA[i] = 0xa2 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x1 + i-- + dAtA[i] = 0xa2 } return len(dAtA) - i, nil } @@ -28891,6 +29283,12 @@ func (m *FailureDetails_IdRequired) MarshalToSizedBufferVT(dAtA []byte) (int, er dAtA[i] = 0x1 i-- dAtA[i] = 0xaa + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x1 + i-- + dAtA[i] = 0xaa } return len(dAtA) - i, nil } @@ -28912,6 +29310,12 @@ func (m *FailureDetails_IndexAlreadyExists) MarshalToSizedBufferVT(dAtA []byte) dAtA[i] = 0x1 i-- dAtA[i] = 0xb2 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x1 + i-- + dAtA[i] = 0xb2 } return len(dAtA) - i, nil } @@ -28933,6 +29337,12 @@ func (m *FailureDetails_IndexNotFound) MarshalToSizedBufferVT(dAtA []byte) (int, dAtA[i] = 0x1 i-- dAtA[i] = 0xba + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x1 + i-- + dAtA[i] = 0xba } return len(dAtA) - i, nil } @@ -28954,6 +29364,12 @@ func (m *FailureDetails_IndexTargetUnsupported) MarshalToSizedBufferVT(dAtA []by dAtA[i] = 0x1 i-- dAtA[i] = 0xc2 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x1 + i-- + dAtA[i] = 0xc2 } return len(dAtA) - i, nil } @@ -28975,6 +29391,12 @@ func (m *FailureDetails_InsufficientFunds) MarshalToSizedBufferVT(dAtA []byte) ( dAtA[i] = 0x1 i-- dAtA[i] = 0xca + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x1 + i-- + dAtA[i] = 0xca } return len(dAtA) - i, nil } @@ -28996,6 +29418,12 @@ func (m *FailureDetails_InvalidCronExpression) MarshalToSizedBufferVT(dAtA []byt dAtA[i] = 0x1 i-- dAtA[i] = 0xd2 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x1 + i-- + dAtA[i] = 0xd2 } return len(dAtA) - i, nil } @@ -29017,6 +29445,12 @@ func (m *FailureDetails_InvalidPattern) MarshalToSizedBufferVT(dAtA []byte) (int dAtA[i] = 0x1 i-- dAtA[i] = 0xda + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x1 + i-- + dAtA[i] = 0xda } return len(dAtA) - i, nil } @@ -29038,6 +29472,12 @@ func (m *FailureDetails_InvalidSkippableReason) MarshalToSizedBufferVT(dAtA []by dAtA[i] = 0x1 i-- dAtA[i] = 0xe2 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x1 + i-- + dAtA[i] = 0xe2 } return len(dAtA) - i, nil } @@ -29059,6 +29499,12 @@ func (m *FailureDetails_LedgerAlreadyExists) MarshalToSizedBufferVT(dAtA []byte) dAtA[i] = 0x1 i-- dAtA[i] = 0xea + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x1 + i-- + dAtA[i] = 0xea } return len(dAtA) - i, nil } @@ -29080,6 +29526,12 @@ func (m *FailureDetails_LedgerDeleted) MarshalToSizedBufferVT(dAtA []byte) (int, dAtA[i] = 0x1 i-- dAtA[i] = 0xf2 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x1 + i-- + dAtA[i] = 0xf2 } return len(dAtA) - i, nil } @@ -29101,6 +29553,12 @@ func (m *FailureDetails_LedgerInMirrorMode) MarshalToSizedBufferVT(dAtA []byte) dAtA[i] = 0x1 i-- dAtA[i] = 0xfa + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x1 + i-- + dAtA[i] = 0xfa } return len(dAtA) - i, nil } @@ -29122,6 +29580,12 @@ func (m *FailureDetails_LedgerNameInvalidChar) MarshalToSizedBufferVT(dAtA []byt dAtA[i] = 0x2 i-- dAtA[i] = 0x82 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x2 + i-- + dAtA[i] = 0x82 } return len(dAtA) - i, nil } @@ -29143,6 +29607,12 @@ func (m *FailureDetails_LedgerNameRequired) MarshalToSizedBufferVT(dAtA []byte) dAtA[i] = 0x2 i-- dAtA[i] = 0x8a + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x2 + i-- + dAtA[i] = 0x8a } return len(dAtA) - i, nil } @@ -29164,6 +29634,12 @@ func (m *FailureDetails_LedgerNameReservedPrefix) MarshalToSizedBufferVT(dAtA [] dAtA[i] = 0x2 i-- dAtA[i] = 0x92 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x2 + i-- + dAtA[i] = 0x92 } return len(dAtA) - i, nil } @@ -29185,6 +29661,12 @@ func (m *FailureDetails_LedgerNameTooLong) MarshalToSizedBufferVT(dAtA []byte) ( dAtA[i] = 0x2 i-- dAtA[i] = 0x9a + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x2 + i-- + dAtA[i] = 0x9a } return len(dAtA) - i, nil } @@ -29206,6 +29688,12 @@ func (m *FailureDetails_LedgerNotFound) MarshalToSizedBufferVT(dAtA []byte) (int dAtA[i] = 0x2 i-- dAtA[i] = 0xa2 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x2 + i-- + dAtA[i] = 0xa2 } return len(dAtA) - i, nil } @@ -29227,6 +29715,12 @@ func (m *FailureDetails_LedgerNotInMirrorMode) MarshalToSizedBufferVT(dAtA []byt dAtA[i] = 0x2 i-- dAtA[i] = 0xaa + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x2 + i-- + dAtA[i] = 0xaa } return len(dAtA) - i, nil } @@ -29248,6 +29742,12 @@ func (m *FailureDetails_MetadataFieldNotInSchema) MarshalToSizedBufferVT(dAtA [] dAtA[i] = 0x2 i-- dAtA[i] = 0xb2 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x2 + i-- + dAtA[i] = 0xb2 } return len(dAtA) - i, nil } @@ -29269,6 +29769,12 @@ func (m *FailureDetails_MetadataKeyEmpty) MarshalToSizedBufferVT(dAtA []byte) (i dAtA[i] = 0x2 i-- dAtA[i] = 0xba + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x2 + i-- + dAtA[i] = 0xba } return len(dAtA) - i, nil } @@ -29290,6 +29796,12 @@ func (m *FailureDetails_MetadataKeyInvalidChar) MarshalToSizedBufferVT(dAtA []by dAtA[i] = 0x2 i-- dAtA[i] = 0xc2 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x2 + i-- + dAtA[i] = 0xc2 } return len(dAtA) - i, nil } @@ -29311,6 +29823,12 @@ func (m *FailureDetails_MetadataKeyRequired) MarshalToSizedBufferVT(dAtA []byte) dAtA[i] = 0x2 i-- dAtA[i] = 0xca + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x2 + i-- + dAtA[i] = 0xca } return len(dAtA) - i, nil } @@ -29332,6 +29850,12 @@ func (m *FailureDetails_MetadataLimitExceeded) MarshalToSizedBufferVT(dAtA []byt dAtA[i] = 0x2 i-- dAtA[i] = 0xd2 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x2 + i-- + dAtA[i] = 0xd2 } return len(dAtA) - i, nil } @@ -29353,6 +29877,12 @@ func (m *FailureDetails_MetadataNotFound) MarshalToSizedBufferVT(dAtA []byte) (i dAtA[i] = 0x2 i-- dAtA[i] = 0xda + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x2 + i-- + dAtA[i] = 0xda } return len(dAtA) - i, nil } @@ -29374,6 +29904,12 @@ func (m *FailureDetails_MetadataValueContainsNullByte) MarshalToSizedBufferVT(dA dAtA[i] = 0x2 i-- dAtA[i] = 0xe2 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x2 + i-- + dAtA[i] = 0xe2 } return len(dAtA) - i, nil } @@ -29395,6 +29931,12 @@ func (m *FailureDetails_MirrorHttpUrlInvalid) MarshalToSizedBufferVT(dAtA []byte dAtA[i] = 0x2 i-- dAtA[i] = 0xea + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x2 + i-- + dAtA[i] = 0xea } return len(dAtA) - i, nil } @@ -29416,6 +29958,12 @@ func (m *FailureDetails_MirrorIamRegionRequired) MarshalToSizedBufferVT(dAtA []b dAtA[i] = 0x2 i-- dAtA[i] = 0xf2 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x2 + i-- + dAtA[i] = 0xf2 } return len(dAtA) - i, nil } @@ -29437,6 +29985,12 @@ func (m *FailureDetails_MirrorIamRequiresTls) MarshalToSizedBufferVT(dAtA []byte dAtA[i] = 0x2 i-- dAtA[i] = 0xfa + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x2 + i-- + dAtA[i] = 0xfa } return len(dAtA) - i, nil } @@ -29458,6 +30012,12 @@ func (m *FailureDetails_MirrorRewriteRuleInvalid) MarshalToSizedBufferVT(dAtA [] dAtA[i] = 0x3 i-- dAtA[i] = 0x82 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x3 + i-- + dAtA[i] = 0x82 } return len(dAtA) - i, nil } @@ -29479,6 +30039,12 @@ func (m *FailureDetails_NumscriptAssetMismatch) MarshalToSizedBufferVT(dAtA []by dAtA[i] = 0x3 i-- dAtA[i] = 0x8a + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x3 + i-- + dAtA[i] = 0x8a } return len(dAtA) - i, nil } @@ -29500,6 +30066,12 @@ func (m *FailureDetails_NumscriptBadArity) MarshalToSizedBufferVT(dAtA []byte) ( dAtA[i] = 0x3 i-- dAtA[i] = 0x92 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x3 + i-- + dAtA[i] = 0x92 } return len(dAtA) - i, nil } @@ -29521,6 +30093,12 @@ func (m *FailureDetails_NumscriptBadMetaValue) MarshalToSizedBufferVT(dAtA []byt dAtA[i] = 0x3 i-- dAtA[i] = 0x9a + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x3 + i-- + dAtA[i] = 0x9a } return len(dAtA) - i, nil } @@ -29542,6 +30120,12 @@ func (m *FailureDetails_NumscriptBadPortion) MarshalToSizedBufferVT(dAtA []byte) dAtA[i] = 0x3 i-- dAtA[i] = 0xa2 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x3 + i-- + dAtA[i] = 0xa2 } return len(dAtA) - i, nil } @@ -29563,6 +30147,12 @@ func (m *FailureDetails_NumscriptCannotCastScopedAccount) MarshalToSizedBufferVT dAtA[i] = 0x3 i-- dAtA[i] = 0xaa + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x3 + i-- + dAtA[i] = 0xaa } return len(dAtA) - i, nil } @@ -29584,6 +30174,12 @@ func (m *FailureDetails_NumscriptCannotCastToString) MarshalToSizedBufferVT(dAtA dAtA[i] = 0x3 i-- dAtA[i] = 0xb2 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x3 + i-- + dAtA[i] = 0xb2 } return len(dAtA) - i, nil } @@ -29605,6 +30201,12 @@ func (m *FailureDetails_NumscriptCannotStoreScopedAccount) MarshalToSizedBufferV dAtA[i] = 0x3 i-- dAtA[i] = 0xba + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x3 + i-- + dAtA[i] = 0xba } return len(dAtA) - i, nil } @@ -29626,6 +30228,12 @@ func (m *FailureDetails_NumscriptCompileBadArity) MarshalToSizedBufferVT(dAtA [] dAtA[i] = 0x3 i-- dAtA[i] = 0xc2 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x3 + i-- + dAtA[i] = 0xc2 } return len(dAtA) - i, nil } @@ -29647,6 +30255,12 @@ func (m *FailureDetails_NumscriptCompileCannotCastToString) MarshalToSizedBuffer dAtA[i] = 0x3 i-- dAtA[i] = 0xca + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x3 + i-- + dAtA[i] = 0xca } return len(dAtA) - i, nil } @@ -29668,6 +30282,12 @@ func (m *FailureDetails_NumscriptCompileCannotStoreScopedAccount) MarshalToSized dAtA[i] = 0x3 i-- dAtA[i] = 0xd2 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x3 + i-- + dAtA[i] = 0xd2 } return len(dAtA) - i, nil } @@ -29689,6 +30309,12 @@ func (m *FailureDetails_NumscriptCompileDuplicateRemaining) MarshalToSizedBuffer dAtA[i] = 0x3 i-- dAtA[i] = 0xda + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x3 + i-- + dAtA[i] = 0xda } return len(dAtA) - i, nil } @@ -29710,6 +30336,12 @@ func (m *FailureDetails_NumscriptCompileDuplicateVariable) MarshalToSizedBufferV dAtA[i] = 0x3 i-- dAtA[i] = 0xe2 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x3 + i-- + dAtA[i] = 0xe2 } return len(dAtA) - i, nil } @@ -29731,6 +30363,12 @@ func (m *FailureDetails_NumscriptCompileExperimentalFeature) MarshalToSizedBuffe dAtA[i] = 0x3 i-- dAtA[i] = 0xea + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x3 + i-- + dAtA[i] = 0xea } return len(dAtA) - i, nil } @@ -29752,6 +30390,12 @@ func (m *FailureDetails_NumscriptCompileInvalidFeature) MarshalToSizedBufferVT(d dAtA[i] = 0x3 i-- dAtA[i] = 0xf2 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x3 + i-- + dAtA[i] = 0xf2 } return len(dAtA) - i, nil } @@ -29773,6 +30417,12 @@ func (m *FailureDetails_NumscriptCompileInvalidMetaPosition) MarshalToSizedBuffe dAtA[i] = 0x3 i-- dAtA[i] = 0xfa + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x3 + i-- + dAtA[i] = 0xfa } return len(dAtA) - i, nil } @@ -29794,6 +30444,12 @@ func (m *FailureDetails_NumscriptCompileInvalidType) MarshalToSizedBufferVT(dAtA dAtA[i] = 0x4 i-- dAtA[i] = 0x82 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x4 + i-- + dAtA[i] = 0x82 } return len(dAtA) - i, nil } @@ -29815,6 +30471,12 @@ func (m *FailureDetails_NumscriptCompileInvalidUncappedSource) MarshalToSizedBuf dAtA[i] = 0x4 i-- dAtA[i] = 0x8a + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x4 + i-- + dAtA[i] = 0x8a } return len(dAtA) - i, nil } @@ -29836,6 +30498,12 @@ func (m *FailureDetails_NumscriptCompileInvalidVariableValue) MarshalToSizedBuff dAtA[i] = 0x4 i-- dAtA[i] = 0x92 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x4 + i-- + dAtA[i] = 0x92 } return len(dAtA) - i, nil } @@ -29857,6 +30525,12 @@ func (m *FailureDetails_NumscriptCompileMissingVariable) MarshalToSizedBufferVT( dAtA[i] = 0x4 i-- dAtA[i] = 0x9a + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x4 + i-- + dAtA[i] = 0x9a } return len(dAtA) - i, nil } @@ -29878,6 +30552,12 @@ func (m *FailureDetails_NumscriptCompileTypeMismatch) MarshalToSizedBufferVT(dAt dAtA[i] = 0x4 i-- dAtA[i] = 0xa2 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x4 + i-- + dAtA[i] = 0xa2 } return len(dAtA) - i, nil } @@ -29899,6 +30579,12 @@ func (m *FailureDetails_NumscriptCompileUnboundVariable) MarshalToSizedBufferVT( dAtA[i] = 0x4 i-- dAtA[i] = 0xaa + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x4 + i-- + dAtA[i] = 0xaa } return len(dAtA) - i, nil } @@ -29920,6 +30606,12 @@ func (m *FailureDetails_NumscriptCompileUnknownFunction) MarshalToSizedBufferVT( dAtA[i] = 0x4 i-- dAtA[i] = 0xb2 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x4 + i-- + dAtA[i] = 0xb2 } return len(dAtA) - i, nil } @@ -29941,6 +30633,12 @@ func (m *FailureDetails_NumscriptContentRequired) MarshalToSizedBufferVT(dAtA [] dAtA[i] = 0x4 i-- dAtA[i] = 0xba + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x4 + i-- + dAtA[i] = 0xba } return len(dAtA) - i, nil } @@ -29962,6 +30660,12 @@ func (m *FailureDetails_NumscriptCurrencyMismatch) MarshalToSizedBufferVT(dAtA [ dAtA[i] = 0x4 i-- dAtA[i] = 0xc2 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x4 + i-- + dAtA[i] = 0xc2 } return len(dAtA) - i, nil } @@ -29983,6 +30687,12 @@ func (m *FailureDetails_NumscriptDivideByZero) MarshalToSizedBufferVT(dAtA []byt dAtA[i] = 0x4 i-- dAtA[i] = 0xca + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x4 + i-- + dAtA[i] = 0xca } return len(dAtA) - i, nil } @@ -30004,6 +30714,12 @@ func (m *FailureDetails_NumscriptExperimentalFeature) MarshalToSizedBufferVT(dAt dAtA[i] = 0x4 i-- dAtA[i] = 0xd2 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x4 + i-- + dAtA[i] = 0xd2 } return len(dAtA) - i, nil } @@ -30025,6 +30741,12 @@ func (m *FailureDetails_NumscriptInvalidAccountName) MarshalToSizedBufferVT(dAtA dAtA[i] = 0x4 i-- dAtA[i] = 0xda + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x4 + i-- + dAtA[i] = 0xda } return len(dAtA) - i, nil } @@ -30046,6 +30768,12 @@ func (m *FailureDetails_NumscriptInvalidAllotmentInSendAll) MarshalToSizedBuffer dAtA[i] = 0x4 i-- dAtA[i] = 0xe2 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x4 + i-- + dAtA[i] = 0xe2 } return len(dAtA) - i, nil } @@ -30067,6 +30795,12 @@ func (m *FailureDetails_NumscriptInvalidAllotmentSum) MarshalToSizedBufferVT(dAt dAtA[i] = 0x4 i-- dAtA[i] = 0xea + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x4 + i-- + dAtA[i] = 0xea } return len(dAtA) - i, nil } @@ -30088,6 +30822,12 @@ func (m *FailureDetails_NumscriptInvalidAsset) MarshalToSizedBufferVT(dAtA []byt dAtA[i] = 0x4 i-- dAtA[i] = 0xf2 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x4 + i-- + dAtA[i] = 0xf2 } return len(dAtA) - i, nil } @@ -30109,6 +30849,12 @@ func (m *FailureDetails_NumscriptInvalidColor) MarshalToSizedBufferVT(dAtA []byt dAtA[i] = 0x4 i-- dAtA[i] = 0xfa + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x4 + i-- + dAtA[i] = 0xfa } return len(dAtA) - i, nil } @@ -30130,6 +30876,12 @@ func (m *FailureDetails_NumscriptInvalidFeature) MarshalToSizedBufferVT(dAtA []b dAtA[i] = 0x5 i-- dAtA[i] = 0x82 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x5 + i-- + dAtA[i] = 0x82 } return len(dAtA) - i, nil } @@ -30151,6 +30903,12 @@ func (m *FailureDetails_NumscriptInvalidMonetaryLiteral) MarshalToSizedBufferVT( dAtA[i] = 0x5 i-- dAtA[i] = 0x8a + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x5 + i-- + dAtA[i] = 0x8a } return len(dAtA) - i, nil } @@ -30172,6 +30930,12 @@ func (m *FailureDetails_NumscriptInvalidNestedMeta) MarshalToSizedBufferVT(dAtA dAtA[i] = 0x5 i-- dAtA[i] = 0x92 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x5 + i-- + dAtA[i] = 0x92 } return len(dAtA) - i, nil } @@ -30193,6 +30957,12 @@ func (m *FailureDetails_NumscriptInvalidNumberLiteral) MarshalToSizedBufferVT(dA dAtA[i] = 0x5 i-- dAtA[i] = 0x9a + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x5 + i-- + dAtA[i] = 0x9a } return len(dAtA) - i, nil } @@ -30214,6 +30984,12 @@ func (m *FailureDetails_NumscriptInvalidRemainingAllotment) MarshalToSizedBuffer dAtA[i] = 0x5 i-- dAtA[i] = 0xa2 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x5 + i-- + dAtA[i] = 0xa2 } return len(dAtA) - i, nil } @@ -30235,6 +31011,12 @@ func (m *FailureDetails_NumscriptInvalidScope) MarshalToSizedBufferVT(dAtA []byt dAtA[i] = 0x5 i-- dAtA[i] = 0xaa + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x5 + i-- + dAtA[i] = 0xaa } return len(dAtA) - i, nil } @@ -30256,6 +31038,12 @@ func (m *FailureDetails_NumscriptInvalidType) MarshalToSizedBufferVT(dAtA []byte dAtA[i] = 0x5 i-- dAtA[i] = 0xb2 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x5 + i-- + dAtA[i] = 0xb2 } return len(dAtA) - i, nil } @@ -30277,6 +31065,12 @@ func (m *FailureDetails_NumscriptInvalidUnboundedScalingAddress) MarshalToSizedB dAtA[i] = 0x5 i-- dAtA[i] = 0xba + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x5 + i-- + dAtA[i] = 0xba } return len(dAtA) - i, nil } @@ -30298,6 +31092,12 @@ func (m *FailureDetails_NumscriptInvalidUnboundedSendAll) MarshalToSizedBufferVT dAtA[i] = 0x5 i-- dAtA[i] = 0xc2 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x5 + i-- + dAtA[i] = 0xc2 } return len(dAtA) - i, nil } @@ -30319,6 +31119,12 @@ func (m *FailureDetails_NumscriptInvalidUncappedSource) MarshalToSizedBufferVT(d dAtA[i] = 0x5 i-- dAtA[i] = 0xca + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x5 + i-- + dAtA[i] = 0xca } return len(dAtA) - i, nil } @@ -30340,6 +31146,12 @@ func (m *FailureDetails_NumscriptInvalidVersion) MarshalToSizedBufferVT(dAtA []b dAtA[i] = 0x5 i-- dAtA[i] = 0xd2 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x5 + i-- + dAtA[i] = 0xd2 } return len(dAtA) - i, nil } @@ -30361,6 +31173,12 @@ func (m *FailureDetails_NumscriptMetadataNotFound) MarshalToSizedBufferVT(dAtA [ dAtA[i] = 0x5 i-- dAtA[i] = 0xda + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x5 + i-- + dAtA[i] = 0xda } return len(dAtA) - i, nil } @@ -30382,6 +31200,12 @@ func (m *FailureDetails_NumscriptMissingFunds) MarshalToSizedBufferVT(dAtA []byt dAtA[i] = 0x5 i-- dAtA[i] = 0xe2 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x5 + i-- + dAtA[i] = 0xe2 } return len(dAtA) - i, nil } @@ -30403,6 +31227,12 @@ func (m *FailureDetails_NumscriptMissingVariable) MarshalToSizedBufferVT(dAtA [] dAtA[i] = 0x5 i-- dAtA[i] = 0xea + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x5 + i-- + dAtA[i] = 0xea } return len(dAtA) - i, nil } @@ -30424,6 +31254,12 @@ func (m *FailureDetails_NumscriptNameInvalidChar) MarshalToSizedBufferVT(dAtA [] dAtA[i] = 0x5 i-- dAtA[i] = 0xf2 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x5 + i-- + dAtA[i] = 0xf2 } return len(dAtA) - i, nil } @@ -30445,6 +31281,12 @@ func (m *FailureDetails_NumscriptNameRequired) MarshalToSizedBufferVT(dAtA []byt dAtA[i] = 0x5 i-- dAtA[i] = 0xfa + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x5 + i-- + dAtA[i] = 0xfa } return len(dAtA) - i, nil } @@ -30466,6 +31308,12 @@ func (m *FailureDetails_NumscriptNameTooLong) MarshalToSizedBufferVT(dAtA []byte dAtA[i] = 0x6 i-- dAtA[i] = 0x82 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x6 + i-- + dAtA[i] = 0x82 } return len(dAtA) - i, nil } @@ -30487,6 +31335,12 @@ func (m *FailureDetails_NumscriptNegativeAmount) MarshalToSizedBufferVT(dAtA []b dAtA[i] = 0x6 i-- dAtA[i] = 0x8a + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x6 + i-- + dAtA[i] = 0x8a } return len(dAtA) - i, nil } @@ -30508,6 +31362,12 @@ func (m *FailureDetails_NumscriptNegativeBalance) MarshalToSizedBufferVT(dAtA [] dAtA[i] = 0x6 i-- dAtA[i] = 0x92 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x6 + i-- + dAtA[i] = 0x92 } return len(dAtA) - i, nil } @@ -30529,6 +31389,12 @@ func (m *FailureDetails_NumscriptNegativePortion) MarshalToSizedBufferVT(dAtA [] dAtA[i] = 0x6 i-- dAtA[i] = 0x9a + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x6 + i-- + dAtA[i] = 0x9a } return len(dAtA) - i, nil } @@ -30550,6 +31416,12 @@ func (m *FailureDetails_NumscriptNotFound) MarshalToSizedBufferVT(dAtA []byte) ( dAtA[i] = 0x6 i-- dAtA[i] = 0xa2 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x6 + i-- + dAtA[i] = 0xa2 } return len(dAtA) - i, nil } @@ -30571,6 +31443,12 @@ func (m *FailureDetails_NumscriptParseError) MarshalToSizedBufferVT(dAtA []byte) dAtA[i] = 0x6 i-- dAtA[i] = 0xaa + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x6 + i-- + dAtA[i] = 0xaa } return len(dAtA) - i, nil } @@ -30592,6 +31470,12 @@ func (m *FailureDetails_NumscriptPostingAmountOverflow) MarshalToSizedBufferVT(d dAtA[i] = 0x6 i-- dAtA[i] = 0xb2 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x6 + i-- + dAtA[i] = 0xb2 } return len(dAtA) - i, nil } @@ -30613,6 +31497,12 @@ func (m *FailureDetails_NumscriptPostingNegativeAmount) MarshalToSizedBufferVT(d dAtA[i] = 0x6 i-- dAtA[i] = 0xba + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x6 + i-- + dAtA[i] = 0xba } return len(dAtA) - i, nil } @@ -30634,6 +31524,12 @@ func (m *FailureDetails_NumscriptScalingUnsupported) MarshalToSizedBufferVT(dAtA dAtA[i] = 0x6 i-- dAtA[i] = 0xc2 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x6 + i-- + dAtA[i] = 0xc2 } return len(dAtA) - i, nil } @@ -30655,6 +31551,12 @@ func (m *FailureDetails_NumscriptTypeError) MarshalToSizedBufferVT(dAtA []byte) dAtA[i] = 0x6 i-- dAtA[i] = 0xca + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x6 + i-- + dAtA[i] = 0xca } return len(dAtA) - i, nil } @@ -30676,6 +31578,12 @@ func (m *FailureDetails_NumscriptUnboundFunction) MarshalToSizedBufferVT(dAtA [] dAtA[i] = 0x6 i-- dAtA[i] = 0xd2 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x6 + i-- + dAtA[i] = 0xd2 } return len(dAtA) - i, nil } @@ -30697,6 +31605,12 @@ func (m *FailureDetails_NumscriptUnboundVariable) MarshalToSizedBufferVT(dAtA [] dAtA[i] = 0x6 i-- dAtA[i] = 0xda + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x6 + i-- + dAtA[i] = 0xda } return len(dAtA) - i, nil } @@ -30718,6 +31632,12 @@ func (m *FailureDetails_NumscriptVersionAlreadyExists) MarshalToSizedBufferVT(dA dAtA[i] = 0x6 i-- dAtA[i] = 0xe2 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x6 + i-- + dAtA[i] = 0xe2 } return len(dAtA) - i, nil } @@ -30739,6 +31659,12 @@ func (m *FailureDetails_PostingsAndScriptConflict) MarshalToSizedBufferVT(dAtA [ dAtA[i] = 0x6 i-- dAtA[i] = 0xea + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x6 + i-- + dAtA[i] = 0xea } return len(dAtA) - i, nil } @@ -30760,6 +31686,12 @@ func (m *FailureDetails_PreparedQueryAlreadyExists) MarshalToSizedBufferVT(dAtA dAtA[i] = 0x6 i-- dAtA[i] = 0xf2 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x6 + i-- + dAtA[i] = 0xf2 } return len(dAtA) - i, nil } @@ -30781,6 +31713,12 @@ func (m *FailureDetails_PreparedQueryNameInvalidChar) MarshalToSizedBufferVT(dAt dAtA[i] = 0x6 i-- dAtA[i] = 0xfa + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x6 + i-- + dAtA[i] = 0xfa } return len(dAtA) - i, nil } @@ -30802,6 +31740,12 @@ func (m *FailureDetails_PreparedQueryNameRequired) MarshalToSizedBufferVT(dAtA [ dAtA[i] = 0x7 i-- dAtA[i] = 0x82 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x7 + i-- + dAtA[i] = 0x82 } return len(dAtA) - i, nil } @@ -30823,6 +31767,12 @@ func (m *FailureDetails_PreparedQueryNameTooLong) MarshalToSizedBufferVT(dAtA [] dAtA[i] = 0x7 i-- dAtA[i] = 0x8a + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x7 + i-- + dAtA[i] = 0x8a } return len(dAtA) - i, nil } @@ -30844,6 +31794,12 @@ func (m *FailureDetails_PreparedQueryNotFound) MarshalToSizedBufferVT(dAtA []byt dAtA[i] = 0x7 i-- dAtA[i] = 0x92 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x7 + i-- + dAtA[i] = 0x92 } return len(dAtA) - i, nil } @@ -30865,6 +31821,12 @@ func (m *FailureDetails_PreparedQueryRequired) MarshalToSizedBufferVT(dAtA []byt dAtA[i] = 0x7 i-- dAtA[i] = 0x9a + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x7 + i-- + dAtA[i] = 0x9a } return len(dAtA) - i, nil } @@ -30886,6 +31848,12 @@ func (m *FailureDetails_PreparedQueryTargetUnsupported) MarshalToSizedBufferVT(d dAtA[i] = 0x7 i-- dAtA[i] = 0xa2 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x7 + i-- + dAtA[i] = 0xa2 } return len(dAtA) - i, nil } @@ -30907,6 +31875,12 @@ func (m *FailureDetails_RevertTargetCreatedInBatch) MarshalToSizedBufferVT(dAtA dAtA[i] = 0x7 i-- dAtA[i] = 0xaa + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x7 + i-- + dAtA[i] = 0xaa } return len(dAtA) - i, nil } @@ -30928,6 +31902,12 @@ func (m *FailureDetails_ScopedBalanceUnsupported) MarshalToSizedBufferVT(dAtA [] dAtA[i] = 0x7 i-- dAtA[i] = 0xb2 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x7 + i-- + dAtA[i] = 0xb2 } return len(dAtA) - i, nil } @@ -30949,6 +31929,12 @@ func (m *FailureDetails_ScriptAndReferenceConflict) MarshalToSizedBufferVT(dAtA dAtA[i] = 0x7 i-- dAtA[i] = 0xba + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x7 + i-- + dAtA[i] = 0xba } return len(dAtA) - i, nil } @@ -30970,6 +31956,12 @@ func (m *FailureDetails_ScriptRequired) MarshalToSizedBufferVT(dAtA []byte) (int dAtA[i] = 0x7 i-- dAtA[i] = 0xc2 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x7 + i-- + dAtA[i] = 0xc2 } return len(dAtA) - i, nil } @@ -30991,6 +31983,12 @@ func (m *FailureDetails_SigningKeyIdInvalidChar) MarshalToSizedBufferVT(dAtA []b dAtA[i] = 0x7 i-- dAtA[i] = 0xca + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x7 + i-- + dAtA[i] = 0xca } return len(dAtA) - i, nil } @@ -31012,6 +32010,12 @@ func (m *FailureDetails_SigningKeyIdRequired) MarshalToSizedBufferVT(dAtA []byte dAtA[i] = 0x7 i-- dAtA[i] = 0xd2 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x7 + i-- + dAtA[i] = 0xd2 } return len(dAtA) - i, nil } @@ -31033,6 +32037,12 @@ func (m *FailureDetails_SigningKeyIdTooLong) MarshalToSizedBufferVT(dAtA []byte) dAtA[i] = 0x7 i-- dAtA[i] = 0xda + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x7 + i-- + dAtA[i] = 0xda } return len(dAtA) - i, nil } @@ -31054,6 +32064,12 @@ func (m *FailureDetails_SigningKeyInvalidLength) MarshalToSizedBufferVT(dAtA []b dAtA[i] = 0x7 i-- dAtA[i] = 0xe2 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x7 + i-- + dAtA[i] = 0xe2 } return len(dAtA) - i, nil } @@ -31075,6 +32091,12 @@ func (m *FailureDetails_SinkAlreadyExists) MarshalToSizedBufferVT(dAtA []byte) ( dAtA[i] = 0x7 i-- dAtA[i] = 0xea + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x7 + i-- + dAtA[i] = 0xea } return len(dAtA) - i, nil } @@ -31096,6 +32118,12 @@ func (m *FailureDetails_SinkBatchSizeTooLarge) MarshalToSizedBufferVT(dAtA []byt dAtA[i] = 0x7 i-- dAtA[i] = 0xf2 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x7 + i-- + dAtA[i] = 0xf2 } return len(dAtA) - i, nil } @@ -31117,6 +32145,12 @@ func (m *FailureDetails_SinkControllerMismatch) MarshalToSizedBufferVT(dAtA []by dAtA[i] = 0x7 i-- dAtA[i] = 0xfa + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x7 + i-- + dAtA[i] = 0xfa } return len(dAtA) - i, nil } @@ -31138,6 +32172,12 @@ func (m *FailureDetails_SinkNotFound) MarshalToSizedBufferVT(dAtA []byte) (int, dAtA[i] = 0x8 i-- dAtA[i] = 0x82 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x8 + i-- + dAtA[i] = 0x82 } return len(dAtA) - i, nil } @@ -31159,6 +32199,12 @@ func (m *FailureDetails_TargetRequired) MarshalToSizedBufferVT(dAtA []byte) (int dAtA[i] = 0x8 i-- dAtA[i] = 0x8a + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x8 + i-- + dAtA[i] = 0x8a } return len(dAtA) - i, nil } @@ -31180,6 +32226,12 @@ func (m *FailureDetails_TransactionAlreadyReverted) MarshalToSizedBufferVT(dAtA dAtA[i] = 0x8 i-- dAtA[i] = 0x92 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x8 + i-- + dAtA[i] = 0x92 } return len(dAtA) - i, nil } @@ -31201,6 +32253,12 @@ func (m *FailureDetails_TransactionNotFound) MarshalToSizedBufferVT(dAtA []byte) dAtA[i] = 0x8 i-- dAtA[i] = 0x9a + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x8 + i-- + dAtA[i] = 0x9a } return len(dAtA) - i, nil } @@ -31222,6 +32280,12 @@ func (m *FailureDetails_TransactionReferenceConflict) MarshalToSizedBufferVT(dAt dAtA[i] = 0x8 i-- dAtA[i] = 0xa2 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x8 + i-- + dAtA[i] = 0xa2 } return len(dAtA) - i, nil } @@ -31243,6 +32307,12 @@ func (m *FailureDetails_TransactionReferenceNotFound) MarshalToSizedBufferVT(dAt dAtA[i] = 0x8 i-- dAtA[i] = 0xaa + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x8 + i-- + dAtA[i] = 0xaa } return len(dAtA) - i, nil } @@ -31264,6 +32334,12 @@ func (m *FailureDetails_TransactionReferenceTooLong) MarshalToSizedBufferVT(dAtA dAtA[i] = 0x8 i-- dAtA[i] = 0xb2 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x8 + i-- + dAtA[i] = 0xb2 } return len(dAtA) - i, nil } @@ -31285,6 +32361,12 @@ func (m *FailureDetails_TransactionTargetMissing) MarshalToSizedBufferVT(dAtA [] dAtA[i] = 0x8 i-- dAtA[i] = 0xba + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x8 + i-- + dAtA[i] = 0xba } return len(dAtA) - i, nil } @@ -31306,6 +32388,12 @@ func (m *FailureDetails_TransientAccountNonZero) MarshalToSizedBufferVT(dAtA []b dAtA[i] = 0x8 i-- dAtA[i] = 0xc2 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x8 + i-- + dAtA[i] = 0xc2 } return len(dAtA) - i, nil } @@ -31327,6 +32415,12 @@ func (m *FailureDetails_VolumeOverflow) MarshalToSizedBufferVT(dAtA []byte) (int dAtA[i] = 0x8 i-- dAtA[i] = 0xca + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x8 + i-- + dAtA[i] = 0xca } return len(dAtA) - i, nil } @@ -31348,6 +32442,12 @@ func (m *FailureDetails_AccountMetadataKeyEmpty) MarshalToSizedBufferVT(dAtA []b dAtA[i] = 0x8 i-- dAtA[i] = 0xd2 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x8 + i-- + dAtA[i] = 0xd2 } return len(dAtA) - i, nil } @@ -31369,6 +32469,12 @@ func (m *FailureDetails_AccountMetadataKeyInvalidChar) MarshalToSizedBufferVT(dA dAtA[i] = 0x8 i-- dAtA[i] = 0xda + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x8 + i-- + dAtA[i] = 0xda } return len(dAtA) - i, nil } @@ -31390,6 +32496,12 @@ func (m *FailureDetails_AccountMetadataValueContainsNullByte) MarshalToSizedBuff dAtA[i] = 0x8 i-- dAtA[i] = 0xe2 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x8 + i-- + dAtA[i] = 0xe2 } return len(dAtA) - i, nil } @@ -31411,6 +32523,12 @@ func (m *FailureDetails_AccountMetadataLimitExceeded) MarshalToSizedBufferVT(dAt dAtA[i] = 0x8 i-- dAtA[i] = 0xea + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x8 + i-- + dAtA[i] = 0xea } return len(dAtA) - i, nil } @@ -31432,6 +32550,12 @@ func (m *FailureDetails_MetadataKeyLimitExceeded) MarshalToSizedBufferVT(dAtA [] dAtA[i] = 0x8 i-- dAtA[i] = 0xf2 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x8 + i-- + dAtA[i] = 0xf2 } return len(dAtA) - i, nil } @@ -31453,6 +32577,12 @@ func (m *FailureDetails_AccountMetadataKeyLimitExceeded) MarshalToSizedBufferVT( dAtA[i] = 0x8 i-- dAtA[i] = 0xfa + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x8 + i-- + dAtA[i] = 0xfa } return len(dAtA) - i, nil } @@ -31474,6 +32604,12 @@ func (m *FailureDetails_NumscriptInsufficientFunds) MarshalToSizedBufferVT(dAtA dAtA[i] = 0x9 i-- dAtA[i] = 0x82 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x9 + i-- + dAtA[i] = 0x82 } return len(dAtA) - i, nil } @@ -31495,6 +32631,12 @@ func (m *FailureDetails_NumscriptDivideByZeroWithNumerator) MarshalToSizedBuffer dAtA[i] = 0x9 i-- dAtA[i] = 0x8a + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x9 + i-- + dAtA[i] = 0x8a } return len(dAtA) - i, nil } @@ -31516,6 +32658,12 @@ func (m *FailureDetails_NumscriptScopedMetadataNotFound) MarshalToSizedBufferVT( dAtA[i] = 0x9 i-- dAtA[i] = 0x92 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x9 + i-- + dAtA[i] = 0x92 } return len(dAtA) - i, nil } @@ -31537,6 +32685,12 @@ func (m *FailureDetails_NumscriptScopedNegativeBalance) MarshalToSizedBufferVT(d dAtA[i] = 0x9 i-- dAtA[i] = 0x9a + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x9 + i-- + dAtA[i] = 0x9a } return len(dAtA) - i, nil } @@ -31558,6 +32712,12 @@ func (m *FailureDetails_NumscriptVersionNotFound) MarshalToSizedBufferVT(dAtA [] dAtA[i] = 0x9 i-- dAtA[i] = 0xa2 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x9 + i-- + dAtA[i] = 0xa2 } return len(dAtA) - i, nil } @@ -38155,6 +39315,10 @@ func (m *MirrorSourceConfig_Http) MarshalToSizedBufferVT(dAtA []byte) (int, erro i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x12 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x12 } return len(dAtA) - i, nil } @@ -38174,6 +39338,10 @@ func (m *MirrorSourceConfig_Postgres) MarshalToSizedBufferVT(dAtA []byte) (int, i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x1a + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x1a } return len(dAtA) - i, nil } @@ -38299,6 +39467,10 @@ func (m *MirrorRewriteRule_CreatedTransaction) MarshalToSizedBufferVT(dAtA []byt i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0xa + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0xa } return len(dAtA) - i, nil } @@ -38318,6 +39490,10 @@ func (m *MirrorRewriteRule_RevertedTransaction) MarshalToSizedBufferVT(dAtA []by i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x12 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x12 } return len(dAtA) - i, nil } @@ -38337,6 +39513,10 @@ func (m *MirrorRewriteRule_SavedMetadata) MarshalToSizedBufferVT(dAtA []byte) (i i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x1a + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x1a } return len(dAtA) - i, nil } @@ -38356,6 +39536,10 @@ func (m *MirrorRewriteRule_DeletedMetadata) MarshalToSizedBufferVT(dAtA []byte) i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x22 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x22 } return len(dAtA) - i, nil } @@ -38375,6 +39559,10 @@ func (m *MirrorRewriteRule_AnyVariant) MarshalToSizedBufferVT(dAtA []byte) (int, i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x2a + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x2a } return len(dAtA) - i, nil } @@ -38696,6 +39884,10 @@ func (m *CreatedTransactionAction_RewriteAddress) MarshalToSizedBufferVT(dAtA [] i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0xa + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0xa } return len(dAtA) - i, nil } @@ -38715,6 +39907,10 @@ func (m *CreatedTransactionAction_SetMetadata) MarshalToSizedBufferVT(dAtA []byt i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x12 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x12 } return len(dAtA) - i, nil } @@ -38734,6 +39930,10 @@ func (m *CreatedTransactionAction_DeleteMetadata) MarshalToSizedBufferVT(dAtA [] i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x1a + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x1a } return len(dAtA) - i, nil } @@ -38753,6 +39953,10 @@ func (m *CreatedTransactionAction_SetAccountMetadata) MarshalToSizedBufferVT(dAt i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x22 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x22 } return len(dAtA) - i, nil } @@ -38772,6 +39976,10 @@ func (m *CreatedTransactionAction_DeleteAccountMetadata) MarshalToSizedBufferVT( i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x2a + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x2a } return len(dAtA) - i, nil } @@ -38791,6 +39999,10 @@ func (m *CreatedTransactionAction_SetAccountMetadataFromAddress) MarshalToSizedB i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x32 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x32 } return len(dAtA) - i, nil } @@ -38810,6 +40022,10 @@ func (m *CreatedTransactionAction_Drop) MarshalToSizedBufferVT(dAtA []byte) (int i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x3a + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x3a } return len(dAtA) - i, nil } @@ -38871,6 +40087,10 @@ func (m *RevertedTransactionAction_RewriteAddress) MarshalToSizedBufferVT(dAtA [ i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0xa + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0xa } return len(dAtA) - i, nil } @@ -38890,6 +40110,10 @@ func (m *RevertedTransactionAction_SetMetadata) MarshalToSizedBufferVT(dAtA []by i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x12 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x12 } return len(dAtA) - i, nil } @@ -38909,6 +40133,10 @@ func (m *RevertedTransactionAction_DeleteMetadata) MarshalToSizedBufferVT(dAtA [ i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x1a + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x1a } return len(dAtA) - i, nil } @@ -38928,6 +40156,10 @@ func (m *RevertedTransactionAction_Drop) MarshalToSizedBufferVT(dAtA []byte) (in i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x22 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x22 } return len(dAtA) - i, nil } @@ -38989,6 +40221,10 @@ func (m *SavedMetadataAction_RewriteAddress) MarshalToSizedBufferVT(dAtA []byte) i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0xa + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0xa } return len(dAtA) - i, nil } @@ -39008,6 +40244,10 @@ func (m *SavedMetadataAction_SetMetadata) MarshalToSizedBufferVT(dAtA []byte) (i i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x12 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x12 } return len(dAtA) - i, nil } @@ -39027,6 +40267,10 @@ func (m *SavedMetadataAction_DeleteMetadata) MarshalToSizedBufferVT(dAtA []byte) i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x1a + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x1a } return len(dAtA) - i, nil } @@ -39046,6 +40290,10 @@ func (m *SavedMetadataAction_Drop) MarshalToSizedBufferVT(dAtA []byte) (int, err i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x22 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x22 } return len(dAtA) - i, nil } @@ -39107,6 +40355,10 @@ func (m *DeletedMetadataAction_RewriteAddress) MarshalToSizedBufferVT(dAtA []byt i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0xa + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0xa } return len(dAtA) - i, nil } @@ -39126,6 +40378,10 @@ func (m *DeletedMetadataAction_Drop) MarshalToSizedBufferVT(dAtA []byte) (int, e i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x12 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x12 } return len(dAtA) - i, nil } @@ -39187,6 +40443,10 @@ func (m *AnyVariantAction_RewriteAddress) MarshalToSizedBufferVT(dAtA []byte) (i i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0xa + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0xa } return len(dAtA) - i, nil } @@ -39206,6 +40466,10 @@ func (m *AnyVariantAction_Drop) MarshalToSizedBufferVT(dAtA []byte) (int, error) i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x12 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x12 } return len(dAtA) - i, nil } @@ -40624,6 +41888,10 @@ func (m *SegmentType_Uuid) MarshalToSizedBufferVT(dAtA []byte) (int, error) { i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x12 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x12 } return len(dAtA) - i, nil } @@ -40643,6 +41911,10 @@ func (m *SegmentType_Uint64) MarshalToSizedBufferVT(dAtA []byte) (int, error) { i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x1a + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x1a } return len(dAtA) - i, nil } @@ -40662,6 +41934,10 @@ func (m *SegmentType_Bytes) MarshalToSizedBufferVT(dAtA []byte) (int, error) { i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x22 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x22 } return len(dAtA) - i, nil } @@ -41017,6 +42293,10 @@ func (m *QueryFilter_Field) MarshalToSizedBufferVT(dAtA []byte) (int, error) { i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0xa + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0xa } return len(dAtA) - i, nil } @@ -41036,6 +42316,10 @@ func (m *QueryFilter_Address) MarshalToSizedBufferVT(dAtA []byte) (int, error) { i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x12 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x12 } return len(dAtA) - i, nil } @@ -41055,6 +42339,10 @@ func (m *QueryFilter_And) MarshalToSizedBufferVT(dAtA []byte) (int, error) { i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x1a + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x1a } return len(dAtA) - i, nil } @@ -41074,6 +42362,10 @@ func (m *QueryFilter_Or) MarshalToSizedBufferVT(dAtA []byte) (int, error) { i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x22 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x22 } return len(dAtA) - i, nil } @@ -41093,6 +42385,10 @@ func (m *QueryFilter_Not) MarshalToSizedBufferVT(dAtA []byte) (int, error) { i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x2a + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x2a } return len(dAtA) - i, nil } @@ -41112,6 +42408,10 @@ func (m *QueryFilter_Reference) MarshalToSizedBufferVT(dAtA []byte) (int, error) i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x32 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x32 } return len(dAtA) - i, nil } @@ -41131,6 +42431,10 @@ func (m *QueryFilter_BuiltinUint) MarshalToSizedBufferVT(dAtA []byte) (int, erro i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x3a + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x3a } return len(dAtA) - i, nil } @@ -41150,6 +42454,10 @@ func (m *QueryFilter_Ledger) MarshalToSizedBufferVT(dAtA []byte) (int, error) { i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x42 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x42 } return len(dAtA) - i, nil } @@ -41169,6 +42477,10 @@ func (m *QueryFilter_LogId) MarshalToSizedBufferVT(dAtA []byte) (int, error) { i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x4a + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x4a } return len(dAtA) - i, nil } @@ -41188,6 +42500,10 @@ func (m *QueryFilter_LogBuiltinUint) MarshalToSizedBufferVT(dAtA []byte) (int, e i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x52 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x52 } return len(dAtA) - i, nil } @@ -41207,6 +42523,10 @@ func (m *QueryFilter_AccountHasAsset) MarshalToSizedBufferVT(dAtA []byte) (int, i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x5a + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x5a } return len(dAtA) - i, nil } @@ -41226,6 +42546,10 @@ func (m *QueryFilter_Reverted) MarshalToSizedBufferVT(dAtA []byte) (int, error) i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x62 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x62 } return len(dAtA) - i, nil } @@ -41245,6 +42569,10 @@ func (m *QueryFilter_Audit) MarshalToSizedBufferVT(dAtA []byte) (int, error) { i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x6a + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x6a } return len(dAtA) - i, nil } @@ -41397,6 +42725,10 @@ func (m *AuditCondition_StringCond) MarshalToSizedBufferVT(dAtA []byte) (int, er i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x12 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x12 } return len(dAtA) - i, nil } @@ -41416,6 +42748,10 @@ func (m *AuditCondition_UintCond) MarshalToSizedBufferVT(dAtA []byte) (int, erro i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x1a + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x1a } return len(dAtA) - i, nil } @@ -41901,6 +43237,10 @@ func (m *FieldCondition_StringCond) MarshalToSizedBufferVT(dAtA []byte) (int, er i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x12 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x12 } return len(dAtA) - i, nil } @@ -41920,6 +43260,10 @@ func (m *FieldCondition_IntCond) MarshalToSizedBufferVT(dAtA []byte) (int, error i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x1a + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x1a } return len(dAtA) - i, nil } @@ -41939,6 +43283,10 @@ func (m *FieldCondition_UintCond) MarshalToSizedBufferVT(dAtA []byte) (int, erro i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x22 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x22 } return len(dAtA) - i, nil } @@ -41958,6 +43306,10 @@ func (m *FieldCondition_BoolCond) MarshalToSizedBufferVT(dAtA []byte) (int, erro i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x2a + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x2a } return len(dAtA) - i, nil } @@ -41977,6 +43329,10 @@ func (m *FieldCondition_ExistsCond) MarshalToSizedBufferVT(dAtA []byte) (int, er i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x32 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x32 } return len(dAtA) - i, nil } @@ -43203,6 +44559,10 @@ func (m *CallerSnapshot_Authenticated) MarshalToSizedBufferVT(dAtA []byte) (int, i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0xa + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0xa } return len(dAtA) - i, nil } @@ -43222,6 +44582,10 @@ func (m *CallerSnapshot_Anonymous) MarshalToSizedBufferVT(dAtA []byte) (int, err i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x12 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x12 } return len(dAtA) - i, nil } @@ -43241,6 +44605,10 @@ func (m *CallerSnapshot_System) MarshalToSizedBufferVT(dAtA []byte) (int, error) i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x1a + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x1a } return len(dAtA) - i, nil } @@ -43260,6 +44628,10 @@ func (m *CallerSnapshot_AuthDisabled) MarshalToSizedBufferVT(dAtA []byte) (int, i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x22 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x22 } return len(dAtA) - i, nil } @@ -43450,6 +44822,10 @@ func (m *BackupStorage_S3) MarshalToSizedBufferVT(dAtA []byte) (int, error) { i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0xa + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0xa } return len(dAtA) - i, nil } @@ -43469,6 +44845,10 @@ func (m *BackupStorage_Azure) MarshalToSizedBufferVT(dAtA []byte) (int, error) { i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x12 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x12 } return len(dAtA) - i, nil } @@ -43586,6 +44966,55 @@ func (m *ListOptions) MarshalToSizedBufferVT(dAtA []byte) (int, error) { return len(dAtA) - i, nil } +func (m *CapabilityActivation) MarshalVT() (dAtA []byte, err error) { + if m == nil { + return nil, nil + } + size := m.SizeVT() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBufferVT(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *CapabilityActivation) MarshalToVT(dAtA []byte) (int, error) { + size := m.SizeVT() + return m.MarshalToSizedBufferVT(dAtA[:size]) +} + +func (m *CapabilityActivation) MarshalToSizedBufferVT(dAtA []byte) (int, error) { + if m == nil { + return 0, nil + } + i := len(dAtA) + _ = i + var l int + _ = l + if m.unknownFields != nil { + i -= len(m.unknownFields) + copy(dAtA[i:], m.unknownFields) + } + if m.Active { + i-- + if m.Active { + dAtA[i] = 1 + } else { + dAtA[i] = 0 + } + i-- + dAtA[i] = 0x10 + } + if m.Id != 0 { + i -= 4 + binary.LittleEndian.PutUint32(dAtA[i:], uint32(m.Id)) + i-- + dAtA[i] = 0xd + } + return len(dAtA) - i, nil +} + func (m *MethodAuthPolicy) SizeVT() (n int) { if m == nil { return 0 @@ -43703,6 +45132,8 @@ func (m *MetadataValue_NullValue) SizeVT() (n int) { if m.NullValue != nil { l = m.NullValue.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -44175,6 +45606,8 @@ func (m *Target_Account) SizeVT() (n int) { if m.Account != nil { l = m.Account.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -44335,6 +45768,8 @@ func (m *IndexID_Metadata) SizeVT() (n int) { if m.Metadata != nil { l = m.Metadata.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -44423,6 +45858,8 @@ func (m *LogPayload_CreateLedger) SizeVT() (n int) { if m.CreateLedger != nil { l = m.CreateLedger.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -44435,6 +45872,8 @@ func (m *LogPayload_DeleteLedger) SizeVT() (n int) { if m.DeleteLedger != nil { l = m.DeleteLedger.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -44447,6 +45886,8 @@ func (m *LogPayload_Apply) SizeVT() (n int) { if m.Apply != nil { l = m.Apply.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -44459,6 +45900,8 @@ func (m *LogPayload_RegisterSigningKey) SizeVT() (n int) { if m.RegisterSigningKey != nil { l = m.RegisterSigningKey.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -44471,6 +45914,8 @@ func (m *LogPayload_RevokeSigningKey) SizeVT() (n int) { if m.RevokeSigningKey != nil { l = m.RevokeSigningKey.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -44483,6 +45928,8 @@ func (m *LogPayload_SetSigningConfig) SizeVT() (n int) { if m.SetSigningConfig != nil { l = m.SetSigningConfig.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -44495,6 +45942,8 @@ func (m *LogPayload_AddedEventsSink) SizeVT() (n int) { if m.AddedEventsSink != nil { l = m.AddedEventsSink.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -44507,6 +45956,8 @@ func (m *LogPayload_RemovedEventsSink) SizeVT() (n int) { if m.RemovedEventsSink != nil { l = m.RemovedEventsSink.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -44519,6 +45970,8 @@ func (m *LogPayload_SetMaintenanceMode) SizeVT() (n int) { if m.SetMaintenanceMode != nil { l = m.SetMaintenanceMode.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -44531,6 +45984,8 @@ func (m *LogPayload_PromoteLedger) SizeVT() (n int) { if m.PromoteLedger != nil { l = m.PromoteLedger.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -44543,6 +45998,8 @@ func (m *LogPayload_CreatedPreparedQuery) SizeVT() (n int) { if m.CreatedPreparedQuery != nil { l = m.CreatedPreparedQuery.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -44555,6 +46012,8 @@ func (m *LogPayload_UpdatedPreparedQuery) SizeVT() (n int) { if m.UpdatedPreparedQuery != nil { l = m.UpdatedPreparedQuery.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -44567,6 +46026,8 @@ func (m *LogPayload_DeletedPreparedQuery) SizeVT() (n int) { if m.DeletedPreparedQuery != nil { l = m.DeletedPreparedQuery.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -44579,6 +46040,8 @@ func (m *LogPayload_SavedNumscript) SizeVT() (n int) { if m.SavedNumscript != nil { l = m.SavedNumscript.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -44591,6 +46054,8 @@ func (m *LogPayload_CreatedQueryCheckpoint) SizeVT() (n int) { if m.CreatedQueryCheckpoint != nil { l = m.CreatedQueryCheckpoint.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -44603,6 +46068,8 @@ func (m *LogPayload_DeletedQueryCheckpoint) SizeVT() (n int) { if m.DeletedQueryCheckpoint != nil { l = m.DeletedQueryCheckpoint.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -44615,6 +46082,8 @@ func (m *LogPayload_SetQueryCheckpointSchedule) SizeVT() (n int) { if m.SetQueryCheckpointSchedule != nil { l = m.SetQueryCheckpointSchedule.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -44627,6 +46096,8 @@ func (m *LogPayload_DeleteQueryCheckpointSchedule) SizeVT() (n int) { if m.DeleteQueryCheckpointSchedule != nil { l = m.DeleteQueryCheckpointSchedule.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -44639,6 +46110,8 @@ func (m *LogPayload_SavedLedgerMetadata) SizeVT() (n int) { if m.SavedLedgerMetadata != nil { l = m.SavedLedgerMetadata.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -44651,6 +46124,8 @@ func (m *LogPayload_DeletedLedgerMetadata) SizeVT() (n int) { if m.DeletedLedgerMetadata != nil { l = m.DeletedLedgerMetadata.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -44663,6 +46138,8 @@ func (m *LogPayload_SetClusterPolicy) SizeVT() (n int) { if m.SetClusterPolicy != nil { l = m.SetClusterPolicy.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -44939,6 +46416,24 @@ func (m *ClusterPolicy) SizeVT() (n int) { if m.MetadataMaxCommandBytes != 0 { n += 9 } + if m.CapabilityActivationArmed { + n += 2 + } + if m.CapabilityAdmissionPolicy != 0 { + n += 1 + protohelpers.SizeOfVarint(uint64(m.CapabilityAdmissionPolicy)) + } + if m.MinimumReplayProtocol != 0 { + n += 1 + protohelpers.SizeOfVarint(uint64(m.MinimumReplayProtocol)) + } + if m.CapabilityMembershipRevision != 0 { + n += 9 + } + if len(m.CapabilityActivations) > 0 { + for _, e := range m.CapabilityActivations { + l = e.SizeVT() + n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } + } n += len(m.unknownFields) return n } @@ -45242,6 +46737,8 @@ func (m *SinkConfig_Nats) SizeVT() (n int) { if m.Nats != nil { l = m.Nats.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -45254,6 +46751,8 @@ func (m *SinkConfig_Clickhouse) SizeVT() (n int) { if m.Clickhouse != nil { l = m.Clickhouse.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -45266,6 +46765,8 @@ func (m *SinkConfig_Kafka) SizeVT() (n int) { if m.Kafka != nil { l = m.Kafka.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -45278,6 +46779,8 @@ func (m *SinkConfig_Http) SizeVT() (n int) { if m.Http != nil { l = m.Http.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -45290,6 +46793,8 @@ func (m *SinkConfig_Databricks) SizeVT() (n int) { if m.Databricks != nil { l = m.Databricks.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -45476,6 +46981,8 @@ func (m *DatabricksSinkConfig_OauthM2M) SizeVT() (n int) { if m.OauthM2M != nil { l = m.OauthM2M.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -45677,6 +47184,8 @@ func (m *LedgerLogPayload_CreatedTransaction) SizeVT() (n int) { if m.CreatedTransaction != nil { l = m.CreatedTransaction.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -45689,6 +47198,8 @@ func (m *LedgerLogPayload_RevertedTransaction) SizeVT() (n int) { if m.RevertedTransaction != nil { l = m.RevertedTransaction.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -45701,6 +47212,8 @@ func (m *LedgerLogPayload_SavedMetadata) SizeVT() (n int) { if m.SavedMetadata != nil { l = m.SavedMetadata.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -45713,6 +47226,8 @@ func (m *LedgerLogPayload_DeletedMetadata) SizeVT() (n int) { if m.DeletedMetadata != nil { l = m.DeletedMetadata.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -45725,6 +47240,8 @@ func (m *LedgerLogPayload_SetMetadataFieldType) SizeVT() (n int) { if m.SetMetadataFieldType != nil { l = m.SetMetadataFieldType.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -45737,6 +47254,8 @@ func (m *LedgerLogPayload_RemovedMetadataFieldType) SizeVT() (n int) { if m.RemovedMetadataFieldType != nil { l = m.RemovedMetadataFieldType.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -45749,6 +47268,8 @@ func (m *LedgerLogPayload_FillGap) SizeVT() (n int) { if m.FillGap != nil { l = m.FillGap.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -45761,6 +47282,8 @@ func (m *LedgerLogPayload_CreateIndex) SizeVT() (n int) { if m.CreateIndex != nil { l = m.CreateIndex.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -45773,6 +47296,8 @@ func (m *LedgerLogPayload_DropIndex) SizeVT() (n int) { if m.DropIndex != nil { l = m.DropIndex.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -45785,6 +47310,8 @@ func (m *LedgerLogPayload_AddedAccountType) SizeVT() (n int) { if m.AddedAccountType != nil { l = m.AddedAccountType.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -45797,6 +47324,8 @@ func (m *LedgerLogPayload_RemovedAccountType) SizeVT() (n int) { if m.RemovedAccountType != nil { l = m.RemovedAccountType.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -45809,6 +47338,8 @@ func (m *LedgerLogPayload_UpdatedDefaultEnforcementMode) SizeVT() (n int) { if m.UpdatedDefaultEnforcementMode != nil { l = m.UpdatedDefaultEnforcementMode.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -45821,6 +47352,8 @@ func (m *LedgerLogPayload_OrderSkipped) SizeVT() (n int) { if m.OrderSkipped != nil { l = m.OrderSkipped.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -45846,6 +47379,8 @@ func (m *FailureDetails_AccountAddressEmpty) SizeVT() (n int) { if m.AccountAddressEmpty != nil { l = m.AccountAddressEmpty.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -45858,6 +47393,8 @@ func (m *FailureDetails_AccountAddressEmptySegment) SizeVT() (n int) { if m.AccountAddressEmptySegment != nil { l = m.AccountAddressEmptySegment.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -45870,6 +47407,8 @@ func (m *FailureDetails_AccountAddressInvalidChar) SizeVT() (n int) { if m.AccountAddressInvalidChar != nil { l = m.AccountAddressInvalidChar.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -45882,6 +47421,8 @@ func (m *FailureDetails_AccountAddressTooLong) SizeVT() (n int) { if m.AccountAddressTooLong != nil { l = m.AccountAddressTooLong.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -45894,6 +47435,8 @@ func (m *FailureDetails_AccountNotMatchingType) SizeVT() (n int) { if m.AccountNotMatchingType != nil { l = m.AccountNotMatchingType.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -45906,6 +47449,8 @@ func (m *FailureDetails_AccountTypeAlreadyExists) SizeVT() (n int) { if m.AccountTypeAlreadyExists != nil { l = m.AccountTypeAlreadyExists.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -45918,6 +47463,8 @@ func (m *FailureDetails_AccountTypeConflict) SizeVT() (n int) { if m.AccountTypeConflict != nil { l = m.AccountTypeConflict.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -45930,6 +47477,8 @@ func (m *FailureDetails_AccountTypeHasAccounts) SizeVT() (n int) { if m.AccountTypeHasAccounts != nil { l = m.AccountTypeHasAccounts.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -45942,6 +47491,8 @@ func (m *FailureDetails_AccountTypeNameTooLong) SizeVT() (n int) { if m.AccountTypeNameTooLong != nil { l = m.AccountTypeNameTooLong.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -45954,6 +47505,8 @@ func (m *FailureDetails_AccountTypeNotFound) SizeVT() (n int) { if m.AccountTypeNotFound != nil { l = m.AccountTypeNotFound.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -45966,6 +47519,8 @@ func (m *FailureDetails_AccountTypePatternTooLong) SizeVT() (n int) { if m.AccountTypePatternTooLong != nil { l = m.AccountTypePatternTooLong.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -45978,6 +47533,8 @@ func (m *FailureDetails_AssetInvalid) SizeVT() (n int) { if m.AssetInvalid != nil { l = m.AssetInvalid.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -45990,6 +47547,8 @@ func (m *FailureDetails_BalanceNotFound) SizeVT() (n int) { if m.BalanceNotFound != nil { l = m.BalanceNotFound.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -46002,6 +47561,8 @@ func (m *FailureDetails_CheckpointIdRequired) SizeVT() (n int) { if m.CheckpointIdRequired != nil { l = m.CheckpointIdRequired.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -46014,6 +47575,8 @@ func (m *FailureDetails_CheckpointLimitReached) SizeVT() (n int) { if m.CheckpointLimitReached != nil { l = m.CheckpointLimitReached.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -46026,6 +47589,8 @@ func (m *FailureDetails_CheckpointNotFound) SizeVT() (n int) { if m.CheckpointNotFound != nil { l = m.CheckpointNotFound.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -46038,6 +47603,8 @@ func (m *FailureDetails_ColorInvalid) SizeVT() (n int) { if m.ColorInvalid != nil { l = m.ColorInvalid.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -46050,6 +47617,8 @@ func (m *FailureDetails_ColorTooLong) SizeVT() (n int) { if m.ColorTooLong != nil { l = m.ColorTooLong.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -46062,6 +47631,8 @@ func (m *FailureDetails_EmptyTransaction) SizeVT() (n int) { if m.EmptyTransaction != nil { l = m.EmptyTransaction.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -46074,6 +47645,8 @@ func (m *FailureDetails_IdempotencyKeyConflict) SizeVT() (n int) { if m.IdempotencyKeyConflict != nil { l = m.IdempotencyKeyConflict.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -46086,6 +47659,8 @@ func (m *FailureDetails_IdRequired) SizeVT() (n int) { if m.IdRequired != nil { l = m.IdRequired.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -46098,6 +47673,8 @@ func (m *FailureDetails_IndexAlreadyExists) SizeVT() (n int) { if m.IndexAlreadyExists != nil { l = m.IndexAlreadyExists.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -46110,6 +47687,8 @@ func (m *FailureDetails_IndexNotFound) SizeVT() (n int) { if m.IndexNotFound != nil { l = m.IndexNotFound.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -46122,6 +47701,8 @@ func (m *FailureDetails_IndexTargetUnsupported) SizeVT() (n int) { if m.IndexTargetUnsupported != nil { l = m.IndexTargetUnsupported.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -46134,6 +47715,8 @@ func (m *FailureDetails_InsufficientFunds) SizeVT() (n int) { if m.InsufficientFunds != nil { l = m.InsufficientFunds.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -46146,6 +47729,8 @@ func (m *FailureDetails_InvalidCronExpression) SizeVT() (n int) { if m.InvalidCronExpression != nil { l = m.InvalidCronExpression.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -46158,6 +47743,8 @@ func (m *FailureDetails_InvalidPattern) SizeVT() (n int) { if m.InvalidPattern != nil { l = m.InvalidPattern.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -46170,6 +47757,8 @@ func (m *FailureDetails_InvalidSkippableReason) SizeVT() (n int) { if m.InvalidSkippableReason != nil { l = m.InvalidSkippableReason.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -46182,6 +47771,8 @@ func (m *FailureDetails_LedgerAlreadyExists) SizeVT() (n int) { if m.LedgerAlreadyExists != nil { l = m.LedgerAlreadyExists.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -46194,6 +47785,8 @@ func (m *FailureDetails_LedgerDeleted) SizeVT() (n int) { if m.LedgerDeleted != nil { l = m.LedgerDeleted.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -46206,6 +47799,8 @@ func (m *FailureDetails_LedgerInMirrorMode) SizeVT() (n int) { if m.LedgerInMirrorMode != nil { l = m.LedgerInMirrorMode.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -46218,6 +47813,8 @@ func (m *FailureDetails_LedgerNameInvalidChar) SizeVT() (n int) { if m.LedgerNameInvalidChar != nil { l = m.LedgerNameInvalidChar.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -46230,6 +47827,8 @@ func (m *FailureDetails_LedgerNameRequired) SizeVT() (n int) { if m.LedgerNameRequired != nil { l = m.LedgerNameRequired.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -46242,6 +47841,8 @@ func (m *FailureDetails_LedgerNameReservedPrefix) SizeVT() (n int) { if m.LedgerNameReservedPrefix != nil { l = m.LedgerNameReservedPrefix.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -46254,6 +47855,8 @@ func (m *FailureDetails_LedgerNameTooLong) SizeVT() (n int) { if m.LedgerNameTooLong != nil { l = m.LedgerNameTooLong.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -46266,6 +47869,8 @@ func (m *FailureDetails_LedgerNotFound) SizeVT() (n int) { if m.LedgerNotFound != nil { l = m.LedgerNotFound.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -46278,6 +47883,8 @@ func (m *FailureDetails_LedgerNotInMirrorMode) SizeVT() (n int) { if m.LedgerNotInMirrorMode != nil { l = m.LedgerNotInMirrorMode.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -46290,6 +47897,8 @@ func (m *FailureDetails_MetadataFieldNotInSchema) SizeVT() (n int) { if m.MetadataFieldNotInSchema != nil { l = m.MetadataFieldNotInSchema.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -46302,6 +47911,8 @@ func (m *FailureDetails_MetadataKeyEmpty) SizeVT() (n int) { if m.MetadataKeyEmpty != nil { l = m.MetadataKeyEmpty.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -46314,6 +47925,8 @@ func (m *FailureDetails_MetadataKeyInvalidChar) SizeVT() (n int) { if m.MetadataKeyInvalidChar != nil { l = m.MetadataKeyInvalidChar.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -46326,6 +47939,8 @@ func (m *FailureDetails_MetadataKeyRequired) SizeVT() (n int) { if m.MetadataKeyRequired != nil { l = m.MetadataKeyRequired.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -46338,6 +47953,8 @@ func (m *FailureDetails_MetadataLimitExceeded) SizeVT() (n int) { if m.MetadataLimitExceeded != nil { l = m.MetadataLimitExceeded.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -46350,6 +47967,8 @@ func (m *FailureDetails_MetadataNotFound) SizeVT() (n int) { if m.MetadataNotFound != nil { l = m.MetadataNotFound.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -46362,6 +47981,8 @@ func (m *FailureDetails_MetadataValueContainsNullByte) SizeVT() (n int) { if m.MetadataValueContainsNullByte != nil { l = m.MetadataValueContainsNullByte.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -46374,6 +47995,8 @@ func (m *FailureDetails_MirrorHttpUrlInvalid) SizeVT() (n int) { if m.MirrorHttpUrlInvalid != nil { l = m.MirrorHttpUrlInvalid.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -46386,6 +48009,8 @@ func (m *FailureDetails_MirrorIamRegionRequired) SizeVT() (n int) { if m.MirrorIamRegionRequired != nil { l = m.MirrorIamRegionRequired.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -46398,6 +48023,8 @@ func (m *FailureDetails_MirrorIamRequiresTls) SizeVT() (n int) { if m.MirrorIamRequiresTls != nil { l = m.MirrorIamRequiresTls.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -46410,6 +48037,8 @@ func (m *FailureDetails_MirrorRewriteRuleInvalid) SizeVT() (n int) { if m.MirrorRewriteRuleInvalid != nil { l = m.MirrorRewriteRuleInvalid.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -46422,6 +48051,8 @@ func (m *FailureDetails_NumscriptAssetMismatch) SizeVT() (n int) { if m.NumscriptAssetMismatch != nil { l = m.NumscriptAssetMismatch.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -46434,6 +48065,8 @@ func (m *FailureDetails_NumscriptBadArity) SizeVT() (n int) { if m.NumscriptBadArity != nil { l = m.NumscriptBadArity.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -46446,6 +48079,8 @@ func (m *FailureDetails_NumscriptBadMetaValue) SizeVT() (n int) { if m.NumscriptBadMetaValue != nil { l = m.NumscriptBadMetaValue.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -46458,6 +48093,8 @@ func (m *FailureDetails_NumscriptBadPortion) SizeVT() (n int) { if m.NumscriptBadPortion != nil { l = m.NumscriptBadPortion.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -46470,6 +48107,8 @@ func (m *FailureDetails_NumscriptCannotCastScopedAccount) SizeVT() (n int) { if m.NumscriptCannotCastScopedAccount != nil { l = m.NumscriptCannotCastScopedAccount.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -46482,6 +48121,8 @@ func (m *FailureDetails_NumscriptCannotCastToString) SizeVT() (n int) { if m.NumscriptCannotCastToString != nil { l = m.NumscriptCannotCastToString.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -46494,6 +48135,8 @@ func (m *FailureDetails_NumscriptCannotStoreScopedAccount) SizeVT() (n int) { if m.NumscriptCannotStoreScopedAccount != nil { l = m.NumscriptCannotStoreScopedAccount.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -46506,6 +48149,8 @@ func (m *FailureDetails_NumscriptCompileBadArity) SizeVT() (n int) { if m.NumscriptCompileBadArity != nil { l = m.NumscriptCompileBadArity.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -46518,6 +48163,8 @@ func (m *FailureDetails_NumscriptCompileCannotCastToString) SizeVT() (n int) { if m.NumscriptCompileCannotCastToString != nil { l = m.NumscriptCompileCannotCastToString.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -46530,6 +48177,8 @@ func (m *FailureDetails_NumscriptCompileCannotStoreScopedAccount) SizeVT() (n in if m.NumscriptCompileCannotStoreScopedAccount != nil { l = m.NumscriptCompileCannotStoreScopedAccount.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -46542,6 +48191,8 @@ func (m *FailureDetails_NumscriptCompileDuplicateRemaining) SizeVT() (n int) { if m.NumscriptCompileDuplicateRemaining != nil { l = m.NumscriptCompileDuplicateRemaining.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -46554,6 +48205,8 @@ func (m *FailureDetails_NumscriptCompileDuplicateVariable) SizeVT() (n int) { if m.NumscriptCompileDuplicateVariable != nil { l = m.NumscriptCompileDuplicateVariable.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -46566,6 +48219,8 @@ func (m *FailureDetails_NumscriptCompileExperimentalFeature) SizeVT() (n int) { if m.NumscriptCompileExperimentalFeature != nil { l = m.NumscriptCompileExperimentalFeature.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -46578,6 +48233,8 @@ func (m *FailureDetails_NumscriptCompileInvalidFeature) SizeVT() (n int) { if m.NumscriptCompileInvalidFeature != nil { l = m.NumscriptCompileInvalidFeature.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -46590,6 +48247,8 @@ func (m *FailureDetails_NumscriptCompileInvalidMetaPosition) SizeVT() (n int) { if m.NumscriptCompileInvalidMetaPosition != nil { l = m.NumscriptCompileInvalidMetaPosition.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -46602,6 +48261,8 @@ func (m *FailureDetails_NumscriptCompileInvalidType) SizeVT() (n int) { if m.NumscriptCompileInvalidType != nil { l = m.NumscriptCompileInvalidType.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -46614,6 +48275,8 @@ func (m *FailureDetails_NumscriptCompileInvalidUncappedSource) SizeVT() (n int) if m.NumscriptCompileInvalidUncappedSource != nil { l = m.NumscriptCompileInvalidUncappedSource.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -46626,6 +48289,8 @@ func (m *FailureDetails_NumscriptCompileInvalidVariableValue) SizeVT() (n int) { if m.NumscriptCompileInvalidVariableValue != nil { l = m.NumscriptCompileInvalidVariableValue.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -46638,6 +48303,8 @@ func (m *FailureDetails_NumscriptCompileMissingVariable) SizeVT() (n int) { if m.NumscriptCompileMissingVariable != nil { l = m.NumscriptCompileMissingVariable.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -46650,6 +48317,8 @@ func (m *FailureDetails_NumscriptCompileTypeMismatch) SizeVT() (n int) { if m.NumscriptCompileTypeMismatch != nil { l = m.NumscriptCompileTypeMismatch.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -46662,6 +48331,8 @@ func (m *FailureDetails_NumscriptCompileUnboundVariable) SizeVT() (n int) { if m.NumscriptCompileUnboundVariable != nil { l = m.NumscriptCompileUnboundVariable.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -46674,6 +48345,8 @@ func (m *FailureDetails_NumscriptCompileUnknownFunction) SizeVT() (n int) { if m.NumscriptCompileUnknownFunction != nil { l = m.NumscriptCompileUnknownFunction.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -46686,6 +48359,8 @@ func (m *FailureDetails_NumscriptContentRequired) SizeVT() (n int) { if m.NumscriptContentRequired != nil { l = m.NumscriptContentRequired.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -46698,6 +48373,8 @@ func (m *FailureDetails_NumscriptCurrencyMismatch) SizeVT() (n int) { if m.NumscriptCurrencyMismatch != nil { l = m.NumscriptCurrencyMismatch.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -46710,6 +48387,8 @@ func (m *FailureDetails_NumscriptDivideByZero) SizeVT() (n int) { if m.NumscriptDivideByZero != nil { l = m.NumscriptDivideByZero.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -46722,6 +48401,8 @@ func (m *FailureDetails_NumscriptExperimentalFeature) SizeVT() (n int) { if m.NumscriptExperimentalFeature != nil { l = m.NumscriptExperimentalFeature.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -46734,6 +48415,8 @@ func (m *FailureDetails_NumscriptInvalidAccountName) SizeVT() (n int) { if m.NumscriptInvalidAccountName != nil { l = m.NumscriptInvalidAccountName.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -46746,6 +48429,8 @@ func (m *FailureDetails_NumscriptInvalidAllotmentInSendAll) SizeVT() (n int) { if m.NumscriptInvalidAllotmentInSendAll != nil { l = m.NumscriptInvalidAllotmentInSendAll.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -46758,6 +48443,8 @@ func (m *FailureDetails_NumscriptInvalidAllotmentSum) SizeVT() (n int) { if m.NumscriptInvalidAllotmentSum != nil { l = m.NumscriptInvalidAllotmentSum.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -46770,6 +48457,8 @@ func (m *FailureDetails_NumscriptInvalidAsset) SizeVT() (n int) { if m.NumscriptInvalidAsset != nil { l = m.NumscriptInvalidAsset.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -46782,6 +48471,8 @@ func (m *FailureDetails_NumscriptInvalidColor) SizeVT() (n int) { if m.NumscriptInvalidColor != nil { l = m.NumscriptInvalidColor.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -46794,6 +48485,8 @@ func (m *FailureDetails_NumscriptInvalidFeature) SizeVT() (n int) { if m.NumscriptInvalidFeature != nil { l = m.NumscriptInvalidFeature.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -46806,6 +48499,8 @@ func (m *FailureDetails_NumscriptInvalidMonetaryLiteral) SizeVT() (n int) { if m.NumscriptInvalidMonetaryLiteral != nil { l = m.NumscriptInvalidMonetaryLiteral.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -46818,6 +48513,8 @@ func (m *FailureDetails_NumscriptInvalidNestedMeta) SizeVT() (n int) { if m.NumscriptInvalidNestedMeta != nil { l = m.NumscriptInvalidNestedMeta.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -46830,6 +48527,8 @@ func (m *FailureDetails_NumscriptInvalidNumberLiteral) SizeVT() (n int) { if m.NumscriptInvalidNumberLiteral != nil { l = m.NumscriptInvalidNumberLiteral.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -46842,6 +48541,8 @@ func (m *FailureDetails_NumscriptInvalidRemainingAllotment) SizeVT() (n int) { if m.NumscriptInvalidRemainingAllotment != nil { l = m.NumscriptInvalidRemainingAllotment.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -46854,6 +48555,8 @@ func (m *FailureDetails_NumscriptInvalidScope) SizeVT() (n int) { if m.NumscriptInvalidScope != nil { l = m.NumscriptInvalidScope.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -46866,6 +48569,8 @@ func (m *FailureDetails_NumscriptInvalidType) SizeVT() (n int) { if m.NumscriptInvalidType != nil { l = m.NumscriptInvalidType.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -46878,6 +48583,8 @@ func (m *FailureDetails_NumscriptInvalidUnboundedScalingAddress) SizeVT() (n int if m.NumscriptInvalidUnboundedScalingAddress != nil { l = m.NumscriptInvalidUnboundedScalingAddress.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -46890,6 +48597,8 @@ func (m *FailureDetails_NumscriptInvalidUnboundedSendAll) SizeVT() (n int) { if m.NumscriptInvalidUnboundedSendAll != nil { l = m.NumscriptInvalidUnboundedSendAll.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -46902,6 +48611,8 @@ func (m *FailureDetails_NumscriptInvalidUncappedSource) SizeVT() (n int) { if m.NumscriptInvalidUncappedSource != nil { l = m.NumscriptInvalidUncappedSource.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -46914,6 +48625,8 @@ func (m *FailureDetails_NumscriptInvalidVersion) SizeVT() (n int) { if m.NumscriptInvalidVersion != nil { l = m.NumscriptInvalidVersion.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -46926,6 +48639,8 @@ func (m *FailureDetails_NumscriptMetadataNotFound) SizeVT() (n int) { if m.NumscriptMetadataNotFound != nil { l = m.NumscriptMetadataNotFound.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -46938,6 +48653,8 @@ func (m *FailureDetails_NumscriptMissingFunds) SizeVT() (n int) { if m.NumscriptMissingFunds != nil { l = m.NumscriptMissingFunds.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -46950,6 +48667,8 @@ func (m *FailureDetails_NumscriptMissingVariable) SizeVT() (n int) { if m.NumscriptMissingVariable != nil { l = m.NumscriptMissingVariable.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -46962,6 +48681,8 @@ func (m *FailureDetails_NumscriptNameInvalidChar) SizeVT() (n int) { if m.NumscriptNameInvalidChar != nil { l = m.NumscriptNameInvalidChar.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -46974,6 +48695,8 @@ func (m *FailureDetails_NumscriptNameRequired) SizeVT() (n int) { if m.NumscriptNameRequired != nil { l = m.NumscriptNameRequired.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -46986,6 +48709,8 @@ func (m *FailureDetails_NumscriptNameTooLong) SizeVT() (n int) { if m.NumscriptNameTooLong != nil { l = m.NumscriptNameTooLong.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -46998,6 +48723,8 @@ func (m *FailureDetails_NumscriptNegativeAmount) SizeVT() (n int) { if m.NumscriptNegativeAmount != nil { l = m.NumscriptNegativeAmount.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -47010,6 +48737,8 @@ func (m *FailureDetails_NumscriptNegativeBalance) SizeVT() (n int) { if m.NumscriptNegativeBalance != nil { l = m.NumscriptNegativeBalance.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -47022,6 +48751,8 @@ func (m *FailureDetails_NumscriptNegativePortion) SizeVT() (n int) { if m.NumscriptNegativePortion != nil { l = m.NumscriptNegativePortion.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -47034,6 +48765,8 @@ func (m *FailureDetails_NumscriptNotFound) SizeVT() (n int) { if m.NumscriptNotFound != nil { l = m.NumscriptNotFound.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -47046,6 +48779,8 @@ func (m *FailureDetails_NumscriptParseError) SizeVT() (n int) { if m.NumscriptParseError != nil { l = m.NumscriptParseError.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -47058,6 +48793,8 @@ func (m *FailureDetails_NumscriptPostingAmountOverflow) SizeVT() (n int) { if m.NumscriptPostingAmountOverflow != nil { l = m.NumscriptPostingAmountOverflow.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -47070,6 +48807,8 @@ func (m *FailureDetails_NumscriptPostingNegativeAmount) SizeVT() (n int) { if m.NumscriptPostingNegativeAmount != nil { l = m.NumscriptPostingNegativeAmount.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -47082,6 +48821,8 @@ func (m *FailureDetails_NumscriptScalingUnsupported) SizeVT() (n int) { if m.NumscriptScalingUnsupported != nil { l = m.NumscriptScalingUnsupported.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -47094,6 +48835,8 @@ func (m *FailureDetails_NumscriptTypeError) SizeVT() (n int) { if m.NumscriptTypeError != nil { l = m.NumscriptTypeError.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -47106,6 +48849,8 @@ func (m *FailureDetails_NumscriptUnboundFunction) SizeVT() (n int) { if m.NumscriptUnboundFunction != nil { l = m.NumscriptUnboundFunction.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -47118,6 +48863,8 @@ func (m *FailureDetails_NumscriptUnboundVariable) SizeVT() (n int) { if m.NumscriptUnboundVariable != nil { l = m.NumscriptUnboundVariable.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -47130,6 +48877,8 @@ func (m *FailureDetails_NumscriptVersionAlreadyExists) SizeVT() (n int) { if m.NumscriptVersionAlreadyExists != nil { l = m.NumscriptVersionAlreadyExists.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -47142,6 +48891,8 @@ func (m *FailureDetails_PostingsAndScriptConflict) SizeVT() (n int) { if m.PostingsAndScriptConflict != nil { l = m.PostingsAndScriptConflict.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -47154,6 +48905,8 @@ func (m *FailureDetails_PreparedQueryAlreadyExists) SizeVT() (n int) { if m.PreparedQueryAlreadyExists != nil { l = m.PreparedQueryAlreadyExists.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -47166,6 +48919,8 @@ func (m *FailureDetails_PreparedQueryNameInvalidChar) SizeVT() (n int) { if m.PreparedQueryNameInvalidChar != nil { l = m.PreparedQueryNameInvalidChar.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -47178,6 +48933,8 @@ func (m *FailureDetails_PreparedQueryNameRequired) SizeVT() (n int) { if m.PreparedQueryNameRequired != nil { l = m.PreparedQueryNameRequired.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -47190,6 +48947,8 @@ func (m *FailureDetails_PreparedQueryNameTooLong) SizeVT() (n int) { if m.PreparedQueryNameTooLong != nil { l = m.PreparedQueryNameTooLong.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -47202,6 +48961,8 @@ func (m *FailureDetails_PreparedQueryNotFound) SizeVT() (n int) { if m.PreparedQueryNotFound != nil { l = m.PreparedQueryNotFound.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -47214,6 +48975,8 @@ func (m *FailureDetails_PreparedQueryRequired) SizeVT() (n int) { if m.PreparedQueryRequired != nil { l = m.PreparedQueryRequired.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -47226,6 +48989,8 @@ func (m *FailureDetails_PreparedQueryTargetUnsupported) SizeVT() (n int) { if m.PreparedQueryTargetUnsupported != nil { l = m.PreparedQueryTargetUnsupported.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -47238,6 +49003,8 @@ func (m *FailureDetails_RevertTargetCreatedInBatch) SizeVT() (n int) { if m.RevertTargetCreatedInBatch != nil { l = m.RevertTargetCreatedInBatch.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -47250,6 +49017,8 @@ func (m *FailureDetails_ScopedBalanceUnsupported) SizeVT() (n int) { if m.ScopedBalanceUnsupported != nil { l = m.ScopedBalanceUnsupported.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -47262,6 +49031,8 @@ func (m *FailureDetails_ScriptAndReferenceConflict) SizeVT() (n int) { if m.ScriptAndReferenceConflict != nil { l = m.ScriptAndReferenceConflict.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -47274,6 +49045,8 @@ func (m *FailureDetails_ScriptRequired) SizeVT() (n int) { if m.ScriptRequired != nil { l = m.ScriptRequired.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -47286,6 +49059,8 @@ func (m *FailureDetails_SigningKeyIdInvalidChar) SizeVT() (n int) { if m.SigningKeyIdInvalidChar != nil { l = m.SigningKeyIdInvalidChar.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -47298,6 +49073,8 @@ func (m *FailureDetails_SigningKeyIdRequired) SizeVT() (n int) { if m.SigningKeyIdRequired != nil { l = m.SigningKeyIdRequired.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -47310,6 +49087,8 @@ func (m *FailureDetails_SigningKeyIdTooLong) SizeVT() (n int) { if m.SigningKeyIdTooLong != nil { l = m.SigningKeyIdTooLong.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -47322,6 +49101,8 @@ func (m *FailureDetails_SigningKeyInvalidLength) SizeVT() (n int) { if m.SigningKeyInvalidLength != nil { l = m.SigningKeyInvalidLength.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -47334,6 +49115,8 @@ func (m *FailureDetails_SinkAlreadyExists) SizeVT() (n int) { if m.SinkAlreadyExists != nil { l = m.SinkAlreadyExists.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -47346,6 +49129,8 @@ func (m *FailureDetails_SinkBatchSizeTooLarge) SizeVT() (n int) { if m.SinkBatchSizeTooLarge != nil { l = m.SinkBatchSizeTooLarge.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -47358,6 +49143,8 @@ func (m *FailureDetails_SinkControllerMismatch) SizeVT() (n int) { if m.SinkControllerMismatch != nil { l = m.SinkControllerMismatch.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -47370,6 +49157,8 @@ func (m *FailureDetails_SinkNotFound) SizeVT() (n int) { if m.SinkNotFound != nil { l = m.SinkNotFound.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -47382,6 +49171,8 @@ func (m *FailureDetails_TargetRequired) SizeVT() (n int) { if m.TargetRequired != nil { l = m.TargetRequired.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -47394,6 +49185,8 @@ func (m *FailureDetails_TransactionAlreadyReverted) SizeVT() (n int) { if m.TransactionAlreadyReverted != nil { l = m.TransactionAlreadyReverted.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -47406,6 +49199,8 @@ func (m *FailureDetails_TransactionNotFound) SizeVT() (n int) { if m.TransactionNotFound != nil { l = m.TransactionNotFound.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -47418,6 +49213,8 @@ func (m *FailureDetails_TransactionReferenceConflict) SizeVT() (n int) { if m.TransactionReferenceConflict != nil { l = m.TransactionReferenceConflict.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -47430,6 +49227,8 @@ func (m *FailureDetails_TransactionReferenceNotFound) SizeVT() (n int) { if m.TransactionReferenceNotFound != nil { l = m.TransactionReferenceNotFound.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -47442,6 +49241,8 @@ func (m *FailureDetails_TransactionReferenceTooLong) SizeVT() (n int) { if m.TransactionReferenceTooLong != nil { l = m.TransactionReferenceTooLong.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -47454,6 +49255,8 @@ func (m *FailureDetails_TransactionTargetMissing) SizeVT() (n int) { if m.TransactionTargetMissing != nil { l = m.TransactionTargetMissing.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -47466,6 +49269,8 @@ func (m *FailureDetails_TransientAccountNonZero) SizeVT() (n int) { if m.TransientAccountNonZero != nil { l = m.TransientAccountNonZero.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -47478,6 +49283,8 @@ func (m *FailureDetails_VolumeOverflow) SizeVT() (n int) { if m.VolumeOverflow != nil { l = m.VolumeOverflow.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -47490,6 +49297,8 @@ func (m *FailureDetails_AccountMetadataKeyEmpty) SizeVT() (n int) { if m.AccountMetadataKeyEmpty != nil { l = m.AccountMetadataKeyEmpty.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -47502,6 +49311,8 @@ func (m *FailureDetails_AccountMetadataKeyInvalidChar) SizeVT() (n int) { if m.AccountMetadataKeyInvalidChar != nil { l = m.AccountMetadataKeyInvalidChar.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -47514,6 +49325,8 @@ func (m *FailureDetails_AccountMetadataValueContainsNullByte) SizeVT() (n int) { if m.AccountMetadataValueContainsNullByte != nil { l = m.AccountMetadataValueContainsNullByte.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -47526,6 +49339,8 @@ func (m *FailureDetails_AccountMetadataLimitExceeded) SizeVT() (n int) { if m.AccountMetadataLimitExceeded != nil { l = m.AccountMetadataLimitExceeded.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -47538,6 +49353,8 @@ func (m *FailureDetails_MetadataKeyLimitExceeded) SizeVT() (n int) { if m.MetadataKeyLimitExceeded != nil { l = m.MetadataKeyLimitExceeded.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -47550,6 +49367,8 @@ func (m *FailureDetails_AccountMetadataKeyLimitExceeded) SizeVT() (n int) { if m.AccountMetadataKeyLimitExceeded != nil { l = m.AccountMetadataKeyLimitExceeded.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -47562,6 +49381,8 @@ func (m *FailureDetails_NumscriptInsufficientFunds) SizeVT() (n int) { if m.NumscriptInsufficientFunds != nil { l = m.NumscriptInsufficientFunds.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -47574,6 +49395,8 @@ func (m *FailureDetails_NumscriptDivideByZeroWithNumerator) SizeVT() (n int) { if m.NumscriptDivideByZeroWithNumerator != nil { l = m.NumscriptDivideByZeroWithNumerator.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -47586,6 +49409,8 @@ func (m *FailureDetails_NumscriptScopedMetadataNotFound) SizeVT() (n int) { if m.NumscriptScopedMetadataNotFound != nil { l = m.NumscriptScopedMetadataNotFound.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -47598,6 +49423,8 @@ func (m *FailureDetails_NumscriptScopedNegativeBalance) SizeVT() (n int) { if m.NumscriptScopedNegativeBalance != nil { l = m.NumscriptScopedNegativeBalance.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -47610,6 +49437,8 @@ func (m *FailureDetails_NumscriptVersionNotFound) SizeVT() (n int) { if m.NumscriptVersionNotFound != nil { l = m.NumscriptVersionNotFound.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -49947,6 +51776,8 @@ func (m *MirrorSourceConfig_Http) SizeVT() (n int) { if m.Http != nil { l = m.Http.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -49959,6 +51790,8 @@ func (m *MirrorSourceConfig_Postgres) SizeVT() (n int) { if m.Postgres != nil { l = m.Postgres.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -50009,6 +51842,8 @@ func (m *MirrorRewriteRule_CreatedTransaction) SizeVT() (n int) { if m.CreatedTransaction != nil { l = m.CreatedTransaction.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -50021,6 +51856,8 @@ func (m *MirrorRewriteRule_RevertedTransaction) SizeVT() (n int) { if m.RevertedTransaction != nil { l = m.RevertedTransaction.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -50033,6 +51870,8 @@ func (m *MirrorRewriteRule_SavedMetadata) SizeVT() (n int) { if m.SavedMetadata != nil { l = m.SavedMetadata.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -50045,6 +51884,8 @@ func (m *MirrorRewriteRule_DeletedMetadata) SizeVT() (n int) { if m.DeletedMetadata != nil { l = m.DeletedMetadata.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -50057,6 +51898,8 @@ func (m *MirrorRewriteRule_AnyVariant) SizeVT() (n int) { if m.AnyVariant != nil { l = m.AnyVariant.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -50182,6 +52025,8 @@ func (m *CreatedTransactionAction_RewriteAddress) SizeVT() (n int) { if m.RewriteAddress != nil { l = m.RewriteAddress.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -50194,6 +52039,8 @@ func (m *CreatedTransactionAction_SetMetadata) SizeVT() (n int) { if m.SetMetadata != nil { l = m.SetMetadata.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -50206,6 +52053,8 @@ func (m *CreatedTransactionAction_DeleteMetadata) SizeVT() (n int) { if m.DeleteMetadata != nil { l = m.DeleteMetadata.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -50218,6 +52067,8 @@ func (m *CreatedTransactionAction_SetAccountMetadata) SizeVT() (n int) { if m.SetAccountMetadata != nil { l = m.SetAccountMetadata.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -50230,6 +52081,8 @@ func (m *CreatedTransactionAction_DeleteAccountMetadata) SizeVT() (n int) { if m.DeleteAccountMetadata != nil { l = m.DeleteAccountMetadata.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -50242,6 +52095,8 @@ func (m *CreatedTransactionAction_SetAccountMetadataFromAddress) SizeVT() (n int if m.SetAccountMetadataFromAddress != nil { l = m.SetAccountMetadataFromAddress.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -50254,6 +52109,8 @@ func (m *CreatedTransactionAction_Drop) SizeVT() (n int) { if m.Drop != nil { l = m.Drop.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -50279,6 +52136,8 @@ func (m *RevertedTransactionAction_RewriteAddress) SizeVT() (n int) { if m.RewriteAddress != nil { l = m.RewriteAddress.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -50291,6 +52150,8 @@ func (m *RevertedTransactionAction_SetMetadata) SizeVT() (n int) { if m.SetMetadata != nil { l = m.SetMetadata.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -50303,6 +52164,8 @@ func (m *RevertedTransactionAction_DeleteMetadata) SizeVT() (n int) { if m.DeleteMetadata != nil { l = m.DeleteMetadata.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -50315,6 +52178,8 @@ func (m *RevertedTransactionAction_Drop) SizeVT() (n int) { if m.Drop != nil { l = m.Drop.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -50340,6 +52205,8 @@ func (m *SavedMetadataAction_RewriteAddress) SizeVT() (n int) { if m.RewriteAddress != nil { l = m.RewriteAddress.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -50352,6 +52219,8 @@ func (m *SavedMetadataAction_SetMetadata) SizeVT() (n int) { if m.SetMetadata != nil { l = m.SetMetadata.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -50364,6 +52233,8 @@ func (m *SavedMetadataAction_DeleteMetadata) SizeVT() (n int) { if m.DeleteMetadata != nil { l = m.DeleteMetadata.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -50376,6 +52247,8 @@ func (m *SavedMetadataAction_Drop) SizeVT() (n int) { if m.Drop != nil { l = m.Drop.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -50401,6 +52274,8 @@ func (m *DeletedMetadataAction_RewriteAddress) SizeVT() (n int) { if m.RewriteAddress != nil { l = m.RewriteAddress.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -50413,6 +52288,8 @@ func (m *DeletedMetadataAction_Drop) SizeVT() (n int) { if m.Drop != nil { l = m.Drop.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -50438,6 +52315,8 @@ func (m *AnyVariantAction_RewriteAddress) SizeVT() (n int) { if m.RewriteAddress != nil { l = m.RewriteAddress.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -50450,6 +52329,8 @@ func (m *AnyVariantAction_Drop) SizeVT() (n int) { if m.Drop != nil { l = m.Drop.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -51033,6 +52914,8 @@ func (m *SegmentType_Uuid) SizeVT() (n int) { if m.Uuid != nil { l = m.Uuid.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -51045,6 +52928,8 @@ func (m *SegmentType_Uint64) SizeVT() (n int) { if m.Uint64 != nil { l = m.Uint64.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -51057,6 +52942,8 @@ func (m *SegmentType_Bytes) SizeVT() (n int) { if m.Bytes != nil { l = m.Bytes.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -51187,6 +53074,8 @@ func (m *QueryFilter_Field) SizeVT() (n int) { if m.Field != nil { l = m.Field.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -51199,6 +53088,8 @@ func (m *QueryFilter_Address) SizeVT() (n int) { if m.Address != nil { l = m.Address.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -51211,6 +53102,8 @@ func (m *QueryFilter_And) SizeVT() (n int) { if m.And != nil { l = m.And.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -51223,6 +53116,8 @@ func (m *QueryFilter_Or) SizeVT() (n int) { if m.Or != nil { l = m.Or.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -51235,6 +53130,8 @@ func (m *QueryFilter_Not) SizeVT() (n int) { if m.Not != nil { l = m.Not.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -51247,6 +53144,8 @@ func (m *QueryFilter_Reference) SizeVT() (n int) { if m.Reference != nil { l = m.Reference.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -51259,6 +53158,8 @@ func (m *QueryFilter_BuiltinUint) SizeVT() (n int) { if m.BuiltinUint != nil { l = m.BuiltinUint.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -51271,6 +53172,8 @@ func (m *QueryFilter_Ledger) SizeVT() (n int) { if m.Ledger != nil { l = m.Ledger.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -51283,6 +53186,8 @@ func (m *QueryFilter_LogId) SizeVT() (n int) { if m.LogId != nil { l = m.LogId.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -51295,6 +53200,8 @@ func (m *QueryFilter_LogBuiltinUint) SizeVT() (n int) { if m.LogBuiltinUint != nil { l = m.LogBuiltinUint.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -51307,6 +53214,8 @@ func (m *QueryFilter_AccountHasAsset) SizeVT() (n int) { if m.AccountHasAsset != nil { l = m.AccountHasAsset.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -51319,6 +53228,8 @@ func (m *QueryFilter_Reverted) SizeVT() (n int) { if m.Reverted != nil { l = m.Reverted.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -51331,6 +53242,8 @@ func (m *QueryFilter_Audit) SizeVT() (n int) { if m.Audit != nil { l = m.Audit.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -51386,6 +53299,8 @@ func (m *AuditCondition_StringCond) SizeVT() (n int) { if m.StringCond != nil { l = m.StringCond.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -51398,6 +53313,8 @@ func (m *AuditCondition_UintCond) SizeVT() (n int) { if m.UintCond != nil { l = m.UintCond.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -51576,6 +53493,8 @@ func (m *FieldCondition_StringCond) SizeVT() (n int) { if m.StringCond != nil { l = m.StringCond.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -51588,6 +53507,8 @@ func (m *FieldCondition_IntCond) SizeVT() (n int) { if m.IntCond != nil { l = m.IntCond.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -51600,6 +53521,8 @@ func (m *FieldCondition_UintCond) SizeVT() (n int) { if m.UintCond != nil { l = m.UintCond.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -51612,6 +53535,8 @@ func (m *FieldCondition_BoolCond) SizeVT() (n int) { if m.BoolCond != nil { l = m.BoolCond.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -51624,6 +53549,8 @@ func (m *FieldCondition_ExistsCond) SizeVT() (n int) { if m.ExistsCond != nil { l = m.ExistsCond.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -52131,6 +54058,8 @@ func (m *CallerSnapshot_Authenticated) SizeVT() (n int) { if m.Authenticated != nil { l = m.Authenticated.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -52143,6 +54072,8 @@ func (m *CallerSnapshot_Anonymous) SizeVT() (n int) { if m.Anonymous != nil { l = m.Anonymous.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -52155,6 +54086,8 @@ func (m *CallerSnapshot_System) SizeVT() (n int) { if m.System != nil { l = m.System.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -52167,6 +54100,8 @@ func (m *CallerSnapshot_AuthDisabled) SizeVT() (n int) { if m.AuthDisabled != nil { l = m.AuthDisabled.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -52248,6 +54183,8 @@ func (m *BackupStorage_S3) SizeVT() (n int) { if m.S3 != nil { l = m.S3.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -52260,6 +54197,8 @@ func (m *BackupStorage_Azure) SizeVT() (n int) { if m.Azure != nil { l = m.Azure.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -52304,6 +54243,22 @@ func (m *ListOptions) SizeVT() (n int) { return n } +func (m *CapabilityActivation) SizeVT() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + if m.Id != 0 { + n += 5 + } + if m.Active { + n += 2 + } + n += len(m.unknownFields) + return n +} + func (m *MethodAuthPolicy) UnmarshalVT(dAtA []byte) error { l := len(dAtA) iNdEx := 0 @@ -59594,6 +61549,108 @@ func (m *ClusterPolicy) UnmarshalVT(dAtA []byte) error { } m.MetadataMaxCommandBytes = uint64(binary.LittleEndian.Uint64(dAtA[iNdEx:])) iNdEx += 8 + case 9: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field CapabilityActivationArmed", wireType) + } + var v int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + v |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + m.CapabilityActivationArmed = bool(v != 0) + case 10: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field CapabilityAdmissionPolicy", wireType) + } + m.CapabilityAdmissionPolicy = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.CapabilityAdmissionPolicy |= CapabilityAdmissionPolicy(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 11: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field MinimumReplayProtocol", wireType) + } + m.MinimumReplayProtocol = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.MinimumReplayProtocol |= uint32(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 12: + if wireType != 1 { + return fmt.Errorf("proto: wrong wireType = %d for field CapabilityMembershipRevision", wireType) + } + m.CapabilityMembershipRevision = 0 + if (iNdEx + 8) > l { + return io.ErrUnexpectedEOF + } + m.CapabilityMembershipRevision = uint64(binary.LittleEndian.Uint64(dAtA[iNdEx:])) + iNdEx += 8 + case 13: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field CapabilityActivations", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return protohelpers.ErrInvalidLength + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return protohelpers.ErrInvalidLength + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.CapabilityActivations = append(m.CapabilityActivations, &CapabilityActivation{}) + if err := m.CapabilityActivations[len(m.CapabilityActivations)-1].UnmarshalVT(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex default: iNdEx = preIndex skippy, err := protohelpers.Skip(dAtA[iNdEx:]) @@ -96618,3 +98675,84 @@ func (m *ListOptions) UnmarshalVT(dAtA []byte) error { } return nil } +func (m *CapabilityActivation) UnmarshalVT(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: CapabilityActivation: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: CapabilityActivation: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 5 { + return fmt.Errorf("proto: wrong wireType = %d for field Id", wireType) + } + m.Id = 0 + if (iNdEx + 4) > l { + return io.ErrUnexpectedEOF + } + m.Id = uint32(binary.LittleEndian.Uint32(dAtA[iNdEx:])) + iNdEx += 4 + case 2: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field Active", wireType) + } + var v int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + v |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + m.Active = bool(v != 0) + default: + iNdEx = preIndex + skippy, err := protohelpers.Skip(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return protohelpers.ErrInvalidLength + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + m.unknownFields = append(m.unknownFields, dAtA[iNdEx:iNdEx+skippy]...) + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} diff --git a/internal/proto/eventspb/events.pb.go b/internal/proto/eventspb/events.pb.go index 3fbadb9282..3f055a53aa 100644 --- a/internal/proto/eventspb/events.pb.go +++ b/internal/proto/eventspb/events.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.12 -// protoc v7.34.2 +// protoc v7.36.2 // source: events.proto package eventspb diff --git a/internal/proto/eventspb/events_vtproto.pb.go b/internal/proto/eventspb/events_vtproto.pb.go index afa329ab7f..e6877aec2c 100644 --- a/internal/proto/eventspb/events_vtproto.pb.go +++ b/internal/proto/eventspb/events_vtproto.pb.go @@ -1,5 +1,5 @@ // Code generated by protoc-gen-go-vtproto. DO NOT EDIT. -// protoc-gen-go-vtproto version: (devel) +// protoc-gen-go-vtproto version: v0.6.1-0.20240319094008-0393e58bdf10 // source: events.proto package eventspb diff --git a/internal/proto/proposalpb/proposal.pb.go b/internal/proto/proposalpb/proposal.pb.go index d4b8affd1e..769dfc761d 100644 --- a/internal/proto/proposalpb/proposal.pb.go +++ b/internal/proto/proposalpb/proposal.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.12 -// protoc v7.34.2 +// protoc v7.36.2 // source: proposal.proto package proposalpb diff --git a/internal/proto/proposalpb/proposal_vtproto.pb.go b/internal/proto/proposalpb/proposal_vtproto.pb.go index 996f6fc0b3..aa7da5e375 100644 --- a/internal/proto/proposalpb/proposal_vtproto.pb.go +++ b/internal/proto/proposalpb/proposal_vtproto.pb.go @@ -1,5 +1,5 @@ // Code generated by protoc-gen-go-vtproto. DO NOT EDIT. -// protoc-gen-go-vtproto version: (devel) +// protoc-gen-go-vtproto version: v0.6.1-0.20240319094008-0393e58bdf10 // source: proposal.proto package proposalpb diff --git a/internal/proto/raftcmdpb/raft_cmd.pb.go b/internal/proto/raftcmdpb/raft_cmd.pb.go index 89ff76ef20..09ffda9090 100644 --- a/internal/proto/raftcmdpb/raft_cmd.pb.go +++ b/internal/proto/raftcmdpb/raft_cmd.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.12 -// protoc v7.34.2 +// protoc v7.36.2 // source: raft_cmd.proto package raftcmdpb @@ -3434,6 +3434,9 @@ type Proposal struct { // // Deprecated: Marked as deprecated in raft_cmd.proto. TechnicalUpdates []*TechnicalUpdate `protobuf:"bytes,9,rep,name=technical_updates,json=technicalUpdates,proto3" json:"technical_updates,omitempty"` + // Stable capability IDs required by the final serialized proposal. This is + // checked after any plan rebuild and before the bytes enter Raft. + RequiredCapabilities []uint32 `protobuf:"fixed32,10,rep,packed,name=required_capabilities,json=requiredCapabilities,proto3" json:"required_capabilities,omitempty"` // Exclusive with legacy fields 2 and 9. Reading this envelope is supported; // emitting it on Raft requires a separately validated cluster-wide cutover. // @@ -3541,6 +3544,13 @@ func (x *Proposal) GetTechnicalUpdates() []*TechnicalUpdate { return nil } +func (x *Proposal) GetRequiredCapabilities() []uint32 { + if x != nil { + return x.RequiredCapabilities + } + return nil +} + func (x *Proposal) GetPayload() isProposal_Payload { if x != nil { return x.Payload @@ -5991,7 +6001,7 @@ const file_raft_cmd_proto_rawDesc = "" + "\x06orders\x18\x01 \x03(\v2\v.raft.OrderR\x06orders\x12B\n" + "\x11technical_updates\x18\x02 \x03(\v2\x15.raft.TechnicalUpdateR\x10technicalUpdates\"I\n" + "\x18SetClusterPolicyProposal\x12-\n" + - "\x06policy\x18\x01 \x01(\v2\x15.common.ClusterPolicyR\x06policy\"\xd3\x04\n" + + "\x06policy\x18\x01 \x01(\v2\x15.common.ClusterPolicyR\x06policy\"\x88\x05\n" + "\bProposal\x12\x0e\n" + "\x02id\x18\x01 \x01(\x06R\x02id\x12'\n" + "\x06orders\x18\x02 \x03(\v2\v.raft.OrderB\x02\x18\x01R\x06orders\x12%\n" + @@ -6001,7 +6011,9 @@ const file_raft_cmd_proto_rawDesc = "" + "\x0fcaller_snapshot\x18\x06 \x01(\v2\x16.common.CallerSnapshotR\x0ecallerSnapshot\x125\n" + "\vidempotency\x18\a \x01(\v2\x13.common.IdempotencyR\vidempotency\x129\n" + "\tsignature\x18\b \x01(\v2\x1b.signature.SignedApplyBatchR\tsignature\x12F\n" + - "\x11technical_updates\x18\t \x03(\v2\x15.raft.TechnicalUpdateB\x02\x18\x01R\x10technicalUpdates\x12,\n" + + "\x11technical_updates\x18\t \x03(\v2\x15.raft.TechnicalUpdateB\x02\x18\x01R\x10technicalUpdates\x123\n" + + "\x15required_capabilities\x18\n" + + " \x03(\aR\x14requiredCapabilities\x12,\n" + "\x05batch\x18\v \x01(\v2\x14.raft.ExecutionBatchH\x00R\x05batch\x12N\n" + "\x12set_cluster_policy\x18\f \x01(\v2\x1e.raft.SetClusterPolicyProposalH\x00R\x10setClusterPolicyB\t\n" + "\apayload\"\xe8\x04\n" + diff --git a/internal/proto/raftcmdpb/raft_cmd_dethash.pb.go b/internal/proto/raftcmdpb/raft_cmd_dethash.pb.go index faf50cee54..76071a867e 100644 --- a/internal/proto/raftcmdpb/raft_cmd_dethash.pb.go +++ b/internal/proto/raftcmdpb/raft_cmd_dethash.pb.go @@ -1565,6 +1565,15 @@ func (m *Proposal) MarshalToSizedBufferDeterministicVT(dAtA []byte) (int, error) i -= len(m.unknownFields) copy(dAtA[i:], m.unknownFields) } + if len(m.RequiredCapabilities) > 0 { + i -= 4 * len(m.RequiredCapabilities) + for iNdEx, v := range m.RequiredCapabilities { + binary.LittleEndian.PutUint32(dAtA[i+iNdEx*4:], uint32(v)) + } + i = protohelpers.EncodeVarint(dAtA, i, uint64(4*len(m.RequiredCapabilities))) + i-- + dAtA[i] = 0x52 + } if len(m.TechnicalUpdates) > 0 { for iNdEx := len(m.TechnicalUpdates) - 1; iNdEx >= 0; iNdEx-- { size, _ := m.TechnicalUpdates[iNdEx].MarshalToSizedBufferVT(dAtA[:i]) diff --git a/internal/proto/raftcmdpb/raft_cmd_reader.pb.go b/internal/proto/raftcmdpb/raft_cmd_reader.pb.go index 2245bd304b..b5e7bf6456 100644 --- a/internal/proto/raftcmdpb/raft_cmd_reader.pb.go +++ b/internal/proto/raftcmdpb/raft_cmd_reader.pb.go @@ -3872,6 +3872,7 @@ type ProposalReader interface { GetIdempotency() commonpb.IdempotencyReader GetSignature() signaturepb.SignedApplyBatchReader GetTechnicalUpdates() TechnicalUpdateListReader + GetRequiredCapabilities() []uint32 GetPayload() isProposal_Payload Mutate() *Proposal } @@ -3934,6 +3935,10 @@ func (r *proposalReadonly) GetTechnicalUpdates() TechnicalUpdateListReader { return NewTechnicalUpdateListReader((*Proposal)(r).GetTechnicalUpdates()) } +func (r *proposalReadonly) GetRequiredCapabilities() []uint32 { + return slices.Clone((*Proposal)(r).GetRequiredCapabilities()) +} + func (r *proposalReadonly) GetPayload() isProposal_Payload { return (*Proposal)(r).GetPayload() } diff --git a/internal/proto/raftcmdpb/raft_cmd_vtproto.pb.go b/internal/proto/raftcmdpb/raft_cmd_vtproto.pb.go index 732a54601f..6c701d53d1 100644 --- a/internal/proto/raftcmdpb/raft_cmd_vtproto.pb.go +++ b/internal/proto/raftcmdpb/raft_cmd_vtproto.pb.go @@ -1,5 +1,5 @@ // Code generated by protoc-gen-go-vtproto. DO NOT EDIT. -// protoc-gen-go-vtproto version: (devel) +// protoc-gen-go-vtproto version: v0.6.1-0.20240319094008-0393e58bdf10 // source: raft_cmd.proto package raftcmdpb @@ -1409,6 +1409,11 @@ func (m *Proposal) CloneVT() *Proposal { } r.TechnicalUpdates = tmpContainer } + if rhs := m.RequiredCapabilities; rhs != nil { + tmpContainer := make([]uint32, len(rhs)) + copy(tmpContainer, rhs) + r.RequiredCapabilities = tmpContainer + } if m.Payload != nil { r.Payload = m.Payload.(interface{ CloneVT() isProposal_Payload }).CloneVT() } @@ -4674,6 +4679,15 @@ func (this *Proposal) EqualVT(that *Proposal) bool { } } } + if len(this.RequiredCapabilities) != len(that.RequiredCapabilities) { + return false + } + for i, vx := range this.RequiredCapabilities { + vy := that.RequiredCapabilities[i] + if vx != vy { + return false + } + } return string(this.unknownFields) == string(that.unknownFields) } @@ -6059,6 +6073,10 @@ func (m *Order_LedgerScoped) MarshalToSizedBufferVT(dAtA []byte) (int, error) { i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0xa + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0xa } return len(dAtA) - i, nil } @@ -6078,6 +6096,10 @@ func (m *Order_SystemScoped) MarshalToSizedBufferVT(dAtA []byte) (int, error) { i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x12 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x12 } return len(dAtA) - i, nil } @@ -6210,6 +6232,10 @@ func (m *LedgerScopedOrder_Apply) MarshalToSizedBufferVT(dAtA []byte) (int, erro i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x12 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x12 } return len(dAtA) - i, nil } @@ -6229,6 +6255,10 @@ func (m *LedgerScopedOrder_CreateLedger) MarshalToSizedBufferVT(dAtA []byte) (in i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x1a + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x1a } return len(dAtA) - i, nil } @@ -6248,6 +6278,10 @@ func (m *LedgerScopedOrder_DeleteLedger) MarshalToSizedBufferVT(dAtA []byte) (in i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x22 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x22 } return len(dAtA) - i, nil } @@ -6267,6 +6301,10 @@ func (m *LedgerScopedOrder_MirrorIngest) MarshalToSizedBufferVT(dAtA []byte) (in i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x2a + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x2a } return len(dAtA) - i, nil } @@ -6286,6 +6324,10 @@ func (m *LedgerScopedOrder_PromoteLedger) MarshalToSizedBufferVT(dAtA []byte) (i i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x32 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x32 } return len(dAtA) - i, nil } @@ -6305,6 +6347,10 @@ func (m *LedgerScopedOrder_SaveLedgerMetadata) MarshalToSizedBufferVT(dAtA []byt i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x3a + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x3a } return len(dAtA) - i, nil } @@ -6324,6 +6370,10 @@ func (m *LedgerScopedOrder_DeleteLedgerMetadata) MarshalToSizedBufferVT(dAtA []b i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x42 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x42 } return len(dAtA) - i, nil } @@ -6343,6 +6393,10 @@ func (m *LedgerScopedOrder_SaveNumscript) MarshalToSizedBufferVT(dAtA []byte) (i i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x4a + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x4a } return len(dAtA) - i, nil } @@ -6362,6 +6416,10 @@ func (m *LedgerScopedOrder_CreatePreparedQuery) MarshalToSizedBufferVT(dAtA []by i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x52 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x52 } return len(dAtA) - i, nil } @@ -6381,6 +6439,10 @@ func (m *LedgerScopedOrder_UpdatePreparedQuery) MarshalToSizedBufferVT(dAtA []by i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x5a + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x5a } return len(dAtA) - i, nil } @@ -6400,6 +6462,10 @@ func (m *LedgerScopedOrder_DeletePreparedQuery) MarshalToSizedBufferVT(dAtA []by i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x62 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x62 } return len(dAtA) - i, nil } @@ -6461,6 +6527,10 @@ func (m *SystemScopedOrder_RegisterSigningKey) MarshalToSizedBufferVT(dAtA []byt i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0xa + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0xa } return len(dAtA) - i, nil } @@ -6480,6 +6550,10 @@ func (m *SystemScopedOrder_RevokeSigningKey) MarshalToSizedBufferVT(dAtA []byte) i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x12 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x12 } return len(dAtA) - i, nil } @@ -6499,6 +6573,10 @@ func (m *SystemScopedOrder_SetSigningConfig) MarshalToSizedBufferVT(dAtA []byte) i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x1a + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x1a } return len(dAtA) - i, nil } @@ -6518,6 +6596,10 @@ func (m *SystemScopedOrder_AddEventsSink) MarshalToSizedBufferVT(dAtA []byte) (i i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x22 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x22 } return len(dAtA) - i, nil } @@ -6537,6 +6619,10 @@ func (m *SystemScopedOrder_RemoveEventsSink) MarshalToSizedBufferVT(dAtA []byte) i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x2a + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x2a } return len(dAtA) - i, nil } @@ -6556,6 +6642,10 @@ func (m *SystemScopedOrder_SetMaintenanceMode) MarshalToSizedBufferVT(dAtA []byt i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x32 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x32 } return len(dAtA) - i, nil } @@ -6575,6 +6665,10 @@ func (m *SystemScopedOrder_CreateQueryCheckpoint) MarshalToSizedBufferVT(dAtA [] i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x3a + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x3a } return len(dAtA) - i, nil } @@ -6594,6 +6688,10 @@ func (m *SystemScopedOrder_DeleteQueryCheckpoint) MarshalToSizedBufferVT(dAtA [] i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x42 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x42 } return len(dAtA) - i, nil } @@ -6613,6 +6711,10 @@ func (m *SystemScopedOrder_SetQueryCheckpointSchedule) MarshalToSizedBufferVT(dA i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x4a + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x4a } return len(dAtA) - i, nil } @@ -6632,6 +6734,10 @@ func (m *SystemScopedOrder_DeleteQueryCheckpointSchedule) MarshalToSizedBufferVT i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x52 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x52 } return len(dAtA) - i, nil } @@ -6651,6 +6757,10 @@ func (m *SystemScopedOrder_SetClusterPolicy) MarshalToSizedBufferVT(dAtA []byte) i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x5a + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x5a } return len(dAtA) - i, nil } @@ -7611,6 +7721,10 @@ func (m *MirrorLogEntry_CreatedTransaction) MarshalToSizedBufferVT(dAtA []byte) i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x1a + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x1a } return len(dAtA) - i, nil } @@ -7630,6 +7744,10 @@ func (m *MirrorLogEntry_SavedMetadata) MarshalToSizedBufferVT(dAtA []byte) (int, i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x22 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x22 } return len(dAtA) - i, nil } @@ -7649,6 +7767,10 @@ func (m *MirrorLogEntry_RevertedTransaction) MarshalToSizedBufferVT(dAtA []byte) i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x2a + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x2a } return len(dAtA) - i, nil } @@ -7668,6 +7790,10 @@ func (m *MirrorLogEntry_DeletedMetadata) MarshalToSizedBufferVT(dAtA []byte) (in i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x32 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x32 } return len(dAtA) - i, nil } @@ -7687,6 +7813,10 @@ func (m *MirrorLogEntry_FillGap) MarshalToSizedBufferVT(dAtA []byte) (int, error i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x3a + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x3a } return len(dAtA) - i, nil } @@ -8193,6 +8323,10 @@ func (m *LedgerApplyOrder_CreateTransaction) MarshalToSizedBufferVT(dAtA []byte) i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0xa + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0xa } return len(dAtA) - i, nil } @@ -8212,6 +8346,10 @@ func (m *LedgerApplyOrder_AddMetadata) MarshalToSizedBufferVT(dAtA []byte) (int, i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x12 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x12 } return len(dAtA) - i, nil } @@ -8231,6 +8369,10 @@ func (m *LedgerApplyOrder_RevertTransaction) MarshalToSizedBufferVT(dAtA []byte) i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x1a + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x1a } return len(dAtA) - i, nil } @@ -8250,6 +8392,10 @@ func (m *LedgerApplyOrder_DeleteMetadata) MarshalToSizedBufferVT(dAtA []byte) (i i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x22 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x22 } return len(dAtA) - i, nil } @@ -8269,6 +8415,10 @@ func (m *LedgerApplyOrder_SetMetadataFieldType) MarshalToSizedBufferVT(dAtA []by i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x2a + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x2a } return len(dAtA) - i, nil } @@ -8288,6 +8438,10 @@ func (m *LedgerApplyOrder_RemoveMetadataFieldType) MarshalToSizedBufferVT(dAtA [ i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x32 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x32 } return len(dAtA) - i, nil } @@ -8307,6 +8461,10 @@ func (m *LedgerApplyOrder_CreateIndex) MarshalToSizedBufferVT(dAtA []byte) (int, i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x3a + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x3a } return len(dAtA) - i, nil } @@ -8326,6 +8484,10 @@ func (m *LedgerApplyOrder_DropIndex) MarshalToSizedBufferVT(dAtA []byte) (int, e i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x42 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x42 } return len(dAtA) - i, nil } @@ -8345,6 +8507,10 @@ func (m *LedgerApplyOrder_AddAccountType) MarshalToSizedBufferVT(dAtA []byte) (i i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x4a + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x4a } return len(dAtA) - i, nil } @@ -8364,6 +8530,10 @@ func (m *LedgerApplyOrder_RemoveAccountType) MarshalToSizedBufferVT(dAtA []byte) i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x52 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x52 } return len(dAtA) - i, nil } @@ -8383,6 +8553,10 @@ func (m *LedgerApplyOrder_UpdateDefaultEnforcementMode) MarshalToSizedBufferVT(d i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x5a + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x5a } return len(dAtA) - i, nil } @@ -9320,6 +9494,15 @@ func (m *Proposal) MarshalToSizedBufferVT(dAtA []byte) (int, error) { } i -= size } + if len(m.RequiredCapabilities) > 0 { + for iNdEx := len(m.RequiredCapabilities) - 1; iNdEx >= 0; iNdEx-- { + i -= 4 + binary.LittleEndian.PutUint32(dAtA[i:], uint32(m.RequiredCapabilities[iNdEx])) + } + i = protohelpers.EncodeVarint(dAtA, i, uint64(len(m.RequiredCapabilities)*4)) + i-- + dAtA[i] = 0x52 + } if len(m.TechnicalUpdates) > 0 { for iNdEx := len(m.TechnicalUpdates) - 1; iNdEx >= 0; iNdEx-- { size, err := m.TechnicalUpdates[iNdEx].MarshalToSizedBufferVT(dAtA[:i]) @@ -9425,6 +9608,10 @@ func (m *Proposal_Batch) MarshalToSizedBufferVT(dAtA []byte) (int, error) { i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x5a + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x5a } return len(dAtA) - i, nil } @@ -9444,6 +9631,10 @@ func (m *Proposal_SetClusterPolicy) MarshalToSizedBufferVT(dAtA []byte) (int, er i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x62 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x62 } return len(dAtA) - i, nil } @@ -9512,6 +9703,10 @@ func (m *TechnicalUpdate_MirrorSync) MarshalToSizedBufferVT(dAtA []byte) (int, e i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x12 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x12 } return len(dAtA) - i, nil } @@ -9531,6 +9726,10 @@ func (m *TechnicalUpdate_EventsSink) MarshalToSizedBufferVT(dAtA []byte) (int, e i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x1a + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x1a } return len(dAtA) - i, nil } @@ -9550,6 +9749,10 @@ func (m *TechnicalUpdate_IdempotencyEviction) MarshalToSizedBufferVT(dAtA []byte i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x22 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x22 } return len(dAtA) - i, nil } @@ -9569,6 +9772,10 @@ func (m *TechnicalUpdate_ClusterConfig) MarshalToSizedBufferVT(dAtA []byte) (int i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x2a + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x2a } return len(dAtA) - i, nil } @@ -9588,6 +9795,10 @@ func (m *TechnicalUpdate_BackupOrder) MarshalToSizedBufferVT(dAtA []byte) (int, i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x32 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x32 } return len(dAtA) - i, nil } @@ -9607,6 +9818,10 @@ func (m *TechnicalUpdate_IncrementalBackupOrder) MarshalToSizedBufferVT(dAtA []b i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x3a + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x3a } return len(dAtA) - i, nil } @@ -9640,6 +9855,10 @@ func (m *TechnicalUpdate_DrainMarker) MarshalToSizedBufferVT(dAtA []byte) (int, i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x4a + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x4a } return len(dAtA) - i, nil } @@ -9659,6 +9878,10 @@ func (m *TechnicalUpdate_DrainRelease) MarshalToSizedBufferVT(dAtA []byte) (int, i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x52 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x52 } return len(dAtA) - i, nil } @@ -9915,6 +10138,10 @@ func (m *BackupDestination_S3) MarshalToSizedBufferVT(dAtA []byte) (int, error) i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x1a + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x1a } return len(dAtA) - i, nil } @@ -9934,6 +10161,10 @@ func (m *BackupDestination_Azure) MarshalToSizedBufferVT(dAtA []byte) (int, erro i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x22 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x22 } return len(dAtA) - i, nil } @@ -10216,6 +10447,10 @@ func (m *BackupOrder_Start) MarshalToSizedBufferVT(dAtA []byte) (int, error) { i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0xa + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0xa } return len(dAtA) - i, nil } @@ -10235,6 +10470,10 @@ func (m *BackupOrder_Complete) MarshalToSizedBufferVT(dAtA []byte) (int, error) i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x12 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x12 } return len(dAtA) - i, nil } @@ -10254,6 +10493,10 @@ func (m *BackupOrder_Fail) MarshalToSizedBufferVT(dAtA []byte) (int, error) { i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x1a + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x1a } return len(dAtA) - i, nil } @@ -10315,6 +10558,10 @@ func (m *IncrementalBackupOrder_Start) MarshalToSizedBufferVT(dAtA []byte) (int, i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0xa + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0xa } return len(dAtA) - i, nil } @@ -10334,6 +10581,10 @@ func (m *IncrementalBackupOrder_Complete) MarshalToSizedBufferVT(dAtA []byte) (i i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x12 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x12 } return len(dAtA) - i, nil } @@ -10353,6 +10604,10 @@ func (m *IncrementalBackupOrder_Fail) MarshalToSizedBufferVT(dAtA []byte) (int, i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x1a + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x1a } return len(dAtA) - i, nil } @@ -10765,6 +11020,10 @@ func (m *CreatedLogOrReference_CreatedLog) MarshalToSizedBufferVT(dAtA []byte) ( i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0xa + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0xa } return len(dAtA) - i, nil } @@ -11411,12 +11670,14 @@ func (m *Proposal) ResetVT() { mm.ResetVT() } f1 := m.TechnicalUpdates[:0] + f2 := m.RequiredCapabilities[:0] if oneof, ok := m.Payload.(*Proposal_Batch); ok { oneof.Batch.ReturnToVTPool() } m.Reset() m.Orders = f0 m.TechnicalUpdates = f1 + m.RequiredCapabilities = f2 } } func (m *Proposal) ReturnToVTPool() { @@ -11579,6 +11840,8 @@ func (m *Order_LedgerScoped) SizeVT() (n int) { if m.LedgerScoped != nil { l = m.LedgerScoped.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -11591,6 +11854,8 @@ func (m *Order_SystemScoped) SizeVT() (n int) { if m.SystemScoped != nil { l = m.SystemScoped.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -11645,6 +11910,8 @@ func (m *LedgerScopedOrder_Apply) SizeVT() (n int) { if m.Apply != nil { l = m.Apply.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -11657,6 +11924,8 @@ func (m *LedgerScopedOrder_CreateLedger) SizeVT() (n int) { if m.CreateLedger != nil { l = m.CreateLedger.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -11669,6 +11938,8 @@ func (m *LedgerScopedOrder_DeleteLedger) SizeVT() (n int) { if m.DeleteLedger != nil { l = m.DeleteLedger.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -11681,6 +11952,8 @@ func (m *LedgerScopedOrder_MirrorIngest) SizeVT() (n int) { if m.MirrorIngest != nil { l = m.MirrorIngest.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -11693,6 +11966,8 @@ func (m *LedgerScopedOrder_PromoteLedger) SizeVT() (n int) { if m.PromoteLedger != nil { l = m.PromoteLedger.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -11705,6 +11980,8 @@ func (m *LedgerScopedOrder_SaveLedgerMetadata) SizeVT() (n int) { if m.SaveLedgerMetadata != nil { l = m.SaveLedgerMetadata.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -11717,6 +11994,8 @@ func (m *LedgerScopedOrder_DeleteLedgerMetadata) SizeVT() (n int) { if m.DeleteLedgerMetadata != nil { l = m.DeleteLedgerMetadata.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -11729,6 +12008,8 @@ func (m *LedgerScopedOrder_SaveNumscript) SizeVT() (n int) { if m.SaveNumscript != nil { l = m.SaveNumscript.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -11741,6 +12022,8 @@ func (m *LedgerScopedOrder_CreatePreparedQuery) SizeVT() (n int) { if m.CreatePreparedQuery != nil { l = m.CreatePreparedQuery.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -11753,6 +12036,8 @@ func (m *LedgerScopedOrder_UpdatePreparedQuery) SizeVT() (n int) { if m.UpdatePreparedQuery != nil { l = m.UpdatePreparedQuery.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -11765,6 +12050,8 @@ func (m *LedgerScopedOrder_DeletePreparedQuery) SizeVT() (n int) { if m.DeletePreparedQuery != nil { l = m.DeletePreparedQuery.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -11790,6 +12077,8 @@ func (m *SystemScopedOrder_RegisterSigningKey) SizeVT() (n int) { if m.RegisterSigningKey != nil { l = m.RegisterSigningKey.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -11802,6 +12091,8 @@ func (m *SystemScopedOrder_RevokeSigningKey) SizeVT() (n int) { if m.RevokeSigningKey != nil { l = m.RevokeSigningKey.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -11814,6 +12105,8 @@ func (m *SystemScopedOrder_SetSigningConfig) SizeVT() (n int) { if m.SetSigningConfig != nil { l = m.SetSigningConfig.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -11826,6 +12119,8 @@ func (m *SystemScopedOrder_AddEventsSink) SizeVT() (n int) { if m.AddEventsSink != nil { l = m.AddEventsSink.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -11838,6 +12133,8 @@ func (m *SystemScopedOrder_RemoveEventsSink) SizeVT() (n int) { if m.RemoveEventsSink != nil { l = m.RemoveEventsSink.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -11850,6 +12147,8 @@ func (m *SystemScopedOrder_SetMaintenanceMode) SizeVT() (n int) { if m.SetMaintenanceMode != nil { l = m.SetMaintenanceMode.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -11862,6 +12161,8 @@ func (m *SystemScopedOrder_CreateQueryCheckpoint) SizeVT() (n int) { if m.CreateQueryCheckpoint != nil { l = m.CreateQueryCheckpoint.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -11874,6 +12175,8 @@ func (m *SystemScopedOrder_DeleteQueryCheckpoint) SizeVT() (n int) { if m.DeleteQueryCheckpoint != nil { l = m.DeleteQueryCheckpoint.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -11886,6 +12189,8 @@ func (m *SystemScopedOrder_SetQueryCheckpointSchedule) SizeVT() (n int) { if m.SetQueryCheckpointSchedule != nil { l = m.SetQueryCheckpointSchedule.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -11898,6 +12203,8 @@ func (m *SystemScopedOrder_DeleteQueryCheckpointSchedule) SizeVT() (n int) { if m.DeleteQueryCheckpointSchedule != nil { l = m.DeleteQueryCheckpointSchedule.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -11910,6 +12217,8 @@ func (m *SystemScopedOrder_SetClusterPolicy) SizeVT() (n int) { if m.SetClusterPolicy != nil { l = m.SetClusterPolicy.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -12263,6 +12572,8 @@ func (m *MirrorLogEntry_CreatedTransaction) SizeVT() (n int) { if m.CreatedTransaction != nil { l = m.CreatedTransaction.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -12275,6 +12586,8 @@ func (m *MirrorLogEntry_SavedMetadata) SizeVT() (n int) { if m.SavedMetadata != nil { l = m.SavedMetadata.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -12287,6 +12600,8 @@ func (m *MirrorLogEntry_RevertedTransaction) SizeVT() (n int) { if m.RevertedTransaction != nil { l = m.RevertedTransaction.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -12299,6 +12614,8 @@ func (m *MirrorLogEntry_DeletedMetadata) SizeVT() (n int) { if m.DeletedMetadata != nil { l = m.DeletedMetadata.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -12311,6 +12628,8 @@ func (m *MirrorLogEntry_FillGap) SizeVT() (n int) { if m.FillGap != nil { l = m.FillGap.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -12513,6 +12832,8 @@ func (m *LedgerApplyOrder_CreateTransaction) SizeVT() (n int) { if m.CreateTransaction != nil { l = m.CreateTransaction.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -12525,6 +12846,8 @@ func (m *LedgerApplyOrder_AddMetadata) SizeVT() (n int) { if m.AddMetadata != nil { l = m.AddMetadata.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -12537,6 +12860,8 @@ func (m *LedgerApplyOrder_RevertTransaction) SizeVT() (n int) { if m.RevertTransaction != nil { l = m.RevertTransaction.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -12549,6 +12874,8 @@ func (m *LedgerApplyOrder_DeleteMetadata) SizeVT() (n int) { if m.DeleteMetadata != nil { l = m.DeleteMetadata.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -12561,6 +12888,8 @@ func (m *LedgerApplyOrder_SetMetadataFieldType) SizeVT() (n int) { if m.SetMetadataFieldType != nil { l = m.SetMetadataFieldType.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -12573,6 +12902,8 @@ func (m *LedgerApplyOrder_RemoveMetadataFieldType) SizeVT() (n int) { if m.RemoveMetadataFieldType != nil { l = m.RemoveMetadataFieldType.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -12585,6 +12916,8 @@ func (m *LedgerApplyOrder_CreateIndex) SizeVT() (n int) { if m.CreateIndex != nil { l = m.CreateIndex.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -12597,6 +12930,8 @@ func (m *LedgerApplyOrder_DropIndex) SizeVT() (n int) { if m.DropIndex != nil { l = m.DropIndex.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -12609,6 +12944,8 @@ func (m *LedgerApplyOrder_AddAccountType) SizeVT() (n int) { if m.AddAccountType != nil { l = m.AddAccountType.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -12621,6 +12958,8 @@ func (m *LedgerApplyOrder_RemoveAccountType) SizeVT() (n int) { if m.RemoveAccountType != nil { l = m.RemoveAccountType.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -12633,6 +12972,8 @@ func (m *LedgerApplyOrder_UpdateDefaultEnforcementMode) SizeVT() (n int) { if m.UpdateDefaultEnforcementMode != nil { l = m.UpdateDefaultEnforcementMode.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -13023,6 +13364,9 @@ func (m *Proposal) SizeVT() (n int) { n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) } } + if len(m.RequiredCapabilities) > 0 { + n += 1 + protohelpers.SizeOfVarint(uint64(len(m.RequiredCapabilities)*4)) + len(m.RequiredCapabilities)*4 + } if vtmsg, ok := m.Payload.(interface{ SizeVT() int }); ok { n += vtmsg.SizeVT() } @@ -13039,6 +13383,8 @@ func (m *Proposal_Batch) SizeVT() (n int) { if m.Batch != nil { l = m.Batch.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -13051,6 +13397,8 @@ func (m *Proposal_SetClusterPolicy) SizeVT() (n int) { if m.SetClusterPolicy != nil { l = m.SetClusterPolicy.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -13080,6 +13428,8 @@ func (m *TechnicalUpdate_MirrorSync) SizeVT() (n int) { if m.MirrorSync != nil { l = m.MirrorSync.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -13092,6 +13442,8 @@ func (m *TechnicalUpdate_EventsSink) SizeVT() (n int) { if m.EventsSink != nil { l = m.EventsSink.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -13104,6 +13456,8 @@ func (m *TechnicalUpdate_IdempotencyEviction) SizeVT() (n int) { if m.IdempotencyEviction != nil { l = m.IdempotencyEviction.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -13116,6 +13470,8 @@ func (m *TechnicalUpdate_ClusterConfig) SizeVT() (n int) { if m.ClusterConfig != nil { l = m.ClusterConfig.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -13128,6 +13484,8 @@ func (m *TechnicalUpdate_BackupOrder) SizeVT() (n int) { if m.BackupOrder != nil { l = m.BackupOrder.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -13140,6 +13498,8 @@ func (m *TechnicalUpdate_IncrementalBackupOrder) SizeVT() (n int) { if m.IncrementalBackupOrder != nil { l = m.IncrementalBackupOrder.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -13162,6 +13522,8 @@ func (m *TechnicalUpdate_DrainMarker) SizeVT() (n int) { if m.DrainMarker != nil { l = m.DrainMarker.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -13174,6 +13536,8 @@ func (m *TechnicalUpdate_DrainRelease) SizeVT() (n int) { if m.DrainRelease != nil { l = m.DrainRelease.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -13275,6 +13639,8 @@ func (m *BackupDestination_S3) SizeVT() (n int) { if m.S3 != nil { l = m.S3.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -13287,6 +13653,8 @@ func (m *BackupDestination_Azure) SizeVT() (n int) { if m.Azure != nil { l = m.Azure.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -13411,6 +13779,8 @@ func (m *BackupOrder_Start) SizeVT() (n int) { if m.Start != nil { l = m.Start.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -13423,6 +13793,8 @@ func (m *BackupOrder_Complete) SizeVT() (n int) { if m.Complete != nil { l = m.Complete.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -13435,6 +13807,8 @@ func (m *BackupOrder_Fail) SizeVT() (n int) { if m.Fail != nil { l = m.Fail.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -13460,6 +13834,8 @@ func (m *IncrementalBackupOrder_Start) SizeVT() (n int) { if m.Start != nil { l = m.Start.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -13472,6 +13848,8 @@ func (m *IncrementalBackupOrder_Complete) SizeVT() (n int) { if m.Complete != nil { l = m.Complete.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -13484,6 +13862,8 @@ func (m *IncrementalBackupOrder_Fail) SizeVT() (n int) { if m.Fail != nil { l = m.Fail.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -13645,6 +14025,8 @@ func (m *CreatedLogOrReference_CreatedLog) SizeVT() (n int) { if m.CreatedLog != nil { l = m.CreatedLog.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -22116,6 +22498,58 @@ func (m *Proposal) UnmarshalVT(dAtA []byte) error { return err } iNdEx = postIndex + case 10: + if wireType == 5 { + var v uint32 + if (iNdEx + 4) > l { + return io.ErrUnexpectedEOF + } + v = uint32(binary.LittleEndian.Uint32(dAtA[iNdEx:])) + iNdEx += 4 + m.RequiredCapabilities = append(m.RequiredCapabilities, v) + } else if wireType == 2 { + var packedLen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + packedLen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if packedLen < 0 { + return protohelpers.ErrInvalidLength + } + postIndex := iNdEx + packedLen + if postIndex < 0 { + return protohelpers.ErrInvalidLength + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + var elementCount int + elementCount = packedLen / 4 + if elementCount != 0 && len(m.RequiredCapabilities) == 0 && cap(m.RequiredCapabilities) < elementCount { + m.RequiredCapabilities = make([]uint32, 0, elementCount) + } + for iNdEx < postIndex { + var v uint32 + if (iNdEx + 4) > l { + return io.ErrUnexpectedEOF + } + v = uint32(binary.LittleEndian.Uint32(dAtA[iNdEx:])) + iNdEx += 4 + m.RequiredCapabilities = append(m.RequiredCapabilities, v) + } + } else { + return fmt.Errorf("proto: wrong wireType = %d for field RequiredCapabilities", wireType) + } case 11: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field Batch", wireType) diff --git a/internal/proto/rafttransportpb/raft_transport.pb.go b/internal/proto/rafttransportpb/raft_transport.pb.go index 1a4e713e42..e3b4e8b5fa 100644 --- a/internal/proto/rafttransportpb/raft_transport.pb.go +++ b/internal/proto/rafttransportpb/raft_transport.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.12 -// protoc v7.34.2 +// protoc v7.36.2 // source: raft_transport.proto package rafttransportpb diff --git a/internal/proto/rafttransportpb/raft_transport_grpc.pb.go b/internal/proto/rafttransportpb/raft_transport_grpc.pb.go index 5a2bec35d2..7ccd746846 100644 --- a/internal/proto/rafttransportpb/raft_transport_grpc.pb.go +++ b/internal/proto/rafttransportpb/raft_transport_grpc.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: // - protoc-gen-go-grpc v1.6.2 -// - protoc v7.34.2 +// - protoc v7.36.2 // source: raft_transport.proto package rafttransportpb diff --git a/internal/proto/rafttransportpb/raft_transport_vtproto.pb.go b/internal/proto/rafttransportpb/raft_transport_vtproto.pb.go index c147702af3..c33b070551 100644 --- a/internal/proto/rafttransportpb/raft_transport_vtproto.pb.go +++ b/internal/proto/rafttransportpb/raft_transport_vtproto.pb.go @@ -1,5 +1,5 @@ // Code generated by protoc-gen-go-vtproto. DO NOT EDIT. -// protoc-gen-go-vtproto version: (devel) +// protoc-gen-go-vtproto version: v0.6.1-0.20240319094008-0393e58bdf10 // source: raft_transport.proto package rafttransportpb @@ -1078,6 +1078,10 @@ func (m *SendMessageRequest_Raft) MarshalToSizedBufferVT(dAtA []byte) (int, erro i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0xa + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0xa } return len(dAtA) - i, nil } @@ -1097,6 +1101,10 @@ func (m *SendMessageRequest_Ping) MarshalToSizedBufferVT(dAtA []byte) (int, erro i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x12 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x12 } return len(dAtA) - i, nil } @@ -1158,6 +1166,10 @@ func (m *SendMessageResponse_Raft) MarshalToSizedBufferVT(dAtA []byte) (int, err i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0xa + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0xa } return len(dAtA) - i, nil } @@ -1177,6 +1189,10 @@ func (m *SendMessageResponse_Pong) MarshalToSizedBufferVT(dAtA []byte) (int, err i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x12 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x12 } return len(dAtA) - i, nil } @@ -1345,6 +1361,8 @@ func (m *SendMessageRequest_Raft) SizeVT() (n int) { if m.Raft != nil { l = m.Raft.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -1357,6 +1375,8 @@ func (m *SendMessageRequest_Ping) SizeVT() (n int) { if m.Ping != nil { l = m.Ping.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -1382,6 +1402,8 @@ func (m *SendMessageResponse_Raft) SizeVT() (n int) { if m.Raft != nil { l = m.Raft.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -1394,6 +1416,8 @@ func (m *SendMessageResponse_Pong) SizeVT() (n int) { if m.Pong != nil { l = m.Pong.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } diff --git a/internal/proto/restorepb/restore.pb.go b/internal/proto/restorepb/restore.pb.go index c881e82769..1593fd5753 100644 --- a/internal/proto/restorepb/restore.pb.go +++ b/internal/proto/restorepb/restore.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.12 -// protoc v7.34.2 +// protoc v7.36.2 // source: restore.proto package restorepb diff --git a/internal/proto/restorepb/restore_grpc.pb.go b/internal/proto/restorepb/restore_grpc.pb.go index 51b2ef4603..1b06f5b71a 100644 --- a/internal/proto/restorepb/restore_grpc.pb.go +++ b/internal/proto/restorepb/restore_grpc.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: // - protoc-gen-go-grpc v1.6.2 -// - protoc v7.34.2 +// - protoc v7.36.2 // source: restore.proto package restorepb diff --git a/internal/proto/restorepb/restore_vtproto.pb.go b/internal/proto/restorepb/restore_vtproto.pb.go index 3b069df5fc..8f879105cf 100644 --- a/internal/proto/restorepb/restore_vtproto.pb.go +++ b/internal/proto/restorepb/restore_vtproto.pb.go @@ -1,5 +1,5 @@ // Code generated by protoc-gen-go-vtproto. DO NOT EDIT. -// protoc-gen-go-vtproto version: (devel) +// protoc-gen-go-vtproto version: v0.6.1-0.20240319094008-0393e58bdf10 // source: restore.proto package restorepb @@ -1038,6 +1038,10 @@ func (m *ValidateRestoreEvent_Error) MarshalToSizedBufferVT(dAtA []byte) (int, e i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0xa + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0xa } return len(dAtA) - i, nil } @@ -1057,6 +1061,10 @@ func (m *ValidateRestoreEvent_Progress) MarshalToSizedBufferVT(dAtA []byte) (int i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x12 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x12 } return len(dAtA) - i, nil } @@ -1457,6 +1465,8 @@ func (m *ValidateRestoreEvent_Error) SizeVT() (n int) { if m.Error != nil { l = m.Error.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -1469,6 +1479,8 @@ func (m *ValidateRestoreEvent_Progress) SizeVT() (n int) { if m.Progress != nil { l = m.Progress.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } diff --git a/internal/proto/servicepb/bucket.pb.go b/internal/proto/servicepb/bucket.pb.go index 6ff8f2f98b..3c0f643c36 100644 --- a/internal/proto/servicepb/bucket.pb.go +++ b/internal/proto/servicepb/bucket.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.12 -// protoc v7.34.2 +// protoc v7.36.2 // source: bucket.proto package servicepb diff --git a/internal/proto/servicepb/bucket_grpc.pb.go b/internal/proto/servicepb/bucket_grpc.pb.go index f7b6b63aa7..9f4f2e6305 100644 --- a/internal/proto/servicepb/bucket_grpc.pb.go +++ b/internal/proto/servicepb/bucket_grpc.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: // - protoc-gen-go-grpc v1.6.2 -// - protoc v7.34.2 +// - protoc v7.36.2 // source: bucket.proto package servicepb diff --git a/internal/proto/servicepb/bucket_vtproto.pb.go b/internal/proto/servicepb/bucket_vtproto.pb.go index 008f41bbac..1994cbe441 100644 --- a/internal/proto/servicepb/bucket_vtproto.pb.go +++ b/internal/proto/servicepb/bucket_vtproto.pb.go @@ -1,5 +1,5 @@ // Code generated by protoc-gen-go-vtproto. DO NOT EDIT. -// protoc-gen-go-vtproto version: (devel) +// protoc-gen-go-vtproto version: v0.6.1-0.20240319094008-0393e58bdf10 // source: bucket.proto package servicepb @@ -8509,6 +8509,10 @@ func (m *ApplyRequest_Unsigned) MarshalToSizedBufferVT(dAtA []byte) (int, error) i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0xa + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0xa } return len(dAtA) - i, nil } @@ -8528,6 +8532,10 @@ func (m *ApplyRequest_Signed) MarshalToSizedBufferVT(dAtA []byte) (int, error) { i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x12 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x12 } return len(dAtA) - i, nil } @@ -8686,6 +8694,10 @@ func (m *Request_Apply) MarshalToSizedBufferVT(dAtA []byte) (int, error) { i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0xa + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0xa } return len(dAtA) - i, nil } @@ -8705,6 +8717,10 @@ func (m *Request_CreateLedger) MarshalToSizedBufferVT(dAtA []byte) (int, error) i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x12 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x12 } return len(dAtA) - i, nil } @@ -8724,6 +8740,10 @@ func (m *Request_DeleteLedger) MarshalToSizedBufferVT(dAtA []byte) (int, error) i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x1a + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x1a } return len(dAtA) - i, nil } @@ -8743,6 +8763,10 @@ func (m *Request_RegisterSigningKey) MarshalToSizedBufferVT(dAtA []byte) (int, e i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x22 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x22 } return len(dAtA) - i, nil } @@ -8762,6 +8786,10 @@ func (m *Request_RevokeSigningKey) MarshalToSizedBufferVT(dAtA []byte) (int, err i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x2a + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x2a } return len(dAtA) - i, nil } @@ -8781,6 +8809,10 @@ func (m *Request_SetSigningConfig) MarshalToSizedBufferVT(dAtA []byte) (int, err i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x32 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x32 } return len(dAtA) - i, nil } @@ -8800,6 +8832,10 @@ func (m *Request_AddEventsSink) MarshalToSizedBufferVT(dAtA []byte) (int, error) i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x3a + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x3a } return len(dAtA) - i, nil } @@ -8819,6 +8855,10 @@ func (m *Request_RemoveEventsSink) MarshalToSizedBufferVT(dAtA []byte) (int, err i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x42 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x42 } return len(dAtA) - i, nil } @@ -8838,6 +8878,10 @@ func (m *Request_SetMaintenanceMode) MarshalToSizedBufferVT(dAtA []byte) (int, e i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x4a + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x4a } return len(dAtA) - i, nil } @@ -8857,6 +8901,10 @@ func (m *Request_SetMetadataFieldType) MarshalToSizedBufferVT(dAtA []byte) (int, i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x52 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x52 } return len(dAtA) - i, nil } @@ -8876,6 +8924,10 @@ func (m *Request_RemoveMetadataFieldType) MarshalToSizedBufferVT(dAtA []byte) (i i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x5a + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x5a } return len(dAtA) - i, nil } @@ -8895,6 +8947,10 @@ func (m *Request_PromoteLedger) MarshalToSizedBufferVT(dAtA []byte) (int, error) i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x62 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x62 } return len(dAtA) - i, nil } @@ -8914,6 +8970,10 @@ func (m *Request_CreatePreparedQuery) MarshalToSizedBufferVT(dAtA []byte) (int, i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x6a + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x6a } return len(dAtA) - i, nil } @@ -8933,6 +8993,10 @@ func (m *Request_UpdatePreparedQuery) MarshalToSizedBufferVT(dAtA []byte) (int, i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x72 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x72 } return len(dAtA) - i, nil } @@ -8952,6 +9016,10 @@ func (m *Request_DeletePreparedQuery) MarshalToSizedBufferVT(dAtA []byte) (int, i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x7a + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x7a } return len(dAtA) - i, nil } @@ -8973,6 +9041,12 @@ func (m *Request_CreateIndex) MarshalToSizedBufferVT(dAtA []byte) (int, error) { dAtA[i] = 0x1 i-- dAtA[i] = 0x82 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x1 + i-- + dAtA[i] = 0x82 } return len(dAtA) - i, nil } @@ -8994,6 +9068,12 @@ func (m *Request_DropIndex) MarshalToSizedBufferVT(dAtA []byte) (int, error) { dAtA[i] = 0x1 i-- dAtA[i] = 0x8a + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x1 + i-- + dAtA[i] = 0x8a } return len(dAtA) - i, nil } @@ -9015,6 +9095,12 @@ func (m *Request_SaveNumscript) MarshalToSizedBufferVT(dAtA []byte) (int, error) dAtA[i] = 0x1 i-- dAtA[i] = 0x92 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x1 + i-- + dAtA[i] = 0x92 } return len(dAtA) - i, nil } @@ -9036,6 +9122,12 @@ func (m *Request_AddAccountType) MarshalToSizedBufferVT(dAtA []byte) (int, error dAtA[i] = 0x1 i-- dAtA[i] = 0x9a + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x1 + i-- + dAtA[i] = 0x9a } return len(dAtA) - i, nil } @@ -9057,6 +9149,12 @@ func (m *Request_RemoveAccountType) MarshalToSizedBufferVT(dAtA []byte) (int, er dAtA[i] = 0x1 i-- dAtA[i] = 0xa2 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x1 + i-- + dAtA[i] = 0xa2 } return len(dAtA) - i, nil } @@ -9078,6 +9176,12 @@ func (m *Request_SetDefaultEnforcementMode) MarshalToSizedBufferVT(dAtA []byte) dAtA[i] = 0x1 i-- dAtA[i] = 0xaa + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x1 + i-- + dAtA[i] = 0xaa } return len(dAtA) - i, nil } @@ -9099,6 +9203,12 @@ func (m *Request_CreateQueryCheckpoint) MarshalToSizedBufferVT(dAtA []byte) (int dAtA[i] = 0x1 i-- dAtA[i] = 0xb2 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x1 + i-- + dAtA[i] = 0xb2 } return len(dAtA) - i, nil } @@ -9120,6 +9230,12 @@ func (m *Request_DeleteQueryCheckpoint) MarshalToSizedBufferVT(dAtA []byte) (int dAtA[i] = 0x1 i-- dAtA[i] = 0xba + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x1 + i-- + dAtA[i] = 0xba } return len(dAtA) - i, nil } @@ -9141,6 +9257,12 @@ func (m *Request_SetQueryCheckpointSchedule) MarshalToSizedBufferVT(dAtA []byte) dAtA[i] = 0x1 i-- dAtA[i] = 0xc2 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x1 + i-- + dAtA[i] = 0xc2 } return len(dAtA) - i, nil } @@ -9162,6 +9284,12 @@ func (m *Request_DeleteQueryCheckpointSchedule) MarshalToSizedBufferVT(dAtA []by dAtA[i] = 0x1 i-- dAtA[i] = 0xca + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x1 + i-- + dAtA[i] = 0xca } return len(dAtA) - i, nil } @@ -9183,6 +9311,12 @@ func (m *Request_SaveLedgerMetadata) MarshalToSizedBufferVT(dAtA []byte) (int, e dAtA[i] = 0x1 i-- dAtA[i] = 0xd2 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x1 + i-- + dAtA[i] = 0xd2 } return len(dAtA) - i, nil } @@ -9204,6 +9338,12 @@ func (m *Request_DeleteLedgerMetadata) MarshalToSizedBufferVT(dAtA []byte) (int, dAtA[i] = 0x1 i-- dAtA[i] = 0xda + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x1 + i-- + dAtA[i] = 0xda } return len(dAtA) - i, nil } @@ -11132,6 +11272,10 @@ func (m *LedgerAction_CreateTransaction) MarshalToSizedBufferVT(dAtA []byte) (in i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0xa + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0xa } return len(dAtA) - i, nil } @@ -11151,6 +11295,10 @@ func (m *LedgerAction_AddMetadata) MarshalToSizedBufferVT(dAtA []byte) (int, err i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x12 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x12 } return len(dAtA) - i, nil } @@ -11170,6 +11318,10 @@ func (m *LedgerAction_RevertTransaction) MarshalToSizedBufferVT(dAtA []byte) (in i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x1a + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x1a } return len(dAtA) - i, nil } @@ -11189,6 +11341,10 @@ func (m *LedgerAction_DeleteMetadata) MarshalToSizedBufferVT(dAtA []byte) (int, i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x22 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x22 } return len(dAtA) - i, nil } @@ -11208,6 +11364,10 @@ func (m *LedgerAction_AddAccountType) MarshalToSizedBufferVT(dAtA []byte) (int, i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x2a + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x2a } return len(dAtA) - i, nil } @@ -11227,6 +11387,10 @@ func (m *LedgerAction_RemoveAccountType) MarshalToSizedBufferVT(dAtA []byte) (in i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x32 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x32 } return len(dAtA) - i, nil } @@ -11246,6 +11410,10 @@ func (m *LedgerAction_SetDefaultEnforcementMode) MarshalToSizedBufferVT(dAtA []b i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x3a + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x3a } return len(dAtA) - i, nil } @@ -12645,6 +12813,10 @@ func (m *CheckStoreEvent_Error) MarshalToSizedBufferVT(dAtA []byte) (int, error) i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0xa + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0xa } return len(dAtA) - i, nil } @@ -12664,6 +12836,10 @@ func (m *CheckStoreEvent_Progress) MarshalToSizedBufferVT(dAtA []byte) (int, err i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x12 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x12 } return len(dAtA) - i, nil } @@ -13440,6 +13616,10 @@ func (m *AnalyzeAccountsEvent_Progress) MarshalToSizedBufferVT(dAtA []byte) (int i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0xa + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0xa } return len(dAtA) - i, nil } @@ -13459,6 +13639,10 @@ func (m *AnalyzeAccountsEvent_Result) MarshalToSizedBufferVT(dAtA []byte) (int, i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x12 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x12 } return len(dAtA) - i, nil } @@ -13520,6 +13704,10 @@ func (m *AnalyzeTransactionsEvent_Progress) MarshalToSizedBufferVT(dAtA []byte) i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0xa + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0xa } return len(dAtA) - i, nil } @@ -13539,6 +13727,10 @@ func (m *AnalyzeTransactionsEvent_Result) MarshalToSizedBufferVT(dAtA []byte) (i i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x12 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x12 } return len(dAtA) - i, nil } @@ -14541,6 +14733,10 @@ func (m *ExecutePreparedQueryResponse_Cursor) MarshalToSizedBufferVT(dAtA []byte i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0xa + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0xa } return len(dAtA) - i, nil } @@ -14560,6 +14756,10 @@ func (m *ExecutePreparedQueryResponse_Aggregate) MarshalToSizedBufferVT(dAtA []b i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x12 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x12 } return len(dAtA) - i, nil } @@ -15361,6 +15561,10 @@ func (m *InspectIndexResponse_DistinctValues) MarshalToSizedBufferVT(dAtA []byte i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0xa + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0xa } return len(dAtA) - i, nil } @@ -15380,6 +15584,10 @@ func (m *InspectIndexResponse_Facets) MarshalToSizedBufferVT(dAtA []byte) (int, i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x12 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x12 } return len(dAtA) - i, nil } @@ -15399,6 +15607,10 @@ func (m *InspectIndexResponse_Summary) MarshalToSizedBufferVT(dAtA []byte) (int, i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x1a + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x1a } return len(dAtA) - i, nil } @@ -15967,6 +16179,8 @@ func (m *ApplyRequest_Unsigned) SizeVT() (n int) { if m.Unsigned != nil { l = m.Unsigned.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -15979,6 +16193,8 @@ func (m *ApplyRequest_Signed) SizeVT() (n int) { if m.Signed != nil { l = m.Signed.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -16040,6 +16256,8 @@ func (m *Request_Apply) SizeVT() (n int) { if m.Apply != nil { l = m.Apply.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -16052,6 +16270,8 @@ func (m *Request_CreateLedger) SizeVT() (n int) { if m.CreateLedger != nil { l = m.CreateLedger.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -16064,6 +16284,8 @@ func (m *Request_DeleteLedger) SizeVT() (n int) { if m.DeleteLedger != nil { l = m.DeleteLedger.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -16076,6 +16298,8 @@ func (m *Request_RegisterSigningKey) SizeVT() (n int) { if m.RegisterSigningKey != nil { l = m.RegisterSigningKey.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -16088,6 +16312,8 @@ func (m *Request_RevokeSigningKey) SizeVT() (n int) { if m.RevokeSigningKey != nil { l = m.RevokeSigningKey.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -16100,6 +16326,8 @@ func (m *Request_SetSigningConfig) SizeVT() (n int) { if m.SetSigningConfig != nil { l = m.SetSigningConfig.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -16112,6 +16340,8 @@ func (m *Request_AddEventsSink) SizeVT() (n int) { if m.AddEventsSink != nil { l = m.AddEventsSink.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -16124,6 +16354,8 @@ func (m *Request_RemoveEventsSink) SizeVT() (n int) { if m.RemoveEventsSink != nil { l = m.RemoveEventsSink.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -16136,6 +16368,8 @@ func (m *Request_SetMaintenanceMode) SizeVT() (n int) { if m.SetMaintenanceMode != nil { l = m.SetMaintenanceMode.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -16148,6 +16382,8 @@ func (m *Request_SetMetadataFieldType) SizeVT() (n int) { if m.SetMetadataFieldType != nil { l = m.SetMetadataFieldType.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -16160,6 +16396,8 @@ func (m *Request_RemoveMetadataFieldType) SizeVT() (n int) { if m.RemoveMetadataFieldType != nil { l = m.RemoveMetadataFieldType.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -16172,6 +16410,8 @@ func (m *Request_PromoteLedger) SizeVT() (n int) { if m.PromoteLedger != nil { l = m.PromoteLedger.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -16184,6 +16424,8 @@ func (m *Request_CreatePreparedQuery) SizeVT() (n int) { if m.CreatePreparedQuery != nil { l = m.CreatePreparedQuery.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -16196,6 +16438,8 @@ func (m *Request_UpdatePreparedQuery) SizeVT() (n int) { if m.UpdatePreparedQuery != nil { l = m.UpdatePreparedQuery.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -16208,6 +16452,8 @@ func (m *Request_DeletePreparedQuery) SizeVT() (n int) { if m.DeletePreparedQuery != nil { l = m.DeletePreparedQuery.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -16220,6 +16466,8 @@ func (m *Request_CreateIndex) SizeVT() (n int) { if m.CreateIndex != nil { l = m.CreateIndex.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -16232,6 +16480,8 @@ func (m *Request_DropIndex) SizeVT() (n int) { if m.DropIndex != nil { l = m.DropIndex.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -16244,6 +16494,8 @@ func (m *Request_SaveNumscript) SizeVT() (n int) { if m.SaveNumscript != nil { l = m.SaveNumscript.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -16256,6 +16508,8 @@ func (m *Request_AddAccountType) SizeVT() (n int) { if m.AddAccountType != nil { l = m.AddAccountType.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -16268,6 +16522,8 @@ func (m *Request_RemoveAccountType) SizeVT() (n int) { if m.RemoveAccountType != nil { l = m.RemoveAccountType.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -16280,6 +16536,8 @@ func (m *Request_SetDefaultEnforcementMode) SizeVT() (n int) { if m.SetDefaultEnforcementMode != nil { l = m.SetDefaultEnforcementMode.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -16292,6 +16550,8 @@ func (m *Request_CreateQueryCheckpoint) SizeVT() (n int) { if m.CreateQueryCheckpoint != nil { l = m.CreateQueryCheckpoint.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -16304,6 +16564,8 @@ func (m *Request_DeleteQueryCheckpoint) SizeVT() (n int) { if m.DeleteQueryCheckpoint != nil { l = m.DeleteQueryCheckpoint.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -16316,6 +16578,8 @@ func (m *Request_SetQueryCheckpointSchedule) SizeVT() (n int) { if m.SetQueryCheckpointSchedule != nil { l = m.SetQueryCheckpointSchedule.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -16328,6 +16592,8 @@ func (m *Request_DeleteQueryCheckpointSchedule) SizeVT() (n int) { if m.DeleteQueryCheckpointSchedule != nil { l = m.DeleteQueryCheckpointSchedule.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -16340,6 +16606,8 @@ func (m *Request_SaveLedgerMetadata) SizeVT() (n int) { if m.SaveLedgerMetadata != nil { l = m.SaveLedgerMetadata.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -16352,6 +16620,8 @@ func (m *Request_DeleteLedgerMetadata) SizeVT() (n int) { if m.DeleteLedgerMetadata != nil { l = m.DeleteLedgerMetadata.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -17091,6 +17361,8 @@ func (m *LedgerAction_CreateTransaction) SizeVT() (n int) { if m.CreateTransaction != nil { l = m.CreateTransaction.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -17103,6 +17375,8 @@ func (m *LedgerAction_AddMetadata) SizeVT() (n int) { if m.AddMetadata != nil { l = m.AddMetadata.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -17115,6 +17389,8 @@ func (m *LedgerAction_RevertTransaction) SizeVT() (n int) { if m.RevertTransaction != nil { l = m.RevertTransaction.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -17127,6 +17403,8 @@ func (m *LedgerAction_DeleteMetadata) SizeVT() (n int) { if m.DeleteMetadata != nil { l = m.DeleteMetadata.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -17139,6 +17417,8 @@ func (m *LedgerAction_AddAccountType) SizeVT() (n int) { if m.AddAccountType != nil { l = m.AddAccountType.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -17151,6 +17431,8 @@ func (m *LedgerAction_RemoveAccountType) SizeVT() (n int) { if m.RemoveAccountType != nil { l = m.RemoveAccountType.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -17163,6 +17445,8 @@ func (m *LedgerAction_SetDefaultEnforcementMode) SizeVT() (n int) { if m.SetDefaultEnforcementMode != nil { l = m.SetDefaultEnforcementMode.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -17706,6 +17990,8 @@ func (m *CheckStoreEvent_Error) SizeVT() (n int) { if m.Error != nil { l = m.Error.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -17718,6 +18004,8 @@ func (m *CheckStoreEvent_Progress) SizeVT() (n int) { if m.Progress != nil { l = m.Progress.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -18019,6 +18307,8 @@ func (m *AnalyzeAccountsEvent_Progress) SizeVT() (n int) { if m.Progress != nil { l = m.Progress.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -18031,6 +18321,8 @@ func (m *AnalyzeAccountsEvent_Result) SizeVT() (n int) { if m.Result != nil { l = m.Result.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -18056,6 +18348,8 @@ func (m *AnalyzeTransactionsEvent_Progress) SizeVT() (n int) { if m.Progress != nil { l = m.Progress.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -18068,6 +18362,8 @@ func (m *AnalyzeTransactionsEvent_Result) SizeVT() (n int) { if m.Result != nil { l = m.Result.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -18483,6 +18779,8 @@ func (m *ExecutePreparedQueryResponse_Cursor) SizeVT() (n int) { if m.Cursor != nil { l = m.Cursor.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -18495,6 +18793,8 @@ func (m *ExecutePreparedQueryResponse_Aggregate) SizeVT() (n int) { if m.Aggregate != nil { l = m.Aggregate.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -18828,6 +19128,8 @@ func (m *InspectIndexResponse_DistinctValues) SizeVT() (n int) { if m.DistinctValues != nil { l = m.DistinctValues.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -18840,6 +19142,8 @@ func (m *InspectIndexResponse_Facets) SizeVT() (n int) { if m.Facets != nil { l = m.Facets.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } @@ -18852,6 +19156,8 @@ func (m *InspectIndexResponse_Summary) SizeVT() (n int) { if m.Summary != nil { l = m.Summary.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 3 } return n } diff --git a/internal/proto/signaturepb/signature.pb.go b/internal/proto/signaturepb/signature.pb.go index 0eb98fa36a..bec53d4112 100644 --- a/internal/proto/signaturepb/signature.pb.go +++ b/internal/proto/signaturepb/signature.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.12 -// protoc v7.34.2 +// protoc v7.36.2 // source: signature.proto package signaturepb diff --git a/internal/proto/signaturepb/signature_vtproto.pb.go b/internal/proto/signaturepb/signature_vtproto.pb.go index dae3335ca1..114dcca3c9 100644 --- a/internal/proto/signaturepb/signature_vtproto.pb.go +++ b/internal/proto/signaturepb/signature_vtproto.pb.go @@ -1,5 +1,5 @@ // Code generated by protoc-gen-go-vtproto. DO NOT EDIT. -// protoc-gen-go-vtproto version: (devel) +// protoc-gen-go-vtproto version: v0.6.1-0.20240319094008-0393e58bdf10 // source: signature.proto package signaturepb diff --git a/internal/proto/snapshotpb/snapshot.pb.go b/internal/proto/snapshotpb/snapshot.pb.go index a8b9831947..3014abe92f 100644 --- a/internal/proto/snapshotpb/snapshot.pb.go +++ b/internal/proto/snapshotpb/snapshot.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.12 -// protoc v7.34.2 +// protoc v7.36.2 // source: snapshot.proto package snapshotpb diff --git a/internal/proto/snapshotpb/snapshot_grpc.pb.go b/internal/proto/snapshotpb/snapshot_grpc.pb.go index 99d5388f6e..2871a54bff 100644 --- a/internal/proto/snapshotpb/snapshot_grpc.pb.go +++ b/internal/proto/snapshotpb/snapshot_grpc.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: // - protoc-gen-go-grpc v1.6.2 -// - protoc v7.34.2 +// - protoc v7.36.2 // source: snapshot.proto package snapshotpb diff --git a/internal/proto/snapshotpb/snapshot_vtproto.pb.go b/internal/proto/snapshotpb/snapshot_vtproto.pb.go index e09c27dbd2..60eae1c42f 100644 --- a/internal/proto/snapshotpb/snapshot_vtproto.pb.go +++ b/internal/proto/snapshotpb/snapshot_vtproto.pb.go @@ -1,5 +1,5 @@ // Code generated by protoc-gen-go-vtproto. DO NOT EDIT. -// protoc-gen-go-vtproto version: (devel) +// protoc-gen-go-vtproto version: v0.6.1-0.20240319094008-0393e58bdf10 // source: snapshot.proto package snapshotpb diff --git a/misc/proto/common.proto b/misc/proto/common.proto index 857f4ee2c3..b2eea9c53e 100644 --- a/misc/proto/common.proto +++ b/misc/proto/common.proto @@ -552,6 +552,14 @@ message ClusterPolicy { fixed64 metadata_max_value_bytes = 6; fixed64 metadata_max_entity_bytes = 7; fixed64 metadata_max_command_bytes = 8; +// Capability admission is replicated policy, never a node-local flag. The +// fields are optional so nodes that predate the capability foundation decode +// the policy with their existing defaults. + bool capability_activation_armed = 9; + CapabilityAdmissionPolicy capability_admission_policy = 10; + uint32 minimum_replay_protocol = 11; + fixed64 capability_membership_revision = 12; + repeated CapabilityActivation capability_activations = 13; } // CreatedPreparedQueryLog records the creation of a prepared query. @@ -2866,3 +2874,20 @@ message ListOptions { // rejects unsupported variants with InvalidArgument. QueryFilter filter = 5; } + +// CapabilityAdmissionPolicy controls which proposals are closed when an +// activation barrier is armed. FEATURE_ONLY closes only dependent proposals; +// ALL_WRITES closes business writes while retaining the narrow control-plane +// recovery path. +enum CapabilityAdmissionPolicy { + CAPABILITY_ADMISSION_POLICY_UNSPECIFIED = 0; + CAPABILITY_ADMISSION_POLICY_FEATURE_ONLY = 1; + CAPABILITY_ADMISSION_POLICY_ALL_WRITES = 2; +} + +// CapabilityActivation is intentionally small and wire-stable. IDs are fixed +// integers owned by the producer, not hashes of implementation names. +message CapabilityActivation { + fixed32 id = 1; + bool active = 2; +} diff --git a/misc/proto/raft_cmd.proto b/misc/proto/raft_cmd.proto index 4bfc079382..5c5ffe0f22 100644 --- a/misc/proto/raft_cmd.proto +++ b/misc/proto/raft_cmd.proto @@ -451,6 +451,10 @@ message Proposal { // Order.coverage_bits. repeated TechnicalUpdate technical_updates = 9 [deprecated = true]; + // Stable capability IDs required by the final serialized proposal. This is + // checked after any plan rebuild and before the bytes enter Raft. + repeated fixed32 required_capabilities = 10; + // Exclusive with legacy fields 2 and 9. Reading this envelope is supported; // emitting it on Raft requires a separately validated cluster-wide cutover. oneof payload { From 653a4bd7720cb5e5d78a5f01ea2d6c002bb9a8dd Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Wed, 21 Oct 2026 16:58:13 +0200 Subject: [PATCH 002/127] feat(ledger): enforce capability barriers end to end --- .../subsystems/consensus/capabilities.md | 15 +-- internal/application/admission/admission.go | 65 +++++++++- .../admission/capability_policy_test.go | 66 ++++++++++ .../cluster_policy_reconciler_test.go | 63 +++++----- internal/bootstrap/config_validation.go | 11 ++ internal/bootstrap/module.go | 19 ++- .../bootstrap/node_hook_commit_drain_test.go | 8 +- .../processing/processor_cluster_policy.go | 15 +++ .../processor_cluster_policy_test.go | 49 ++++++++ internal/infra/capabilities/capabilities.go | 94 ++++++++++++++- .../capabilities/capabilities_extra_test.go | 8 ++ .../infra/capabilities/capabilities_test.go | 57 ++++++++- internal/infra/membership/confchange.go | 54 ++++++++- .../membership/confchange_identity_test.go | 13 ++ internal/infra/membership/membership.go | 95 +++++++++++++-- internal/infra/membership/membership_test.go | 15 +++ internal/infra/membership/peer_store.go | 29 +++-- internal/infra/node/node.go | 114 ++++++++++++++++-- internal/infra/plan/builder.go | 11 ++ internal/infra/plan/capability_policy.go | 73 +++++++++++ internal/infra/plan/capability_policy_test.go | 68 +++++++++++ internal/infra/plan/runner.go | 40 ++++++ .../state/cluster_policy_recovery_test.go | 24 ++++ internal/infra/state/decoded_entry.go | 32 +++++ internal/infra/state/decoded_entry_test.go | 29 +++++ internal/infra/state/recovery.go | 15 +++ .../cluster_bootstrap.pb.go | 36 ++++-- .../cluster_bootstrap_reader.pb.go | 11 ++ .../cluster_bootstrap_vtproto.pb.go | 102 ++++++++++++++++ internal/proto/raftcmdpb/raft_cmd.pb.go | 29 ++++- .../proto/raftcmdpb/raft_cmd_reader.pb.go | 10 ++ .../proto/raftcmdpb/raft_cmd_vtproto.pb.go | 101 ++++++++++++++++ misc/proto/cluster_bootstrap.proto | 2 + misc/proto/raft_cmd.proto | 5 + 34 files changed, 1286 insertions(+), 92 deletions(-) create mode 100644 internal/application/admission/capability_policy_test.go create mode 100644 internal/infra/plan/capability_policy.go create mode 100644 internal/infra/plan/capability_policy_test.go diff --git a/docs/technical/architecture/subsystems/consensus/capabilities.md b/docs/technical/architecture/subsystems/consensus/capabilities.md index df6152d634..66aeebee58 100644 --- a/docs/technical/architecture/subsystems/consensus/capabilities.md +++ b/docs/technical/architecture/subsystems/consensus/capabilities.md @@ -32,8 +32,9 @@ business writes. `ALL_WRITES` rejects all business writes, with only the narrow control-plane recovery and activation path exempted by its producer inventory. FSM apply never consults node-local capability flags. -The replay floor is monotonic and is checked before decoding an entry or -snapshot that could require a newer protocol. This floor is intentionally not +The replay floor is monotonic, is bounded by the local replay protocol during +policy apply, and is checked immediately after decoding an entry or snapshot +that could require a newer protocol. This floor is intentionally not `pkg/grpcprotocol.Version`. ## Rollout and recovery @@ -46,8 +47,8 @@ below the committed floor is refused. Membership changes racing activation are serialized at the Raft proposal boundary and must be revalidated immediately before `rawNode.Propose`, because `Node.Propose` queues work. -The current package is the deterministic contract and wire foundation. Wiring -each admission, mirror, events, technical-maintenance, and backup producer to -the final-proposal validator and to the node's pre-`Propose` revalidation is a -follow-up integration step; no producer may claim support until that inventory -is complete. +The admission path and the shared plan runner apply the policy check to +business and technical producers (including mirror, events, maintenance, and +backup flows). Direct `Node.Propose` callers remain low-level APIs and must +stamp their proposal before use; membership `ConfChange` operations carry and +validate their capability evidence separately. diff --git a/internal/application/admission/admission.go b/internal/application/admission/admission.go index 0fb796c564..574f1f2c50 100644 --- a/internal/application/admission/admission.go +++ b/internal/application/admission/admission.go @@ -25,6 +25,7 @@ import ( "github.com/formancehq/ledger/v3/internal/domain/indexes" "github.com/formancehq/ledger/v3/internal/domain/processing/numscript" "github.com/formancehq/ledger/v3/internal/infra/attributes" + "github.com/formancehq/ledger/v3/internal/infra/capabilities" "github.com/formancehq/ledger/v3/internal/infra/health" "github.com/formancehq/ledger/v3/internal/infra/node" "github.com/formancehq/ledger/v3/internal/infra/plan" @@ -42,6 +43,48 @@ import ( var tracer = otel.Tracer("admission") +// capabilityPolicyProposer is the final admission barrier for capability +// policy. It deliberately wraps the node proposer instead of checking during +// request preparation: plan.Builder.Run may rebuild a proposal under its +// tracker guard, and this wrapper therefore observes the exact bytes that are +// about to be queued for Raft. +type capabilityPolicyProposer struct { + delegate plan.Proposer + readPolicy func() (*commonpb.ClusterPolicy, error) + readMembers func() []capabilities.Member + businessWrite bool +} + +func (p capabilityPolicyProposer) Propose(ctx context.Context, proposal *node.Proposal) (*futures.Future[state.ApplyResult], error) { + policy, err := p.readPolicy() + if err != nil { + return nil, fmt.Errorf("reading cluster capability policy before proposal: %w", err) + } + state := capabilities.FromPolicy(policy) + if err := state.Validate(); err != nil { + return nil, err + } + + final := new(raftcmdpb.Proposal) + if err := final.UnmarshalVT(proposal.Data()); err != nil { + return nil, fmt.Errorf("decode proposal for capability admission: %w", err) + } + required := make([]capabilities.ID, len(final.GetRequiredCapabilities())) + for i, id := range final.GetRequiredCapabilities() { + required[i] = capabilities.ID(id) + } + var members []capabilities.Member + if p.readMembers != nil { + members = p.readMembers() + } + decision := capabilities.Evaluate(state, required, p.businessWrite, members) + if !decision.Allowed { + return nil, fmt.Errorf("%w: %s", capabilities.ErrCapabilityUnsupported, decision.Reason) + } + + return p.delegate.Propose(ctx, proposal) +} + //go:generate mockgen -write_source_comment=false -write_package_comment=false -source admission.go -destination proposer_generated_test.go -typed -package admission . Proposer type Proposer interface { Propose(context.Context, *node.Proposal) (*futures.Future[state.ApplyResult], error) @@ -63,7 +106,8 @@ type Admission struct { waitLeaderReady func(context.Context) error // auditProjectionState is node-local admission state. It may reject a // checkpoint proposal before Raft, but never affects deterministic apply. - auditProjectionState func() (disabled, rebuilding bool) + auditProjectionState func() (disabled, rebuilding bool) + readCapabilityMembers func() []capabilities.Member // clusterPolicyCommitted latches once the replicated cluster policy is // observed committed (revision > 0). The steady-state write path then skips @@ -135,6 +179,10 @@ func WithAuditProjectionState(state func() (disabled, rebuilding bool)) func(*Ad } } +func WithCapabilityMembership(read func() []capabilities.Member) func(*Admission) { + return func(a *Admission) { a.readCapabilityMembers = read } +} + func NewAdmission( store *dal.Store, logger logging.Logger, @@ -586,7 +634,7 @@ func (a *Admission) admit(ctx context.Context, prepare func(context.Context, *co return nil, err } - fsmFuture, cleanup, err := a.proposeAdmission(ctx, prepared.command, prepared.build) + fsmFuture, cleanup, err := a.proposeAdmission(ctx, prepared.command, prepared.build, prepared.command.GetBatch() != nil) if err != nil { return nil, err } @@ -771,7 +819,7 @@ func (a *Admission) buildPreload(ctx context.Context, cmd *raftcmdpb.Proposal, n return build, nil } -func (a *Admission) proposeAdmission(ctx context.Context, cmd *raftcmdpb.Proposal, build *plan.BuildResult) (*futures.Future[state.ApplyResult], func(), error) { +func (a *Admission) proposeAdmission(ctx context.Context, cmd *raftcmdpb.Proposal, build *plan.BuildResult, businessWrite bool) (*futures.Future[state.ApplyResult], func(), error) { // Step 5: Marshal + acquire proposal guard + set PredictedIndex // + propose, all via the shared preload runner. The runner also // patches PredictedIndex onto the pre-marshaled buffer (or @@ -785,6 +833,15 @@ func (a *Admission) proposeAdmission(ctx context.Context, cmd *raftcmdpb.Proposa // rebuild, keeping bits and plans in sync. ctx, proposeSpan := tracer.Start(ctx, "admission.propose") + // Keep the policy check inside the canonical runner's proposer callback. This + // is the last point before the proposal enters the node queue, including the + // runner's rebuild path, so a policy update cannot be bypassed by a stale + // preflight check. The policy is read from the replicated store rather than + // from process-local configuration. + policyProposer := capabilityPolicyProposer{delegate: a.proposer, readPolicy: func() (*commonpb.ClusterPolicy, error) { + return query.ReadClusterPolicy(a.store) + }, readMembers: a.readCapabilityMembers, businessWrite: businessWrite} + runResult, err := a.builder.Run( ctx, cmd, build, func(c *raftcmdpb.Proposal) ([]byte, error) { @@ -793,7 +850,7 @@ func (a *Admission) proposeAdmission(ctx context.Context, cmd *raftcmdpb.Proposa // admission metrics. return a.marshalCommand(ctx, c) }, - a.proposer, + policyProposer, ) if err != nil { proposeSpan.End() diff --git a/internal/application/admission/capability_policy_test.go b/internal/application/admission/capability_policy_test.go new file mode 100644 index 0000000000..8c880283f1 --- /dev/null +++ b/internal/application/admission/capability_policy_test.go @@ -0,0 +1,66 @@ +package admission + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/formancehq/ledger/v3/internal/infra/capabilities" + "github.com/formancehq/ledger/v3/internal/infra/node" + "github.com/formancehq/ledger/v3/internal/infra/plan" + "github.com/formancehq/ledger/v3/internal/infra/state" + "github.com/formancehq/ledger/v3/internal/pkg/futures" + "github.com/formancehq/ledger/v3/internal/proto/commonpb" + "github.com/formancehq/ledger/v3/internal/proto/raftcmdpb" +) + +type capabilityTestProposer struct{ called bool } + +func (p *capabilityTestProposer) Propose(context.Context, *node.Proposal) (*futures.Future[state.ApplyResult], error) { + p.called = true + return futures.New[state.ApplyResult](), nil +} + +var _ plan.Proposer = (*capabilityTestProposer)(nil) + +func TestCapabilityPolicyProposerBlocksAllWritesAtFinalBoundary(t *testing.T) { + delegate := new(capabilityTestProposer) + proposer := capabilityPolicyProposer{ + delegate: delegate, + readPolicy: func() (*commonpb.ClusterPolicy, error) { + return &commonpb.ClusterPolicy{ + CapabilityActivationArmed: true, + CapabilityAdmissionPolicy: commonpb.CapabilityAdmissionPolicy_CAPABILITY_ADMISSION_POLICY_ALL_WRITES, + CapabilityMembershipRevision: 1, + }, nil + }, + businessWrite: true, + } + data, err := (&raftcmdpb.Proposal{}).MarshalVT() + require.NoError(t, err) + _, err = proposer.Propose(context.Background(), node.NewProposal(1, data)) + require.Error(t, err) + require.ErrorIs(t, err, capabilities.ErrCapabilityUnsupported) + require.False(t, delegate.called) +} + +func TestCapabilityPolicyProposerAllowsFeatureOnlyUnrelatedWrite(t *testing.T) { + delegate := new(capabilityTestProposer) + proposer := capabilityPolicyProposer{ + delegate: delegate, + readPolicy: func() (*commonpb.ClusterPolicy, error) { + return &commonpb.ClusterPolicy{ + CapabilityActivationArmed: true, + CapabilityAdmissionPolicy: commonpb.CapabilityAdmissionPolicy_CAPABILITY_ADMISSION_POLICY_FEATURE_ONLY, + CapabilityMembershipRevision: 1, + }, nil + }, + businessWrite: true, + } + data, err := (&raftcmdpb.Proposal{}).MarshalVT() + require.NoError(t, err) + _, err = proposer.Propose(context.Background(), node.NewProposal(1, data)) + require.NoError(t, err) + require.True(t, delegate.called) +} diff --git a/internal/bootstrap/cluster_policy_reconciler_test.go b/internal/bootstrap/cluster_policy_reconciler_test.go index da61066438..a7b8b9f05e 100644 --- a/internal/bootstrap/cluster_policy_reconciler_test.go +++ b/internal/bootstrap/cluster_policy_reconciler_test.go @@ -14,6 +14,7 @@ import ( logging "github.com/formancehq/go-libs/v5/pkg/observe/log" "github.com/formancehq/ledger/v3/internal/domain" + "github.com/formancehq/ledger/v3/internal/infra/capabilities" "github.com/formancehq/ledger/v3/internal/infra/state" "github.com/formancehq/ledger/v3/internal/proto/commonpb" "github.com/formancehq/ledger/v3/internal/proto/servicepb" @@ -48,10 +49,10 @@ func TestClusterPolicyReconciler_TicksUntilStopped(t *testing.T) { } type fakeAdmission struct { - mu sync.Mutex - calls int - err error - proposed *commonpb.ClusterPolicy + mu sync.Mutex + calls int + err error + lastPolicy *commonpb.ClusterPolicy } func (f *fakeAdmission) Admit(_ context.Context, _ *servicepb.ApplyRequest) (*domain.ApplyResult, error) { @@ -66,7 +67,7 @@ func (f *fakeAdmission) AdmitClusterPolicy(_ context.Context, policy *commonpb.C f.mu.Lock() defer f.mu.Unlock() f.calls++ - f.proposed = policy + f.lastPolicy = policy.CloneVT() return nil, f.err } @@ -174,35 +175,29 @@ func TestReconcileClusterPolicy(t *testing.T) { require.Zero(t, admission.callCount(), "a newer applied revision must not be overwritten") }) -} -// Only an explicit zero TTL may reach the committed policy as 0, the -// never-expire sentinel. -func TestReconcileClusterPolicy_ProposedIdempotencyTTL(t *testing.T) { - t.Parallel() + t.Run("preserves the committed replay floor on a higher revision", func(t *testing.T) { + t.Parallel() + + store := newReconcilerTestStore(t) + cfg := validBaseConfig() + cfg.ClusterPolicyRevision++ - for _, tc := range []struct { - ttl time.Duration - wantMicros uint64 - }{ - {ttl: 0, wantMicros: 0}, - {ttl: time.Microsecond, wantMicros: 1}, - {ttl: 1500 * time.Nanosecond, wantMicros: 1}, - {ttl: 24 * time.Hour, wantMicros: uint64((24 * time.Hour).Microseconds())}, - } { - t.Run(tc.ttl.String(), func(t *testing.T) { - t.Parallel() - - cfg := validBaseConfig() - cfg.IdempotencyTTL = tc.ttl - require.NoError(t, cfg.Validate()) - - admission := &fakeAdmission{} - reconcileClusterPolicy(context.Background(), admission, newReconcilerTestStore(t), cfg, - func() bool { return true }, logging.Testing()) - - require.Equal(t, 1, admission.callCount()) - require.Equal(t, tc.wantMicros, admission.proposed.GetIdempotencyTtlMicros()) - }) - } + batch := store.OpenWriteSession() + require.NoError(t, state.SaveClusterPolicy(batch, &commonpb.ClusterPolicy{ + Revision: cfg.ClusterPolicyRevision - 1, + QueryCheckpointLimit: cfg.QueryCheckpointLimit, + MinimumReplayProtocol: capabilities.CurrentReplayProtocol, + })) + require.NoError(t, batch.Commit()) + + admission := &fakeAdmission{} + reconcileClusterPolicy(context.Background(), admission, store, cfg, func() bool { return true }, logging.Testing()) + + require.Equal(t, 1, admission.callCount()) + admission.mu.Lock() + gotFloor := admission.lastPolicy.GetMinimumReplayProtocol() + admission.mu.Unlock() + require.Equal(t, capabilities.CurrentReplayProtocol, gotFloor) + }) } diff --git a/internal/bootstrap/config_validation.go b/internal/bootstrap/config_validation.go index 6f18a7f14f..0346bbc6d8 100644 --- a/internal/bootstrap/config_validation.go +++ b/internal/bootstrap/config_validation.go @@ -8,6 +8,7 @@ import ( logging "github.com/formancehq/go-libs/v5/pkg/observe/log" "github.com/formancehq/ledger/v3/internal/domain" + "github.com/formancehq/ledger/v3/internal/infra/capabilities" "github.com/formancehq/ledger/v3/internal/proto/commonpb" "github.com/formancehq/ledger/v3/internal/query" "github.com/formancehq/ledger/v3/internal/storage/dal" @@ -167,6 +168,16 @@ func validateCommittedMetadataLimits(store *dal.Store, cfg Config) error { return fmt.Errorf("reading committed cluster policy: %w", err) } + // The replay floor is checked during bootstrap, before the node opens a + // state machine or decodes historical entries. A binary that cannot execute + // the committed protocol must fail closed even when identity validation is + // run with --unsafe-skip-config-validation. + if applied != nil { + if err := capabilities.ValidateReplayFloor(applied.GetMinimumReplayProtocol(), capabilities.CurrentReplayProtocol); err != nil { + return fmt.Errorf("committed cluster policy requires an unsupported replay protocol: %w", err) + } + } + // No policy committed yet: the leader's reconciler commits the first one, // and admission holds business writes until it does. if applied.GetRevision() == 0 { diff --git a/internal/bootstrap/module.go b/internal/bootstrap/module.go index 6b7b68845b..f847470284 100644 --- a/internal/bootstrap/module.go +++ b/internal/bootstrap/module.go @@ -47,6 +47,7 @@ import ( "github.com/formancehq/ledger/v3/internal/infra/attributes" "github.com/formancehq/ledger/v3/internal/infra/bloom" "github.com/formancehq/ledger/v3/internal/infra/cache" + "github.com/formancehq/ledger/v3/internal/infra/capabilities" clusterhealth "github.com/formancehq/ledger/v3/internal/infra/health" raftmembership "github.com/formancehq/ledger/v3/internal/infra/membership" "github.com/formancehq/ledger/v3/internal/infra/monitoring/diskusage" @@ -259,7 +260,8 @@ func Module() fx.Option { }, newCommittedBloomFilterSet, func(cfg Config, n *node.Node, c *cache.Cache, attrs *attributes.Attributes, store *dal.Store, bloomFilters *bloom.FilterSet, logger logging.Logger) *plan.Builder { - return plan.NewBuilder(n.IndexTracker(), c, attrs, store, bloomFilters, logger, cfg.MaxExecutionPlanSize) + return plan.NewBuilder(n.IndexTracker(), c, attrs, store, bloomFilters, logger, cfg.MaxExecutionPlanSize). + WithCapabilityMembers(n.CapabilityMembers) }, fx.Annotate(func( cfg Config, @@ -569,6 +571,7 @@ func Module() fx.Option { } opts = append(opts, admission.WithAuditProjectionState(rs.AuditProjectionState)) + opts = append(opts, admission.WithCapabilityMembership(node.CapabilityMembers)) return admission.NewAdmission( store, @@ -1307,6 +1310,7 @@ func tryAddLearner(ctx context.Context, cfg Config, tlsCfg TLSConfig, logger log RaftAddress: cfg.RaftConfig.AdvertiseAddr, ServiceAddress: cfg.ServiceAdvertiseAddr(), InstanceId: instanceID, + Capabilities: capabilityIDsAsUint32(capabilities.SupportedIDs()), } creds, _, err := ClientTransportCredentials(tlsCfg) @@ -1462,6 +1466,14 @@ func tryAddLearner(ctx context.Context, cfg Config, tlsCfg TLSConfig, logger log } } +func capabilityIDsAsUint32(ids []capabilities.ID) []uint32 { + result := make([]uint32, len(ids)) + for i, id := range ids { + result[i] = uint32(id) + } + return result +} + // isStaleRaftProgress reports whether a FailedPrecondition status carries the // EN-1436 STALE_RAFT_PROGRESS ErrorInfo reason, distinguishing a stale-raft- // progress join rejection (remediation: `remove-node --force`) from the @@ -1604,6 +1616,11 @@ func reconcileClusterPolicy(ctx context.Context, admission ctrl.Admission, store MetadataMaxValueBytes: cfg.MetadataMaxValueBytes, MetadataMaxEntityBytes: cfg.MetadataMaxEntityBytes, MetadataMaxCommandBytes: cfg.MetadataMaxCommandBytes, + // The replay floor is monotonic and is not a node-local config default. + // Preserve the committed value when the reconciler proposes a higher + // policy revision; otherwise every ordinary config reconciliation would + // accidentally attempt to lower the floor back to zero. + MinimumReplayProtocol: applied.GetMinimumReplayProtocol(), } appliedRev := applied.GetRevision() diff --git a/internal/bootstrap/node_hook_commit_drain_test.go b/internal/bootstrap/node_hook_commit_drain_test.go index 61401c1e91..4a4fd4feee 100644 --- a/internal/bootstrap/node_hook_commit_drain_test.go +++ b/internal/bootstrap/node_hook_commit_drain_test.go @@ -17,6 +17,7 @@ import ( "github.com/formancehq/ledger/v3/internal/domain" "github.com/formancehq/ledger/v3/internal/infra/attributes" "github.com/formancehq/ledger/v3/internal/infra/cache" + "github.com/formancehq/ledger/v3/internal/infra/capabilities" "github.com/formancehq/ledger/v3/internal/infra/membership" "github.com/formancehq/ledger/v3/internal/infra/node" "github.com/formancehq/ledger/v3/internal/infra/state" @@ -99,9 +100,14 @@ func TestNodeHookStopCancellationRetainsRealCommitDrain(t *testing.T) { command.ExecutionPlan = &raftcmdpb.ExecutionPlan{Attributes: []*raftcmdpb.AttributeCoverage{{ Id: &raftcmdpb.AttributeID{Id: ledgerKey[:]}, AttrCode: uint32(dal.SubAttrLedger), }}} + require.NoError(t, capabilities.StampProposal(command)) data, err := command.MarshalVT() require.NoError(t, err) - future, err := n.Propose(ctx, node.NewProposal(command.GetId(), data)) + required, err := capabilities.RequiredCapabilities(command) + require.NoError(t, err) + proposal := node.NewProposal(command.GetId(), data) + proposal.RequireCapabilities(required...) + future, err := n.Propose(ctx, proposal) require.NoError(t, err) select { case <-commitEntered: diff --git a/internal/domain/processing/processor_cluster_policy.go b/internal/domain/processing/processor_cluster_policy.go index 7bc4d7ad65..15de142772 100644 --- a/internal/domain/processing/processor_cluster_policy.go +++ b/internal/domain/processing/processor_cluster_policy.go @@ -2,6 +2,7 @@ package processing import ( "github.com/formancehq/ledger/v3/internal/domain" + "github.com/formancehq/ledger/v3/internal/infra/capabilities" "github.com/formancehq/ledger/v3/internal/proto/commonpb" "github.com/formancehq/ledger/v3/internal/proto/raftcmdpb" ) @@ -45,6 +46,20 @@ func applyClusterPolicy(newPolicy *commonpb.ClusterPolicy, scope Scope) (*common appliedRev := current.GetRevision() newRev := newPolicy.GetRevision() + // A policy may only raise the floor to a replay protocol this binary can + // execute. Rejecting it at the FSM boundary prevents a committed policy + // from making the current node unable to replay its own state after restart. + if err := capabilities.ValidateReplayFloor(newPolicy.GetMinimumReplayProtocol(), capabilities.CurrentReplayProtocol); err != nil { + return nil, &domain.ErrClusterPolicyInvalid{Detail: err.Error()} + } + + // The replay floor is a durable safety boundary, not a revision-scoped + // setting that can be rolled back by a later policy. Validate it before the + // revision switch so every replica rejects a downgrade deterministically. + if _, err := capabilities.AdvanceReplayFloor(current.GetMinimumReplayProtocol(), newPolicy.GetMinimumReplayProtocol()); err != nil { + return nil, &domain.ErrClusterPolicyInvalid{Detail: err.Error()} + } + switch { case newRev > appliedRev: scope.SetClusterPolicy(newPolicy) diff --git a/internal/domain/processing/processor_cluster_policy_test.go b/internal/domain/processing/processor_cluster_policy_test.go index 02f974d02b..8a1b9cbbb2 100644 --- a/internal/domain/processing/processor_cluster_policy_test.go +++ b/internal/domain/processing/processor_cluster_policy_test.go @@ -7,6 +7,7 @@ import ( "go.uber.org/mock/gomock" "github.com/formancehq/ledger/v3/internal/domain" + "github.com/formancehq/ledger/v3/internal/infra/capabilities" "github.com/formancehq/ledger/v3/internal/proto/commonpb" "github.com/formancehq/ledger/v3/internal/proto/raftcmdpb" ) @@ -101,6 +102,54 @@ func TestProcessSetClusterPolicy_LowerRevisionStale(t *testing.T) { require.Equal(t, uint64(5), stale.AppliedRevision) } +// A higher policy revision cannot lower the replay floor. The floor protects +// restart and restore paths, so accepting a rollback here would let a later +// policy make an old binary appear safe again. +func TestProcessSetClusterPolicy_ReplayFloorCannotDecrease(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + mockStore := NewMockScope(ctrl) + processor, err := NewRequestProcessor(nil, 0) + require.NoError(t, err) + + current := withMetadataLimits(&commonpb.ClusterPolicy{Revision: 2, MinimumReplayProtocol: capabilities.CurrentReplayProtocol, QueryCheckpointLimit: 1}) + mockStore.EXPECT().GetClusterPolicy().Return(current) + + proposed := withMetadataLimits(&commonpb.ClusterPolicy{Revision: 3, MinimumReplayProtocol: capabilities.CurrentReplayProtocol - 1, QueryCheckpointLimit: 1}) + _, procErr := processor.ProcessOrder(clusterPolicyOrder(proposed), mockStore) + + var invalid *domain.ErrClusterPolicyInvalid + require.ErrorAs(t, procErr, &invalid) + require.Contains(t, invalid.Detail, "replay protocol floor cannot decrease") +} + +// A policy cannot commit a replay floor newer than the protocol implemented +// by this binary: otherwise the node would accept the entry and then be unable +// to replay its own store after restart. +func TestProcessSetClusterPolicy_ReplayFloorCannotExceedLocalProtocol(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + mockStore := NewMockScope(ctrl) + processor, err := NewRequestProcessor(nil, 0) + require.NoError(t, err) + + current := withMetadataLimits(&commonpb.ClusterPolicy{Revision: 2, QueryCheckpointLimit: 1}) + mockStore.EXPECT().GetClusterPolicy().Return(current) + + proposed := withMetadataLimits(&commonpb.ClusterPolicy{ + Revision: 3, + MinimumReplayProtocol: capabilities.CurrentReplayProtocol + 1, + QueryCheckpointLimit: 1, + }) + _, procErr := processor.ProcessOrder(clusterPolicyOrder(proposed), mockStore) + + var invalid *domain.ErrClusterPolicyInvalid + require.ErrorAs(t, procErr, &invalid) + require.Contains(t, invalid.Detail, "persisted floor") +} + // Structural validation runs before the revision check, so it needs no applied // policy: a missing policy, a zero revision, and a zero limit each reject. func TestProcessSetClusterPolicy_StructuralValidation(t *testing.T) { diff --git a/internal/infra/capabilities/capabilities.go b/internal/infra/capabilities/capabilities.go index 698b4c6392..62958b8764 100644 --- a/internal/infra/capabilities/capabilities.go +++ b/internal/infra/capabilities/capabilities.go @@ -22,6 +22,20 @@ const ( IDSemanticReplayV1 ID = 2 ) +// SupportedIDs is the immutable capability set implemented by this binary. +// It is used as membership evidence during join; callers must copy the result +// before attaching it to a mutable wire message. +func SupportedIDs() []ID { + return []ID{IDExecutionPlanV1, IDSemanticReplayV1} +} + +// CurrentReplayProtocol is the replay encoding understood by this binary. +// It is intentionally independent from the client-facing gRPC protocol and +// from the capability IDs above. Once a policy raises its replay floor, a +// binary whose supported floor is lower must refuse to open the store rather +// than decode state it cannot execute deterministically. +const CurrentReplayProtocol uint32 = 1 + type AdmissionPolicy uint8 const ( @@ -94,12 +108,68 @@ func AdvanceReplayFloor(current, next uint32) (uint32, error) { } var ( - ErrInvalidState = errors.New("invalid capability state") - ErrInvalidDeclaration = errors.New("invalid capability declaration") - ErrMissingCapability = errors.New("proposal is missing a required capability declaration") - ErrCapabilityUnsupported = errors.New("capability is unsupported by a cluster member") + ErrInvalidState = errors.New("invalid capability state") + ErrInvalidDeclaration = errors.New("invalid capability declaration") + ErrMissingCapability = errors.New("proposal is missing a required capability declaration") + ErrCapabilityUnsupported = errors.New("capability is unsupported by a cluster member") + ErrReplayProtocolUnsupported = errors.New("replay protocol floor is unsupported") ) +// RequiredCapabilities derives semantic protocol dependencies from the final +// proposal shape. The registry is deliberately colocated with the wire +// contract so a producer cannot make a feature-specific payload appear safe by +// omitting required_capabilities. Predicates inspect only replicated fields, +// which makes the result deterministic on every member. +func RequiredCapabilities(proposal *raftcmdpb.Proposal) ([]ID, error) { + if proposal == nil { + return nil, fmt.Errorf("%w: nil proposal", ErrInvalidDeclaration) + } + + var required []ID + // ExecutionPlan is the admission/apply coverage contract. Its presence is + // significant even when the message is empty (for no-read proposals): an + // old binary cannot safely replay the envelope without understanding it. + if proposal.GetExecutionPlan() != nil { + required = append(required, IDExecutionPlanV1) + } + // Idempotency freezes a proposal's semantic outcome for replay. Attach the + // dependency to the envelope rather than to the number of orders so an + // empty or metadata-only proposal cannot evade the barrier. + if proposal.GetIdempotency() != nil { + required = append(required, IDSemanticReplayV1) + } + + return Normalize(required) +} + +// StampProposal replaces required_capabilities with the deterministic result +// of RequiredCapabilities. Producers call this after rebuilding the final +// execution plan and immediately before marshaling; repeated calls are safe on +// retry and rebuild paths. +func StampProposal(proposal *raftcmdpb.Proposal) error { + required, err := RequiredCapabilities(proposal) + if err != nil { + return err + } + proposal.RequiredCapabilities = make([]uint32, len(required)) + for i, id := range required { + proposal.RequiredCapabilities[i] = uint32(id) + } + return nil +} + +// ValidateReplayFloor checks the persisted replay floor before any other +// replicated state is decoded. A zero floor is the legacy policy and is +// accepted by every binary; newer floors are safe only when this binary knows +// how to decode and execute the corresponding entries. +func ValidateReplayFloor(required, supported uint32) error { + if required > supported { + return fmt.Errorf("%w: persisted floor %d exceeds supported protocol %d", ErrReplayProtocolUnsupported, required, supported) + } + + return nil +} + func (s State) Validate() error { if !s.Armed { return nil @@ -160,6 +230,13 @@ func Evaluate(state State, required []ID, businessWrite bool, members []Member) return Decision{Reason: err.Error()} } blockers := make([]Blocker, 0) + // A required capability is not admitted merely because members advertise + // support: the replicated activation must also have been committed. + for _, id := range required { + if !state.Activations[id] { + blockers = append(blockers, Blocker{Reason: fmt.Sprintf("capability %d is inactive", id)}) + } + } for _, member := range members { for _, id := range required { if !slices.Contains(member.Capabilities, id) { @@ -187,11 +264,15 @@ func ValidateFinalProposal(data []byte, declared []ID) error { if err := proposal.UnmarshalVT(data); err != nil { return fmt.Errorf("decode final proposal: %w", err) } + expected, err := RequiredCapabilities(proposal) + if err != nil { + return err + } wire := make([]ID, len(proposal.GetRequiredCapabilities())) for i, id := range proposal.GetRequiredCapabilities() { wire[i] = ID(id) } - wire, err := Normalize(wire) + wire, err = Normalize(wire) if err != nil { return err } @@ -199,6 +280,9 @@ func ValidateFinalProposal(data []byte, declared []ID) error { if err != nil { return err } + if !bytes.Equal(idsBytes(wire), idsBytes(expected)) { + return fmt.Errorf("%w: wire declaration does not match semantic payload", ErrMissingCapability) + } if !bytes.Equal(idsBytes(wire), idsBytes(declared)) { return fmt.Errorf("%w: declaration does not match final payload", ErrMissingCapability) } diff --git a/internal/infra/capabilities/capabilities_extra_test.go b/internal/infra/capabilities/capabilities_extra_test.go index ab876b9124..b7d2a3f507 100644 --- a/internal/infra/capabilities/capabilities_extra_test.go +++ b/internal/infra/capabilities/capabilities_extra_test.go @@ -19,3 +19,11 @@ func TestReplayFloorIsMonotonic(t *testing.T) { _, err = AdvanceReplayFloor(floor, 2) require.Error(t, err) } + +func TestValidateReplayFloorRejectsUnsupportedDowngrade(t *testing.T) { + require.NoError(t, ValidateReplayFloor(0, 0)) + require.NoError(t, ValidateReplayFloor(CurrentReplayProtocol, CurrentReplayProtocol)) + + err := ValidateReplayFloor(CurrentReplayProtocol+1, CurrentReplayProtocol) + require.ErrorIs(t, err, ErrReplayProtocolUnsupported) +} diff --git a/internal/infra/capabilities/capabilities_test.go b/internal/infra/capabilities/capabilities_test.go index 262c8f4dc6..f5c9dd0567 100644 --- a/internal/infra/capabilities/capabilities_test.go +++ b/internal/infra/capabilities/capabilities_test.go @@ -14,7 +14,9 @@ func armedState(policy AdmissionPolicy) State { } func TestEvaluateIncludesVotersAndLearners(t *testing.T) { - decision := Evaluate(armedState(FeatureOnly), []ID{IDExecutionPlanV1}, true, []Member{ + state := armedState(FeatureOnly) + state.Activations[IDExecutionPlanV1] = true + decision := Evaluate(state, []ID{IDExecutionPlanV1}, true, []Member{ {NodeID: 1, Role: Voter, Reachable: true, Capabilities: []ID{IDExecutionPlanV1}}, {NodeID: 2, Role: Learner, Reachable: true}, }) @@ -22,6 +24,26 @@ func TestEvaluateIncludesVotersAndLearners(t *testing.T) { require.Equal(t, uint64(2), decision.Blockers[0].NodeID) } +func TestFeatureOnlyBlocksInactiveRequiredCapability(t *testing.T) { + decision := Evaluate(armedState(FeatureOnly), []ID{IDExecutionPlanV1}, true, []Member{ + {NodeID: 1, Role: Voter, Reachable: true, Capabilities: []ID{IDExecutionPlanV1}}, + {NodeID: 2, Role: Learner, Reachable: true, Capabilities: []ID{IDExecutionPlanV1}}, + }) + require.False(t, decision.Allowed) + require.Contains(t, decision.Reason, ErrCapabilityUnsupported.Error()) + require.Contains(t, decision.Blockers, Blocker{Reason: "capability 1 is inactive"}) +} + +func TestFeatureOnlyAllowsActiveRequiredCapabilityWhenMembersSupport(t *testing.T) { + state := armedState(FeatureOnly) + state.Activations[IDExecutionPlanV1] = true + decision := Evaluate(state, []ID{IDExecutionPlanV1}, true, []Member{ + {NodeID: 1, Role: Voter, Reachable: true, Capabilities: []ID{IDExecutionPlanV1}}, + {NodeID: 2, Role: Learner, Reachable: true, Capabilities: []ID{IDExecutionPlanV1}}, + }) + require.True(t, decision.Allowed) +} + func TestAllWritesBlocksUnrelatedBusinessWrite(t *testing.T) { decision := Evaluate(armedState(AllWrites), nil, true, nil) require.False(t, decision.Allowed) @@ -34,11 +56,42 @@ func TestFeatureOnlyAllowsUnrelatedWrite(t *testing.T) { } func TestValidateFinalProposalRejectsMissingDeclaration(t *testing.T) { - data, err := (&raftcmdpb.Proposal{RequiredCapabilities: []uint32{uint32(IDExecutionPlanV1)}}).MarshalVT() + data, err := (&raftcmdpb.Proposal{ExecutionPlan: &raftcmdpb.ExecutionPlan{}, RequiredCapabilities: []uint32{uint32(IDExecutionPlanV1)}}).MarshalVT() + require.NoError(t, err) + require.ErrorIs(t, ValidateFinalProposal(data, nil), ErrMissingCapability) +} + +func TestValidateFinalProposalRejectsSemanticDependencyOmittedFromWire(t *testing.T) { + data, err := (&raftcmdpb.Proposal{ExecutionPlan: &raftcmdpb.ExecutionPlan{}}).MarshalVT() require.NoError(t, err) + require.ErrorIs(t, ValidateFinalProposal(data, nil), ErrMissingCapability) } +func TestValidateFinalProposalRequiresAllSemanticDependencies(t *testing.T) { + proposal := &raftcmdpb.Proposal{ + ExecutionPlan: &raftcmdpb.ExecutionPlan{}, + Idempotency: &commonpb.Idempotency{Key: "retry"}, + } + require.NoError(t, StampProposal(proposal)) + require.Equal(t, []uint32{uint32(IDExecutionPlanV1), uint32(IDSemanticReplayV1)}, proposal.GetRequiredCapabilities()) + + data, err := proposal.MarshalVT() + require.NoError(t, err) + require.NoError(t, ValidateFinalProposal(data, []ID{IDSemanticReplayV1, IDExecutionPlanV1})) + + proposal.RequiredCapabilities = []uint32{uint32(IDExecutionPlanV1)} + data, err = proposal.MarshalVT() + require.NoError(t, err) + require.ErrorIs(t, ValidateFinalProposal(data, []ID{IDExecutionPlanV1}), ErrMissingCapability) +} + +func TestRequiredCapabilitiesIsDeterministicForEmptyExecutionPlan(t *testing.T) { + required, err := RequiredCapabilities(&raftcmdpb.Proposal{ExecutionPlan: &raftcmdpb.ExecutionPlan{}}) + require.NoError(t, err) + require.Equal(t, []ID{IDExecutionPlanV1}, required) +} + func TestFromPolicyRoundTrip(t *testing.T) { state := FromPolicy(&commonpb.ClusterPolicy{ CapabilityActivationArmed: true, diff --git a/internal/infra/membership/confchange.go b/internal/infra/membership/confchange.go index 805e9f8145..1196c686d0 100644 --- a/internal/infra/membership/confchange.go +++ b/internal/infra/membership/confchange.go @@ -4,6 +4,7 @@ import ( "bytes" "encoding/json" "fmt" + "slices" "go.etcd.io/raft/v3/raftpb" "google.golang.org/protobuf/proto" @@ -23,6 +24,49 @@ func ValidateInstanceID(instanceID []byte) error { return nil } +// ValidateCapabilities enforces the canonical representation used in +// membership evidence. IDs are non-zero, sorted, and unique so the same +// concrete member cannot present two byte-level representations of support. +func ValidateCapabilities(ids []uint32) error { + for i, id := range ids { + if id == 0 { + return fmt.Errorf("capability id must be non-zero") + } + if i > 0 && ids[i-1] >= id { + return fmt.Errorf("capabilities must be sorted and unique") + } + } + + return nil +} + +// ValidateCapabilityEvidence validates the proof attached to a membership +// transition. Unarmed clusters retain backwards-compatible empty evidence; +// once the capability barrier is armed, every Add/Update/Promote transition +// must carry a fresh revision-bound capability set. +func ValidateCapabilityEvidence(ctx *ConfChangeContext, armed bool, expectedRevision uint64) error { + if ctx == nil { + return fmt.Errorf("missing capability membership evidence") + } + if err := ValidateCapabilities(ctx.Capabilities); err != nil { + return fmt.Errorf("invalid capability evidence: %w", err) + } + if !armed { + return nil + } + if expectedRevision == 0 { + return fmt.Errorf("capability membership revision is not configured") + } + if ctx.CapabilityMembershipRevision != expectedRevision { + return fmt.Errorf("capability membership revision %d does not match active revision %d", ctx.CapabilityMembershipRevision, expectedRevision) + } + if len(ctx.Capabilities) == 0 { + return fmt.Errorf("capability evidence is empty while activation is armed") + } + + return nil +} + // ConfChangeContext carries peer addresses alongside a Raft ConfChange so that // all nodes (not just the leader) can learn the new peer's addresses when the // ConfChange is committed. @@ -35,6 +79,12 @@ type ConfChangeContext struct { RaftAddress string `json:"raftAddress"` ServiceAddress string `json:"serviceAddress"` InstanceID []byte `json:"instanceId,omitempty"` + // Capabilities and CapabilityMembershipRevision are the signed-by- + // consensus membership evidence for this concrete peer incarnation. They + // travel in every registration context and are persisted in PeerAddress so + // activation cannot accidentally inherit support from a replaced pod. + Capabilities []uint32 `json:"capabilities,omitempty"` + CapabilityMembershipRevision uint64 `json:"capabilityMembershipRevision,omitempty"` // ProposalID is a leader-local correlation token. It is committed with the // ConfChange so the proposing leader can resolve only the matching waiter; // FSM apply deliberately ignores it. @@ -48,7 +98,9 @@ type ConfChangeContext struct { func (c ConfChangeContext) Equal(other ConfChangeContext) bool { return c.RaftAddress == other.RaftAddress && c.ServiceAddress == other.ServiceAddress && - bytes.Equal(c.InstanceID, other.InstanceID) + bytes.Equal(c.InstanceID, other.InstanceID) && + slices.Equal(c.Capabilities, other.Capabilities) && + c.CapabilityMembershipRevision == other.CapabilityMembershipRevision } // MarshalConfChangeContext serialises a ConfChangeContext to JSON bytes diff --git a/internal/infra/membership/confchange_identity_test.go b/internal/infra/membership/confchange_identity_test.go index 40ce1f37c9..779b367c5c 100644 --- a/internal/infra/membership/confchange_identity_test.go +++ b/internal/infra/membership/confchange_identity_test.go @@ -8,6 +8,19 @@ import ( "google.golang.org/protobuf/proto" ) +func TestValidateCapabilityEvidenceBindsActivationToRevisionAndIdentity(t *testing.T) { + ctx := &ConfChangeContext{ + InstanceID: bytesN(0x42), + Capabilities: []uint32{1, 2}, + CapabilityMembershipRevision: 7, + } + + require.NoError(t, ValidateCapabilityEvidence(ctx, true, 7)) + require.ErrorContains(t, ValidateCapabilityEvidence(ctx, true, 8), "does not match active revision") + require.ErrorContains(t, ValidateCapabilityEvidence(&ConfChangeContext{InstanceID: bytesN(0x42), CapabilityMembershipRevision: 7}, true, 7), "evidence is empty") + require.NoError(t, ValidateCapabilityEvidence(&ConfChangeContext{InstanceID: bytesN(0x42)}, false, 0)) +} + func TestWriteConfChangeRejectsIncompleteRegistration(t *testing.T) { t.Parallel() for _, kind := range []raftpb.ConfChangeType{raftpb.ConfChangeAddNode, raftpb.ConfChangeAddLearnerNode, raftpb.ConfChangeUpdateNode} { diff --git a/internal/infra/membership/membership.go b/internal/infra/membership/membership.go index 7975279a5c..83925e84af 100644 --- a/internal/infra/membership/membership.go +++ b/internal/infra/membership/membership.go @@ -69,6 +69,12 @@ type Membership struct { mu sync.RWMutex addresses map[uint64]ConfChangeContext + // capabilityGate is updated from the committed ClusterPolicy projection. + // It is intentionally kept beside the membership cache so Add/Promote + // admission observes one lock-consistent view of activation revision and + // member identity evidence. + capabilityArmed bool + capabilityRevision uint64 // started gates transport / service-pool wiring. Until Start fires // (OnStart hook, after the local Raft gRPC server is listening), // Set / Remove / Register / Rehydrate mutate only the cache — the @@ -237,6 +243,43 @@ func (m *Membership) SnapshotDonorCandidates() []uint64 { slices.Sort(candidates) return candidates + +} + +// SetCapabilityGate publishes the committed activation gate used by +// membership transitions. Callers must pass the policy's membership revision; +// a zero revision is accepted only while the barrier is disarmed. +func (m *Membership) SetCapabilityGate(armed bool, revision uint64) error { + if armed && revision == 0 { + return fmt.Errorf("capability membership revision must be non-zero when armed") + } + + m.mu.Lock() + defer m.mu.Unlock() + m.capabilityArmed = armed + m.capabilityRevision = revision + + return nil +} + +// CapabilityGate returns the current committed activation gate. +func (m *Membership) CapabilityGate() (armed bool, revision uint64) { + m.mu.RLock() + defer m.mu.RUnlock() + + return m.capabilityArmed, m.capabilityRevision +} + +// ValidateTransitionEvidence checks a registration context against the +// committed gate while holding the same membership lock used to publish the +// gate. This closes the race where activation changes between an admission +// read and rawNode.Propose. +func (m *Membership) ValidateTransitionEvidence(ctx *ConfChangeContext) error { + m.mu.RLock() + armed, revision := m.capabilityArmed, m.capabilityRevision + m.mu.RUnlock() + + return ValidateCapabilityEvidence(ctx, armed, revision) } // GetInstanceID returns the peer's 16-byte identity UUID from the in-memory @@ -270,17 +313,30 @@ func (m *Membership) GetInstanceID(nodeID uint64) ([]byte, bool) { // they stay in lockstep with a concurrent Rehydrate. See Rehydrate's // locking note. func (m *Membership) Set(nodeID uint64, raftAddr, serviceAddr string, instanceID []byte) error { + return m.SetWithEvidence(nodeID, raftAddr, serviceAddr, instanceID, nil, 0) +} + +// SetWithEvidence upserts a peer and persists its capability evidence in the +// in-memory registration. The FSM write path stores the same fields in the +// durable PeerAddress row, while finishReady uses this method to keep the +// transport cache and evidence synchronized before resolving the proposal. +func (m *Membership) SetWithEvidence(nodeID uint64, raftAddr, serviceAddr string, instanceID []byte, capabilityIDs []uint32, membershipRevision uint64) error { if err := ValidateInstanceID(instanceID); err != nil { return fmt.Errorf("setting peer %d: %w", nodeID, err) } + if err := ValidateCapabilities(capabilityIDs); err != nil { + return fmt.Errorf("setting peer %d: %w", nodeID, err) + } m.mu.Lock() defer m.mu.Unlock() m.addresses[nodeID] = ConfChangeContext{ - RaftAddress: raftAddr, - ServiceAddress: serviceAddr, - InstanceID: bytes.Clone(instanceID), + RaftAddress: raftAddr, + ServiceAddress: serviceAddr, + InstanceID: bytes.Clone(instanceID), + Capabilities: append([]uint32(nil), capabilityIDs...), + CapabilityMembershipRevision: membershipRevision, } m.wireAdd(nodeID, raftAddr, serviceAddr) @@ -309,11 +365,27 @@ func (m *Membership) Remove(nodeID uint64) { // instanceID is always the peer's 16-byte WAL/PVC identity. Identity-less // membership rows are rejected rather than persisted as a partial member. func (m *Membership) Register(nodeID uint64, raftAddr, serviceAddr string, instanceID []byte) error { - if err := m.store.Put(nodeID, raftAddr, serviceAddr, instanceID); err != nil { + return m.RegisterWithEvidence(nodeID, raftAddr, serviceAddr, instanceID, nil, 0) +} + +// RegisterWithEvidence is the durable/cache-atomic lifecycle variant of +// Register. It is used for self refresh and bootstrap rows that already carry +// a capability proof. +func (m *Membership) RegisterWithEvidence(nodeID uint64, raftAddr, serviceAddr string, instanceID []byte, capabilityIDs []uint32, membershipRevision uint64) error { + // A restart refreshes the local addresses. Preserve evidence for the same + // concrete instance when the caller has no new proof; a changed instance + // must never inherit the previous pod's capabilities. + if len(capabilityIDs) == 0 && membershipRevision == 0 { + if existing, ok := m.PeerAddresses()[nodeID]; ok && bytes.Equal(existing.InstanceID, instanceID) { + capabilityIDs = existing.Capabilities + membershipRevision = existing.CapabilityMembershipRevision + } + } + if err := m.store.PutWithEvidence(nodeID, raftAddr, serviceAddr, instanceID, capabilityIDs, membershipRevision); err != nil { return fmt.Errorf("persisting peer %d: %w", nodeID, err) } - if err := m.Set(nodeID, raftAddr, serviceAddr, instanceID); err != nil { + if err := m.SetWithEvidence(nodeID, raftAddr, serviceAddr, instanceID, capabilityIDs, membershipRevision); err != nil { return err } @@ -584,11 +656,16 @@ func (m *Membership) WriteConfChange(entry *raftpb.Entry, session *dal.WriteSess } writeRegistration := func(nodeID uint64, ctx *ConfChangeContext) error { + if err := ValidateCapabilities(ctx.Capabilities); err != nil { + return fmt.Errorf("invalid capability evidence for peer %d: %w", nodeID, err) + } if err := session.SetProto(peerKey(nodeID), &raftcmdpb.PeerAddress{ - NodeId: nodeID, - RaftAddress: ctx.RaftAddress, - ServiceAddress: ctx.ServiceAddress, - InstanceId: ctx.InstanceID, + NodeId: nodeID, + RaftAddress: ctx.RaftAddress, + ServiceAddress: ctx.ServiceAddress, + InstanceId: ctx.InstanceID, + Capabilities: ctx.Capabilities, + CapabilityMembershipRevision: ctx.CapabilityMembershipRevision, }); err != nil { return fmt.Errorf("session write peer %d: %w", nodeID, err) } diff --git a/internal/infra/membership/membership_test.go b/internal/infra/membership/membership_test.go index ea9e7587c4..4b83aeafb8 100644 --- a/internal/infra/membership/membership_test.go +++ b/internal/infra/membership/membership_test.go @@ -660,6 +660,21 @@ func TestMembership_ClonesInstanceIDs(t *testing.T) { require.Equal(t, byte(1), again[0], "PeerAddresses must deep-clone instance IDs") } +func TestPeerStore_PersistsCapabilityEvidenceWithIdentity(t *testing.T) { + ps := newTestPeerStore(t) + caps := []uint32{1, 2} + identity := fixedInstanceID(0x7a) + require.NoError(t, ps.PutWithEvidence(9, "raft:9", "service:9", identity, caps, 11)) + + rows, err := ps.LoadAll() + require.NoError(t, err) + row, ok := rows[9] + require.True(t, ok) + require.Equal(t, identity, row.InstanceID) + require.Equal(t, caps, row.Capabilities) + require.Equal(t, uint64(11), row.CapabilityMembershipRevision) +} + // TestMembership_ReconcileAgainstConfState pins the boot-time cleanup // pass that drops peers whose NodeID is no longer in the durable // ConfState. This covers two crash windows: interrupted ForceRemoveNode diff --git a/internal/infra/membership/peer_store.go b/internal/infra/membership/peer_store.go index fd3797e738..9b247ac8c1 100644 --- a/internal/infra/membership/peer_store.go +++ b/internal/infra/membership/peer_store.go @@ -92,16 +92,29 @@ func peerKeyRange() (lower, upper []byte) { // apply uses the caller's WriteSession directly. Every persisted peer must // carry the concrete member's 16-byte WAL/PVC identity. func (p *PeerStore) Put(nodeID uint64, raftAddr, serviceAddr string, instanceID []byte) error { + return p.PutWithEvidence(nodeID, raftAddr, serviceAddr, instanceID, nil, 0) +} + +// PutWithEvidence persists a peer directory row together with the capability +// evidence bound to its concrete instance. The evidence is deliberately part +// of the same Pebble value as the identity so a crash cannot expose a peer +// with a fresh instance_id and stale capabilities. +func (p *PeerStore) PutWithEvidence(nodeID uint64, raftAddr, serviceAddr string, instanceID []byte, capabilities []uint32, membershipRevision uint64) error { if err := ValidateInstanceID(instanceID); err != nil { return fmt.Errorf("writing peer %d: %w", nodeID, err) } + if err := ValidateCapabilities(capabilities); err != nil { + return fmt.Errorf("writing peer %d: %w", nodeID, err) + } session := p.store.OpenWriteSession() if err := session.SetProto(peerKey(nodeID), &raftcmdpb.PeerAddress{ - NodeId: nodeID, - RaftAddress: raftAddr, - ServiceAddress: serviceAddr, - InstanceId: instanceID, + NodeId: nodeID, + RaftAddress: raftAddr, + ServiceAddress: serviceAddr, + InstanceId: instanceID, + Capabilities: capabilities, + CapabilityMembershipRevision: membershipRevision, }); err != nil { _ = session.Cancel() @@ -179,9 +192,11 @@ func (p *PeerStore) LoadAll() (map[uint64]ConfChangeContext, error) { } out[pa.GetNodeId()] = ConfChangeContext{ - RaftAddress: pa.GetRaftAddress(), - ServiceAddress: pa.GetServiceAddress(), - InstanceID: pa.GetInstanceId(), + RaftAddress: pa.GetRaftAddress(), + ServiceAddress: pa.GetServiceAddress(), + InstanceID: pa.GetInstanceId(), + Capabilities: append([]uint32(nil), pa.GetCapabilities()...), + CapabilityMembershipRevision: pa.GetCapabilityMembershipRevision(), } } diff --git a/internal/infra/node/node.go b/internal/infra/node/node.go index dd8c79298c..3fbf41236e 100644 --- a/internal/infra/node/node.go +++ b/internal/infra/node/node.go @@ -98,6 +98,14 @@ var ( ErrNodeSyncing = errors.New("node is syncing") ) +func capabilityIDsAsUint32(ids []capabilities.ID) []uint32 { + result := make([]uint32, len(ids)) + for i, id := range ids { + result[i] = uint32(id) + } + return result +} + // StaleRaftProgressReason is the machine-readable ErrorInfo reason attached to // the FailedPrecondition status a leader returns when a JoinAsLearner call hits // ErrNodeStaleProgress (EN-1436). The joining node matches on it to tell the @@ -690,7 +698,7 @@ func NewNode( // writes self for Bootstrap/Restore, but the restart branch never // did — that's the gap the previous bootstrap hook used to cover // before transport wiring moved into Membership. - if err := membership.Register(cfg.NodeID, cfg.AdvertiseAddr, cfg.ServiceAdvertiseAddr, cfg.InstanceID); err != nil { + if err := membership.RegisterWithEvidence(cfg.NodeID, cfg.AdvertiseAddr, cfg.ServiceAdvertiseAddr, cfg.InstanceID, capabilityIDsAsUint32(capabilities.SupportedIDs()), 0); err != nil { return nil, fmt.Errorf("refreshing self in peer store: %w", err) } @@ -1751,7 +1759,10 @@ func (node *Node) finishReady(result readyResult, stop chan struct{}) error { err := membership.WalkConfChangeContexts(cc, func(t raftpb.ConfChangeType, nodeID uint64, ctx *membership.ConfChangeContext) error { switch t { case raftpb.ConfChangeAddNode, raftpb.ConfChangeAddLearnerNode, raftpb.ConfChangeUpdateNode: - if err := node.membership.Set(nodeID, ctx.RaftAddress, ctx.ServiceAddress, ctx.InstanceID); err != nil { + if ctx == nil { + return fmt.Errorf("invariant: committed registration for node %d has no identity context", nodeID) + } + if err := node.membership.SetWithEvidence(nodeID, ctx.RaftAddress, ctx.ServiceAddress, ctx.InstanceID, ctx.Capabilities, ctx.CapabilityMembershipRevision); err != nil { return fmt.Errorf("invariant: applying membership cache update: %w", err) } case raftpb.ConfChangeRemoveNode: @@ -2551,6 +2562,47 @@ func (node *Node) MemberIDs() []uint64 { return ids } +// CapabilityMembers returns a lock-consistent membership evidence snapshot for +// admission. Missing durable rows are represented as unreachable members so +// activation cannot proceed on an incomplete directory. +func (node *Node) CapabilityMembers() []capabilities.Member { + confState := node.confState.Load() + if confState == nil { + return nil + } + addresses := node.membership.PeerAddresses() + roles := make(map[uint64]capabilities.MemberRole) + for _, id := range confState.GetVoters() { + roles[id] = capabilities.Voter + } + for _, id := range confState.GetLearners() { + roles[id] = capabilities.Learner + } + result := make([]capabilities.Member, 0, len(roles)) + for id, role := range roles { + peer, ok := addresses[id] + member := capabilities.Member{NodeID: id, Role: role, Reachable: ok && peer.RaftAddress != "" && peer.ServiceAddress != ""} + if ok { + member.InstanceID = peer.InstanceID + member.Capabilities = make([]capabilities.ID, len(peer.Capabilities)) + for i, capabilityID := range peer.Capabilities { + member.Capabilities[i] = capabilities.ID(capabilityID) + } + } + result = append(result, member) + } + slices.SortFunc(result, func(a, b capabilities.Member) int { + if a.NodeID < b.NodeID { + return -1 + } + if a.NodeID > b.NodeID { + return 1 + } + return 0 + }) + return result +} + // Logger returns the node's logger. func (node *Node) Logger() logging.Logger { return node.logger @@ -3118,7 +3170,14 @@ func classifyExistingLearner(match uint64, existingInstanceID, incomingInstanceI // // Must be called on the leader. func (node *Node) AddLearner(ctx context.Context, nodeID uint64, raftAddr, serviceAddr string, instanceID []byte) error { - return node.addLearner(ctx, nodeID, raftAddr, serviceAddr, instanceID, false) + return node.AddLearnerWithCapabilities(ctx, nodeID, raftAddr, serviceAddr, instanceID, nil, 0) +} + +// AddLearnerWithCapabilities proposes a learner registration carrying the +// caller's capability proof. The proof is persisted with the peer identity and +// is required once the capability activation gate is armed. +func (node *Node) AddLearnerWithCapabilities(ctx context.Context, nodeID uint64, raftAddr, serviceAddr string, instanceID []byte, capabilityIDs []uint32, membershipRevision uint64) error { + return node.addLearnerWithCapabilities(ctx, nodeID, raftAddr, serviceAddr, instanceID, capabilityIDs, membershipRevision, false) } // WithStableDrainMembership serializes a drain capability preflight and its @@ -3150,20 +3209,40 @@ func (node *Node) WithStableDrainMembership(ctx context.Context, fn func() error // WAL. It deliberately keeps EN-1436's stricter stale-progress handling // separate from administrative AddLearner retries. func (node *Node) JoinAsLearner(ctx context.Context, nodeID uint64, raftAddr, serviceAddr string, instanceID []byte) error { - return node.addLearner(ctx, nodeID, raftAddr, serviceAddr, instanceID, true) + return node.JoinAsLearnerWithCapabilities(ctx, nodeID, raftAddr, serviceAddr, instanceID, nil, 0) +} + +// JoinAsLearnerWithCapabilities is the bootstrap admission variant carrying +// the joining node's capability proof. +func (node *Node) JoinAsLearnerWithCapabilities(ctx context.Context, nodeID uint64, raftAddr, serviceAddr string, instanceID []byte, capabilityIDs []uint32, membershipRevision uint64) error { + return node.addLearnerWithCapabilities(ctx, nodeID, raftAddr, serviceAddr, instanceID, capabilityIDs, membershipRevision, true) } +// addLearner is kept as a compatibility shim for package-local tests and +// callers that exercise the pre-evidence admission path directly. func (node *Node) addLearner(ctx context.Context, nodeID uint64, raftAddr, serviceAddr string, instanceID []byte, bootJoin bool) error { + return node.addLearnerWithCapabilities(ctx, nodeID, raftAddr, serviceAddr, instanceID, nil, 0, bootJoin) +} + +func (node *Node) addLearnerWithCapabilities(ctx context.Context, nodeID uint64, raftAddr, serviceAddr string, instanceID []byte, capabilityIDs []uint32, membershipRevision uint64, bootJoin bool) error { if err := membership.ValidateInstanceID(instanceID); err != nil { return fmt.Errorf("invalid learner identity: %w", err) } + if armed, activeRevision := node.membership.CapabilityGate(); armed && membershipRevision == 0 { + // The leader binds a joiner's capability declaration to the currently + // committed revision. A stale/omitted caller revision must not be able + // to smuggle evidence across a policy transition. + membershipRevision = activeRevision + } proposalID := uuid.NewString() ccCtx, err := membership.MarshalConfChangeContext(membership.ConfChangeContext{ - RaftAddress: raftAddr, - ServiceAddress: serviceAddr, - InstanceID: instanceID, - ProposalID: proposalID, + RaftAddress: raftAddr, + ServiceAddress: serviceAddr, + InstanceID: instanceID, + Capabilities: append([]uint32(nil), capabilityIDs...), + CapabilityMembershipRevision: membershipRevision, + ProposalID: proposalID, }) if err != nil { return fmt.Errorf("marshaling conf change context: %w", err) @@ -3177,6 +3256,18 @@ func (node *Node) addLearner(ctx context.Context, nodeID uint64, raftAddr, servi if status.RaftState != raft.StateLeader { return ErrNotLeader } + // Revalidate immediately before RawNode.Propose: policy activation and + // membership revision may have changed while the request waited for the + // serialized ConfChange boundary. + if err := node.membership.ValidateTransitionEvidence(&membership.ConfChangeContext{ + RaftAddress: raftAddr, + ServiceAddress: serviceAddr, + InstanceID: instanceID, + Capabilities: capabilityIDs, + CapabilityMembershipRevision: membershipRevision, + }); err != nil { + return err + } // EN-1045 defense against a race between JoinAsLearner admission // (Pebble IsRemoved check) and RemoveNode commit: re-check the @@ -3271,6 +3362,13 @@ func (node *Node) PromoteLearner(ctx context.Context, nodeID uint64) error { if err != nil { return err } + promotionCtx, decodeErr := membership.UnmarshalConfChangeContext(ccCtx) + if decodeErr != nil { + return fmt.Errorf("decoding learner membership evidence: %w", decodeErr) + } + if err := node.membership.ValidateTransitionEvidence(&promotionCtx); err != nil { + return err + } return node.rawNode.ProposeConfChange(&raftpb.ConfChangeV2{ Changes: []*raftpb.ConfChangeSingle{{ diff --git a/internal/infra/plan/builder.go b/internal/infra/plan/builder.go index 6f65c2c963..53e796a461 100644 --- a/internal/infra/plan/builder.go +++ b/internal/infra/plan/builder.go @@ -12,6 +12,7 @@ import ( "github.com/formancehq/ledger/v3/internal/infra/attributes" "github.com/formancehq/ledger/v3/internal/infra/bloom" "github.com/formancehq/ledger/v3/internal/infra/cache" + "github.com/formancehq/ledger/v3/internal/infra/capabilities" "github.com/formancehq/ledger/v3/internal/infra/node" "github.com/formancehq/ledger/v3/internal/infra/preload" "github.com/formancehq/ledger/v3/internal/infra/state" @@ -51,6 +52,7 @@ type Builder struct { // front rather than paying for proportionally-large coverage slices // on every NewScope downstream. 0 disables the cap. maxPlanSize int + readMembers func() []capabilities.Member } // BuildResult carries the result of an optimistic Build call. @@ -136,6 +138,15 @@ func NewBuilder(tracker *node.IndexTracker, c *cache.Cache, attrs *attributes.At return b } +// WithCapabilityMembers supplies the committed membership evidence used by +// the final capability admission barrier. It remains optional for lightweight +// builders used by tests and non-Raft tooling. +func (b *Builder) WithCapabilityMembers(read func() []capabilities.Member) *Builder { + b.readMembers = read + + return b +} + // Loaders returns the shared preload.Loaders instance, allowing callers to // release tokens from a BuildResult on error paths. func (p *Builder) Loaders() *preload.Loaders { diff --git a/internal/infra/plan/capability_policy.go b/internal/infra/plan/capability_policy.go new file mode 100644 index 0000000000..2756e3d539 --- /dev/null +++ b/internal/infra/plan/capability_policy.go @@ -0,0 +1,73 @@ +package plan + +import ( + "context" + "fmt" + + "github.com/formancehq/ledger/v3/internal/infra/capabilities" + "github.com/formancehq/ledger/v3/internal/infra/node" + "github.com/formancehq/ledger/v3/internal/infra/state" + "github.com/formancehq/ledger/v3/internal/pkg/futures" + "github.com/formancehq/ledger/v3/internal/proto/commonpb" + "github.com/formancehq/ledger/v3/internal/proto/raftcmdpb" +) + +// CapabilityPolicyProposer rechecks the committed capability policy against +// the final serialized proposal immediately before handing it to the Raft +// proposer. Technical producers use businessWrite=false: ALL_WRITES blocks +// business traffic while retaining the control-plane recovery path. +type CapabilityPolicyProposer struct { + delegate Proposer + readPolicy func() (*commonpb.ClusterPolicy, error) + readMembers func() []capabilities.Member + businessWrite bool +} + +// NewCapabilityPolicyProposer wraps a proposer with the replicated policy +// admission check. The callback must read the committed policy projection, not +// process-local configuration. +func NewCapabilityPolicyProposer( + delegate Proposer, + readPolicy func() (*commonpb.ClusterPolicy, error), + readMembers func() []capabilities.Member, + businessWrite bool, +) Proposer { + return CapabilityPolicyProposer{ + delegate: delegate, + readPolicy: readPolicy, + readMembers: readMembers, + businessWrite: businessWrite, + } +} + +func (p CapabilityPolicyProposer) Propose(ctx context.Context, proposal *node.Proposal) (*futures.Future[state.ApplyResult], error) { + policy, err := p.readPolicy() + if err != nil { + return nil, fmt.Errorf("reading cluster capability policy before proposal: %w", err) + } + + state := capabilities.FromPolicy(policy) + if err := state.Validate(); err != nil { + return nil, err + } + + final := new(raftcmdpb.Proposal) + if err := final.UnmarshalVT(proposal.Data()); err != nil { + return nil, fmt.Errorf("decode proposal for capability admission: %w", err) + } + + required := make([]capabilities.ID, len(final.GetRequiredCapabilities())) + for i, id := range final.GetRequiredCapabilities() { + required[i] = capabilities.ID(id) + } + var members []capabilities.Member + if p.readMembers != nil { + members = p.readMembers() + } + decision := capabilities.Evaluate(state, required, p.businessWrite, members) + if !decision.Allowed { + return nil, fmt.Errorf("%w: %s", capabilities.ErrCapabilityUnsupported, decision.Reason) + } + + return p.delegate.Propose(ctx, proposal) +} diff --git a/internal/infra/plan/capability_policy_test.go b/internal/infra/plan/capability_policy_test.go new file mode 100644 index 0000000000..88f156ff4c --- /dev/null +++ b/internal/infra/plan/capability_policy_test.go @@ -0,0 +1,68 @@ +package plan + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/formancehq/ledger/v3/internal/infra/capabilities" + "github.com/formancehq/ledger/v3/internal/infra/node" + "github.com/formancehq/ledger/v3/internal/infra/state" + "github.com/formancehq/ledger/v3/internal/pkg/futures" + "github.com/formancehq/ledger/v3/internal/proto/commonpb" + "github.com/formancehq/ledger/v3/internal/proto/raftcmdpb" +) + +type capabilityPolicyTestProposer struct{ called bool } + +func (p *capabilityPolicyTestProposer) Propose(context.Context, *node.Proposal) (*futures.Future[state.ApplyResult], error) { + p.called = true + return futures.New[state.ApplyResult](), nil +} + +func capabilityPolicyProposal(t *testing.T) *node.Proposal { + t.Helper() + + data, err := (&raftcmdpb.Proposal{ + ExecutionPlan: &raftcmdpb.ExecutionPlan{}, + RequiredCapabilities: []uint32{uint32(capabilities.IDExecutionPlanV1)}, + }).MarshalVT() + require.NoError(t, err) + + return node.NewProposal(1, data) +} + +func TestCapabilityPolicyProposerBlocksInactiveCapability(t *testing.T) { + delegate := new(capabilityPolicyTestProposer) + proposer := NewCapabilityPolicyProposer(delegate, func() (*commonpb.ClusterPolicy, error) { + return &commonpb.ClusterPolicy{ + CapabilityActivationArmed: true, + CapabilityAdmissionPolicy: commonpb.CapabilityAdmissionPolicy_CAPABILITY_ADMISSION_POLICY_FEATURE_ONLY, + CapabilityMembershipRevision: 1, + }, nil + }, nil, false) + + _, err := proposer.Propose(context.Background(), capabilityPolicyProposal(t)) + require.ErrorIs(t, err, capabilities.ErrCapabilityUnsupported) + require.False(t, delegate.called) +} + +func TestCapabilityPolicyProposerAllowsActiveTechnicalCapability(t *testing.T) { + delegate := new(capabilityPolicyTestProposer) + proposer := NewCapabilityPolicyProposer(delegate, func() (*commonpb.ClusterPolicy, error) { + return &commonpb.ClusterPolicy{ + CapabilityActivationArmed: true, + CapabilityAdmissionPolicy: commonpb.CapabilityAdmissionPolicy_CAPABILITY_ADMISSION_POLICY_FEATURE_ONLY, + CapabilityMembershipRevision: 1, + CapabilityActivations: []*commonpb.CapabilityActivation{{ + Id: uint32(capabilities.IDExecutionPlanV1), + Active: true, + }}, + }, nil + }, nil, false) + + _, err := proposer.Propose(context.Background(), capabilityPolicyProposal(t)) + require.NoError(t, err) + require.True(t, delegate.called) +} diff --git a/internal/infra/plan/runner.go b/internal/infra/plan/runner.go index 2f7c72c141..de17d5375e 100644 --- a/internal/infra/plan/runner.go +++ b/internal/infra/plan/runner.go @@ -6,12 +6,15 @@ import ( "fmt" "time" + "github.com/formancehq/ledger/v3/internal/infra/capabilities" "github.com/formancehq/ledger/v3/internal/infra/node" "github.com/formancehq/ledger/v3/internal/infra/state" "github.com/formancehq/ledger/v3/internal/pkg/commands" "github.com/formancehq/ledger/v3/internal/pkg/futures" "github.com/formancehq/ledger/v3/internal/pkg/proposals" + "github.com/formancehq/ledger/v3/internal/proto/commonpb" "github.com/formancehq/ledger/v3/internal/proto/raftcmdpb" + "github.com/formancehq/ledger/v3/internal/query" ) // Sentinel errors returned by Run to let callers distinguish which @@ -140,6 +143,16 @@ func (p *Builder) Run( return nil, ErrUnsupportedTechnicalComposition } + // Every producer that uses the shared runner (mirror, events, backup and + // bootstrap maintenance) gets the same final policy check. Admission wraps + // its business writes separately with businessWrite=true; this default + // wrapper covers technical/control-plane proposals. + if p.store != nil { + proposer = NewCapabilityPolicyProposer(proposer, func() (*commonpb.ClusterPolicy, error) { + return query.ReadClusterPolicy(p.store) + }, p.readMembers, false) + } + // Guarantee a unique command ID. The applier keys FSM futures by // `cmd.Id` (`node.NewProposal(cmd.GetId(), data)` below); two in-flight // proposals sharing Id=0 would overwrite each other in @@ -172,6 +185,11 @@ func (p *Builder) Run( } cmd.ExecutionPlan = build.ExecutionPlan + if err := capabilities.StampProposal(cmd); err != nil { + build.ReleaseLoaders() + + return nil, fmt.Errorf("%w: %w", ErrMarshalProposal, err) + } if err := build.applyBits(cmd, build.ExecutionPlan.GetAttributes()); err != nil { build.ReleaseLoaders() @@ -213,6 +231,11 @@ func (p *Builder) Run( // possibly values. result.Rebuilt = true cmd.ExecutionPlan = updatedPreloads + if err := capabilities.StampProposal(cmd); err != nil { + guard.ReleaseAll() + + return nil, fmt.Errorf("%w (under guard): %w", ErrMarshalProposal, err) + } if err := build.applyBits(cmd, updatedPreloads.GetAttributes()); err != nil { guard.ReleaseAll() @@ -233,6 +256,12 @@ func (p *Builder) Run( } proposal := node.NewProposal(cmd.GetId(), data) + required, err := capabilities.RequiredCapabilities(cmd) + if err != nil { + guard.ReleaseAll() + return nil, fmt.Errorf("%w: %w", ErrMarshalProposal, err) + } + proposal.RequireCapabilities(required...) result.ProposeStartTime = time.Now() fsmFuture, err := proposer.Propose(ctx, proposal) @@ -335,6 +364,11 @@ func (p *Builder) runWithoutPreload( } else { cmd.ExecutionPlan = nil } + if err := capabilities.StampProposal(cmd); err != nil { + build.ReleaseLoaders() + + return nil, fmt.Errorf("%w: %w", ErrMarshalProposal, err) + } if err := build.applyBits(cmd, nil); err != nil { build.ReleaseLoaders() @@ -359,6 +393,12 @@ func (p *Builder) runWithoutPreload( data = AppendProposalPredictedIndex(data, cmd.GetPredictedIndex()) proposal := node.NewProposal(cmd.GetId(), data) + required, err := capabilities.RequiredCapabilities(cmd) + if err != nil { + guard.ReleaseLoaders() + return nil, fmt.Errorf("%w: %w", ErrMarshalProposal, err) + } + proposal.RequireCapabilities(required...) proposeStart := time.Now() fsmFuture, err := proposer.Propose(ctx, proposal) diff --git a/internal/infra/state/cluster_policy_recovery_test.go b/internal/infra/state/cluster_policy_recovery_test.go index f85dd11499..ce9291ee71 100644 --- a/internal/infra/state/cluster_policy_recovery_test.go +++ b/internal/infra/state/cluster_policy_recovery_test.go @@ -5,6 +5,7 @@ import ( "github.com/stretchr/testify/require" + "github.com/formancehq/ledger/v3/internal/infra/capabilities" "github.com/formancehq/ledger/v3/internal/proto/commonpb" ) @@ -32,6 +33,29 @@ func TestRecoverState_ClusterPolicyRoundtrip(t *testing.T) { "the persisted policy must be recovered verbatim into FSMState") } +// Recovery must inspect the persisted policy floor before hydrating the rest +// of FSMState. This models a downgrade opening a store written by a newer +// replay protocol and proves that state remains untouched on refusal. +func TestRecoverState_RejectsUnsupportedReplayFloorBeforeHydration(t *testing.T) { + t.Parallel() + + machine, store, _ := newTestMachine(t) + stateBefore := machine.State + + policy := &commonpb.ClusterPolicy{ + Revision: 4, + MinimumReplayProtocol: capabilities.CurrentReplayProtocol + 1, + QueryCheckpointLimit: 1, + } + batch := store.OpenWriteSession() + require.NoError(t, SaveClusterPolicy(batch, policy)) + require.NoError(t, batch.Commit()) + + err := NewRecovery(machine, store).RecoverState() + require.ErrorIs(t, err, capabilities.ErrReplayProtocolUnsupported) + require.Same(t, stateBefore, machine.State) +} + // NewFSMState seeds a non-nil revision-0 default so the apply path and the // readiness gate can read the policy before any is committed. func TestNewFSMState_ClusterPolicyDefault(t *testing.T) { diff --git a/internal/infra/state/decoded_entry.go b/internal/infra/state/decoded_entry.go index 46768f4189..c9a5789303 100644 --- a/internal/infra/state/decoded_entry.go +++ b/internal/infra/state/decoded_entry.go @@ -5,6 +5,7 @@ import ( "go.etcd.io/raft/v3/raftpb" + "github.com/formancehq/ledger/v3/internal/infra/capabilities" "github.com/formancehq/ledger/v3/internal/proto/raftcmdpb" ) @@ -58,12 +59,43 @@ func DecodeEntries(entries []*raftpb.Entry) ([]DecodedEntry, error) { return nil, fmt.Errorf("unmarshaling proposal at raft index %d: %w", entries[i].GetIndex(), err) } + if err := validateReplayFloor(cmd); err != nil { + cmd.ReturnToVTPool() + ReleaseDecodedEntries(decoded[:i]) + + return nil, fmt.Errorf("validating proposal replay floor at raft index %d: %w", entries[i].GetIndex(), err) + } + decoded[i].Proposal = cmd } return decoded, nil } +// validateReplayFloor rejects a policy entry that this binary could commit +// but cannot later replay. It runs immediately after wire decoding, before the +// entry reaches checkpoint detection or FSM apply, so the unsupported floor +// cannot cause any partial state mutation. +func validateReplayFloor(proposal *raftcmdpb.Proposal) error { + for _, order := range proposal.GetOrders() { + policyOrder := order.GetSystemScoped().GetSetClusterPolicy() + if policyOrder == nil { + continue + } + + policy := policyOrder.GetPolicy() + if policy == nil { + continue + } + + if err := capabilities.ValidateReplayFloor(policy.GetMinimumReplayProtocol(), capabilities.CurrentReplayProtocol); err != nil { + return err + } + } + + return nil +} + // ReleaseDecodedEntries returns every non-nil Proposal in decoded back to // the VT pool. Safe to call with a nil or empty slice. After this call no // DecodedEntry.Proposal pointer in the slice may be accessed. diff --git a/internal/infra/state/decoded_entry_test.go b/internal/infra/state/decoded_entry_test.go index ea9d5736c4..58c031079d 100644 --- a/internal/infra/state/decoded_entry_test.go +++ b/internal/infra/state/decoded_entry_test.go @@ -6,6 +6,8 @@ import ( "github.com/stretchr/testify/require" "go.etcd.io/raft/v3/raftpb" + "github.com/formancehq/ledger/v3/internal/infra/capabilities" + "github.com/formancehq/ledger/v3/internal/proto/commonpb" "github.com/formancehq/ledger/v3/internal/proto/raftcmdpb" ) @@ -128,6 +130,33 @@ func TestDecodeEntries(t *testing.T) { }) } +func TestDecodeEntriesRejectsUnsupportedReplayFloor(t *testing.T) { + t.Parallel() + + cmd := &raftcmdpb.Proposal{Orders: []*raftcmdpb.Order{ + { + Type: &raftcmdpb.Order_SystemScoped{ + SystemScoped: &raftcmdpb.SystemScopedOrder{ + Payload: &raftcmdpb.SystemScopedOrder_SetClusterPolicy{ + SetClusterPolicy: &raftcmdpb.SetClusterPolicyOrder{ + Policy: &commonpb.ClusterPolicy{ + Revision: 1, + MinimumReplayProtocol: capabilities.CurrentReplayProtocol + 1, + }, + }, + }, + }, + }, + }, + }} + data, err := cmd.MarshalVT() + require.NoError(t, err) + + decoded, err := DecodeEntries([]*raftpb.Entry{entWith(42, 1, raftpb.EntryNormal, data)}) + require.ErrorIs(t, err, capabilities.ErrReplayProtocolUnsupported) + require.Nil(t, decoded) +} + func TestReleaseDecodedEntries(t *testing.T) { t.Parallel() diff --git a/internal/infra/state/recovery.go b/internal/infra/state/recovery.go index 6796098a9a..1fd379c8ba 100644 --- a/internal/infra/state/recovery.go +++ b/internal/infra/state/recovery.go @@ -3,6 +3,7 @@ package state import ( "fmt" + "github.com/formancehq/ledger/v3/internal/infra/capabilities" "github.com/formancehq/ledger/v3/internal/query" "github.com/formancehq/ledger/v3/internal/storage/dal" ) @@ -54,6 +55,20 @@ func (r *Recovery) RecoverState() error { defer func() { _ = handle.Close() }() + // Read and validate the policy before hydrating any other FSM field. The + // policy is the only durable record that carries the replay floor, so this + // preflight is the recovery boundary that prevents a downgraded binary from + // decoding or executing state written by a newer replay protocol. + policy, err := query.ReadClusterPolicy(r.reader) + if err != nil { + return fmt.Errorf("reading cluster policy replay floor: %w", err) + } + if policy != nil { + if err := capabilities.ValidateReplayFloor(policy.GetMinimumReplayProtocol(), capabilities.CurrentReplayProtocol); err != nil { + return fmt.Errorf("recovery refused by replay protocol floor: %w", err) + } + } + // Phase 1: load a fresh FSMState in its entirety. Any error returns // before we touch the Machine. newState, err := LoadFSMStateFromStore(r.reader, handle) diff --git a/internal/proto/clusterbootstrappb/cluster_bootstrap.pb.go b/internal/proto/clusterbootstrappb/cluster_bootstrap.pb.go index ea77e2c28a..dd5d569193 100644 --- a/internal/proto/clusterbootstrappb/cluster_bootstrap.pb.go +++ b/internal/proto/clusterbootstrappb/cluster_bootstrap.pb.go @@ -172,13 +172,15 @@ func (x *PeerInfo) GetInstanceId() []byte { } type JoinAsLearnerRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - NodeId uint64 `protobuf:"varint,1,opt,name=node_id,json=nodeId,proto3" json:"node_id,omitempty"` // Numeric ID for the new learner node - RaftAddress string `protobuf:"bytes,2,opt,name=raft_address,json=raftAddress,proto3" json:"raft_address,omitempty"` // Raft transport address of the learner - ServiceAddress string `protobuf:"bytes,3,opt,name=service_address,json=serviceAddress,proto3" json:"service_address,omitempty"` // Service API address of the learner - InstanceId []byte `protobuf:"bytes,4,opt,name=instance_id,json=instanceId,proto3" json:"instance_id,omitempty"` // 16-byte UUID identifying this peer's WAL/PVC incarnation (EN-1045); required - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + NodeId uint64 `protobuf:"varint,1,opt,name=node_id,json=nodeId,proto3" json:"node_id,omitempty"` // Numeric ID for the new learner node + RaftAddress string `protobuf:"bytes,2,opt,name=raft_address,json=raftAddress,proto3" json:"raft_address,omitempty"` // Raft transport address of the learner + ServiceAddress string `protobuf:"bytes,3,opt,name=service_address,json=serviceAddress,proto3" json:"service_address,omitempty"` // Service API address of the learner + InstanceId []byte `protobuf:"bytes,4,opt,name=instance_id,json=instanceId,proto3" json:"instance_id,omitempty"` // 16-byte UUID identifying this peer's WAL/PVC incarnation (EN-1045); required + Capabilities []uint32 `protobuf:"fixed32,5,rep,packed,name=capabilities,proto3" json:"capabilities,omitempty"` // capabilities supported by this binary + CapabilityMembershipRevision uint64 `protobuf:"fixed64,6,opt,name=capability_membership_revision,json=capabilityMembershipRevision,proto3" json:"capability_membership_revision,omitempty"` // revision examined by the joiner, 0 lets leader bind current revision + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *JoinAsLearnerRequest) Reset() { @@ -239,6 +241,20 @@ func (x *JoinAsLearnerRequest) GetInstanceId() []byte { return nil } +func (x *JoinAsLearnerRequest) GetCapabilities() []uint32 { + if x != nil { + return x.Capabilities + } + return nil +} + +func (x *JoinAsLearnerRequest) GetCapabilityMembershipRevision() uint64 { + if x != nil { + return x.CapabilityMembershipRevision + } + return 0 +} + type JoinAsLearnerResponse struct { state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields @@ -288,13 +304,15 @@ const file_cluster_bootstrap_proto_rawDesc = "" + "\fraft_address\x18\x02 \x01(\tR\vraftAddress\x12'\n" + "\x0fservice_address\x18\x03 \x01(\tR\x0eserviceAddress\x12\x1f\n" + "\vinstance_id\x18\x04 \x01(\fR\n" + - "instanceId\"\x9c\x01\n" + + "instanceId\"\x86\x02\n" + "\x14JoinAsLearnerRequest\x12\x17\n" + "\anode_id\x18\x01 \x01(\x04R\x06nodeId\x12!\n" + "\fraft_address\x18\x02 \x01(\tR\vraftAddress\x12'\n" + "\x0fservice_address\x18\x03 \x01(\tR\x0eserviceAddress\x12\x1f\n" + "\vinstance_id\x18\x04 \x01(\fR\n" + - "instanceId\"\x17\n" + + "instanceId\x12\"\n" + + "\fcapabilities\x18\x05 \x03(\aR\fcapabilities\x12D\n" + + "\x1ecapability_membership_revision\x18\x06 \x01(\x06R\x1ccapabilityMembershipRevision\"\x17\n" + "\x15JoinAsLearnerResponse2\xd2\x01\n" + "\x17ClusterBootstrapService\x12S\n" + "\bGetPeers\x12\".cluster_bootstrap.GetPeersRequest\x1a#.cluster_bootstrap.GetPeersResponse\x12b\n" + diff --git a/internal/proto/clusterbootstrappb/cluster_bootstrap_reader.pb.go b/internal/proto/clusterbootstrappb/cluster_bootstrap_reader.pb.go index e59dbe251f..e110f3631d 100644 --- a/internal/proto/clusterbootstrappb/cluster_bootstrap_reader.pb.go +++ b/internal/proto/clusterbootstrappb/cluster_bootstrap_reader.pb.go @@ -5,6 +5,7 @@ package clusterbootstrappb import ( bytes "bytes" + slices "slices" ) // GetPeersRequestReader provides read-only access to GetPeersRequest. @@ -223,6 +224,8 @@ type JoinAsLearnerRequestReader interface { GetRaftAddress() string GetServiceAddress() string GetInstanceId() []byte + GetCapabilities() []uint32 + GetCapabilityMembershipRevision() uint64 Mutate() *JoinAsLearnerRequest } @@ -244,6 +247,14 @@ func (r *joinAsLearnerRequestReadonly) GetInstanceId() []byte { return bytes.Clone((*JoinAsLearnerRequest)(r).GetInstanceId()) } +func (r *joinAsLearnerRequestReadonly) GetCapabilities() []uint32 { + return slices.Clone((*JoinAsLearnerRequest)(r).GetCapabilities()) +} + +func (r *joinAsLearnerRequestReadonly) GetCapabilityMembershipRevision() uint64 { + return (*JoinAsLearnerRequest)(r).GetCapabilityMembershipRevision() +} + func (r *joinAsLearnerRequestReadonly) Mutate() *JoinAsLearnerRequest { return (*JoinAsLearnerRequest)(r).CloneVT() } diff --git a/internal/proto/clusterbootstrappb/cluster_bootstrap_vtproto.pb.go b/internal/proto/clusterbootstrappb/cluster_bootstrap_vtproto.pb.go index e2cb702f6a..27ccb8985a 100644 --- a/internal/proto/clusterbootstrappb/cluster_bootstrap_vtproto.pb.go +++ b/internal/proto/clusterbootstrappb/cluster_bootstrap_vtproto.pb.go @@ -5,6 +5,7 @@ package clusterbootstrappb import ( + binary "encoding/binary" fmt "fmt" protohelpers "github.com/planetscale/vtprotobuf/protohelpers" proto "google.golang.org/protobuf/proto" @@ -90,11 +91,17 @@ func (m *JoinAsLearnerRequest) CloneVT() *JoinAsLearnerRequest { r.NodeId = m.NodeId r.RaftAddress = m.RaftAddress r.ServiceAddress = m.ServiceAddress + r.CapabilityMembershipRevision = m.CapabilityMembershipRevision if rhs := m.InstanceId; rhs != nil { tmpBytes := make([]byte, len(rhs)) copy(tmpBytes, rhs) r.InstanceId = tmpBytes } + if rhs := m.Capabilities; rhs != nil { + tmpContainer := make([]uint32, len(rhs)) + copy(tmpContainer, rhs) + r.Capabilities = tmpContainer + } if len(m.unknownFields) > 0 { r.unknownFields = make([]byte, len(m.unknownFields)) copy(r.unknownFields, m.unknownFields) @@ -217,6 +224,18 @@ func (this *JoinAsLearnerRequest) EqualVT(that *JoinAsLearnerRequest) bool { if string(this.InstanceId) != string(that.InstanceId) { return false } + if len(this.Capabilities) != len(that.Capabilities) { + return false + } + for i, vx := range this.Capabilities { + vy := that.Capabilities[i] + if vx != vy { + return false + } + } + if this.CapabilityMembershipRevision != that.CapabilityMembershipRevision { + return false + } return string(this.unknownFields) == string(that.unknownFields) } @@ -410,6 +429,21 @@ func (m *JoinAsLearnerRequest) MarshalToSizedBufferVT(dAtA []byte) (int, error) i -= len(m.unknownFields) copy(dAtA[i:], m.unknownFields) } + if m.CapabilityMembershipRevision != 0 { + i -= 8 + binary.LittleEndian.PutUint64(dAtA[i:], uint64(m.CapabilityMembershipRevision)) + i-- + dAtA[i] = 0x31 + } + if len(m.Capabilities) > 0 { + for iNdEx := len(m.Capabilities) - 1; iNdEx >= 0; iNdEx-- { + i -= 4 + binary.LittleEndian.PutUint32(dAtA[i:], uint32(m.Capabilities[iNdEx])) + } + i = protohelpers.EncodeVarint(dAtA, i, uint64(len(m.Capabilities)*4)) + i-- + dAtA[i] = 0x2a + } if len(m.InstanceId) > 0 { i -= len(m.InstanceId) copy(dAtA[i:], m.InstanceId) @@ -544,6 +578,12 @@ func (m *JoinAsLearnerRequest) SizeVT() (n int) { if l > 0 { n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) } + if len(m.Capabilities) > 0 { + n += 1 + protohelpers.SizeOfVarint(uint64(len(m.Capabilities)*4)) + len(m.Capabilities)*4 + } + if m.CapabilityMembershipRevision != 0 { + n += 9 + } n += len(m.unknownFields) return n } @@ -1008,6 +1048,68 @@ func (m *JoinAsLearnerRequest) UnmarshalVT(dAtA []byte) error { m.InstanceId = []byte{} } iNdEx = postIndex + case 5: + if wireType == 5 { + var v uint32 + if (iNdEx + 4) > l { + return io.ErrUnexpectedEOF + } + v = uint32(binary.LittleEndian.Uint32(dAtA[iNdEx:])) + iNdEx += 4 + m.Capabilities = append(m.Capabilities, v) + } else if wireType == 2 { + var packedLen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + packedLen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if packedLen < 0 { + return protohelpers.ErrInvalidLength + } + postIndex := iNdEx + packedLen + if postIndex < 0 { + return protohelpers.ErrInvalidLength + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + var elementCount int + elementCount = packedLen / 4 + if elementCount != 0 && len(m.Capabilities) == 0 { + m.Capabilities = make([]uint32, 0, elementCount) + } + for iNdEx < postIndex { + var v uint32 + if (iNdEx + 4) > l { + return io.ErrUnexpectedEOF + } + v = uint32(binary.LittleEndian.Uint32(dAtA[iNdEx:])) + iNdEx += 4 + m.Capabilities = append(m.Capabilities, v) + } + } else { + return fmt.Errorf("proto: wrong wireType = %d for field Capabilities", wireType) + } + case 6: + if wireType != 1 { + return fmt.Errorf("proto: wrong wireType = %d for field CapabilityMembershipRevision", wireType) + } + m.CapabilityMembershipRevision = 0 + if (iNdEx + 8) > l { + return io.ErrUnexpectedEOF + } + m.CapabilityMembershipRevision = uint64(binary.LittleEndian.Uint64(dAtA[iNdEx:])) + iNdEx += 8 default: iNdEx = preIndex skippy, err := protohelpers.Skip(dAtA[iNdEx:]) diff --git a/internal/proto/raftcmdpb/raft_cmd.pb.go b/internal/proto/raftcmdpb/raft_cmd.pb.go index 09ffda9090..cb9b7847b5 100644 --- a/internal/proto/raftcmdpb/raft_cmd.pb.go +++ b/internal/proto/raftcmdpb/raft_cmd.pb.go @@ -5581,8 +5581,13 @@ type PeerAddress struct { RaftAddress string `protobuf:"bytes,2,opt,name=raft_address,json=raftAddress,proto3" json:"raft_address,omitempty"` ServiceAddress string `protobuf:"bytes,3,opt,name=service_address,json=serviceAddress,proto3" json:"service_address,omitempty"` InstanceId []byte `protobuf:"bytes,4,opt,name=instance_id,json=instanceId,proto3" json:"instance_id,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // Capability evidence is bound to this concrete (node_id, instance_id) + // incarnation. It is persisted with the peer row so a restart cannot + // accidentally reuse evidence from an older WAL/PVC incarnation. + Capabilities []uint32 `protobuf:"fixed32,5,rep,packed,name=capabilities,proto3" json:"capabilities,omitempty"` + CapabilityMembershipRevision uint64 `protobuf:"fixed64,6,opt,name=capability_membership_revision,json=capabilityMembershipRevision,proto3" json:"capability_membership_revision,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *PeerAddress) Reset() { @@ -5643,6 +5648,20 @@ func (x *PeerAddress) GetInstanceId() []byte { return nil } +func (x *PeerAddress) GetCapabilities() []uint32 { + if x != nil { + return x.Capabilities + } + return nil +} + +func (x *PeerAddress) GetCapabilityMembershipRevision() uint64 { + if x != nil { + return x.CapabilityMembershipRevision + } + return 0 +} + // RemovedMemberEntry is the persisted blacklist row keyed by // [ZoneClusterPersistent][SubGlobRemovedMembers][node_id BE 8][instance_id 16]. Written // by both the consensus and force removal paths, read by JoinAsLearner @@ -6149,13 +6168,15 @@ const file_raft_cmd_proto_rawDesc = "" + "\n" + "base_index\x18\x01 \x01(\x06R\tbaseIndex\"B\n" + "\x11CacheSnapshotMeta\x12-\n" + - "\x12current_generation\x18\x01 \x01(\x06R\x11currentGeneration\"\x93\x01\n" + + "\x12current_generation\x18\x01 \x01(\x06R\x11currentGeneration\"\xfd\x01\n" + "\vPeerAddress\x12\x17\n" + "\anode_id\x18\x01 \x01(\x04R\x06nodeId\x12!\n" + "\fraft_address\x18\x02 \x01(\tR\vraftAddress\x12'\n" + "\x0fservice_address\x18\x03 \x01(\tR\x0eserviceAddress\x12\x1f\n" + "\vinstance_id\x18\x04 \x01(\fR\n" + - "instanceId\"\x85\x01\n" + + "instanceId\x12\"\n" + + "\fcapabilities\x18\x05 \x03(\aR\fcapabilities\x12D\n" + + "\x1ecapability_membership_revision\x18\x06 \x01(\x06R\x1ccapabilityMembershipRevision\"\x85\x01\n" + "\x12RemovedMemberEntry\x12\x17\n" + "\anode_id\x18\x01 \x01(\x04R\x06nodeId\x12\x1f\n" + "\vinstance_id\x18\x02 \x01(\fR\n" + diff --git a/internal/proto/raftcmdpb/raft_cmd_reader.pb.go b/internal/proto/raftcmdpb/raft_cmd_reader.pb.go index b5e7bf6456..712a8a9719 100644 --- a/internal/proto/raftcmdpb/raft_cmd_reader.pb.go +++ b/internal/proto/raftcmdpb/raft_cmd_reader.pb.go @@ -5965,6 +5965,8 @@ type PeerAddressReader interface { GetRaftAddress() string GetServiceAddress() string GetInstanceId() []byte + GetCapabilities() []uint32 + GetCapabilityMembershipRevision() uint64 Mutate() *PeerAddress } @@ -5986,6 +5988,14 @@ func (r *peerAddressReadonly) GetInstanceId() []byte { return bytes.Clone((*PeerAddress)(r).GetInstanceId()) } +func (r *peerAddressReadonly) GetCapabilities() []uint32 { + return slices.Clone((*PeerAddress)(r).GetCapabilities()) +} + +func (r *peerAddressReadonly) GetCapabilityMembershipRevision() uint64 { + return (*PeerAddress)(r).GetCapabilityMembershipRevision() +} + func (r *peerAddressReadonly) Mutate() *PeerAddress { return (*PeerAddress)(r).CloneVT() } diff --git a/internal/proto/raftcmdpb/raft_cmd_vtproto.pb.go b/internal/proto/raftcmdpb/raft_cmd_vtproto.pb.go index 6c701d53d1..5d2d0f5d57 100644 --- a/internal/proto/raftcmdpb/raft_cmd_vtproto.pb.go +++ b/internal/proto/raftcmdpb/raft_cmd_vtproto.pb.go @@ -2168,11 +2168,17 @@ func (m *PeerAddress) CloneVT() *PeerAddress { r.NodeId = m.NodeId r.RaftAddress = m.RaftAddress r.ServiceAddress = m.ServiceAddress + r.CapabilityMembershipRevision = m.CapabilityMembershipRevision if rhs := m.InstanceId; rhs != nil { tmpBytes := make([]byte, len(rhs)) copy(tmpBytes, rhs) r.InstanceId = tmpBytes } + if rhs := m.Capabilities; rhs != nil { + tmpContainer := make([]uint32, len(rhs)) + copy(tmpContainer, rhs) + r.Capabilities = tmpContainer + } if len(m.unknownFields) > 0 { r.unknownFields = make([]byte, len(m.unknownFields)) copy(r.unknownFields, m.unknownFields) @@ -5945,6 +5951,18 @@ func (this *PeerAddress) EqualVT(that *PeerAddress) bool { if string(this.InstanceId) != string(that.InstanceId) { return false } + if len(this.Capabilities) != len(that.Capabilities) { + return false + } + for i, vx := range this.Capabilities { + vy := that.Capabilities[i] + if vx != vy { + return false + } + } + if this.CapabilityMembershipRevision != that.CapabilityMembershipRevision { + return false + } return string(this.unknownFields) == string(that.unknownFields) } @@ -11469,6 +11487,21 @@ func (m *PeerAddress) MarshalToSizedBufferVT(dAtA []byte) (int, error) { i -= len(m.unknownFields) copy(dAtA[i:], m.unknownFields) } + if m.CapabilityMembershipRevision != 0 { + i -= 8 + binary.LittleEndian.PutUint64(dAtA[i:], uint64(m.CapabilityMembershipRevision)) + i-- + dAtA[i] = 0x31 + } + if len(m.Capabilities) > 0 { + for iNdEx := len(m.Capabilities) - 1; iNdEx >= 0; iNdEx-- { + i -= 4 + binary.LittleEndian.PutUint32(dAtA[i:], uint32(m.Capabilities[iNdEx])) + } + i = protohelpers.EncodeVarint(dAtA, i, uint64(len(m.Capabilities)*4)) + i-- + dAtA[i] = 0x2a + } if len(m.InstanceId) > 0 { i -= len(m.InstanceId) copy(dAtA[i:], m.InstanceId) @@ -14204,6 +14237,12 @@ func (m *PeerAddress) SizeVT() (n int) { if l > 0 { n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) } + if len(m.Capabilities) > 0 { + n += 1 + protohelpers.SizeOfVarint(uint64(len(m.Capabilities)*4)) + len(m.Capabilities)*4 + } + if m.CapabilityMembershipRevision != 0 { + n += 9 + } n += len(m.unknownFields) return n } @@ -26609,6 +26648,68 @@ func (m *PeerAddress) UnmarshalVT(dAtA []byte) error { m.InstanceId = []byte{} } iNdEx = postIndex + case 5: + if wireType == 5 { + var v uint32 + if (iNdEx + 4) > l { + return io.ErrUnexpectedEOF + } + v = uint32(binary.LittleEndian.Uint32(dAtA[iNdEx:])) + iNdEx += 4 + m.Capabilities = append(m.Capabilities, v) + } else if wireType == 2 { + var packedLen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + packedLen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if packedLen < 0 { + return protohelpers.ErrInvalidLength + } + postIndex := iNdEx + packedLen + if postIndex < 0 { + return protohelpers.ErrInvalidLength + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + var elementCount int + elementCount = packedLen / 4 + if elementCount != 0 && len(m.Capabilities) == 0 { + m.Capabilities = make([]uint32, 0, elementCount) + } + for iNdEx < postIndex { + var v uint32 + if (iNdEx + 4) > l { + return io.ErrUnexpectedEOF + } + v = uint32(binary.LittleEndian.Uint32(dAtA[iNdEx:])) + iNdEx += 4 + m.Capabilities = append(m.Capabilities, v) + } + } else { + return fmt.Errorf("proto: wrong wireType = %d for field Capabilities", wireType) + } + case 6: + if wireType != 1 { + return fmt.Errorf("proto: wrong wireType = %d for field CapabilityMembershipRevision", wireType) + } + m.CapabilityMembershipRevision = 0 + if (iNdEx + 8) > l { + return io.ErrUnexpectedEOF + } + m.CapabilityMembershipRevision = uint64(binary.LittleEndian.Uint64(dAtA[iNdEx:])) + iNdEx += 8 default: iNdEx = preIndex skippy, err := protohelpers.Skip(dAtA[iNdEx:]) diff --git a/misc/proto/cluster_bootstrap.proto b/misc/proto/cluster_bootstrap.proto index 030ef7ad65..95a6742e02 100644 --- a/misc/proto/cluster_bootstrap.proto +++ b/misc/proto/cluster_bootstrap.proto @@ -45,6 +45,8 @@ message JoinAsLearnerRequest { string raft_address = 2; // Raft transport address of the learner string service_address = 3; // Service API address of the learner bytes instance_id = 4; // 16-byte UUID identifying this peer's WAL/PVC incarnation (EN-1045); required + repeated fixed32 capabilities = 5; // capabilities supported by this binary + fixed64 capability_membership_revision = 6; // revision examined by the joiner, 0 lets leader bind current revision } message JoinAsLearnerResponse {} diff --git a/misc/proto/raft_cmd.proto b/misc/proto/raft_cmd.proto index 5c5ffe0f22..21ddd3b12a 100644 --- a/misc/proto/raft_cmd.proto +++ b/misc/proto/raft_cmd.proto @@ -839,6 +839,11 @@ message PeerAddress { string raft_address = 2; string service_address = 3; bytes instance_id = 4; + // Capability evidence is bound to this concrete (node_id, instance_id) + // incarnation. It is persisted with the peer row so a restart cannot + // accidentally reuse evidence from an older WAL/PVC incarnation. + repeated fixed32 capabilities = 5; + fixed64 capability_membership_revision = 6; } // RemovedMemberEntry is the persisted blacklist row keyed by From 739c5bf627f4108d1f8d66cbcfbd5399fb0ebb0d Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Wed, 21 Oct 2026 17:35:34 +0200 Subject: [PATCH 003/127] fix(ledger): declare capabilities for barriers --- internal/application/admission/admission.go | 12 ++++++++ .../admission/capability_policy_test.go | 30 +++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/internal/application/admission/admission.go b/internal/application/admission/admission.go index 574f1f2c50..6ed7a72c53 100644 --- a/internal/application/admission/admission.go +++ b/internal/application/admission/admission.go @@ -1053,12 +1053,24 @@ func (a *Admission) Barrier(ctx context.Context) (uint64, error) { } cmd := commands.NewCommand() // no orders = no-op + // Barrier bypasses plan.Builder.Run, so it must still stamp the semantic + // capability declaration before serialization. The no-op command created by + // commands.NewCommand carries an empty ExecutionPlan, which is itself a + // protocol dependency for replaying the envelope. + if err := capabilities.StampProposal(cmd); err != nil { + return 0, fmt.Errorf("stamping barrier capabilities: %w", err) + } + requiredCapabilities, err := capabilities.RequiredCapabilities(cmd) + if err != nil { + return 0, fmt.Errorf("deriving barrier capabilities: %w", err) + } proposalData, err := a.marshalCommand(ctx, cmd) if err != nil { return 0, fmt.Errorf("marshaling barrier command: %w", err) } proposal := node.NewProposal(cmd.GetId(), proposalData) + proposal.RequireCapabilities(requiredCapabilities...) // Lock the tracker to serialize the Increment with guarded proposals, // preventing preload boundary mismatches in the FSM. diff --git a/internal/application/admission/capability_policy_test.go b/internal/application/admission/capability_policy_test.go index 8c880283f1..66408bc089 100644 --- a/internal/application/admission/capability_policy_test.go +++ b/internal/application/admission/capability_policy_test.go @@ -7,6 +7,7 @@ import ( "github.com/stretchr/testify/require" "github.com/formancehq/ledger/v3/internal/infra/capabilities" + "github.com/formancehq/ledger/v3/internal/infra/health" "github.com/formancehq/ledger/v3/internal/infra/node" "github.com/formancehq/ledger/v3/internal/infra/plan" "github.com/formancehq/ledger/v3/internal/infra/state" @@ -24,6 +25,35 @@ func (p *capabilityTestProposer) Propose(context.Context, *node.Proposal) (*futu var _ plan.Proposer = (*capabilityTestProposer)(nil) +type barrierTestProposer struct{} + +func (barrierTestProposer) InitialIndex() uint64 { return 1 } + +func (barrierTestProposer) Propose(_ context.Context, proposal *node.Proposal) (*futures.Future[state.ApplyResult], error) { + if err := capabilities.ValidateFinalProposal(proposal.Data(), proposal.DeclaredCapabilities()); err != nil { + return nil, err + } + proposal.Resolve(nil, nil) + future := futures.New[state.ApplyResult]() + future.Resolve(state.ApplyResult{AppliedIndex: 1}, nil) + return future, nil +} + +type allowWrites struct{} + +func (allowWrites) CheckWritesAllowed() error { return nil } + +var _ health.WriteGate = allowWrites{} + +func TestBarrierStampsSemanticCapabilities(t *testing.T) { + a, _ := createTestAdmission(t, createTestStore(t)) + a.writeGate = allowWrites{} + a.proposer = barrierTestProposer{} + + _, err := a.Barrier(context.Background()) + require.NoError(t, err) +} + func TestCapabilityPolicyProposerBlocksAllWritesAtFinalBoundary(t *testing.T) { delegate := new(capabilityTestProposer) proposer := capabilityPolicyProposer{ From e3a13cb5f777c4b07c50298bc2f85c18fe9a4391 Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Wed, 21 Oct 2026 17:47:31 +0200 Subject: [PATCH 004/127] chore(ledger): apply CI lint formatting --- .../application/admission/capability_policy_test.go | 2 ++ internal/bootstrap/module.go | 1 + internal/infra/capabilities/capabilities.go | 11 +++++++++++ internal/infra/membership/confchange.go | 11 ++++++----- internal/infra/membership/membership.go | 2 +- internal/infra/node/node.go | 3 +++ internal/infra/plan/capability_policy_test.go | 1 + internal/infra/plan/runner.go | 2 ++ 8 files changed, 27 insertions(+), 6 deletions(-) diff --git a/internal/application/admission/capability_policy_test.go b/internal/application/admission/capability_policy_test.go index 66408bc089..8f505f0b28 100644 --- a/internal/application/admission/capability_policy_test.go +++ b/internal/application/admission/capability_policy_test.go @@ -20,6 +20,7 @@ type capabilityTestProposer struct{ called bool } func (p *capabilityTestProposer) Propose(context.Context, *node.Proposal) (*futures.Future[state.ApplyResult], error) { p.called = true + return futures.New[state.ApplyResult](), nil } @@ -36,6 +37,7 @@ func (barrierTestProposer) Propose(_ context.Context, proposal *node.Proposal) ( proposal.Resolve(nil, nil) future := futures.New[state.ApplyResult]() future.Resolve(state.ApplyResult{AppliedIndex: 1}, nil) + return future, nil } diff --git a/internal/bootstrap/module.go b/internal/bootstrap/module.go index f847470284..45150de149 100644 --- a/internal/bootstrap/module.go +++ b/internal/bootstrap/module.go @@ -1471,6 +1471,7 @@ func capabilityIDsAsUint32(ids []capabilities.ID) []uint32 { for i, id := range ids { result[i] = uint32(id) } + return result } diff --git a/internal/infra/capabilities/capabilities.go b/internal/infra/capabilities/capabilities.go index 62958b8764..74d607f509 100644 --- a/internal/infra/capabilities/capabilities.go +++ b/internal/infra/capabilities/capabilities.go @@ -90,6 +90,7 @@ func ValidateMembers(members []Member) error { } seen[member.NodeID] = identity } + return nil } @@ -97,6 +98,7 @@ func ValidateMembershipRevision(observed, examined uint64) error { if examined == 0 || observed != examined { return fmt.Errorf("%w: membership revision changed from %d to %d", ErrInvalidState, examined, observed) } + return nil } @@ -104,6 +106,7 @@ func AdvanceReplayFloor(current, next uint32) (uint32, error) { if next < current { return current, fmt.Errorf("%w: replay protocol floor cannot decrease from %d to %d", ErrInvalidState, current, next) } + return next, nil } @@ -155,6 +158,7 @@ func StampProposal(proposal *raftcmdpb.Proposal) error { for i, id := range required { proposal.RequiredCapabilities[i] = uint32(id) } + return nil } @@ -180,6 +184,7 @@ func (s State) Validate() error { if s.MembershipRevision == 0 { return fmt.Errorf("%w: zero membership revision", ErrInvalidState) } + return nil } @@ -201,6 +206,7 @@ func FromPolicy(policy *commonpb.ClusterPolicy) State { s.Activations[ID(activation.GetId())] = activation.GetActive() } } + return s } @@ -212,6 +218,7 @@ func Normalize(ids []ID) ([]ID, error) { return nil, fmt.Errorf("%w: duplicate id %d", ErrInvalidDeclaration, result[i]) } } + return result, nil } @@ -241,6 +248,7 @@ func Evaluate(state State, required []ID, businessWrite bool, members []Member) for _, id := range required { if !slices.Contains(member.Capabilities, id) { blockers = append(blockers, Blocker{NodeID: member.NodeID, Role: member.Role, Reason: fmt.Sprintf("missing capability %d", id)}) + continue } if !member.Reachable { @@ -254,6 +262,7 @@ func Evaluate(state State, required []ID, businessWrite bool, members []Member) if businessWrite && state.Policy == AllWrites && len(required) == 0 { return Decision{Reason: "business writes are blocked while capability activation is armed", Blockers: blockers} } + return Decision{Allowed: true, Blockers: blockers} } @@ -286,6 +295,7 @@ func ValidateFinalProposal(data []byte, declared []ID) error { if !bytes.Equal(idsBytes(wire), idsBytes(declared)) { return fmt.Errorf("%w: declaration does not match final payload", ErrMissingCapability) } + return nil } @@ -297,5 +307,6 @@ func idsBytes(ids []ID) []byte { result[i*4+2] = byte(id >> 16) result[i*4+3] = byte(id >> 24) } + return result } diff --git a/internal/infra/membership/confchange.go b/internal/infra/membership/confchange.go index 1196c686d0..0ec8e09086 100644 --- a/internal/infra/membership/confchange.go +++ b/internal/infra/membership/confchange.go @@ -3,6 +3,7 @@ package membership import ( "bytes" "encoding/json" + "errors" "fmt" "slices" @@ -30,10 +31,10 @@ func ValidateInstanceID(instanceID []byte) error { func ValidateCapabilities(ids []uint32) error { for i, id := range ids { if id == 0 { - return fmt.Errorf("capability id must be non-zero") + return errors.New("capability id must be non-zero") } if i > 0 && ids[i-1] >= id { - return fmt.Errorf("capabilities must be sorted and unique") + return errors.New("capabilities must be sorted and unique") } } @@ -46,7 +47,7 @@ func ValidateCapabilities(ids []uint32) error { // must carry a fresh revision-bound capability set. func ValidateCapabilityEvidence(ctx *ConfChangeContext, armed bool, expectedRevision uint64) error { if ctx == nil { - return fmt.Errorf("missing capability membership evidence") + return errors.New("missing capability membership evidence") } if err := ValidateCapabilities(ctx.Capabilities); err != nil { return fmt.Errorf("invalid capability evidence: %w", err) @@ -55,13 +56,13 @@ func ValidateCapabilityEvidence(ctx *ConfChangeContext, armed bool, expectedRevi return nil } if expectedRevision == 0 { - return fmt.Errorf("capability membership revision is not configured") + return errors.New("capability membership revision is not configured") } if ctx.CapabilityMembershipRevision != expectedRevision { return fmt.Errorf("capability membership revision %d does not match active revision %d", ctx.CapabilityMembershipRevision, expectedRevision) } if len(ctx.Capabilities) == 0 { - return fmt.Errorf("capability evidence is empty while activation is armed") + return errors.New("capability evidence is empty while activation is armed") } return nil diff --git a/internal/infra/membership/membership.go b/internal/infra/membership/membership.go index 83925e84af..bbd0f84af6 100644 --- a/internal/infra/membership/membership.go +++ b/internal/infra/membership/membership.go @@ -251,7 +251,7 @@ func (m *Membership) SnapshotDonorCandidates() []uint64 { // a zero revision is accepted only while the barrier is disarmed. func (m *Membership) SetCapabilityGate(armed bool, revision uint64) error { if armed && revision == 0 { - return fmt.Errorf("capability membership revision must be non-zero when armed") + return errors.New("capability membership revision must be non-zero when armed") } m.mu.Lock() diff --git a/internal/infra/node/node.go b/internal/infra/node/node.go index 3fbf41236e..f5a6b7842b 100644 --- a/internal/infra/node/node.go +++ b/internal/infra/node/node.go @@ -103,6 +103,7 @@ func capabilityIDsAsUint32(ids []capabilities.ID) []uint32 { for i, id := range ids { result[i] = uint32(id) } + return result } @@ -2598,8 +2599,10 @@ func (node *Node) CapabilityMembers() []capabilities.Member { if a.NodeID > b.NodeID { return 1 } + return 0 }) + return result } diff --git a/internal/infra/plan/capability_policy_test.go b/internal/infra/plan/capability_policy_test.go index 88f156ff4c..021dd50143 100644 --- a/internal/infra/plan/capability_policy_test.go +++ b/internal/infra/plan/capability_policy_test.go @@ -18,6 +18,7 @@ type capabilityPolicyTestProposer struct{ called bool } func (p *capabilityPolicyTestProposer) Propose(context.Context, *node.Proposal) (*futures.Future[state.ApplyResult], error) { p.called = true + return futures.New[state.ApplyResult](), nil } diff --git a/internal/infra/plan/runner.go b/internal/infra/plan/runner.go index de17d5375e..1ed836ac41 100644 --- a/internal/infra/plan/runner.go +++ b/internal/infra/plan/runner.go @@ -259,6 +259,7 @@ func (p *Builder) Run( required, err := capabilities.RequiredCapabilities(cmd) if err != nil { guard.ReleaseAll() + return nil, fmt.Errorf("%w: %w", ErrMarshalProposal, err) } proposal.RequireCapabilities(required...) @@ -396,6 +397,7 @@ func (p *Builder) runWithoutPreload( required, err := capabilities.RequiredCapabilities(cmd) if err != nil { guard.ReleaseLoaders() + return nil, fmt.Errorf("%w: %w", ErrMarshalProposal, err) } proposal.RequireCapabilities(required...) From 9e84e39289338b75146620590c905319356bf38c Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Thu, 22 Oct 2026 13:04:18 +0200 Subject: [PATCH 005/127] fix(ledger): refresh capabilities on member restart --- .../subsystems/consensus/capabilities.md | 15 + internal/adapter/grpc/server_bootstrap.go | 47 ++ internal/application/membership/membership.go | 15 + internal/bootstrap/join_preflight_test.go | 25 + internal/bootstrap/module.go | 108 +++- internal/infra/node/learner_admission_test.go | 46 ++ internal/infra/node/node.go | 80 +++ .../cluster_bootstrap.pb.go | 164 +++++- .../cluster_bootstrap_dethash.pb.go | 22 + .../cluster_bootstrap_grpc.pb.go | 50 +- .../cluster_bootstrap_reader.pb.go | 154 +++++ .../cluster_bootstrap_vtproto.pb.go | 531 ++++++++++++++++++ misc/proto/cluster_bootstrap.proto | 20 +- 13 files changed, 1258 insertions(+), 19 deletions(-) diff --git a/docs/technical/architecture/subsystems/consensus/capabilities.md b/docs/technical/architecture/subsystems/consensus/capabilities.md index 66aeebee58..d6c0950f8e 100644 --- a/docs/technical/architecture/subsystems/consensus/capabilities.md +++ b/docs/technical/architecture/subsystems/consensus/capabilities.md @@ -52,3 +52,18 @@ business and technical producers (including mirror, events, maintenance, and backup flows). Direct `Node.Propose` callers remain low-level APIs and must stamp their proposal before use; membership `ConfChange` operations carry and validate their capability evidence separately. + +## Restart refresh + +A normal restart does not run `JoinAsLearner`: the node restores its existing +WAL `ConfState` and keeps the same `(node_id, instance_id)` incarnation. Before +inbound Raft traffic is enabled, a node whose WAL carries `CLUSTER_JOINED` +sends an internal `RefreshMembership` request to a configured peer. Followers +forward that request to the current leader over the Raft gRPC server. The +leader validates the identity and capability evidence, then proposes a +`ConfChangeUpdateNode` containing the current endpoints and capabilities. + +Only the committed change is authoritative. The update is replicated through +Raft and applied to every member's peer store; a local restart-time upsert is +not sufficient to activate a capability for admission. A fresh WAL instead +uses `JoinAsLearner` and `ConfChangeAddLearnerNode`. diff --git a/internal/adapter/grpc/server_bootstrap.go b/internal/adapter/grpc/server_bootstrap.go index 924a34d936..05e74c27c9 100644 --- a/internal/adapter/grpc/server_bootstrap.go +++ b/internal/adapter/grpc/server_bootstrap.go @@ -15,6 +15,7 @@ import ( logging "github.com/formancehq/go-libs/v5/pkg/observe/log" "github.com/formancehq/ledger/v3/internal/application/membership" + raftmembership "github.com/formancehq/ledger/v3/internal/infra/membership" "github.com/formancehq/ledger/v3/internal/infra/node" "github.com/formancehq/ledger/v3/internal/proto/clusterbootstrappb" "github.com/formancehq/ledger/v3/internal/proto/commonpb" @@ -293,6 +294,52 @@ func (impl *ClusterBootstrapServiceServerImpl) JoinAsLearner(ctx context.Context return &clusterbootstrappb.JoinAsLearnerResponse{}, nil } +// RefreshMembership forwards a normal-restart membership refresh to the +// leader. Unlike JoinAsLearner, this endpoint only accepts an incarnation that +// is already present in the committed membership and proposes UpdateNode. +func (impl *ClusterBootstrapServiceServerImpl) RefreshMembership(ctx context.Context, req *clusterbootstrappb.RefreshMembershipRequest) (*clusterbootstrappb.RefreshMembershipResponse, error) { + if err := impl.checkClusterID(ctx); err != nil { + return nil, err + } + + if err := raftmembership.ValidateInstanceID(req.GetInstanceId()); err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + + if !impl.node.IsLeader() { + conn, err := impl.leaderRaftConn() + if err != nil { + return nil, convertToGRPCErrorWithContext(ctx, err, impl.logger) + } + + outCtx := ctx + if impl.clusterID != "" { + outCtx = metadata.AppendToOutgoingContext(ctx, node.MetadataKeyClusterID, impl.clusterID) + } + + resp, err := clusterbootstrappb.NewClusterBootstrapServiceClient(conn).RefreshMembership(outCtx, req) + if err != nil { + return nil, forwardedCallError(err) + } + + return resp, nil + } + + removed, err := impl.membership.IsRemoved(req.GetNodeId(), req.GetInstanceId()) + if err != nil { + return nil, status.Error(codes.Internal, "reading removed-member registry") + } + if removed { + return nil, status.Errorf(codes.FailedPrecondition, "node %d (instance %x) was previously removed from this cluster", req.GetNodeId(), req.GetInstanceId()) + } + + if err := impl.membership.RefreshMembership(ctx, req.GetNodeId(), req.GetRaftAddress(), req.GetServiceAddress(), req.GetInstanceId(), req.GetCapabilities(), req.GetCapabilityMembershipRevision()); err != nil { + return nil, convertToGRPCErrorWithContext(ctx, err, impl.logger) + } + + return &clusterbootstrappb.RefreshMembershipResponse{}, nil +} + // forwardedCallError maps the failure of a call forwarded to the leader. // The leader converts its own context errors to Unavailable, so Canceled // and DeadlineExceeded can only come from the forwarding hop: the shared diff --git a/internal/application/membership/membership.go b/internal/application/membership/membership.go index 5aec056067..7176f966d5 100644 --- a/internal/application/membership/membership.go +++ b/internal/application/membership/membership.go @@ -101,6 +101,21 @@ func (s *Service) JoinAsLearner(ctx context.Context, nodeID uint64, raftAddr, se return s.addLearner(ctx, nodeID, raftAddr, serviceAddr, instanceID, true) } +// RefreshMembership updates the durable identity, endpoints and capability +// evidence of an existing member. The node must already be present in the +// committed Raft membership; the leader commits the update through Raft. +func (s *Service) RefreshMembership(ctx context.Context, nodeID uint64, raftAddr, serviceAddr string, instanceID []byte, capabilityIDs []uint32, membershipRevision uint64) error { + if err := validatePeer(nodeID, raftAddr, serviceAddr, instanceID); err != nil { + return err + } + + if err := s.node.RefreshMembership(ctx, nodeID, raftAddr, serviceAddr, instanceID, capabilityIDs, membershipRevision); err != nil { + return fmt.Errorf("refreshing membership: %w", err) + } + + return nil +} + func (s *Service) addLearner(ctx context.Context, nodeID uint64, raftAddr, serviceAddr string, instanceID []byte, bootJoin bool) error { if err := validatePeer(nodeID, raftAddr, serviceAddr, instanceID); err != nil { return err diff --git a/internal/bootstrap/join_preflight_test.go b/internal/bootstrap/join_preflight_test.go index 5bc556dcaa..0555429709 100644 --- a/internal/bootstrap/join_preflight_test.go +++ b/internal/bootstrap/join_preflight_test.go @@ -68,6 +68,31 @@ func TestShouldRunJoinPreflight(t *testing.T) { }) } +func TestShouldRunMembershipRefresh(t *testing.T) { + t.Parallel() + logger := logging.Testing() + + t.Run("fresh WAL skips refresh", func(t *testing.T) { + t.Parallel() + cfg := Config{RaftConfig: node.NodeConfig{ + Peers: []node.Peer{{ID: 1, Address: "leader:7777"}}, + WalDir: t.TempDir(), + }} + assert.False(t, shouldRunMembershipRefresh(cfg, logger)) + }) + + t.Run("joined WAL refreshes", func(t *testing.T) { + t.Parallel() + walDir := t.TempDir() + require.NoError(t, wal.MarkClusterJoined(walDir)) + cfg := Config{RaftConfig: node.NodeConfig{ + Peers: []node.Peer{{ID: 1, Address: "leader:7777"}}, + WalDir: walDir, + }} + assert.True(t, shouldRunMembershipRefresh(cfg, logger)) + }) +} + // TestJoinPreflightRunsBeforeRaftTraffic proves the EN-1436 ordering invariant // (flemzord review on #1478): the join preflight OnStart hook must run — and, // when it fails, abort startup — BEFORE any hook that lets inbound Raft traffic diff --git a/internal/bootstrap/module.go b/internal/bootstrap/module.go index 45150de149..467804102e 100644 --- a/internal/bootstrap/module.go +++ b/internal/bootstrap/module.go @@ -931,9 +931,19 @@ func Module() fx.Option { } })) }, - func(lc fx.Lifecycle, node *node.Node, defaultTransport *node.DefaultTransport, logger logging.Logger) (*node.Node, error) { + func(lc fx.Lifecycle, node *node.Node, defaultTransport *node.DefaultTransport, cfg Config, logger logging.Logger) (*node.Node, error) { lc.Append(nodeHook(node, logger)) + // A WAL that already joined the cluster must not take the learner + // bootstrap path again. It does, however, need to publish the + // capabilities of the restarted binary and any changed endpoints. + // This hook runs after the Raft server, transport and node are live; + // an UpdateNode for an existing voter may need that node to contribute + // to the commit quorum. + if shouldRunMembershipRefresh(cfg, logger) { + lc.Append(membershipRefreshHook(cfg, logger)) + } + return node, nil }, func(lc fx.Lifecycle, observer *logintegrity.Observer) { @@ -1202,6 +1212,24 @@ func shouldRunJoinPreflight(cfg Config, logger logging.Logger) bool { return true } +// shouldRunMembershipRefresh selects the normal-restart path. A fresh WAL is +// handled by joinPreflightHook; a bootstrap node has no remote leader to +// notify. The CLUSTER_JOINED marker is the durable boundary between those two +// startup protocols. +func shouldRunMembershipRefresh(cfg Config, logger logging.Logger) bool { + if cfg.RaftConfig.Bootstrap || len(cfg.RaftConfig.Peers) == 0 { + return false + } + + if !wal.IsClusterJoined(cfg.RaftConfig.WalDir) { + return false + } + + logger.Infof("CLUSTER_JOINED marker present, refreshing membership evidence") + + return true +} + func registerSnapshotServiceLifecycle(lc fx.Lifecycle, server snapshotpb.SnapshotServiceServer) { lc.Append(fx.Hook{ OnStart: func(_ context.Context) error { @@ -1283,6 +1311,20 @@ func joinPreflightHook(cfg Config, logger logging.Logger) fx.Hook { } } +func membershipRefreshHook(cfg Config, logger logging.Logger) fx.Hook { + return fx.Hook{ + OnStart: func(ctx context.Context) error { + logger.WithFields(map[string]any{ + "nodeID": cfg.RaftConfig.NodeID, + "raftAddress": cfg.RaftConfig.AdvertiseAddr, + "serviceAddress": cfg.ServiceAdvertiseAddr(), + }).Infof("Restart mode: refreshing membership capabilities") + + return tryRefreshMembership(ctx, cfg, cfg.TLSConfig, logger) + }, + } +} + // tryAddLearner attempts to register this node as a learner on an existing // cluster, using the inter-node ClusterBootstrapService exposed on the // RaftServer. It retries on transient errors (Unavailable, no leader) @@ -1466,6 +1508,70 @@ func tryAddLearner(ctx context.Context, cfg Config, tlsCfg TLSConfig, logger log } } +// tryRefreshMembership publishes this incarnation's current endpoints and +// capabilities through the leader. It is deliberately separate from +// tryAddLearner: a node with a populated WAL already owns Raft progress and +// must never be admitted through the empty-WAL learner path. +func tryRefreshMembership(ctx context.Context, cfg Config, tlsCfg TLSConfig, logger logging.Logger) error { + instanceID, err := wal.EnsureInstanceID(cfg.RaftConfig.WalDir) + if err != nil { + return fmt.Errorf("ensuring instance id for membership refresh: %w", err) + } + + req := &clusterbootstrappb.RefreshMembershipRequest{ + NodeId: cfg.RaftConfig.NodeID, + RaftAddress: cfg.RaftConfig.AdvertiseAddr, + ServiceAddress: cfg.ServiceAdvertiseAddr(), + InstanceId: instanceID, + Capabilities: capabilityIDsAsUint32(capabilities.SupportedIDs()), + } + + creds, _, err := ClientTransportCredentials(tlsCfg) + if err != nil { + return fmt.Errorf("loading TLS credentials for membership refresh: %w", err) + } + + dialOpts := []ggrpc.DialOption{ggrpc.WithTransportCredentials(creds)} + if cfg.ClusterSecret != "" { + dialOpts = append(dialOpts, transport.BearerTokenDialOption(cfg.ClusterSecret)) + } + if cfg.ClusterID != "" { + ctx = metadata.AppendToOutgoingContext(ctx, node.MetadataKeyClusterID, cfg.ClusterID) + } + + backoff := 500 * time.Millisecond + for { + for _, peer := range cfg.RaftConfig.Peers { + conn, dialErr := ggrpc.NewClient(peer.Address, dialOpts...) + if dialErr != nil { + logger.WithFields(map[string]any{"peer": peer.ID, "address": peer.Address, "error": dialErr}).Infof("Failed to dial peer for membership refresh, will retry") + continue + } + + _, callErr := clusterbootstrappb.NewClusterBootstrapServiceClient(conn).RefreshMembership(ctx, req) + _ = conn.Close() + if callErr == nil { + logger.WithFields(map[string]any{"peer": peer.ID}).Infof("Successfully refreshed membership capabilities") + + return nil + } + + if st, ok := status.FromError(callErr); ok && st.Code() == codes.Unavailable { + continue + } + + return fmt.Errorf("failed to refresh membership via peer %d (%s): %w", peer.ID, peer.Address, callErr) + } + + select { + case <-ctx.Done(): + return ctx.Err() + case <-time.After(backoff): + } + backoff = min(backoff*2, 5*time.Second) + } +} + func capabilityIDsAsUint32(ids []capabilities.ID) []uint32 { result := make([]uint32, len(ids)) for i, id := range ids { diff --git a/internal/infra/node/learner_admission_test.go b/internal/infra/node/learner_admission_test.go index 66528b8038..1877556bc4 100644 --- a/internal/infra/node/learner_admission_test.go +++ b/internal/infra/node/learner_admission_test.go @@ -130,3 +130,49 @@ func TestAddLearnerZeroProgressRefreshProposesCompleteReplacement(t *testing.T) require.NoError(t, receiveForceRemoveError(t, results)) }) } + +func TestRefreshMembershipProposesUpdateForExistingIdentity(t *testing.T) { + t.Parallel() + + n := newConfiguredPeersTestNode(t) + n.logger = logging.Testing() + setup := newTestApplierSetupWithConfChangeHandler(t, make(LocalResponses, 1024), n.membership.WriteConfChange) + n.fsm = setup.fsm + + ctx, cancel := context.WithTimeout(t.Context(), time.Second) + defer cancel() + results := make(chan error, 1) + go func() { + results <- n.RefreshMembership(ctx, 2, "refreshed:7777", "refreshed:8888", []byte("peer-instance-id"), []uint32{1, 2}, 0) + }() + + cmd := <-n.clusterCommandCh + require.NoError(t, cmd.fn()) + ready := n.rawNode.Ready() + var changes []*raftpb.Entry + for _, entry := range ready.Entries { + if entry.GetType() == raftpb.EntryConfChangeV2 { + changes = append(changes, entry) + } + } + require.Len(t, changes, 1) + cc, ok, err := membership.UnmarshalConfChangeV2(changes[0]) + require.NoError(t, err) + require.True(t, ok) + require.Equal(t, raftpb.ConfChangeUpdateNode, cc.GetChanges()[0].GetType()) + payload, err := membership.UnmarshalConfChangeContext(cc.GetContext()) + require.NoError(t, err) + require.Equal(t, []uint32{1, 2}, payload.Capabilities) + require.Equal(t, []byte("peer-instance-id"), payload.InstanceID) + require.Equal(t, "refreshed:7777", payload.RaftAddress) + + pending, err := n.takePendingConfChange(cc, changes[0].GetIndex()) + require.NoError(t, err) + prefix, err := n.wal.Entries(1, changes[0].GetIndex(), ^uint64(0)) + require.NoError(t, err) + _, err = setup.fsm.ApplyEntries(t.Context(), setup.store, append(prefix, changes[0])...) + require.NoError(t, err) + pending.future.Resolve(pending.index, nil) + cmd.errCh <- nil + require.NoError(t, receiveForceRemoveError(t, results)) +} diff --git a/internal/infra/node/node.go b/internal/infra/node/node.go index f5a6b7842b..2fef208337 100644 --- a/internal/infra/node/node.go +++ b/internal/infra/node/node.go @@ -3221,6 +3221,86 @@ func (node *Node) JoinAsLearnerWithCapabilities(ctx context.Context, nodeID uint return node.addLearnerWithCapabilities(ctx, nodeID, raftAddr, serviceAddr, instanceID, capabilityIDs, membershipRevision, true) } +// RefreshMembership proposes a committed UpdateNode for an existing member. +// Unlike JoinAsLearner, this path is used by a node with an already populated +// WAL after a normal restart. It requires the same persisted instance identity +// and replaces the committed endpoint/capability evidence through Raft. +// Must be called on the leader. +func (node *Node) RefreshMembership(ctx context.Context, nodeID uint64, raftAddr, serviceAddr string, instanceID []byte, capabilityIDs []uint32, membershipRevision uint64) error { + if err := membership.ValidateInstanceID(instanceID); err != nil { + return fmt.Errorf("invalid member identity: %w", err) + } + if armed, activeRevision := node.membership.CapabilityGate(); armed && membershipRevision == 0 { + membershipRevision = activeRevision + } + + proposalID := uuid.NewString() + ccCtx, err := membership.MarshalConfChangeContext(membership.ConfChangeContext{ + RaftAddress: raftAddr, + ServiceAddress: serviceAddr, + InstanceID: instanceID, + Capabilities: append([]uint32(nil), capabilityIDs...), + CapabilityMembershipRevision: membershipRevision, + ProposalID: proposalID, + }) + if err != nil { + return fmt.Errorf("marshaling membership refresh context: %w", err) + } + + return node.retryConfChange(ctx, proposalID, nodeID, "RefreshMembership", []raftpb.ConfChangeType{ + raftpb.ConfChangeUpdateNode, + }, func() error { + status := node.rawNode.Status() + if status.RaftState != raft.StateLeader { + return ErrNotLeader + } + + if _, ok := status.Progress[nodeID]; !ok { + return ErrNodeNotInCluster + } + + existing, ok := node.membership.GetInstanceID(nodeID) + if !ok { + return fmt.Errorf("member %d has no persisted identity", nodeID) + } + if err := membership.ValidateInstanceID(existing); err != nil { + return fmt.Errorf("member %d has invalid persisted identity: %w", nodeID, err) + } + if !bytes.Equal(existing, instanceID) { + return fmt.Errorf("member %d identity does not match the active incarnation", nodeID) + } + + if err := node.membership.ValidateTransitionEvidence(&membership.ConfChangeContext{ + RaftAddress: raftAddr, + ServiceAddress: serviceAddr, + InstanceID: instanceID, + Capabilities: capabilityIDs, + CapabilityMembershipRevision: membershipRevision, + }); err != nil { + return err + } + + if node.isRemovalPending(nodeID, instanceID) { + return ErrNodeRemoved + } + removed, checkErr := node.membership.IsRemoved(nodeID, instanceID) + if checkErr != nil { + return fmt.Errorf("checking removed-member registry for %d: %w", nodeID, checkErr) + } + if removed { + return ErrNodeRemoved + } + + return node.rawNode.ProposeConfChange(&raftpb.ConfChangeV2{ + Changes: []*raftpb.ConfChangeSingle{{ + Type: new(raftpb.ConfChangeUpdateNode), + NodeId: new(nodeID), + }}, + Context: ccCtx, + }) + }, nil) +} + // addLearner is kept as a compatibility shim for package-local tests and // callers that exercise the pre-evidence admission path directly. func (node *Node) addLearner(ctx context.Context, nodeID uint64, raftAddr, serviceAddr string, instanceID []byte, bootJoin bool) error { diff --git a/internal/proto/clusterbootstrappb/cluster_bootstrap.pb.go b/internal/proto/clusterbootstrappb/cluster_bootstrap.pb.go index dd5d569193..d660084def 100644 --- a/internal/proto/clusterbootstrappb/cluster_bootstrap.pb.go +++ b/internal/proto/clusterbootstrappb/cluster_bootstrap.pb.go @@ -291,6 +291,130 @@ func (*JoinAsLearnerResponse) Descriptor() ([]byte, []int) { return file_cluster_bootstrap_proto_rawDescGZIP(), []int{4} } +// RefreshMembership carries the current identity, endpoints and capability +// evidence of a member that already exists in the committed Raft membership. +// It is used after a normal restart, where the node must not re-run the +// learner-join path. +type RefreshMembershipRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + NodeId uint64 `protobuf:"varint,1,opt,name=node_id,json=nodeId,proto3" json:"node_id,omitempty"` + RaftAddress string `protobuf:"bytes,2,opt,name=raft_address,json=raftAddress,proto3" json:"raft_address,omitempty"` + ServiceAddress string `protobuf:"bytes,3,opt,name=service_address,json=serviceAddress,proto3" json:"service_address,omitempty"` + InstanceId []byte `protobuf:"bytes,4,opt,name=instance_id,json=instanceId,proto3" json:"instance_id,omitempty"` + Capabilities []uint32 `protobuf:"fixed32,5,rep,packed,name=capabilities,proto3" json:"capabilities,omitempty"` + CapabilityMembershipRevision uint64 `protobuf:"fixed64,6,opt,name=capability_membership_revision,json=capabilityMembershipRevision,proto3" json:"capability_membership_revision,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RefreshMembershipRequest) Reset() { + *x = RefreshMembershipRequest{} + mi := &file_cluster_bootstrap_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RefreshMembershipRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RefreshMembershipRequest) ProtoMessage() {} + +func (x *RefreshMembershipRequest) ProtoReflect() protoreflect.Message { + mi := &file_cluster_bootstrap_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RefreshMembershipRequest.ProtoReflect.Descriptor instead. +func (*RefreshMembershipRequest) Descriptor() ([]byte, []int) { + return file_cluster_bootstrap_proto_rawDescGZIP(), []int{5} +} + +func (x *RefreshMembershipRequest) GetNodeId() uint64 { + if x != nil { + return x.NodeId + } + return 0 +} + +func (x *RefreshMembershipRequest) GetRaftAddress() string { + if x != nil { + return x.RaftAddress + } + return "" +} + +func (x *RefreshMembershipRequest) GetServiceAddress() string { + if x != nil { + return x.ServiceAddress + } + return "" +} + +func (x *RefreshMembershipRequest) GetInstanceId() []byte { + if x != nil { + return x.InstanceId + } + return nil +} + +func (x *RefreshMembershipRequest) GetCapabilities() []uint32 { + if x != nil { + return x.Capabilities + } + return nil +} + +func (x *RefreshMembershipRequest) GetCapabilityMembershipRevision() uint64 { + if x != nil { + return x.CapabilityMembershipRevision + } + return 0 +} + +type RefreshMembershipResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RefreshMembershipResponse) Reset() { + *x = RefreshMembershipResponse{} + mi := &file_cluster_bootstrap_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RefreshMembershipResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RefreshMembershipResponse) ProtoMessage() {} + +func (x *RefreshMembershipResponse) ProtoReflect() protoreflect.Message { + mi := &file_cluster_bootstrap_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RefreshMembershipResponse.ProtoReflect.Descriptor instead. +func (*RefreshMembershipResponse) Descriptor() ([]byte, []int) { + return file_cluster_bootstrap_proto_rawDescGZIP(), []int{6} +} + var File_cluster_bootstrap_proto protoreflect.FileDescriptor const file_cluster_bootstrap_proto_rawDesc = "" + @@ -313,10 +437,20 @@ const file_cluster_bootstrap_proto_rawDesc = "" + "instanceId\x12\"\n" + "\fcapabilities\x18\x05 \x03(\aR\fcapabilities\x12D\n" + "\x1ecapability_membership_revision\x18\x06 \x01(\x06R\x1ccapabilityMembershipRevision\"\x17\n" + - "\x15JoinAsLearnerResponse2\xd2\x01\n" + + "\x15JoinAsLearnerResponse\"\x8a\x02\n" + + "\x18RefreshMembershipRequest\x12\x17\n" + + "\anode_id\x18\x01 \x01(\x04R\x06nodeId\x12!\n" + + "\fraft_address\x18\x02 \x01(\tR\vraftAddress\x12'\n" + + "\x0fservice_address\x18\x03 \x01(\tR\x0eserviceAddress\x12\x1f\n" + + "\vinstance_id\x18\x04 \x01(\fR\n" + + "instanceId\x12\"\n" + + "\fcapabilities\x18\x05 \x03(\aR\fcapabilities\x12D\n" + + "\x1ecapability_membership_revision\x18\x06 \x01(\x06R\x1ccapabilityMembershipRevision\"\x1b\n" + + "\x19RefreshMembershipResponse2\xc2\x02\n" + "\x17ClusterBootstrapService\x12S\n" + "\bGetPeers\x12\".cluster_bootstrap.GetPeersRequest\x1a#.cluster_bootstrap.GetPeersResponse\x12b\n" + - "\rJoinAsLearner\x12'.cluster_bootstrap.JoinAsLearnerRequest\x1a(.cluster_bootstrap.JoinAsLearnerResponseBCZAgithub.com/formancehq/ledger/v3/internal/proto/clusterbootstrappbb\x06proto3" + "\rJoinAsLearner\x12'.cluster_bootstrap.JoinAsLearnerRequest\x1a(.cluster_bootstrap.JoinAsLearnerResponse\x12n\n" + + "\x11RefreshMembership\x12+.cluster_bootstrap.RefreshMembershipRequest\x1a,.cluster_bootstrap.RefreshMembershipResponseBCZAgithub.com/formancehq/ledger/v3/internal/proto/clusterbootstrappbb\x06proto3" var ( file_cluster_bootstrap_proto_rawDescOnce sync.Once @@ -330,22 +464,26 @@ func file_cluster_bootstrap_proto_rawDescGZIP() []byte { return file_cluster_bootstrap_proto_rawDescData } -var file_cluster_bootstrap_proto_msgTypes = make([]protoimpl.MessageInfo, 5) +var file_cluster_bootstrap_proto_msgTypes = make([]protoimpl.MessageInfo, 7) var file_cluster_bootstrap_proto_goTypes = []any{ - (*GetPeersRequest)(nil), // 0: cluster_bootstrap.GetPeersRequest - (*GetPeersResponse)(nil), // 1: cluster_bootstrap.GetPeersResponse - (*PeerInfo)(nil), // 2: cluster_bootstrap.PeerInfo - (*JoinAsLearnerRequest)(nil), // 3: cluster_bootstrap.JoinAsLearnerRequest - (*JoinAsLearnerResponse)(nil), // 4: cluster_bootstrap.JoinAsLearnerResponse + (*GetPeersRequest)(nil), // 0: cluster_bootstrap.GetPeersRequest + (*GetPeersResponse)(nil), // 1: cluster_bootstrap.GetPeersResponse + (*PeerInfo)(nil), // 2: cluster_bootstrap.PeerInfo + (*JoinAsLearnerRequest)(nil), // 3: cluster_bootstrap.JoinAsLearnerRequest + (*JoinAsLearnerResponse)(nil), // 4: cluster_bootstrap.JoinAsLearnerResponse + (*RefreshMembershipRequest)(nil), // 5: cluster_bootstrap.RefreshMembershipRequest + (*RefreshMembershipResponse)(nil), // 6: cluster_bootstrap.RefreshMembershipResponse } var file_cluster_bootstrap_proto_depIdxs = []int32{ 2, // 0: cluster_bootstrap.GetPeersResponse.peers:type_name -> cluster_bootstrap.PeerInfo 0, // 1: cluster_bootstrap.ClusterBootstrapService.GetPeers:input_type -> cluster_bootstrap.GetPeersRequest 3, // 2: cluster_bootstrap.ClusterBootstrapService.JoinAsLearner:input_type -> cluster_bootstrap.JoinAsLearnerRequest - 1, // 3: cluster_bootstrap.ClusterBootstrapService.GetPeers:output_type -> cluster_bootstrap.GetPeersResponse - 4, // 4: cluster_bootstrap.ClusterBootstrapService.JoinAsLearner:output_type -> cluster_bootstrap.JoinAsLearnerResponse - 3, // [3:5] is the sub-list for method output_type - 1, // [1:3] is the sub-list for method input_type + 5, // 3: cluster_bootstrap.ClusterBootstrapService.RefreshMembership:input_type -> cluster_bootstrap.RefreshMembershipRequest + 1, // 4: cluster_bootstrap.ClusterBootstrapService.GetPeers:output_type -> cluster_bootstrap.GetPeersResponse + 4, // 5: cluster_bootstrap.ClusterBootstrapService.JoinAsLearner:output_type -> cluster_bootstrap.JoinAsLearnerResponse + 6, // 6: cluster_bootstrap.ClusterBootstrapService.RefreshMembership:output_type -> cluster_bootstrap.RefreshMembershipResponse + 4, // [4:7] is the sub-list for method output_type + 1, // [1:4] is the sub-list for method input_type 1, // [1:1] is the sub-list for extension type_name 1, // [1:1] is the sub-list for extension extendee 0, // [0:1] is the sub-list for field type_name @@ -362,7 +500,7 @@ func file_cluster_bootstrap_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_cluster_bootstrap_proto_rawDesc), len(file_cluster_bootstrap_proto_rawDesc)), NumEnums: 0, - NumMessages: 5, + NumMessages: 7, NumExtensions: 0, NumServices: 1, }, diff --git a/internal/proto/clusterbootstrappb/cluster_bootstrap_dethash.pb.go b/internal/proto/clusterbootstrappb/cluster_bootstrap_dethash.pb.go index 4df9fe2ba2..fa13058669 100644 --- a/internal/proto/clusterbootstrappb/cluster_bootstrap_dethash.pb.go +++ b/internal/proto/clusterbootstrappb/cluster_bootstrap_dethash.pb.go @@ -57,3 +57,25 @@ func (m *JoinAsLearnerResponse) MarshalDeterministicVT(dAtA []byte) []byte { } return append(dAtA, b...) } + +func (m *RefreshMembershipRequest) MarshalDeterministicVT(dAtA []byte) []byte { + if m == nil { + return dAtA + } + b, err := m.MarshalVT() + if err != nil { + panic("MarshalDeterministicVT: " + err.Error()) + } + return append(dAtA, b...) +} + +func (m *RefreshMembershipResponse) MarshalDeterministicVT(dAtA []byte) []byte { + if m == nil { + return dAtA + } + b, err := m.MarshalVT() + if err != nil { + panic("MarshalDeterministicVT: " + err.Error()) + } + return append(dAtA, b...) +} diff --git a/internal/proto/clusterbootstrappb/cluster_bootstrap_grpc.pb.go b/internal/proto/clusterbootstrappb/cluster_bootstrap_grpc.pb.go index 106cb82b83..e0cf9f4033 100644 --- a/internal/proto/clusterbootstrappb/cluster_bootstrap_grpc.pb.go +++ b/internal/proto/clusterbootstrappb/cluster_bootstrap_grpc.pb.go @@ -19,8 +19,9 @@ import ( const _ = grpc.SupportPackageIsVersion9 const ( - ClusterBootstrapService_GetPeers_FullMethodName = "/cluster_bootstrap.ClusterBootstrapService/GetPeers" - ClusterBootstrapService_JoinAsLearner_FullMethodName = "/cluster_bootstrap.ClusterBootstrapService/JoinAsLearner" + ClusterBootstrapService_GetPeers_FullMethodName = "/cluster_bootstrap.ClusterBootstrapService/GetPeers" + ClusterBootstrapService_JoinAsLearner_FullMethodName = "/cluster_bootstrap.ClusterBootstrapService/JoinAsLearner" + ClusterBootstrapService_RefreshMembership_FullMethodName = "/cluster_bootstrap.ClusterBootstrapService/RefreshMembership" ) // ClusterBootstrapServiceClient is the client API for ClusterBootstrapService service. @@ -31,7 +32,7 @@ const ( // // Unlike ClusterService (exposed on the external ServiceServer behind the // user JWT/scope pipeline), this service lives on the internal RaftServer -// alongside the Raft transport. It carries the two RPCs required by a node +// alongside the Raft transport. It carries the RPCs required by a node // that is joining an existing cluster: // // 1. GetPeers — discover the current set of voter nodes and their @@ -39,6 +40,8 @@ const ( // snapshot before starting Raft. // 2. JoinAsLearner — register with the leader as a new learner, which // proposes the corresponding ConfChange. +// 3. RefreshMembership — refresh the committed endpoints and capability +// evidence of an existing member after restart. // // Authentication is the RaftServer's own: cluster-id metadata (always) and // the cluster-secret bearer when configured. No user JWT or scope check @@ -46,6 +49,7 @@ const ( type ClusterBootstrapServiceClient interface { GetPeers(ctx context.Context, in *GetPeersRequest, opts ...grpc.CallOption) (*GetPeersResponse, error) JoinAsLearner(ctx context.Context, in *JoinAsLearnerRequest, opts ...grpc.CallOption) (*JoinAsLearnerResponse, error) + RefreshMembership(ctx context.Context, in *RefreshMembershipRequest, opts ...grpc.CallOption) (*RefreshMembershipResponse, error) } type clusterBootstrapServiceClient struct { @@ -76,6 +80,16 @@ func (c *clusterBootstrapServiceClient) JoinAsLearner(ctx context.Context, in *J return out, nil } +func (c *clusterBootstrapServiceClient) RefreshMembership(ctx context.Context, in *RefreshMembershipRequest, opts ...grpc.CallOption) (*RefreshMembershipResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(RefreshMembershipResponse) + err := c.cc.Invoke(ctx, ClusterBootstrapService_RefreshMembership_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + // ClusterBootstrapServiceServer is the server API for ClusterBootstrapService service. // All implementations must embed UnimplementedClusterBootstrapServiceServer // for forward compatibility. @@ -84,7 +98,7 @@ func (c *clusterBootstrapServiceClient) JoinAsLearner(ctx context.Context, in *J // // Unlike ClusterService (exposed on the external ServiceServer behind the // user JWT/scope pipeline), this service lives on the internal RaftServer -// alongside the Raft transport. It carries the two RPCs required by a node +// alongside the Raft transport. It carries the RPCs required by a node // that is joining an existing cluster: // // 1. GetPeers — discover the current set of voter nodes and their @@ -92,6 +106,8 @@ func (c *clusterBootstrapServiceClient) JoinAsLearner(ctx context.Context, in *J // snapshot before starting Raft. // 2. JoinAsLearner — register with the leader as a new learner, which // proposes the corresponding ConfChange. +// 3. RefreshMembership — refresh the committed endpoints and capability +// evidence of an existing member after restart. // // Authentication is the RaftServer's own: cluster-id metadata (always) and // the cluster-secret bearer when configured. No user JWT or scope check @@ -99,6 +115,7 @@ func (c *clusterBootstrapServiceClient) JoinAsLearner(ctx context.Context, in *J type ClusterBootstrapServiceServer interface { GetPeers(context.Context, *GetPeersRequest) (*GetPeersResponse, error) JoinAsLearner(context.Context, *JoinAsLearnerRequest) (*JoinAsLearnerResponse, error) + RefreshMembership(context.Context, *RefreshMembershipRequest) (*RefreshMembershipResponse, error) mustEmbedUnimplementedClusterBootstrapServiceServer() } @@ -115,6 +132,9 @@ func (UnimplementedClusterBootstrapServiceServer) GetPeers(context.Context, *Get func (UnimplementedClusterBootstrapServiceServer) JoinAsLearner(context.Context, *JoinAsLearnerRequest) (*JoinAsLearnerResponse, error) { return nil, status.Error(codes.Unimplemented, "method JoinAsLearner not implemented") } +func (UnimplementedClusterBootstrapServiceServer) RefreshMembership(context.Context, *RefreshMembershipRequest) (*RefreshMembershipResponse, error) { + return nil, status.Error(codes.Unimplemented, "method RefreshMembership not implemented") +} func (UnimplementedClusterBootstrapServiceServer) mustEmbedUnimplementedClusterBootstrapServiceServer() { } func (UnimplementedClusterBootstrapServiceServer) testEmbeddedByValue() {} @@ -173,6 +193,24 @@ func _ClusterBootstrapService_JoinAsLearner_Handler(srv interface{}, ctx context return interceptor(ctx, in, info, handler) } +func _ClusterBootstrapService_RefreshMembership_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(RefreshMembershipRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ClusterBootstrapServiceServer).RefreshMembership(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ClusterBootstrapService_RefreshMembership_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ClusterBootstrapServiceServer).RefreshMembership(ctx, req.(*RefreshMembershipRequest)) + } + return interceptor(ctx, in, info, handler) +} + // ClusterBootstrapService_ServiceDesc is the grpc.ServiceDesc for ClusterBootstrapService service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) @@ -188,6 +226,10 @@ var ClusterBootstrapService_ServiceDesc = grpc.ServiceDesc{ MethodName: "JoinAsLearner", Handler: _ClusterBootstrapService_JoinAsLearner_Handler, }, + { + MethodName: "RefreshMembership", + Handler: _ClusterBootstrapService_RefreshMembership_Handler, + }, }, Streams: []grpc.StreamDesc{}, Metadata: "cluster_bootstrap.proto", diff --git a/internal/proto/clusterbootstrappb/cluster_bootstrap_reader.pb.go b/internal/proto/clusterbootstrappb/cluster_bootstrap_reader.pb.go index e110f3631d..ad2b4ec4b4 100644 --- a/internal/proto/clusterbootstrappb/cluster_bootstrap_reader.pb.go +++ b/internal/proto/clusterbootstrappb/cluster_bootstrap_reader.pb.go @@ -370,3 +370,157 @@ func (l joinAsLearnerResponseListReadonly) Range(yield func(int, JoinAsLearnerRe func NewJoinAsLearnerResponseListReader(s []*JoinAsLearnerResponse) JoinAsLearnerResponseListReader { return joinAsLearnerResponseListReadonly(s) } + +// RefreshMembershipRequestReader provides read-only access to RefreshMembershipRequest. +// Call Mutate() to obtain a mutable clone. +type RefreshMembershipRequestReader interface { + GetNodeId() uint64 + GetRaftAddress() string + GetServiceAddress() string + GetInstanceId() []byte + GetCapabilities() []uint32 + GetCapabilityMembershipRevision() uint64 + Mutate() *RefreshMembershipRequest +} + +type refreshMembershipRequestReadonly RefreshMembershipRequest + +func (r *refreshMembershipRequestReadonly) GetNodeId() uint64 { + return (*RefreshMembershipRequest)(r).GetNodeId() +} + +func (r *refreshMembershipRequestReadonly) GetRaftAddress() string { + return (*RefreshMembershipRequest)(r).GetRaftAddress() +} + +func (r *refreshMembershipRequestReadonly) GetServiceAddress() string { + return (*RefreshMembershipRequest)(r).GetServiceAddress() +} + +func (r *refreshMembershipRequestReadonly) GetInstanceId() []byte { + return bytes.Clone((*RefreshMembershipRequest)(r).GetInstanceId()) +} + +func (r *refreshMembershipRequestReadonly) GetCapabilities() []uint32 { + return slices.Clone((*RefreshMembershipRequest)(r).GetCapabilities()) +} + +func (r *refreshMembershipRequestReadonly) GetCapabilityMembershipRevision() uint64 { + return (*RefreshMembershipRequest)(r).GetCapabilityMembershipRevision() +} + +func (r *refreshMembershipRequestReadonly) Mutate() *RefreshMembershipRequest { + return (*RefreshMembershipRequest)(r).CloneVT() +} + +// AsReader returns a read-only view of this RefreshMembershipRequest. +func (m *RefreshMembershipRequest) AsReader() RefreshMembershipRequestReader { + if m == nil { + return nil + } + return (*refreshMembershipRequestReadonly)(m) +} + +// Mutate returns a mutable deep clone of this RefreshMembershipRequest. +func (m *RefreshMembershipRequest) Mutate() *RefreshMembershipRequest { + return m.CloneVT() +} + +// RefreshMembershipRequestListReader provides read-only iteration over []*RefreshMembershipRequest. +type RefreshMembershipRequestListReader interface { + Len() int + Get(i int) RefreshMembershipRequestReader + Range(yield func(int, RefreshMembershipRequestReader) bool) +} + +type refreshMembershipRequestListReadonly []*RefreshMembershipRequest + +func (l refreshMembershipRequestListReadonly) Len() int { return len(l) } + +func (l refreshMembershipRequestListReadonly) Get(i int) RefreshMembershipRequestReader { + v := l[i] + if v == nil { + return nil + } + return v.AsReader() +} + +func (l refreshMembershipRequestListReadonly) Range(yield func(int, RefreshMembershipRequestReader) bool) { + for i, v := range l { + var r RefreshMembershipRequestReader + if v != nil { + r = v.AsReader() + } + if !yield(i, r) { + return + } + } +} + +// NewRefreshMembershipRequestListReader wraps s for read-only iteration. The returned +// view aliases the underlying slice; do not mutate s afterwards. +func NewRefreshMembershipRequestListReader(s []*RefreshMembershipRequest) RefreshMembershipRequestListReader { + return refreshMembershipRequestListReadonly(s) +} + +// RefreshMembershipResponseReader provides read-only access to RefreshMembershipResponse. +// Call Mutate() to obtain a mutable clone. +type RefreshMembershipResponseReader interface { + Mutate() *RefreshMembershipResponse +} + +type refreshMembershipResponseReadonly RefreshMembershipResponse + +func (r *refreshMembershipResponseReadonly) Mutate() *RefreshMembershipResponse { + return (*RefreshMembershipResponse)(r).CloneVT() +} + +// AsReader returns a read-only view of this RefreshMembershipResponse. +func (m *RefreshMembershipResponse) AsReader() RefreshMembershipResponseReader { + if m == nil { + return nil + } + return (*refreshMembershipResponseReadonly)(m) +} + +// Mutate returns a mutable deep clone of this RefreshMembershipResponse. +func (m *RefreshMembershipResponse) Mutate() *RefreshMembershipResponse { + return m.CloneVT() +} + +// RefreshMembershipResponseListReader provides read-only iteration over []*RefreshMembershipResponse. +type RefreshMembershipResponseListReader interface { + Len() int + Get(i int) RefreshMembershipResponseReader + Range(yield func(int, RefreshMembershipResponseReader) bool) +} + +type refreshMembershipResponseListReadonly []*RefreshMembershipResponse + +func (l refreshMembershipResponseListReadonly) Len() int { return len(l) } + +func (l refreshMembershipResponseListReadonly) Get(i int) RefreshMembershipResponseReader { + v := l[i] + if v == nil { + return nil + } + return v.AsReader() +} + +func (l refreshMembershipResponseListReadonly) Range(yield func(int, RefreshMembershipResponseReader) bool) { + for i, v := range l { + var r RefreshMembershipResponseReader + if v != nil { + r = v.AsReader() + } + if !yield(i, r) { + return + } + } +} + +// NewRefreshMembershipResponseListReader wraps s for read-only iteration. The returned +// view aliases the underlying slice; do not mutate s afterwards. +func NewRefreshMembershipResponseListReader(s []*RefreshMembershipResponse) RefreshMembershipResponseListReader { + return refreshMembershipResponseListReadonly(s) +} diff --git a/internal/proto/clusterbootstrappb/cluster_bootstrap_vtproto.pb.go b/internal/proto/clusterbootstrappb/cluster_bootstrap_vtproto.pb.go index 27ccb8985a..10141d4484 100644 --- a/internal/proto/clusterbootstrappb/cluster_bootstrap_vtproto.pb.go +++ b/internal/proto/clusterbootstrappb/cluster_bootstrap_vtproto.pb.go @@ -129,6 +129,52 @@ func (m *JoinAsLearnerResponse) CloneMessageVT() proto.Message { return m.CloneVT() } +func (m *RefreshMembershipRequest) CloneVT() *RefreshMembershipRequest { + if m == nil { + return (*RefreshMembershipRequest)(nil) + } + r := new(RefreshMembershipRequest) + r.NodeId = m.NodeId + r.RaftAddress = m.RaftAddress + r.ServiceAddress = m.ServiceAddress + r.CapabilityMembershipRevision = m.CapabilityMembershipRevision + if rhs := m.InstanceId; rhs != nil { + tmpBytes := make([]byte, len(rhs)) + copy(tmpBytes, rhs) + r.InstanceId = tmpBytes + } + if rhs := m.Capabilities; rhs != nil { + tmpContainer := make([]uint32, len(rhs)) + copy(tmpContainer, rhs) + r.Capabilities = tmpContainer + } + if len(m.unknownFields) > 0 { + r.unknownFields = make([]byte, len(m.unknownFields)) + copy(r.unknownFields, m.unknownFields) + } + return r +} + +func (m *RefreshMembershipRequest) CloneMessageVT() proto.Message { + return m.CloneVT() +} + +func (m *RefreshMembershipResponse) CloneVT() *RefreshMembershipResponse { + if m == nil { + return (*RefreshMembershipResponse)(nil) + } + r := new(RefreshMembershipResponse) + if len(m.unknownFields) > 0 { + r.unknownFields = make([]byte, len(m.unknownFields)) + copy(r.unknownFields, m.unknownFields) + } + return r +} + +func (m *RefreshMembershipResponse) CloneMessageVT() proto.Message { + return m.CloneVT() +} + func (this *GetPeersRequest) EqualVT(that *GetPeersRequest) bool { if this == that { return true @@ -262,6 +308,62 @@ func (this *JoinAsLearnerResponse) EqualMessageVT(thatMsg proto.Message) bool { } return this.EqualVT(that) } +func (this *RefreshMembershipRequest) EqualVT(that *RefreshMembershipRequest) bool { + if this == that { + return true + } else if this == nil || that == nil { + return false + } + if this.NodeId != that.NodeId { + return false + } + if this.RaftAddress != that.RaftAddress { + return false + } + if this.ServiceAddress != that.ServiceAddress { + return false + } + if string(this.InstanceId) != string(that.InstanceId) { + return false + } + if len(this.Capabilities) != len(that.Capabilities) { + return false + } + for i, vx := range this.Capabilities { + vy := that.Capabilities[i] + if vx != vy { + return false + } + } + if this.CapabilityMembershipRevision != that.CapabilityMembershipRevision { + return false + } + return string(this.unknownFields) == string(that.unknownFields) +} + +func (this *RefreshMembershipRequest) EqualMessageVT(thatMsg proto.Message) bool { + that, ok := thatMsg.(*RefreshMembershipRequest) + if !ok { + return false + } + return this.EqualVT(that) +} +func (this *RefreshMembershipResponse) EqualVT(that *RefreshMembershipResponse) bool { + if this == that { + return true + } else if this == nil || that == nil { + return false + } + return string(this.unknownFields) == string(that.unknownFields) +} + +func (this *RefreshMembershipResponse) EqualMessageVT(thatMsg proto.Message) bool { + that, ok := thatMsg.(*RefreshMembershipResponse) + if !ok { + return false + } + return this.EqualVT(that) +} func (m *GetPeersRequest) MarshalVT() (dAtA []byte, err error) { if m == nil { return nil, nil @@ -506,6 +608,113 @@ func (m *JoinAsLearnerResponse) MarshalToSizedBufferVT(dAtA []byte) (int, error) return len(dAtA) - i, nil } +func (m *RefreshMembershipRequest) MarshalVT() (dAtA []byte, err error) { + if m == nil { + return nil, nil + } + size := m.SizeVT() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBufferVT(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *RefreshMembershipRequest) MarshalToVT(dAtA []byte) (int, error) { + size := m.SizeVT() + return m.MarshalToSizedBufferVT(dAtA[:size]) +} + +func (m *RefreshMembershipRequest) MarshalToSizedBufferVT(dAtA []byte) (int, error) { + if m == nil { + return 0, nil + } + i := len(dAtA) + _ = i + var l int + _ = l + if m.unknownFields != nil { + i -= len(m.unknownFields) + copy(dAtA[i:], m.unknownFields) + } + if m.CapabilityMembershipRevision != 0 { + i -= 8 + binary.LittleEndian.PutUint64(dAtA[i:], uint64(m.CapabilityMembershipRevision)) + i-- + dAtA[i] = 0x31 + } + if len(m.Capabilities) > 0 { + for iNdEx := len(m.Capabilities) - 1; iNdEx >= 0; iNdEx-- { + i -= 4 + binary.LittleEndian.PutUint32(dAtA[i:], uint32(m.Capabilities[iNdEx])) + } + i = protohelpers.EncodeVarint(dAtA, i, uint64(len(m.Capabilities)*4)) + i-- + dAtA[i] = 0x2a + } + if len(m.InstanceId) > 0 { + i -= len(m.InstanceId) + copy(dAtA[i:], m.InstanceId) + i = protohelpers.EncodeVarint(dAtA, i, uint64(len(m.InstanceId))) + i-- + dAtA[i] = 0x22 + } + if len(m.ServiceAddress) > 0 { + i -= len(m.ServiceAddress) + copy(dAtA[i:], m.ServiceAddress) + i = protohelpers.EncodeVarint(dAtA, i, uint64(len(m.ServiceAddress))) + i-- + dAtA[i] = 0x1a + } + if len(m.RaftAddress) > 0 { + i -= len(m.RaftAddress) + copy(dAtA[i:], m.RaftAddress) + i = protohelpers.EncodeVarint(dAtA, i, uint64(len(m.RaftAddress))) + i-- + dAtA[i] = 0x12 + } + if m.NodeId != 0 { + i = protohelpers.EncodeVarint(dAtA, i, uint64(m.NodeId)) + i-- + dAtA[i] = 0x8 + } + return len(dAtA) - i, nil +} + +func (m *RefreshMembershipResponse) MarshalVT() (dAtA []byte, err error) { + if m == nil { + return nil, nil + } + size := m.SizeVT() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBufferVT(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *RefreshMembershipResponse) MarshalToVT(dAtA []byte) (int, error) { + size := m.SizeVT() + return m.MarshalToSizedBufferVT(dAtA[:size]) +} + +func (m *RefreshMembershipResponse) MarshalToSizedBufferVT(dAtA []byte) (int, error) { + if m == nil { + return 0, nil + } + i := len(dAtA) + _ = i + var l int + _ = l + if m.unknownFields != nil { + i -= len(m.unknownFields) + copy(dAtA[i:], m.unknownFields) + } + return len(dAtA) - i, nil +} + func (m *GetPeersRequest) SizeVT() (n int) { if m == nil { return 0 @@ -598,6 +807,47 @@ func (m *JoinAsLearnerResponse) SizeVT() (n int) { return n } +func (m *RefreshMembershipRequest) SizeVT() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + if m.NodeId != 0 { + n += 1 + protohelpers.SizeOfVarint(uint64(m.NodeId)) + } + l = len(m.RaftAddress) + if l > 0 { + n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } + l = len(m.ServiceAddress) + if l > 0 { + n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } + l = len(m.InstanceId) + if l > 0 { + n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } + if len(m.Capabilities) > 0 { + n += 1 + protohelpers.SizeOfVarint(uint64(len(m.Capabilities)*4)) + len(m.Capabilities)*4 + } + if m.CapabilityMembershipRevision != 0 { + n += 9 + } + n += len(m.unknownFields) + return n +} + +func (m *RefreshMembershipResponse) SizeVT() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + n += len(m.unknownFields) + return n +} + func (m *GetPeersRequest) UnmarshalVT(dAtA []byte) error { l := len(dAtA) iNdEx := 0 @@ -1183,3 +1433,284 @@ func (m *JoinAsLearnerResponse) UnmarshalVT(dAtA []byte) error { } return nil } +func (m *RefreshMembershipRequest) UnmarshalVT(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: RefreshMembershipRequest: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: RefreshMembershipRequest: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field NodeId", wireType) + } + m.NodeId = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.NodeId |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field RaftAddress", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return protohelpers.ErrInvalidLength + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return protohelpers.ErrInvalidLength + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.RaftAddress = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 3: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field ServiceAddress", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return protohelpers.ErrInvalidLength + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return protohelpers.ErrInvalidLength + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.ServiceAddress = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 4: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field InstanceId", wireType) + } + var byteLen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + byteLen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if byteLen < 0 { + return protohelpers.ErrInvalidLength + } + postIndex := iNdEx + byteLen + if postIndex < 0 { + return protohelpers.ErrInvalidLength + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.InstanceId = append(m.InstanceId[:0], dAtA[iNdEx:postIndex]...) + if m.InstanceId == nil { + m.InstanceId = []byte{} + } + iNdEx = postIndex + case 5: + if wireType == 5 { + var v uint32 + if (iNdEx + 4) > l { + return io.ErrUnexpectedEOF + } + v = uint32(binary.LittleEndian.Uint32(dAtA[iNdEx:])) + iNdEx += 4 + m.Capabilities = append(m.Capabilities, v) + } else if wireType == 2 { + var packedLen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + packedLen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if packedLen < 0 { + return protohelpers.ErrInvalidLength + } + postIndex := iNdEx + packedLen + if postIndex < 0 { + return protohelpers.ErrInvalidLength + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + var elementCount int + elementCount = packedLen / 4 + if elementCount != 0 && len(m.Capabilities) == 0 { + m.Capabilities = make([]uint32, 0, elementCount) + } + for iNdEx < postIndex { + var v uint32 + if (iNdEx + 4) > l { + return io.ErrUnexpectedEOF + } + v = uint32(binary.LittleEndian.Uint32(dAtA[iNdEx:])) + iNdEx += 4 + m.Capabilities = append(m.Capabilities, v) + } + } else { + return fmt.Errorf("proto: wrong wireType = %d for field Capabilities", wireType) + } + case 6: + if wireType != 1 { + return fmt.Errorf("proto: wrong wireType = %d for field CapabilityMembershipRevision", wireType) + } + m.CapabilityMembershipRevision = 0 + if (iNdEx + 8) > l { + return io.ErrUnexpectedEOF + } + m.CapabilityMembershipRevision = uint64(binary.LittleEndian.Uint64(dAtA[iNdEx:])) + iNdEx += 8 + default: + iNdEx = preIndex + skippy, err := protohelpers.Skip(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return protohelpers.ErrInvalidLength + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + m.unknownFields = append(m.unknownFields, dAtA[iNdEx:iNdEx+skippy]...) + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *RefreshMembershipResponse) UnmarshalVT(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: RefreshMembershipResponse: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: RefreshMembershipResponse: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + default: + iNdEx = preIndex + skippy, err := protohelpers.Skip(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return protohelpers.ErrInvalidLength + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + m.unknownFields = append(m.unknownFields, dAtA[iNdEx:iNdEx+skippy]...) + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} diff --git a/misc/proto/cluster_bootstrap.proto b/misc/proto/cluster_bootstrap.proto index 95a6742e02..7a185771a3 100644 --- a/misc/proto/cluster_bootstrap.proto +++ b/misc/proto/cluster_bootstrap.proto @@ -8,7 +8,7 @@ option go_package = "github.com/formancehq/ledger/v3/internal/proto/clusterboots // // Unlike ClusterService (exposed on the external ServiceServer behind the // user JWT/scope pipeline), this service lives on the internal RaftServer -// alongside the Raft transport. It carries the two RPCs required by a node +// alongside the Raft transport. It carries the RPCs required by a node // that is joining an existing cluster: // // 1. GetPeers — discover the current set of voter nodes and their @@ -16,6 +16,8 @@ option go_package = "github.com/formancehq/ledger/v3/internal/proto/clusterboots // snapshot before starting Raft. // 2. JoinAsLearner — register with the leader as a new learner, which // proposes the corresponding ConfChange. +// 3. RefreshMembership — refresh the committed endpoints and capability +// evidence of an existing member after restart. // // Authentication is the RaftServer's own: cluster-id metadata (always) and // the cluster-secret bearer when configured. No user JWT or scope check @@ -23,6 +25,7 @@ option go_package = "github.com/formancehq/ledger/v3/internal/proto/clusterboots service ClusterBootstrapService { rpc GetPeers(GetPeersRequest) returns (GetPeersResponse); rpc JoinAsLearner(JoinAsLearnerRequest) returns (JoinAsLearnerResponse); + rpc RefreshMembership(RefreshMembershipRequest) returns (RefreshMembershipResponse); } message GetPeersRequest {} @@ -50,3 +53,18 @@ message JoinAsLearnerRequest { } message JoinAsLearnerResponse {} + +// RefreshMembership carries the current identity, endpoints and capability +// evidence of a member that already exists in the committed Raft membership. +// It is used after a normal restart, where the node must not re-run the +// learner-join path. +message RefreshMembershipRequest { + uint64 node_id = 1; + string raft_address = 2; + string service_address = 3; + bytes instance_id = 4; + repeated fixed32 capabilities = 5; + fixed64 capability_membership_revision = 6; +} + +message RefreshMembershipResponse {} From 5d3491f6ebe78be4a729470b043e6be5860ebd3c Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Thu, 22 Oct 2026 13:39:49 +0200 Subject: [PATCH 006/127] feat: arm capabilities after membership refresh --- .../subsystems/consensus/capabilities.md | 7 ++ .../bootstrap/capability_activation_test.go | 22 ++++++ internal/bootstrap/module.go | 73 ++++++++++++++++++- 3 files changed, 99 insertions(+), 3 deletions(-) create mode 100644 internal/bootstrap/capability_activation_test.go diff --git a/docs/technical/architecture/subsystems/consensus/capabilities.md b/docs/technical/architecture/subsystems/consensus/capabilities.md index d6c0950f8e..3b78c5e9b9 100644 --- a/docs/technical/architecture/subsystems/consensus/capabilities.md +++ b/docs/technical/architecture/subsystems/consensus/capabilities.md @@ -67,3 +67,10 @@ Only the committed change is authoritative. The update is replicated through Raft and applied to every member's peer store; a local restart-time upsert is not sufficient to activate a capability for admission. A fresh WAL instead uses `JoinAsLearner` and `ConfChangeAddLearnerNode`. + +The leader's existing cluster-policy reconciler observes those committed peer +rows. Once every current voter and learner advertises the complete capability +set implemented by the binary, it proposes a new, audited policy revision with +the capability gate armed in `FEATURE_ONLY` mode. This is a one-way transition: +an already armed policy is preserved, and a missing or stale member proof keeps +activation disarmed until that member completes `RefreshMembership`. diff --git a/internal/bootstrap/capability_activation_test.go b/internal/bootstrap/capability_activation_test.go new file mode 100644 index 0000000000..778127c842 --- /dev/null +++ b/internal/bootstrap/capability_activation_test.go @@ -0,0 +1,22 @@ +package bootstrap + +import ( + "testing" + + "github.com/stretchr/testify/require" + + raftmembership "github.com/formancehq/ledger/v3/internal/infra/membership" +) + +func TestAllMembersAdvertiseCapabilities(t *testing.T) { + t.Parallel() + + peer := raftmembership.ConfChangeContext{Capabilities: []uint32{1, 2}} + + require.False(t, allMembersAdvertiseCapabilities(nil)) + require.True(t, allMembersAdvertiseCapabilities(map[uint64]raftmembership.ConfChangeContext{1: peer})) + require.False(t, allMembersAdvertiseCapabilities(map[uint64]raftmembership.ConfChangeContext{ + 1: peer, + 2: {Capabilities: []uint32{1}}, + })) +} diff --git a/internal/bootstrap/module.go b/internal/bootstrap/module.go index 467804102e..bc97b360b8 100644 --- a/internal/bootstrap/module.go +++ b/internal/bootstrap/module.go @@ -8,6 +8,7 @@ import ( "net" "net/http" "path/filepath" + "slices" "strconv" "strings" "time" @@ -642,7 +643,7 @@ func Module() fx.Option { return clusterhealth.NewGRPCHealthUpdater(n, hs, clusterPolicyReady, rs.ReadProjectionHealthy) }, - func(admission ctrl.Admission, store *dal.Store, builder *plan.Builder, cfg Config, raftNode *node.Node, logger logging.Logger) *ClusterPolicyReconciler { + func(admission ctrl.Admission, store *dal.Store, builder *plan.Builder, cfg Config, raftNode *node.Node, raftMembership *raftmembership.Membership, logger logging.Logger) *ClusterPolicyReconciler { return NewClusterPolicyReconciler(func(ctx context.Context) { if !raftNode.IsLeader() { return @@ -652,7 +653,7 @@ func Module() fx.Option { return } - reconcileClusterPolicy(ctx, admission, store, cfg, raftNode.IsLeader, logger) + reconcileClusterPolicy(ctx, admission, store, cfg, raftNode.IsLeader, logger, raftMembership) }) }, ), @@ -1545,6 +1546,7 @@ func tryRefreshMembership(ctx context.Context, cfg Config, tlsCfg TLSConfig, log conn, dialErr := ggrpc.NewClient(peer.Address, dialOpts...) if dialErr != nil { logger.WithFields(map[string]any{"peer": peer.ID, "address": peer.Address, "error": dialErr}).Infof("Failed to dial peer for membership refresh, will retry") + continue } @@ -1702,7 +1704,7 @@ func proposeClusterConfigIfNeeded(parent context.Context, n plan.Proposer, build // through AdmitClusterPolicy as an operational SetClusterPolicyProposal. // The write-readiness gate exempts this non-business proposal so it is never // blocked by its own cluster-policy gate. -func reconcileClusterPolicy(ctx context.Context, admission ctrl.Admission, store *dal.Store, cfg Config, isLeader func() bool, logger logging.Logger) { +func reconcileClusterPolicy(ctx context.Context, admission ctrl.Admission, store *dal.Store, cfg Config, isLeader func() bool, logger logging.Logger, memberships ...*raftmembership.Membership) { if !isLeader() { return } @@ -1713,6 +1715,22 @@ func reconcileClusterPolicy(ctx context.Context, admission ctrl.Admission, store return } + var raftMembership *raftmembership.Membership + if len(memberships) > 0 { + raftMembership = memberships[0] + } + + // Publish the committed gate before handling the next membership refresh. + // The leader is the only node that can propose a ConfChange, so syncing it + // from the same ticker closes the window between policy commit and the next + // refresh without adding another poller. + if raftMembership != nil { + if err := raftMembership.SetCapabilityGate(applied.GetCapabilityActivationArmed(), applied.GetCapabilityMembershipRevision()); err != nil { + logger.WithFields(map[string]any{"error": err}).Errorf("Invalid committed capability gate") + + return + } + } desired := &commonpb.ClusterPolicy{ Revision: cfg.ClusterPolicyRevision, @@ -1730,6 +1748,28 @@ func reconcileClusterPolicy(ctx context.Context, admission ctrl.Admission, store MinimumReplayProtocol: applied.GetMinimumReplayProtocol(), } + // Capability activation is a one-way, replicated transition. Once every + // current member has refreshed its membership row with this binary's + // complete capability set, arm the feature-only gate automatically. The + // policy revision is bumped so this transition is auditable and cannot be + // confused with the node-local configuration revision. + if applied.GetCapabilityActivationArmed() { + desired.CapabilityActivationArmed = true + desired.CapabilityAdmissionPolicy = applied.GetCapabilityAdmissionPolicy() + desired.CapabilityMembershipRevision = applied.GetCapabilityMembershipRevision() + desired.CapabilityActivations = cloneCapabilityActivations(applied.GetCapabilityActivations()) + } else if raftMembership != nil && allMembersAdvertiseCapabilities(raftMembership.PeerAddresses()) { + desired.CapabilityActivationArmed = true + desired.CapabilityAdmissionPolicy = commonpb.CapabilityAdmissionPolicy_CAPABILITY_ADMISSION_POLICY_FEATURE_ONLY + desired.CapabilityMembershipRevision = max(applied.GetCapabilityMembershipRevision(), uint64(1)) + for _, id := range capabilities.SupportedIDs() { + desired.CapabilityActivations = append(desired.CapabilityActivations, &commonpb.CapabilityActivation{Id: uint32(id), Active: true}) + } + if desired.GetRevision() <= applied.GetRevision() { + desired.Revision = applied.GetRevision() + 1 + } + } + appliedRev := applied.GetRevision() switch { @@ -1759,6 +1799,33 @@ func reconcileClusterPolicy(ctx context.Context, admission ctrl.Admission, store } } +func allMembersAdvertiseCapabilities(peers map[uint64]raftmembership.ConfChangeContext) bool { + if len(peers) == 0 { + return false + } + + for _, peer := range peers { + for _, id := range capabilities.SupportedIDs() { + if !slices.Contains(peer.Capabilities, uint32(id)) { + return false + } + } + } + + return true +} + +func cloneCapabilityActivations(activations []*commonpb.CapabilityActivation) []*commonpb.CapabilityActivation { + cloned := make([]*commonpb.CapabilityActivation, len(activations)) + for i, activation := range activations { + if activation != nil { + cloned[i] = activation.CloneVT() + } + } + + return cloned +} + // buildResponseSigner returns the Ed25519 response signer for the gRPC // service plane. // From 44586084a0bf8ff9058733e2e5e11152672abb26 Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Thu, 22 Oct 2026 13:57:07 +0200 Subject: [PATCH 007/127] refactor: keep capability barrier feature-neutral --- .../subsystems/consensus/capabilities.md | 23 +++-- .../admission/capability_policy_test.go | 8 +- internal/bootstrap/module.go | 72 +--------------- internal/infra/capabilities/capabilities.go | 51 ++--------- .../infra/capabilities/capabilities_test.go | 84 ++++--------------- internal/infra/plan/capability_policy.go | 3 +- internal/infra/plan/capability_policy_test.go | 6 +- internal/proto/commonpb/common_reader.pb.go | 5 -- internal/proto/commonpb/common_vtproto.pb.go | 31 ------- misc/proto/common.proto | 14 +--- 10 files changed, 44 insertions(+), 253 deletions(-) diff --git a/docs/technical/architecture/subsystems/consensus/capabilities.md b/docs/technical/architecture/subsystems/consensus/capabilities.md index 3b78c5e9b9..eb1695fdff 100644 --- a/docs/technical/architecture/subsystems/consensus/capabilities.md +++ b/docs/technical/architecture/subsystems/consensus/capabilities.md @@ -5,8 +5,8 @@ rolling upgrade. It is separate from the client-facing gRPC protocol revision. ## Wire contract -`common.ClusterPolicy` carries the replicated activation state, admission policy, -membership revision under examination, and a monotonic minimum replay protocol. +`common.ClusterPolicy` carries the replicated activation state, membership +revision under examination, and a monotonic minimum replay protocol. `raft.Proposal.required_capabilities` carries stable producer-owned capability IDs. The fields are optional and therefore backward-readable by binaries that predate this foundation. IDs are never reused; semantic dependencies must be @@ -27,10 +27,10 @@ blocks activation just as an incapable member does. Activation is bound to the membership revision observed by the proposer; a changed revision requires a fresh examination. -While armed, `FEATURE_ONLY` rejects dependent proposals and allows unrelated -business writes. `ALL_WRITES` rejects all business writes, with only the -narrow control-plane recovery and activation path exempted by its producer -inventory. FSM apply never consults node-local capability flags. +While armed, a proposal that declares a capability is admitted only when that +capability is active and every member advertises it. Proposals with no declared +capability remain unaffected. FSM apply never consults node-local capability +flags. The replay floor is monotonic, is bounded by the local replay protocol during policy apply, and is checked immediately after decoding an entry or snapshot @@ -68,9 +68,8 @@ Raft and applied to every member's peer store; a local restart-time upsert is not sufficient to activate a capability for admission. A fresh WAL instead uses `JoinAsLearner` and `ConfChangeAddLearnerNode`. -The leader's existing cluster-policy reconciler observes those committed peer -rows. Once every current voter and learner advertises the complete capability -set implemented by the binary, it proposes a new, audited policy revision with -the capability gate armed in `FEATURE_ONLY` mode. This is a one-way transition: -an already armed policy is preserved, and a missing or stale member proof keeps -activation disarmed until that member completes `RefreshMembership`. +The leader's existing cluster-policy reconciler does not activate a capability +automatically. A future rollout can add an explicit `CapabilityActivation` +entry once every current voter and learner has refreshed its membership proof. +Until then the mechanism remains disarmed and this binary declares no protocol +feature. diff --git a/internal/application/admission/capability_policy_test.go b/internal/application/admission/capability_policy_test.go index 8f505f0b28..83b5884c24 100644 --- a/internal/application/admission/capability_policy_test.go +++ b/internal/application/admission/capability_policy_test.go @@ -56,20 +56,19 @@ func TestBarrierStampsSemanticCapabilities(t *testing.T) { require.NoError(t, err) } -func TestCapabilityPolicyProposerBlocksAllWritesAtFinalBoundary(t *testing.T) { +func TestCapabilityPolicyProposerBlocksUnknownCapabilityAtFinalBoundary(t *testing.T) { delegate := new(capabilityTestProposer) proposer := capabilityPolicyProposer{ delegate: delegate, readPolicy: func() (*commonpb.ClusterPolicy, error) { return &commonpb.ClusterPolicy{ CapabilityActivationArmed: true, - CapabilityAdmissionPolicy: commonpb.CapabilityAdmissionPolicy_CAPABILITY_ADMISSION_POLICY_ALL_WRITES, CapabilityMembershipRevision: 1, }, nil }, businessWrite: true, } - data, err := (&raftcmdpb.Proposal{}).MarshalVT() + data, err := (&raftcmdpb.Proposal{RequiredCapabilities: []uint32{99}}).MarshalVT() require.NoError(t, err) _, err = proposer.Propose(context.Background(), node.NewProposal(1, data)) require.Error(t, err) @@ -77,14 +76,13 @@ func TestCapabilityPolicyProposerBlocksAllWritesAtFinalBoundary(t *testing.T) { require.False(t, delegate.called) } -func TestCapabilityPolicyProposerAllowsFeatureOnlyUnrelatedWrite(t *testing.T) { +func TestCapabilityPolicyProposerAllowsUnrelatedWrite(t *testing.T) { delegate := new(capabilityTestProposer) proposer := capabilityPolicyProposer{ delegate: delegate, readPolicy: func() (*commonpb.ClusterPolicy, error) { return &commonpb.ClusterPolicy{ CapabilityActivationArmed: true, - CapabilityAdmissionPolicy: commonpb.CapabilityAdmissionPolicy_CAPABILITY_ADMISSION_POLICY_FEATURE_ONLY, CapabilityMembershipRevision: 1, }, nil }, diff --git a/internal/bootstrap/module.go b/internal/bootstrap/module.go index bc97b360b8..db854da758 100644 --- a/internal/bootstrap/module.go +++ b/internal/bootstrap/module.go @@ -8,7 +8,6 @@ import ( "net" "net/http" "path/filepath" - "slices" "strconv" "strings" "time" @@ -643,7 +642,7 @@ func Module() fx.Option { return clusterhealth.NewGRPCHealthUpdater(n, hs, clusterPolicyReady, rs.ReadProjectionHealthy) }, - func(admission ctrl.Admission, store *dal.Store, builder *plan.Builder, cfg Config, raftNode *node.Node, raftMembership *raftmembership.Membership, logger logging.Logger) *ClusterPolicyReconciler { + func(admission ctrl.Admission, store *dal.Store, builder *plan.Builder, cfg Config, raftNode *node.Node, logger logging.Logger) *ClusterPolicyReconciler { return NewClusterPolicyReconciler(func(ctx context.Context) { if !raftNode.IsLeader() { return @@ -653,7 +652,7 @@ func Module() fx.Option { return } - reconcileClusterPolicy(ctx, admission, store, cfg, raftNode.IsLeader, logger, raftMembership) + reconcileClusterPolicy(ctx, admission, store, cfg, raftNode.IsLeader, logger) }) }, ), @@ -1704,7 +1703,7 @@ func proposeClusterConfigIfNeeded(parent context.Context, n plan.Proposer, build // through AdmitClusterPolicy as an operational SetClusterPolicyProposal. // The write-readiness gate exempts this non-business proposal so it is never // blocked by its own cluster-policy gate. -func reconcileClusterPolicy(ctx context.Context, admission ctrl.Admission, store *dal.Store, cfg Config, isLeader func() bool, logger logging.Logger, memberships ...*raftmembership.Membership) { +func reconcileClusterPolicy(ctx context.Context, admission ctrl.Admission, store *dal.Store, cfg Config, isLeader func() bool, logger logging.Logger) { if !isLeader() { return } @@ -1715,22 +1714,6 @@ func reconcileClusterPolicy(ctx context.Context, admission ctrl.Admission, store return } - var raftMembership *raftmembership.Membership - if len(memberships) > 0 { - raftMembership = memberships[0] - } - - // Publish the committed gate before handling the next membership refresh. - // The leader is the only node that can propose a ConfChange, so syncing it - // from the same ticker closes the window between policy commit and the next - // refresh without adding another poller. - if raftMembership != nil { - if err := raftMembership.SetCapabilityGate(applied.GetCapabilityActivationArmed(), applied.GetCapabilityMembershipRevision()); err != nil { - logger.WithFields(map[string]any{"error": err}).Errorf("Invalid committed capability gate") - - return - } - } desired := &commonpb.ClusterPolicy{ Revision: cfg.ClusterPolicyRevision, @@ -1748,28 +1731,6 @@ func reconcileClusterPolicy(ctx context.Context, admission ctrl.Admission, store MinimumReplayProtocol: applied.GetMinimumReplayProtocol(), } - // Capability activation is a one-way, replicated transition. Once every - // current member has refreshed its membership row with this binary's - // complete capability set, arm the feature-only gate automatically. The - // policy revision is bumped so this transition is auditable and cannot be - // confused with the node-local configuration revision. - if applied.GetCapabilityActivationArmed() { - desired.CapabilityActivationArmed = true - desired.CapabilityAdmissionPolicy = applied.GetCapabilityAdmissionPolicy() - desired.CapabilityMembershipRevision = applied.GetCapabilityMembershipRevision() - desired.CapabilityActivations = cloneCapabilityActivations(applied.GetCapabilityActivations()) - } else if raftMembership != nil && allMembersAdvertiseCapabilities(raftMembership.PeerAddresses()) { - desired.CapabilityActivationArmed = true - desired.CapabilityAdmissionPolicy = commonpb.CapabilityAdmissionPolicy_CAPABILITY_ADMISSION_POLICY_FEATURE_ONLY - desired.CapabilityMembershipRevision = max(applied.GetCapabilityMembershipRevision(), uint64(1)) - for _, id := range capabilities.SupportedIDs() { - desired.CapabilityActivations = append(desired.CapabilityActivations, &commonpb.CapabilityActivation{Id: uint32(id), Active: true}) - } - if desired.GetRevision() <= applied.GetRevision() { - desired.Revision = applied.GetRevision() + 1 - } - } - appliedRev := applied.GetRevision() switch { @@ -1799,33 +1760,6 @@ func reconcileClusterPolicy(ctx context.Context, admission ctrl.Admission, store } } -func allMembersAdvertiseCapabilities(peers map[uint64]raftmembership.ConfChangeContext) bool { - if len(peers) == 0 { - return false - } - - for _, peer := range peers { - for _, id := range capabilities.SupportedIDs() { - if !slices.Contains(peer.Capabilities, uint32(id)) { - return false - } - } - } - - return true -} - -func cloneCapabilityActivations(activations []*commonpb.CapabilityActivation) []*commonpb.CapabilityActivation { - cloned := make([]*commonpb.CapabilityActivation, len(activations)) - for i, activation := range activations { - if activation != nil { - cloned[i] = activation.CloneVT() - } - } - - return cloned -} - // buildResponseSigner returns the Ed25519 response signer for the gRPC // service plane. // diff --git a/internal/infra/capabilities/capabilities.go b/internal/infra/capabilities/capabilities.go index 74d607f509..6254cff8f6 100644 --- a/internal/infra/capabilities/capabilities.go +++ b/internal/infra/capabilities/capabilities.go @@ -16,17 +16,11 @@ import ( type ID uint32 -// IDs are protocol constants. Once published, an ID is never reused. -const ( - IDExecutionPlanV1 ID = 1 - IDSemanticReplayV1 ID = 2 -) - // SupportedIDs is the immutable capability set implemented by this binary. -// It is used as membership evidence during join; callers must copy the result -// before attaching it to a mutable wire message. +// No protocol feature is activated yet; this empty registry keeps the +// membership-evidence plumbing ready for the first real capability. func SupportedIDs() []ID { - return []ID{IDExecutionPlanV1, IDSemanticReplayV1} + return nil } // CurrentReplayProtocol is the replay encoding understood by this binary. @@ -36,13 +30,6 @@ func SupportedIDs() []ID { // than decode state it cannot execute deterministically. const CurrentReplayProtocol uint32 = 1 -type AdmissionPolicy uint8 - -const ( - FeatureOnly AdmissionPolicy = iota + 1 - AllWrites -) - type MemberRole uint8 const ( @@ -60,7 +47,6 @@ type Member struct { type State struct { Armed bool - Policy AdmissionPolicy MinimumReplay uint32 MembershipRevision uint64 Activations map[ID]bool @@ -128,21 +114,7 @@ func RequiredCapabilities(proposal *raftcmdpb.Proposal) ([]ID, error) { return nil, fmt.Errorf("%w: nil proposal", ErrInvalidDeclaration) } - var required []ID - // ExecutionPlan is the admission/apply coverage contract. Its presence is - // significant even when the message is empty (for no-read proposals): an - // old binary cannot safely replay the envelope without understanding it. - if proposal.GetExecutionPlan() != nil { - required = append(required, IDExecutionPlanV1) - } - // Idempotency freezes a proposal's semantic outcome for replay. Attach the - // dependency to the envelope rather than to the number of orders so an - // empty or metadata-only proposal cannot evade the barrier. - if proposal.GetIdempotency() != nil { - required = append(required, IDSemanticReplayV1) - } - - return Normalize(required) + return nil, nil } // StampProposal replaces required_capabilities with the deterministic result @@ -178,9 +150,6 @@ func (s State) Validate() error { if !s.Armed { return nil } - if s.Policy != FeatureOnly && s.Policy != AllWrites { - return fmt.Errorf("%w: admission policy %d", ErrInvalidState, s.Policy) - } if s.MembershipRevision == 0 { return fmt.Errorf("%w: zero membership revision", ErrInvalidState) } @@ -196,11 +165,6 @@ func FromPolicy(policy *commonpb.ClusterPolicy) State { s.Armed = policy.GetCapabilityActivationArmed() s.MinimumReplay = policy.GetMinimumReplayProtocol() s.MembershipRevision = policy.GetCapabilityMembershipRevision() - if policy.GetCapabilityAdmissionPolicy() == commonpb.CapabilityAdmissionPolicy_CAPABILITY_ADMISSION_POLICY_ALL_WRITES { - s.Policy = AllWrites - } else if policy.GetCapabilityAdmissionPolicy() == commonpb.CapabilityAdmissionPolicy_CAPABILITY_ADMISSION_POLICY_FEATURE_ONLY { - s.Policy = FeatureOnly - } for _, activation := range policy.GetCapabilityActivations() { if activation != nil { s.Activations[ID(activation.GetId())] = activation.GetActive() @@ -225,7 +189,7 @@ func Normalize(ids []ID) ([]ID, error) { // Evaluate is pure. Reachability is reported separately from support: a // temporarily unreachable voter is still a blocker for activation, while a // learner is included because it must replay every committed entry. -func Evaluate(state State, required []ID, businessWrite bool, members []Member) Decision { +func Evaluate(state State, required []ID, _ bool, members []Member) Decision { if err := state.Validate(); err != nil { return Decision{Reason: err.Error()} } @@ -256,12 +220,9 @@ func Evaluate(state State, required []ID, businessWrite bool, members []Member) } } } - if len(blockers) > 0 && (businessWrite && state.Policy == AllWrites || len(required) > 0) { + if len(blockers) > 0 { return Decision{Reason: ErrCapabilityUnsupported.Error(), Blockers: blockers} } - if businessWrite && state.Policy == AllWrites && len(required) == 0 { - return Decision{Reason: "business writes are blocked while capability activation is armed", Blockers: blockers} - } return Decision{Allowed: true, Blockers: blockers} } diff --git a/internal/infra/capabilities/capabilities_test.go b/internal/infra/capabilities/capabilities_test.go index f5c9dd0567..016a460aaa 100644 --- a/internal/infra/capabilities/capabilities_test.go +++ b/internal/infra/capabilities/capabilities_test.go @@ -9,98 +9,46 @@ import ( "github.com/formancehq/ledger/v3/internal/proto/raftcmdpb" ) -func armedState(policy AdmissionPolicy) State { - return State{Armed: true, Policy: policy, MembershipRevision: 7, Activations: map[ID]bool{IDExecutionPlanV1: false}} +func armedState() State { + return State{Armed: true, MembershipRevision: 7, Activations: map[ID]bool{}} } func TestEvaluateIncludesVotersAndLearners(t *testing.T) { - state := armedState(FeatureOnly) - state.Activations[IDExecutionPlanV1] = true - decision := Evaluate(state, []ID{IDExecutionPlanV1}, true, []Member{ - {NodeID: 1, Role: Voter, Reachable: true, Capabilities: []ID{IDExecutionPlanV1}}, + state := armedState() + state.Activations[99] = true + decision := Evaluate(state, []ID{99}, true, []Member{ + {NodeID: 1, Role: Voter, Reachable: true, Capabilities: []ID{99}}, {NodeID: 2, Role: Learner, Reachable: true}, }) require.False(t, decision.Allowed) require.Equal(t, uint64(2), decision.Blockers[0].NodeID) } -func TestFeatureOnlyBlocksInactiveRequiredCapability(t *testing.T) { - decision := Evaluate(armedState(FeatureOnly), []ID{IDExecutionPlanV1}, true, []Member{ - {NodeID: 1, Role: Voter, Reachable: true, Capabilities: []ID{IDExecutionPlanV1}}, - {NodeID: 2, Role: Learner, Reachable: true, Capabilities: []ID{IDExecutionPlanV1}}, - }) - require.False(t, decision.Allowed) - require.Contains(t, decision.Reason, ErrCapabilityUnsupported.Error()) - require.Contains(t, decision.Blockers, Blocker{Reason: "capability 1 is inactive"}) -} - -func TestFeatureOnlyAllowsActiveRequiredCapabilityWhenMembersSupport(t *testing.T) { - state := armedState(FeatureOnly) - state.Activations[IDExecutionPlanV1] = true - decision := Evaluate(state, []ID{IDExecutionPlanV1}, true, []Member{ - {NodeID: 1, Role: Voter, Reachable: true, Capabilities: []ID{IDExecutionPlanV1}}, - {NodeID: 2, Role: Learner, Reachable: true, Capabilities: []ID{IDExecutionPlanV1}}, - }) - require.True(t, decision.Allowed) -} - -func TestAllWritesBlocksUnrelatedBusinessWrite(t *testing.T) { - decision := Evaluate(armedState(AllWrites), nil, true, nil) - require.False(t, decision.Allowed) - require.Contains(t, decision.Reason, "business writes") -} - -func TestFeatureOnlyAllowsUnrelatedWrite(t *testing.T) { - decision := Evaluate(armedState(FeatureOnly), nil, true, []Member{{NodeID: 1, Role: Voter, Reachable: true}}) +func TestEvaluateAllowsUnrelatedWriteWhenNoFeatureIsDeclared(t *testing.T) { + decision := Evaluate(armedState(), nil, true, []Member{{NodeID: 1, Role: Voter, Reachable: true}}) require.True(t, decision.Allowed) } -func TestValidateFinalProposalRejectsMissingDeclaration(t *testing.T) { - data, err := (&raftcmdpb.Proposal{ExecutionPlan: &raftcmdpb.ExecutionPlan{}, RequiredCapabilities: []uint32{uint32(IDExecutionPlanV1)}}).MarshalVT() - require.NoError(t, err) - require.ErrorIs(t, ValidateFinalProposal(data, nil), ErrMissingCapability) -} - -func TestValidateFinalProposalRejectsSemanticDependencyOmittedFromWire(t *testing.T) { +func TestValidateFinalProposalHasNoCurrentDependencies(t *testing.T) { data, err := (&raftcmdpb.Proposal{ExecutionPlan: &raftcmdpb.ExecutionPlan{}}).MarshalVT() require.NoError(t, err) - - require.ErrorIs(t, ValidateFinalProposal(data, nil), ErrMissingCapability) + require.NoError(t, ValidateFinalProposal(data, nil)) } -func TestValidateFinalProposalRequiresAllSemanticDependencies(t *testing.T) { - proposal := &raftcmdpb.Proposal{ - ExecutionPlan: &raftcmdpb.ExecutionPlan{}, - Idempotency: &commonpb.Idempotency{Key: "retry"}, - } +func TestStampProposalClearsFutureDeclarations(t *testing.T) { + proposal := &raftcmdpb.Proposal{RequiredCapabilities: []uint32{99}} require.NoError(t, StampProposal(proposal)) - require.Equal(t, []uint32{uint32(IDExecutionPlanV1), uint32(IDSemanticReplayV1)}, proposal.GetRequiredCapabilities()) - - data, err := proposal.MarshalVT() - require.NoError(t, err) - require.NoError(t, ValidateFinalProposal(data, []ID{IDSemanticReplayV1, IDExecutionPlanV1})) - - proposal.RequiredCapabilities = []uint32{uint32(IDExecutionPlanV1)} - data, err = proposal.MarshalVT() - require.NoError(t, err) - require.ErrorIs(t, ValidateFinalProposal(data, []ID{IDExecutionPlanV1}), ErrMissingCapability) -} - -func TestRequiredCapabilitiesIsDeterministicForEmptyExecutionPlan(t *testing.T) { - required, err := RequiredCapabilities(&raftcmdpb.Proposal{ExecutionPlan: &raftcmdpb.ExecutionPlan{}}) - require.NoError(t, err) - require.Equal(t, []ID{IDExecutionPlanV1}, required) + require.Empty(t, proposal.GetRequiredCapabilities()) } func TestFromPolicyRoundTrip(t *testing.T) { state := FromPolicy(&commonpb.ClusterPolicy{ CapabilityActivationArmed: true, - CapabilityAdmissionPolicy: commonpb.CapabilityAdmissionPolicy_CAPABILITY_ADMISSION_POLICY_ALL_WRITES, MinimumReplayProtocol: 4, CapabilityMembershipRevision: 9, - CapabilityActivations: []*commonpb.CapabilityActivation{{Id: uint32(IDExecutionPlanV1), Active: true}}, + CapabilityActivations: []*commonpb.CapabilityActivation{{Id: 99, Active: true}}, }) - require.Equal(t, AllWrites, state.Policy) - require.True(t, state.Activations[IDExecutionPlanV1]) + require.True(t, state.Armed) + require.True(t, state.Activations[99]) require.NoError(t, state.Validate()) } diff --git a/internal/infra/plan/capability_policy.go b/internal/infra/plan/capability_policy.go index 2756e3d539..1bce531f69 100644 --- a/internal/infra/plan/capability_policy.go +++ b/internal/infra/plan/capability_policy.go @@ -14,8 +14,7 @@ import ( // CapabilityPolicyProposer rechecks the committed capability policy against // the final serialized proposal immediately before handing it to the Raft -// proposer. Technical producers use businessWrite=false: ALL_WRITES blocks -// business traffic while retaining the control-plane recovery path. +// proposer. type CapabilityPolicyProposer struct { delegate Proposer readPolicy func() (*commonpb.ClusterPolicy, error) diff --git a/internal/infra/plan/capability_policy_test.go b/internal/infra/plan/capability_policy_test.go index 021dd50143..c9bb25b0de 100644 --- a/internal/infra/plan/capability_policy_test.go +++ b/internal/infra/plan/capability_policy_test.go @@ -27,7 +27,7 @@ func capabilityPolicyProposal(t *testing.T) *node.Proposal { data, err := (&raftcmdpb.Proposal{ ExecutionPlan: &raftcmdpb.ExecutionPlan{}, - RequiredCapabilities: []uint32{uint32(capabilities.IDExecutionPlanV1)}, + RequiredCapabilities: []uint32{99}, }).MarshalVT() require.NoError(t, err) @@ -39,7 +39,6 @@ func TestCapabilityPolicyProposerBlocksInactiveCapability(t *testing.T) { proposer := NewCapabilityPolicyProposer(delegate, func() (*commonpb.ClusterPolicy, error) { return &commonpb.ClusterPolicy{ CapabilityActivationArmed: true, - CapabilityAdmissionPolicy: commonpb.CapabilityAdmissionPolicy_CAPABILITY_ADMISSION_POLICY_FEATURE_ONLY, CapabilityMembershipRevision: 1, }, nil }, nil, false) @@ -54,10 +53,9 @@ func TestCapabilityPolicyProposerAllowsActiveTechnicalCapability(t *testing.T) { proposer := NewCapabilityPolicyProposer(delegate, func() (*commonpb.ClusterPolicy, error) { return &commonpb.ClusterPolicy{ CapabilityActivationArmed: true, - CapabilityAdmissionPolicy: commonpb.CapabilityAdmissionPolicy_CAPABILITY_ADMISSION_POLICY_FEATURE_ONLY, CapabilityMembershipRevision: 1, CapabilityActivations: []*commonpb.CapabilityActivation{{ - Id: uint32(capabilities.IDExecutionPlanV1), + Id: 99, Active: true, }}, }, nil diff --git a/internal/proto/commonpb/common_reader.pb.go b/internal/proto/commonpb/common_reader.pb.go index 8d7082efa5..c3dfc18c29 100644 --- a/internal/proto/commonpb/common_reader.pb.go +++ b/internal/proto/commonpb/common_reader.pb.go @@ -3523,7 +3523,6 @@ type ClusterPolicyReader interface { GetMetadataMaxEntityBytes() uint64 GetMetadataMaxCommandBytes() uint64 GetCapabilityActivationArmed() bool - GetCapabilityAdmissionPolicy() CapabilityAdmissionPolicy GetMinimumReplayProtocol() uint32 GetCapabilityMembershipRevision() uint64 GetCapabilityActivations() CapabilityActivationListReader @@ -3568,10 +3567,6 @@ func (r *clusterPolicyReadonly) GetCapabilityActivationArmed() bool { return (*ClusterPolicy)(r).GetCapabilityActivationArmed() } -func (r *clusterPolicyReadonly) GetCapabilityAdmissionPolicy() CapabilityAdmissionPolicy { - return (*ClusterPolicy)(r).GetCapabilityAdmissionPolicy() -} - func (r *clusterPolicyReadonly) GetMinimumReplayProtocol() uint32 { return (*ClusterPolicy)(r).GetMinimumReplayProtocol() } diff --git a/internal/proto/commonpb/common_vtproto.pb.go b/internal/proto/commonpb/common_vtproto.pb.go index f1cb44a7ed..e12da78450 100644 --- a/internal/proto/commonpb/common_vtproto.pb.go +++ b/internal/proto/commonpb/common_vtproto.pb.go @@ -1274,7 +1274,6 @@ func (m *ClusterPolicy) CloneVT() *ClusterPolicy { r.MetadataMaxEntityBytes = m.MetadataMaxEntityBytes r.MetadataMaxCommandBytes = m.MetadataMaxCommandBytes r.CapabilityActivationArmed = m.CapabilityActivationArmed - r.CapabilityAdmissionPolicy = m.CapabilityAdmissionPolicy r.MinimumReplayProtocol = m.MinimumReplayProtocol r.CapabilityMembershipRevision = m.CapabilityMembershipRevision if rhs := m.CapabilityActivations; rhs != nil { @@ -11116,9 +11115,6 @@ func (this *ClusterPolicy) EqualVT(that *ClusterPolicy) bool { if this.CapabilityActivationArmed != that.CapabilityActivationArmed { return false } - if this.CapabilityAdmissionPolicy != that.CapabilityAdmissionPolicy { - return false - } if this.MinimumReplayProtocol != that.MinimumReplayProtocol { return false } @@ -26587,11 +26583,6 @@ func (m *ClusterPolicy) MarshalToSizedBufferVT(dAtA []byte) (int, error) { i-- dAtA[i] = 0x58 } - if m.CapabilityAdmissionPolicy != 0 { - i = protohelpers.EncodeVarint(dAtA, i, uint64(m.CapabilityAdmissionPolicy)) - i-- - dAtA[i] = 0x50 - } if m.CapabilityActivationArmed { i-- if m.CapabilityActivationArmed { @@ -46419,9 +46410,6 @@ func (m *ClusterPolicy) SizeVT() (n int) { if m.CapabilityActivationArmed { n += 2 } - if m.CapabilityAdmissionPolicy != 0 { - n += 1 + protohelpers.SizeOfVarint(uint64(m.CapabilityAdmissionPolicy)) - } if m.MinimumReplayProtocol != 0 { n += 1 + protohelpers.SizeOfVarint(uint64(m.MinimumReplayProtocol)) } @@ -61569,25 +61557,6 @@ func (m *ClusterPolicy) UnmarshalVT(dAtA []byte) error { } } m.CapabilityActivationArmed = bool(v != 0) - case 10: - if wireType != 0 { - return fmt.Errorf("proto: wrong wireType = %d for field CapabilityAdmissionPolicy", wireType) - } - m.CapabilityAdmissionPolicy = 0 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return protohelpers.ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - m.CapabilityAdmissionPolicy |= CapabilityAdmissionPolicy(b&0x7F) << shift - if b < 0x80 { - break - } - } case 11: if wireType != 0 { return fmt.Errorf("proto: wrong wireType = %d for field MinimumReplayProtocol", wireType) diff --git a/misc/proto/common.proto b/misc/proto/common.proto index b2eea9c53e..201b03cc30 100644 --- a/misc/proto/common.proto +++ b/misc/proto/common.proto @@ -552,11 +552,11 @@ message ClusterPolicy { fixed64 metadata_max_value_bytes = 6; fixed64 metadata_max_entity_bytes = 7; fixed64 metadata_max_command_bytes = 8; -// Capability admission is replicated policy, never a node-local flag. The +// Capability activation is replicated policy, never a node-local flag. The // fields are optional so nodes that predate the capability foundation decode // the policy with their existing defaults. bool capability_activation_armed = 9; - CapabilityAdmissionPolicy capability_admission_policy = 10; + reserved 10; // removed before release; never reuse this field number uint32 minimum_replay_protocol = 11; fixed64 capability_membership_revision = 12; repeated CapabilityActivation capability_activations = 13; @@ -2875,16 +2875,6 @@ message ListOptions { QueryFilter filter = 5; } -// CapabilityAdmissionPolicy controls which proposals are closed when an -// activation barrier is armed. FEATURE_ONLY closes only dependent proposals; -// ALL_WRITES closes business writes while retaining the narrow control-plane -// recovery path. -enum CapabilityAdmissionPolicy { - CAPABILITY_ADMISSION_POLICY_UNSPECIFIED = 0; - CAPABILITY_ADMISSION_POLICY_FEATURE_ONLY = 1; - CAPABILITY_ADMISSION_POLICY_ALL_WRITES = 2; -} - // CapabilityActivation is intentionally small and wire-stable. IDs are fixed // integers owned by the producer, not hashes of implementation names. message CapabilityActivation { From ceaa62c4ca0865acc5dd8d1ac87bd30392c87bb7 Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Thu, 22 Oct 2026 13:57:16 +0200 Subject: [PATCH 008/127] test: remove obsolete capability activation test --- .../bootstrap/capability_activation_test.go | 22 ------------------- 1 file changed, 22 deletions(-) delete mode 100644 internal/bootstrap/capability_activation_test.go diff --git a/internal/bootstrap/capability_activation_test.go b/internal/bootstrap/capability_activation_test.go deleted file mode 100644 index 778127c842..0000000000 --- a/internal/bootstrap/capability_activation_test.go +++ /dev/null @@ -1,22 +0,0 @@ -package bootstrap - -import ( - "testing" - - "github.com/stretchr/testify/require" - - raftmembership "github.com/formancehq/ledger/v3/internal/infra/membership" -) - -func TestAllMembersAdvertiseCapabilities(t *testing.T) { - t.Parallel() - - peer := raftmembership.ConfChangeContext{Capabilities: []uint32{1, 2}} - - require.False(t, allMembersAdvertiseCapabilities(nil)) - require.True(t, allMembersAdvertiseCapabilities(map[uint64]raftmembership.ConfChangeContext{1: peer})) - require.False(t, allMembersAdvertiseCapabilities(map[uint64]raftmembership.ConfChangeContext{ - 1: peer, - 2: {Capabilities: []uint32{1}}, - })) -} From a43236821c9d824ca7055a1cdb45f240d265fe06 Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Thu, 22 Oct 2026 13:59:12 +0200 Subject: [PATCH 009/127] chore: drop unreleased capability field reservation --- misc/proto/common.proto | 1 - 1 file changed, 1 deletion(-) diff --git a/misc/proto/common.proto b/misc/proto/common.proto index 201b03cc30..c95a88de2f 100644 --- a/misc/proto/common.proto +++ b/misc/proto/common.proto @@ -556,7 +556,6 @@ message ClusterPolicy { // fields are optional so nodes that predate the capability foundation decode // the policy with their existing defaults. bool capability_activation_armed = 9; - reserved 10; // removed before release; never reuse this field number uint32 minimum_replay_protocol = 11; fixed64 capability_membership_revision = 12; repeated CapabilityActivation capability_activations = 13; From 6515ab969c8a1a2878af25abec08ddab88873914 Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Thu, 22 Oct 2026 14:20:28 +0200 Subject: [PATCH 010/127] test: proxy membership refresh through gateway --- pkg/testserver/gateway.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pkg/testserver/gateway.go b/pkg/testserver/gateway.go index 55ccadd0b9..00a7448c64 100644 --- a/pkg/testserver/gateway.go +++ b/pkg/testserver/gateway.go @@ -485,6 +485,10 @@ func (g *clusterBootstrapServiceGateway) JoinAsLearner(ctx context.Context, req return g.client.JoinAsLearner(forwardIncomingMetadata(ctx), req) } +func (g *clusterBootstrapServiceGateway) RefreshMembership(ctx context.Context, req *clusterbootstrappb.RefreshMembershipRequest) (*clusterbootstrappb.RefreshMembershipResponse, error) { + return g.client.RefreshMembership(forwardIncomingMetadata(ctx), req) +} + func forwardIncomingMetadata(ctx context.Context) context.Context { md, _ := metadata.FromIncomingContext(ctx) From 6afc22a65672a686eb9f28c06c7189f49d0221d6 Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Thu, 22 Oct 2026 14:51:18 +0200 Subject: [PATCH 011/127] feat: gate new members on active capabilities --- .../subsystems/consensus/capabilities.md | 16 ++-- internal/adapter/grpc/server_bootstrap.go | 2 +- .../admission/capability_policy_test.go | 6 +- internal/application/membership/membership.go | 4 +- internal/bootstrap/module.go | 15 ++++ .../processing/processor_cluster_policy.go | 24 ++++++ .../processor_cluster_policy_test.go | 47 ++++++++++ internal/infra/capabilities/capabilities.go | 19 +---- .../capabilities/capabilities_extra_test.go | 3 +- .../infra/capabilities/capabilities_test.go | 9 +- internal/infra/membership/confchange.go | 30 +++---- .../membership/confchange_identity_test.go | 15 ++-- internal/infra/membership/membership.go | 60 ++++++------- internal/infra/membership/membership_test.go | 3 +- internal/infra/membership/peer_store.go | 24 +++--- internal/infra/node/learner_admission_test.go | 2 +- internal/infra/node/node.go | 85 +++++++++---------- internal/infra/plan/capability_policy_test.go | 5 +- .../cluster_bootstrap.pb.go | 58 +++++-------- .../cluster_bootstrap_reader.pb.go | 10 --- .../cluster_bootstrap_vtproto.pb.go | 46 ---------- internal/proto/commonpb/common_reader.pb.go | 5 -- internal/proto/commonpb/common_vtproto.pb.go | 23 ----- internal/proto/raftcmdpb/raft_cmd.pb.go | 19 ++--- .../proto/raftcmdpb/raft_cmd_reader.pb.go | 5 -- .../proto/raftcmdpb/raft_cmd_vtproto.pb.go | 23 ----- misc/proto/cluster_bootstrap.proto | 2 - misc/proto/common.proto | 1 - misc/proto/raft_cmd.proto | 1 - 29 files changed, 232 insertions(+), 330 deletions(-) diff --git a/docs/technical/architecture/subsystems/consensus/capabilities.md b/docs/technical/architecture/subsystems/consensus/capabilities.md index eb1695fdff..0d62e9b4f7 100644 --- a/docs/technical/architecture/subsystems/consensus/capabilities.md +++ b/docs/technical/architecture/subsystems/consensus/capabilities.md @@ -5,8 +5,8 @@ rolling upgrade. It is separate from the client-facing gRPC protocol revision. ## Wire contract -`common.ClusterPolicy` carries the replicated activation state, membership -revision under examination, and a monotonic minimum replay protocol. +`common.ClusterPolicy` carries the replicated activation state and a monotonic +minimum replay protocol. `raft.Proposal.required_capabilities` carries stable producer-owned capability IDs. The fields are optional and therefore backward-readable by binaries that predate this foundation. IDs are never reused; semantic dependencies must be @@ -23,9 +23,8 @@ The pure decision contract in `internal/infra/capabilities` evaluates every current voter and learner. A member is represented by `(node_id, instance_id)`; an identity refresh is a new incarnation and cannot silently inherit evidence from the old one. Reachability and support are separate: an unreachable member -blocks activation just as an incapable member does. Activation is bound to the -membership revision observed by the proposer; a changed revision requires a -fresh examination. +blocks activation just as an incapable member does. Once a capability is +active, every new membership announcement must include that capability. While armed, a proposal that declares a capability is admitted only when that capability is active and every member advertises it. Proposals with no declared @@ -45,7 +44,9 @@ be made safe by upgrading only the other nodes. Restart, snapshot install, restore, and downgrade code must preserve the policy and floor; a downgrade below the committed floor is refused. Membership changes racing activation are serialized at the Raft proposal boundary and must be revalidated immediately -before `rawNode.Propose`, because `Node.Propose` queues work. +before `rawNode.Propose`, because `Node.Propose` queues work. Removing a member +remains allowed; adding, promoting, or refreshing one without all active +capabilities is rejected. The admission path and the shared plan runner apply the policy check to business and technical producers (including mirror, events, maintenance, and @@ -60,7 +61,8 @@ WAL `ConfState` and keeps the same `(node_id, instance_id)` incarnation. Before inbound Raft traffic is enabled, a node whose WAL carries `CLUSTER_JOINED` sends an internal `RefreshMembership` request to a configured peer. Followers forward that request to the current leader over the Raft gRPC server. The -leader validates the identity and capability evidence, then proposes a +leader validates the identity and capability evidence against the active policy, +then proposes a `ConfChangeUpdateNode` containing the current endpoints and capabilities. Only the committed change is authoritative. The update is replicated through diff --git a/internal/adapter/grpc/server_bootstrap.go b/internal/adapter/grpc/server_bootstrap.go index 05e74c27c9..900f6b6a5b 100644 --- a/internal/adapter/grpc/server_bootstrap.go +++ b/internal/adapter/grpc/server_bootstrap.go @@ -333,7 +333,7 @@ func (impl *ClusterBootstrapServiceServerImpl) RefreshMembership(ctx context.Con return nil, status.Errorf(codes.FailedPrecondition, "node %d (instance %x) was previously removed from this cluster", req.GetNodeId(), req.GetInstanceId()) } - if err := impl.membership.RefreshMembership(ctx, req.GetNodeId(), req.GetRaftAddress(), req.GetServiceAddress(), req.GetInstanceId(), req.GetCapabilities(), req.GetCapabilityMembershipRevision()); err != nil { + if err := impl.membership.RefreshMembership(ctx, req.GetNodeId(), req.GetRaftAddress(), req.GetServiceAddress(), req.GetInstanceId(), req.GetCapabilities()); err != nil { return nil, convertToGRPCErrorWithContext(ctx, err, impl.logger) } diff --git a/internal/application/admission/capability_policy_test.go b/internal/application/admission/capability_policy_test.go index 83b5884c24..a7b3e9e5aa 100644 --- a/internal/application/admission/capability_policy_test.go +++ b/internal/application/admission/capability_policy_test.go @@ -62,8 +62,7 @@ func TestCapabilityPolicyProposerBlocksUnknownCapabilityAtFinalBoundary(t *testi delegate: delegate, readPolicy: func() (*commonpb.ClusterPolicy, error) { return &commonpb.ClusterPolicy{ - CapabilityActivationArmed: true, - CapabilityMembershipRevision: 1, + CapabilityActivationArmed: true, }, nil }, businessWrite: true, @@ -82,8 +81,7 @@ func TestCapabilityPolicyProposerAllowsUnrelatedWrite(t *testing.T) { delegate: delegate, readPolicy: func() (*commonpb.ClusterPolicy, error) { return &commonpb.ClusterPolicy{ - CapabilityActivationArmed: true, - CapabilityMembershipRevision: 1, + CapabilityActivationArmed: true, }, nil }, businessWrite: true, diff --git a/internal/application/membership/membership.go b/internal/application/membership/membership.go index 7176f966d5..692205bb23 100644 --- a/internal/application/membership/membership.go +++ b/internal/application/membership/membership.go @@ -104,12 +104,12 @@ func (s *Service) JoinAsLearner(ctx context.Context, nodeID uint64, raftAddr, se // RefreshMembership updates the durable identity, endpoints and capability // evidence of an existing member. The node must already be present in the // committed Raft membership; the leader commits the update through Raft. -func (s *Service) RefreshMembership(ctx context.Context, nodeID uint64, raftAddr, serviceAddr string, instanceID []byte, capabilityIDs []uint32, membershipRevision uint64) error { +func (s *Service) RefreshMembership(ctx context.Context, nodeID uint64, raftAddr, serviceAddr string, instanceID []byte, capabilityIDs []uint32) error { if err := validatePeer(nodeID, raftAddr, serviceAddr, instanceID); err != nil { return err } - if err := s.node.RefreshMembership(ctx, nodeID, raftAddr, serviceAddr, instanceID, capabilityIDs, membershipRevision); err != nil { + if err := s.node.RefreshMembership(ctx, nodeID, raftAddr, serviceAddr, instanceID, capabilityIDs); err != nil { return fmt.Errorf("refreshing membership: %w", err) } diff --git a/internal/bootstrap/module.go b/internal/bootstrap/module.go index db854da758..9340bb1427 100644 --- a/internal/bootstrap/module.go +++ b/internal/bootstrap/module.go @@ -8,6 +8,7 @@ import ( "net" "net/http" "path/filepath" + "slices" "strconv" "strings" "time" @@ -647,6 +648,20 @@ func Module() fx.Option { if !raftNode.IsLeader() { return } + if policy, err := query.ReadClusterPolicy(store); err == nil { + required := make([]uint32, 0, len(policy.GetCapabilityActivations())) + for _, activation := range policy.GetCapabilityActivations() { + if activation != nil && activation.GetActive() { + required = append(required, activation.GetId()) + } + } + slices.Sort(required) + if err := raftNode.SetCapabilityGate(policy.GetCapabilityActivationArmed(), required); err != nil { + logger.WithFields(map[string]any{"error": err}).Errorf("Invalid capability gate in committed cluster policy") + + return + } + } if err := proposeAuditKeyIfNeeded(ctx, raftNode, builder, store); err != nil { logger.WithFields(map[string]any{"error": err}).Errorf("Audit key initialization failed; will retry") diff --git a/internal/domain/processing/processor_cluster_policy.go b/internal/domain/processing/processor_cluster_policy.go index 15de142772..d8b5aec708 100644 --- a/internal/domain/processing/processor_cluster_policy.go +++ b/internal/domain/processing/processor_cluster_policy.go @@ -1,6 +1,8 @@ package processing import ( + "fmt" + "github.com/formancehq/ledger/v3/internal/domain" "github.com/formancehq/ledger/v3/internal/infra/capabilities" "github.com/formancehq/ledger/v3/internal/proto/commonpb" @@ -45,6 +47,28 @@ func applyClusterPolicy(newPolicy *commonpb.ClusterPolicy, scope Scope) (*common current := scope.GetClusterPolicy() appliedRev := current.GetRevision() newRev := newPolicy.GetRevision() + if current.GetCapabilityActivationArmed() && !newPolicy.GetCapabilityActivationArmed() { + return nil, &domain.ErrClusterPolicyInvalid{Detail: "capability activation cannot be disarmed"} + } + active := make(map[uint32]struct{}) + for _, activation := range current.GetCapabilityActivations() { + if activation != nil && activation.GetActive() { + active[activation.GetId()] = struct{}{} + } + } + for id := range active { + found := false + for _, activation := range newPolicy.GetCapabilityActivations() { + if activation != nil && activation.GetId() == id && activation.GetActive() { + found = true + + break + } + } + if !found { + return nil, &domain.ErrClusterPolicyInvalid{Detail: fmt.Sprintf("capability %d cannot be deactivated", id)} + } + } // A policy may only raise the floor to a replay protocol this binary can // execute. Rejecting it at the FSM boundary prevents a committed policy diff --git a/internal/domain/processing/processor_cluster_policy_test.go b/internal/domain/processing/processor_cluster_policy_test.go index 8a1b9cbbb2..4fe1c98060 100644 --- a/internal/domain/processing/processor_cluster_policy_test.go +++ b/internal/domain/processing/processor_cluster_policy_test.go @@ -150,6 +150,53 @@ func TestProcessSetClusterPolicy_ReplayFloorCannotExceedLocalProtocol(t *testing require.Contains(t, invalid.Detail, "persisted floor") } +func TestProcessSetClusterPolicy_CapabilityActivationIsMonotonic(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + mockStore := NewMockScope(ctrl) + processor, err := NewRequestProcessor(nil, 0) + require.NoError(t, err) + + current := withMetadataLimits(&commonpb.ClusterPolicy{ + Revision: 2, + CapabilityActivationArmed: true, + CapabilityActivations: []*commonpb.CapabilityActivation{{Id: 7, Active: true}}, + QueryCheckpointLimit: 1, + }) + mockStore.EXPECT().GetClusterPolicy().Return(current) + + proposed := withMetadataLimits(&commonpb.ClusterPolicy{Revision: 3, QueryCheckpointLimit: 1}) + _, procErr := processor.ProcessOrder(clusterPolicyOrder(proposed), mockStore) + + var invalid *domain.ErrClusterPolicyInvalid + require.ErrorAs(t, procErr, &invalid) + require.Contains(t, invalid.Detail, "cannot be disarmed") +} + +func TestProcessSetClusterPolicy_CapabilityCannotDisappear(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + mockStore := NewMockScope(ctrl) + processor, err := NewRequestProcessor(nil, 0) + require.NoError(t, err) + + current := withMetadataLimits(&commonpb.ClusterPolicy{ + Revision: 2, + CapabilityActivations: []*commonpb.CapabilityActivation{{Id: 7, Active: true}}, + QueryCheckpointLimit: 1, + }) + mockStore.EXPECT().GetClusterPolicy().Return(current) + + proposed := withMetadataLimits(&commonpb.ClusterPolicy{Revision: 3, QueryCheckpointLimit: 1}) + _, procErr := processor.ProcessOrder(clusterPolicyOrder(proposed), mockStore) + + var invalid *domain.ErrClusterPolicyInvalid + require.ErrorAs(t, procErr, &invalid) + require.Contains(t, invalid.Detail, "capability 7 cannot be deactivated") +} + // Structural validation runs before the revision check, so it needs no applied // policy: a missing policy, a zero revision, and a zero limit each reject. func TestProcessSetClusterPolicy_StructuralValidation(t *testing.T) { diff --git a/internal/infra/capabilities/capabilities.go b/internal/infra/capabilities/capabilities.go index 6254cff8f6..ae92d7787f 100644 --- a/internal/infra/capabilities/capabilities.go +++ b/internal/infra/capabilities/capabilities.go @@ -46,10 +46,9 @@ type Member struct { } type State struct { - Armed bool - MinimumReplay uint32 - MembershipRevision uint64 - Activations map[ID]bool + Armed bool + MinimumReplay uint32 + Activations map[ID]bool } type Blocker struct { @@ -80,14 +79,6 @@ func ValidateMembers(members []Member) error { return nil } -func ValidateMembershipRevision(observed, examined uint64) error { - if examined == 0 || observed != examined { - return fmt.Errorf("%w: membership revision changed from %d to %d", ErrInvalidState, examined, observed) - } - - return nil -} - func AdvanceReplayFloor(current, next uint32) (uint32, error) { if next < current { return current, fmt.Errorf("%w: replay protocol floor cannot decrease from %d to %d", ErrInvalidState, current, next) @@ -150,9 +141,6 @@ func (s State) Validate() error { if !s.Armed { return nil } - if s.MembershipRevision == 0 { - return fmt.Errorf("%w: zero membership revision", ErrInvalidState) - } return nil } @@ -164,7 +152,6 @@ func FromPolicy(policy *commonpb.ClusterPolicy) State { } s.Armed = policy.GetCapabilityActivationArmed() s.MinimumReplay = policy.GetMinimumReplayProtocol() - s.MembershipRevision = policy.GetCapabilityMembershipRevision() for _, activation := range policy.GetCapabilityActivations() { if activation != nil { s.Activations[ID(activation.GetId())] = activation.GetActive() diff --git a/internal/infra/capabilities/capabilities_extra_test.go b/internal/infra/capabilities/capabilities_extra_test.go index b7d2a3f507..1eb3fc1ac6 100644 --- a/internal/infra/capabilities/capabilities_extra_test.go +++ b/internal/infra/capabilities/capabilities_extra_test.go @@ -6,8 +6,7 @@ import ( "github.com/stretchr/testify/require" ) -func TestMembershipRevisionAndIncarnationValidation(t *testing.T) { - require.Error(t, ValidateMembershipRevision(8, 7)) +func TestIncarnationValidation(t *testing.T) { require.Error(t, ValidateMembers([]Member{{NodeID: 4, InstanceID: []byte("a"), Role: Voter}, {NodeID: 4, InstanceID: []byte("b"), Role: Learner}})) require.NoError(t, ValidateMembers([]Member{{NodeID: 4, InstanceID: []byte("a"), Role: Voter}, {NodeID: 5, InstanceID: []byte("b"), Role: Learner}})) } diff --git a/internal/infra/capabilities/capabilities_test.go b/internal/infra/capabilities/capabilities_test.go index 016a460aaa..ef2a289bda 100644 --- a/internal/infra/capabilities/capabilities_test.go +++ b/internal/infra/capabilities/capabilities_test.go @@ -10,7 +10,7 @@ import ( ) func armedState() State { - return State{Armed: true, MembershipRevision: 7, Activations: map[ID]bool{}} + return State{Armed: true, Activations: map[ID]bool{}} } func TestEvaluateIncludesVotersAndLearners(t *testing.T) { @@ -43,10 +43,9 @@ func TestStampProposalClearsFutureDeclarations(t *testing.T) { func TestFromPolicyRoundTrip(t *testing.T) { state := FromPolicy(&commonpb.ClusterPolicy{ - CapabilityActivationArmed: true, - MinimumReplayProtocol: 4, - CapabilityMembershipRevision: 9, - CapabilityActivations: []*commonpb.CapabilityActivation{{Id: 99, Active: true}}, + CapabilityActivationArmed: true, + MinimumReplayProtocol: 4, + CapabilityActivations: []*commonpb.CapabilityActivation{{Id: 99, Active: true}}, }) require.True(t, state.Armed) require.True(t, state.Activations[99]) diff --git a/internal/infra/membership/confchange.go b/internal/infra/membership/confchange.go index 0ec8e09086..f834d3dfeb 100644 --- a/internal/infra/membership/confchange.go +++ b/internal/infra/membership/confchange.go @@ -44,8 +44,8 @@ func ValidateCapabilities(ids []uint32) error { // ValidateCapabilityEvidence validates the proof attached to a membership // transition. Unarmed clusters retain backwards-compatible empty evidence; // once the capability barrier is armed, every Add/Update/Promote transition -// must carry a fresh revision-bound capability set. -func ValidateCapabilityEvidence(ctx *ConfChangeContext, armed bool, expectedRevision uint64) error { +// must announce all currently active capabilities. +func ValidateCapabilityEvidence(ctx *ConfChangeContext, armed bool, required []uint32) error { if ctx == nil { return errors.New("missing capability membership evidence") } @@ -55,14 +55,10 @@ func ValidateCapabilityEvidence(ctx *ConfChangeContext, armed bool, expectedRevi if !armed { return nil } - if expectedRevision == 0 { - return errors.New("capability membership revision is not configured") - } - if ctx.CapabilityMembershipRevision != expectedRevision { - return fmt.Errorf("capability membership revision %d does not match active revision %d", ctx.CapabilityMembershipRevision, expectedRevision) - } - if len(ctx.Capabilities) == 0 { - return errors.New("capability evidence is empty while activation is armed") + for _, id := range required { + if !slices.Contains(ctx.Capabilities, id) { + return fmt.Errorf("capability %d is required by the active policy", id) + } } return nil @@ -80,12 +76,11 @@ type ConfChangeContext struct { RaftAddress string `json:"raftAddress"` ServiceAddress string `json:"serviceAddress"` InstanceID []byte `json:"instanceId,omitempty"` - // Capabilities and CapabilityMembershipRevision are the signed-by- - // consensus membership evidence for this concrete peer incarnation. They - // travel in every registration context and are persisted in PeerAddress so - // activation cannot accidentally inherit support from a replaced pod. - Capabilities []uint32 `json:"capabilities,omitempty"` - CapabilityMembershipRevision uint64 `json:"capabilityMembershipRevision,omitempty"` + // Capabilities are the signed-by-consensus membership evidence for this + // concrete peer incarnation. They travel in every registration context and + // are persisted in PeerAddress so activation cannot accidentally inherit + // support from a replaced pod. + Capabilities []uint32 `json:"capabilities,omitempty"` // ProposalID is a leader-local correlation token. It is committed with the // ConfChange so the proposing leader can resolve only the matching waiter; // FSM apply deliberately ignores it. @@ -100,8 +95,7 @@ func (c ConfChangeContext) Equal(other ConfChangeContext) bool { return c.RaftAddress == other.RaftAddress && c.ServiceAddress == other.ServiceAddress && bytes.Equal(c.InstanceID, other.InstanceID) && - slices.Equal(c.Capabilities, other.Capabilities) && - c.CapabilityMembershipRevision == other.CapabilityMembershipRevision + slices.Equal(c.Capabilities, other.Capabilities) } // MarshalConfChangeContext serialises a ConfChangeContext to JSON bytes diff --git a/internal/infra/membership/confchange_identity_test.go b/internal/infra/membership/confchange_identity_test.go index 779b367c5c..4119d71f71 100644 --- a/internal/infra/membership/confchange_identity_test.go +++ b/internal/infra/membership/confchange_identity_test.go @@ -8,17 +8,16 @@ import ( "google.golang.org/protobuf/proto" ) -func TestValidateCapabilityEvidenceBindsActivationToRevisionAndIdentity(t *testing.T) { +func TestValidateCapabilityEvidenceRequiresActiveCapabilities(t *testing.T) { ctx := &ConfChangeContext{ - InstanceID: bytesN(0x42), - Capabilities: []uint32{1, 2}, - CapabilityMembershipRevision: 7, + InstanceID: bytesN(0x42), + Capabilities: []uint32{1, 2}, } - require.NoError(t, ValidateCapabilityEvidence(ctx, true, 7)) - require.ErrorContains(t, ValidateCapabilityEvidence(ctx, true, 8), "does not match active revision") - require.ErrorContains(t, ValidateCapabilityEvidence(&ConfChangeContext{InstanceID: bytesN(0x42), CapabilityMembershipRevision: 7}, true, 7), "evidence is empty") - require.NoError(t, ValidateCapabilityEvidence(&ConfChangeContext{InstanceID: bytesN(0x42)}, false, 0)) + require.NoError(t, ValidateCapabilityEvidence(ctx, true, []uint32{1, 2})) + require.ErrorContains(t, ValidateCapabilityEvidence(ctx, true, []uint32{1, 3}), "capability 3") + require.NoError(t, ValidateCapabilityEvidence(&ConfChangeContext{InstanceID: bytesN(0x42)}, true, nil)) + require.NoError(t, ValidateCapabilityEvidence(&ConfChangeContext{InstanceID: bytesN(0x42)}, false, nil)) } func TestWriteConfChangeRejectsIncompleteRegistration(t *testing.T) { diff --git a/internal/infra/membership/membership.go b/internal/infra/membership/membership.go index bbd0f84af6..010f72f3af 100644 --- a/internal/infra/membership/membership.go +++ b/internal/infra/membership/membership.go @@ -71,10 +71,10 @@ type Membership struct { addresses map[uint64]ConfChangeContext // capabilityGate is updated from the committed ClusterPolicy projection. // It is intentionally kept beside the membership cache so Add/Promote - // admission observes one lock-consistent view of activation revision and + // admission observes one lock-consistent view of active requirements and // member identity evidence. - capabilityArmed bool - capabilityRevision uint64 + capabilityArmed bool + requiredCapabilities []uint32 // started gates transport / service-pool wiring. Until Start fires // (OnStart hook, after the local Raft gRPC server is listening), // Set / Remove / Register / Rehydrate mutate only the cache — the @@ -247,27 +247,26 @@ func (m *Membership) SnapshotDonorCandidates() []uint64 { } // SetCapabilityGate publishes the committed activation gate used by -// membership transitions. Callers must pass the policy's membership revision; -// a zero revision is accepted only while the barrier is disarmed. -func (m *Membership) SetCapabilityGate(armed bool, revision uint64) error { - if armed && revision == 0 { - return errors.New("capability membership revision must be non-zero when armed") +// membership transitions. The required IDs are copied and must be canonical. +func (m *Membership) SetCapabilityGate(armed bool, required []uint32) error { + if err := ValidateCapabilities(required); err != nil { + return err } m.mu.Lock() defer m.mu.Unlock() m.capabilityArmed = armed - m.capabilityRevision = revision + m.requiredCapabilities = append([]uint32(nil), required...) return nil } // CapabilityGate returns the current committed activation gate. -func (m *Membership) CapabilityGate() (armed bool, revision uint64) { +func (m *Membership) CapabilityGate() (armed bool, required []uint32) { m.mu.RLock() defer m.mu.RUnlock() - return m.capabilityArmed, m.capabilityRevision + return m.capabilityArmed, append([]uint32(nil), m.requiredCapabilities...) } // ValidateTransitionEvidence checks a registration context against the @@ -276,10 +275,10 @@ func (m *Membership) CapabilityGate() (armed bool, revision uint64) { // read and rawNode.Propose. func (m *Membership) ValidateTransitionEvidence(ctx *ConfChangeContext) error { m.mu.RLock() - armed, revision := m.capabilityArmed, m.capabilityRevision + armed, required := m.capabilityArmed, append([]uint32(nil), m.requiredCapabilities...) m.mu.RUnlock() - return ValidateCapabilityEvidence(ctx, armed, revision) + return ValidateCapabilityEvidence(ctx, armed, required) } // GetInstanceID returns the peer's 16-byte identity UUID from the in-memory @@ -313,14 +312,14 @@ func (m *Membership) GetInstanceID(nodeID uint64) ([]byte, bool) { // they stay in lockstep with a concurrent Rehydrate. See Rehydrate's // locking note. func (m *Membership) Set(nodeID uint64, raftAddr, serviceAddr string, instanceID []byte) error { - return m.SetWithEvidence(nodeID, raftAddr, serviceAddr, instanceID, nil, 0) + return m.SetWithEvidence(nodeID, raftAddr, serviceAddr, instanceID, nil) } // SetWithEvidence upserts a peer and persists its capability evidence in the // in-memory registration. The FSM write path stores the same fields in the // durable PeerAddress row, while finishReady uses this method to keep the // transport cache and evidence synchronized before resolving the proposal. -func (m *Membership) SetWithEvidence(nodeID uint64, raftAddr, serviceAddr string, instanceID []byte, capabilityIDs []uint32, membershipRevision uint64) error { +func (m *Membership) SetWithEvidence(nodeID uint64, raftAddr, serviceAddr string, instanceID []byte, capabilityIDs []uint32) error { if err := ValidateInstanceID(instanceID); err != nil { return fmt.Errorf("setting peer %d: %w", nodeID, err) } @@ -332,11 +331,10 @@ func (m *Membership) SetWithEvidence(nodeID uint64, raftAddr, serviceAddr string defer m.mu.Unlock() m.addresses[nodeID] = ConfChangeContext{ - RaftAddress: raftAddr, - ServiceAddress: serviceAddr, - InstanceID: bytes.Clone(instanceID), - Capabilities: append([]uint32(nil), capabilityIDs...), - CapabilityMembershipRevision: membershipRevision, + RaftAddress: raftAddr, + ServiceAddress: serviceAddr, + InstanceID: bytes.Clone(instanceID), + Capabilities: append([]uint32(nil), capabilityIDs...), } m.wireAdd(nodeID, raftAddr, serviceAddr) @@ -365,27 +363,26 @@ func (m *Membership) Remove(nodeID uint64) { // instanceID is always the peer's 16-byte WAL/PVC identity. Identity-less // membership rows are rejected rather than persisted as a partial member. func (m *Membership) Register(nodeID uint64, raftAddr, serviceAddr string, instanceID []byte) error { - return m.RegisterWithEvidence(nodeID, raftAddr, serviceAddr, instanceID, nil, 0) + return m.RegisterWithEvidence(nodeID, raftAddr, serviceAddr, instanceID, nil) } // RegisterWithEvidence is the durable/cache-atomic lifecycle variant of // Register. It is used for self refresh and bootstrap rows that already carry // a capability proof. -func (m *Membership) RegisterWithEvidence(nodeID uint64, raftAddr, serviceAddr string, instanceID []byte, capabilityIDs []uint32, membershipRevision uint64) error { +func (m *Membership) RegisterWithEvidence(nodeID uint64, raftAddr, serviceAddr string, instanceID []byte, capabilityIDs []uint32) error { // A restart refreshes the local addresses. Preserve evidence for the same // concrete instance when the caller has no new proof; a changed instance // must never inherit the previous pod's capabilities. - if len(capabilityIDs) == 0 && membershipRevision == 0 { + if len(capabilityIDs) == 0 { if existing, ok := m.PeerAddresses()[nodeID]; ok && bytes.Equal(existing.InstanceID, instanceID) { capabilityIDs = existing.Capabilities - membershipRevision = existing.CapabilityMembershipRevision } } - if err := m.store.PutWithEvidence(nodeID, raftAddr, serviceAddr, instanceID, capabilityIDs, membershipRevision); err != nil { + if err := m.store.PutWithEvidence(nodeID, raftAddr, serviceAddr, instanceID, capabilityIDs); err != nil { return fmt.Errorf("persisting peer %d: %w", nodeID, err) } - if err := m.SetWithEvidence(nodeID, raftAddr, serviceAddr, instanceID, capabilityIDs, membershipRevision); err != nil { + if err := m.SetWithEvidence(nodeID, raftAddr, serviceAddr, instanceID, capabilityIDs); err != nil { return err } @@ -660,12 +657,11 @@ func (m *Membership) WriteConfChange(entry *raftpb.Entry, session *dal.WriteSess return fmt.Errorf("invalid capability evidence for peer %d: %w", nodeID, err) } if err := session.SetProto(peerKey(nodeID), &raftcmdpb.PeerAddress{ - NodeId: nodeID, - RaftAddress: ctx.RaftAddress, - ServiceAddress: ctx.ServiceAddress, - InstanceId: ctx.InstanceID, - Capabilities: ctx.Capabilities, - CapabilityMembershipRevision: ctx.CapabilityMembershipRevision, + NodeId: nodeID, + RaftAddress: ctx.RaftAddress, + ServiceAddress: ctx.ServiceAddress, + InstanceId: ctx.InstanceID, + Capabilities: ctx.Capabilities, }); err != nil { return fmt.Errorf("session write peer %d: %w", nodeID, err) } diff --git a/internal/infra/membership/membership_test.go b/internal/infra/membership/membership_test.go index 4b83aeafb8..98be62ac55 100644 --- a/internal/infra/membership/membership_test.go +++ b/internal/infra/membership/membership_test.go @@ -664,7 +664,7 @@ func TestPeerStore_PersistsCapabilityEvidenceWithIdentity(t *testing.T) { ps := newTestPeerStore(t) caps := []uint32{1, 2} identity := fixedInstanceID(0x7a) - require.NoError(t, ps.PutWithEvidence(9, "raft:9", "service:9", identity, caps, 11)) + require.NoError(t, ps.PutWithEvidence(9, "raft:9", "service:9", identity, caps)) rows, err := ps.LoadAll() require.NoError(t, err) @@ -672,7 +672,6 @@ func TestPeerStore_PersistsCapabilityEvidenceWithIdentity(t *testing.T) { require.True(t, ok) require.Equal(t, identity, row.InstanceID) require.Equal(t, caps, row.Capabilities) - require.Equal(t, uint64(11), row.CapabilityMembershipRevision) } // TestMembership_ReconcileAgainstConfState pins the boot-time cleanup diff --git a/internal/infra/membership/peer_store.go b/internal/infra/membership/peer_store.go index 9b247ac8c1..26bb7b1706 100644 --- a/internal/infra/membership/peer_store.go +++ b/internal/infra/membership/peer_store.go @@ -92,14 +92,14 @@ func peerKeyRange() (lower, upper []byte) { // apply uses the caller's WriteSession directly. Every persisted peer must // carry the concrete member's 16-byte WAL/PVC identity. func (p *PeerStore) Put(nodeID uint64, raftAddr, serviceAddr string, instanceID []byte) error { - return p.PutWithEvidence(nodeID, raftAddr, serviceAddr, instanceID, nil, 0) + return p.PutWithEvidence(nodeID, raftAddr, serviceAddr, instanceID, nil) } // PutWithEvidence persists a peer directory row together with the capability // evidence bound to its concrete instance. The evidence is deliberately part // of the same Pebble value as the identity so a crash cannot expose a peer // with a fresh instance_id and stale capabilities. -func (p *PeerStore) PutWithEvidence(nodeID uint64, raftAddr, serviceAddr string, instanceID []byte, capabilities []uint32, membershipRevision uint64) error { +func (p *PeerStore) PutWithEvidence(nodeID uint64, raftAddr, serviceAddr string, instanceID []byte, capabilities []uint32) error { if err := ValidateInstanceID(instanceID); err != nil { return fmt.Errorf("writing peer %d: %w", nodeID, err) } @@ -109,12 +109,11 @@ func (p *PeerStore) PutWithEvidence(nodeID uint64, raftAddr, serviceAddr string, session := p.store.OpenWriteSession() if err := session.SetProto(peerKey(nodeID), &raftcmdpb.PeerAddress{ - NodeId: nodeID, - RaftAddress: raftAddr, - ServiceAddress: serviceAddr, - InstanceId: instanceID, - Capabilities: capabilities, - CapabilityMembershipRevision: membershipRevision, + NodeId: nodeID, + RaftAddress: raftAddr, + ServiceAddress: serviceAddr, + InstanceId: instanceID, + Capabilities: capabilities, }); err != nil { _ = session.Cancel() @@ -192,11 +191,10 @@ func (p *PeerStore) LoadAll() (map[uint64]ConfChangeContext, error) { } out[pa.GetNodeId()] = ConfChangeContext{ - RaftAddress: pa.GetRaftAddress(), - ServiceAddress: pa.GetServiceAddress(), - InstanceID: pa.GetInstanceId(), - Capabilities: append([]uint32(nil), pa.GetCapabilities()...), - CapabilityMembershipRevision: pa.GetCapabilityMembershipRevision(), + RaftAddress: pa.GetRaftAddress(), + ServiceAddress: pa.GetServiceAddress(), + InstanceID: pa.GetInstanceId(), + Capabilities: append([]uint32(nil), pa.GetCapabilities()...), } } diff --git a/internal/infra/node/learner_admission_test.go b/internal/infra/node/learner_admission_test.go index 1877556bc4..5b956cf8a8 100644 --- a/internal/infra/node/learner_admission_test.go +++ b/internal/infra/node/learner_admission_test.go @@ -143,7 +143,7 @@ func TestRefreshMembershipProposesUpdateForExistingIdentity(t *testing.T) { defer cancel() results := make(chan error, 1) go func() { - results <- n.RefreshMembership(ctx, 2, "refreshed:7777", "refreshed:8888", []byte("peer-instance-id"), []uint32{1, 2}, 0) + results <- n.RefreshMembership(ctx, 2, "refreshed:7777", "refreshed:8888", []byte("peer-instance-id"), []uint32{1, 2}) }() cmd := <-n.clusterCommandCh diff --git a/internal/infra/node/node.go b/internal/infra/node/node.go index 2fef208337..9c1e3c41e4 100644 --- a/internal/infra/node/node.go +++ b/internal/infra/node/node.go @@ -699,7 +699,7 @@ func NewNode( // writes self for Bootstrap/Restore, but the restart branch never // did — that's the gap the previous bootstrap hook used to cover // before transport wiring moved into Membership. - if err := membership.RegisterWithEvidence(cfg.NodeID, cfg.AdvertiseAddr, cfg.ServiceAdvertiseAddr, cfg.InstanceID, capabilityIDsAsUint32(capabilities.SupportedIDs()), 0); err != nil { + if err := membership.RegisterWithEvidence(cfg.NodeID, cfg.AdvertiseAddr, cfg.ServiceAdvertiseAddr, cfg.InstanceID, capabilityIDsAsUint32(capabilities.SupportedIDs())); err != nil { return nil, fmt.Errorf("refreshing self in peer store: %w", err) } @@ -1763,7 +1763,7 @@ func (node *Node) finishReady(result readyResult, stop chan struct{}) error { if ctx == nil { return fmt.Errorf("invariant: committed registration for node %d has no identity context", nodeID) } - if err := node.membership.SetWithEvidence(nodeID, ctx.RaftAddress, ctx.ServiceAddress, ctx.InstanceID, ctx.Capabilities, ctx.CapabilityMembershipRevision); err != nil { + if err := node.membership.SetWithEvidence(nodeID, ctx.RaftAddress, ctx.ServiceAddress, ctx.InstanceID, ctx.Capabilities); err != nil { return fmt.Errorf("invariant: applying membership cache update: %w", err) } case raftpb.ConfChangeRemoveNode: @@ -3173,14 +3173,14 @@ func classifyExistingLearner(match uint64, existingInstanceID, incomingInstanceI // // Must be called on the leader. func (node *Node) AddLearner(ctx context.Context, nodeID uint64, raftAddr, serviceAddr string, instanceID []byte) error { - return node.AddLearnerWithCapabilities(ctx, nodeID, raftAddr, serviceAddr, instanceID, nil, 0) + return node.AddLearnerWithCapabilities(ctx, nodeID, raftAddr, serviceAddr, instanceID, nil) } // AddLearnerWithCapabilities proposes a learner registration carrying the // caller's capability proof. The proof is persisted with the peer identity and // is required once the capability activation gate is armed. -func (node *Node) AddLearnerWithCapabilities(ctx context.Context, nodeID uint64, raftAddr, serviceAddr string, instanceID []byte, capabilityIDs []uint32, membershipRevision uint64) error { - return node.addLearnerWithCapabilities(ctx, nodeID, raftAddr, serviceAddr, instanceID, capabilityIDs, membershipRevision, false) +func (node *Node) AddLearnerWithCapabilities(ctx context.Context, nodeID uint64, raftAddr, serviceAddr string, instanceID []byte, capabilityIDs []uint32) error { + return node.addLearnerWithCapabilities(ctx, nodeID, raftAddr, serviceAddr, instanceID, capabilityIDs, false) } // WithStableDrainMembership serializes a drain capability preflight and its @@ -3212,13 +3212,13 @@ func (node *Node) WithStableDrainMembership(ctx context.Context, fn func() error // WAL. It deliberately keeps EN-1436's stricter stale-progress handling // separate from administrative AddLearner retries. func (node *Node) JoinAsLearner(ctx context.Context, nodeID uint64, raftAddr, serviceAddr string, instanceID []byte) error { - return node.JoinAsLearnerWithCapabilities(ctx, nodeID, raftAddr, serviceAddr, instanceID, nil, 0) + return node.JoinAsLearnerWithCapabilities(ctx, nodeID, raftAddr, serviceAddr, instanceID, nil) } // JoinAsLearnerWithCapabilities is the bootstrap admission variant carrying // the joining node's capability proof. -func (node *Node) JoinAsLearnerWithCapabilities(ctx context.Context, nodeID uint64, raftAddr, serviceAddr string, instanceID []byte, capabilityIDs []uint32, membershipRevision uint64) error { - return node.addLearnerWithCapabilities(ctx, nodeID, raftAddr, serviceAddr, instanceID, capabilityIDs, membershipRevision, true) +func (node *Node) JoinAsLearnerWithCapabilities(ctx context.Context, nodeID uint64, raftAddr, serviceAddr string, instanceID []byte, capabilityIDs []uint32) error { + return node.addLearnerWithCapabilities(ctx, nodeID, raftAddr, serviceAddr, instanceID, capabilityIDs, true) } // RefreshMembership proposes a committed UpdateNode for an existing member. @@ -3226,22 +3226,17 @@ func (node *Node) JoinAsLearnerWithCapabilities(ctx context.Context, nodeID uint // WAL after a normal restart. It requires the same persisted instance identity // and replaces the committed endpoint/capability evidence through Raft. // Must be called on the leader. -func (node *Node) RefreshMembership(ctx context.Context, nodeID uint64, raftAddr, serviceAddr string, instanceID []byte, capabilityIDs []uint32, membershipRevision uint64) error { +func (node *Node) RefreshMembership(ctx context.Context, nodeID uint64, raftAddr, serviceAddr string, instanceID []byte, capabilityIDs []uint32) error { if err := membership.ValidateInstanceID(instanceID); err != nil { return fmt.Errorf("invalid member identity: %w", err) } - if armed, activeRevision := node.membership.CapabilityGate(); armed && membershipRevision == 0 { - membershipRevision = activeRevision - } - proposalID := uuid.NewString() ccCtx, err := membership.MarshalConfChangeContext(membership.ConfChangeContext{ - RaftAddress: raftAddr, - ServiceAddress: serviceAddr, - InstanceID: instanceID, - Capabilities: append([]uint32(nil), capabilityIDs...), - CapabilityMembershipRevision: membershipRevision, - ProposalID: proposalID, + RaftAddress: raftAddr, + ServiceAddress: serviceAddr, + InstanceID: instanceID, + Capabilities: append([]uint32(nil), capabilityIDs...), + ProposalID: proposalID, }) if err != nil { return fmt.Errorf("marshaling membership refresh context: %w", err) @@ -3271,11 +3266,10 @@ func (node *Node) RefreshMembership(ctx context.Context, nodeID uint64, raftAddr } if err := node.membership.ValidateTransitionEvidence(&membership.ConfChangeContext{ - RaftAddress: raftAddr, - ServiceAddress: serviceAddr, - InstanceID: instanceID, - Capabilities: capabilityIDs, - CapabilityMembershipRevision: membershipRevision, + RaftAddress: raftAddr, + ServiceAddress: serviceAddr, + InstanceID: instanceID, + Capabilities: capabilityIDs, }); err != nil { return err } @@ -3301,31 +3295,29 @@ func (node *Node) RefreshMembership(ctx context.Context, nodeID uint64, raftAddr }, nil) } +// SetCapabilityGate updates the leader-side membership admission gate from a +// committed ClusterPolicy projection. +func (node *Node) SetCapabilityGate(armed bool, required []uint32) error { + return node.membership.SetCapabilityGate(armed, required) +} + // addLearner is kept as a compatibility shim for package-local tests and // callers that exercise the pre-evidence admission path directly. func (node *Node) addLearner(ctx context.Context, nodeID uint64, raftAddr, serviceAddr string, instanceID []byte, bootJoin bool) error { - return node.addLearnerWithCapabilities(ctx, nodeID, raftAddr, serviceAddr, instanceID, nil, 0, bootJoin) + return node.addLearnerWithCapabilities(ctx, nodeID, raftAddr, serviceAddr, instanceID, nil, bootJoin) } -func (node *Node) addLearnerWithCapabilities(ctx context.Context, nodeID uint64, raftAddr, serviceAddr string, instanceID []byte, capabilityIDs []uint32, membershipRevision uint64, bootJoin bool) error { +func (node *Node) addLearnerWithCapabilities(ctx context.Context, nodeID uint64, raftAddr, serviceAddr string, instanceID []byte, capabilityIDs []uint32, bootJoin bool) error { if err := membership.ValidateInstanceID(instanceID); err != nil { return fmt.Errorf("invalid learner identity: %w", err) } - if armed, activeRevision := node.membership.CapabilityGate(); armed && membershipRevision == 0 { - // The leader binds a joiner's capability declaration to the currently - // committed revision. A stale/omitted caller revision must not be able - // to smuggle evidence across a policy transition. - membershipRevision = activeRevision - } - proposalID := uuid.NewString() ccCtx, err := membership.MarshalConfChangeContext(membership.ConfChangeContext{ - RaftAddress: raftAddr, - ServiceAddress: serviceAddr, - InstanceID: instanceID, - Capabilities: append([]uint32(nil), capabilityIDs...), - CapabilityMembershipRevision: membershipRevision, - ProposalID: proposalID, + RaftAddress: raftAddr, + ServiceAddress: serviceAddr, + InstanceID: instanceID, + Capabilities: append([]uint32(nil), capabilityIDs...), + ProposalID: proposalID, }) if err != nil { return fmt.Errorf("marshaling conf change context: %w", err) @@ -3339,15 +3331,14 @@ func (node *Node) addLearnerWithCapabilities(ctx context.Context, nodeID uint64, if status.RaftState != raft.StateLeader { return ErrNotLeader } - // Revalidate immediately before RawNode.Propose: policy activation and - // membership revision may have changed while the request waited for the - // serialized ConfChange boundary. + // Revalidate immediately before RawNode.Propose: policy activation may + // have changed while the request waited for the serialized ConfChange + // boundary. if err := node.membership.ValidateTransitionEvidence(&membership.ConfChangeContext{ - RaftAddress: raftAddr, - ServiceAddress: serviceAddr, - InstanceID: instanceID, - Capabilities: capabilityIDs, - CapabilityMembershipRevision: membershipRevision, + RaftAddress: raftAddr, + ServiceAddress: serviceAddr, + InstanceID: instanceID, + Capabilities: capabilityIDs, }); err != nil { return err } diff --git a/internal/infra/plan/capability_policy_test.go b/internal/infra/plan/capability_policy_test.go index c9bb25b0de..48fe4fc7c0 100644 --- a/internal/infra/plan/capability_policy_test.go +++ b/internal/infra/plan/capability_policy_test.go @@ -38,8 +38,7 @@ func TestCapabilityPolicyProposerBlocksInactiveCapability(t *testing.T) { delegate := new(capabilityPolicyTestProposer) proposer := NewCapabilityPolicyProposer(delegate, func() (*commonpb.ClusterPolicy, error) { return &commonpb.ClusterPolicy{ - CapabilityActivationArmed: true, - CapabilityMembershipRevision: 1, + CapabilityActivationArmed: true, }, nil }, nil, false) @@ -52,8 +51,6 @@ func TestCapabilityPolicyProposerAllowsActiveTechnicalCapability(t *testing.T) { delegate := new(capabilityPolicyTestProposer) proposer := NewCapabilityPolicyProposer(delegate, func() (*commonpb.ClusterPolicy, error) { return &commonpb.ClusterPolicy{ - CapabilityActivationArmed: true, - CapabilityMembershipRevision: 1, CapabilityActivations: []*commonpb.CapabilityActivation{{ Id: 99, Active: true, diff --git a/internal/proto/clusterbootstrappb/cluster_bootstrap.pb.go b/internal/proto/clusterbootstrappb/cluster_bootstrap.pb.go index d660084def..fb87fe693a 100644 --- a/internal/proto/clusterbootstrappb/cluster_bootstrap.pb.go +++ b/internal/proto/clusterbootstrappb/cluster_bootstrap.pb.go @@ -172,15 +172,14 @@ func (x *PeerInfo) GetInstanceId() []byte { } type JoinAsLearnerRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - NodeId uint64 `protobuf:"varint,1,opt,name=node_id,json=nodeId,proto3" json:"node_id,omitempty"` // Numeric ID for the new learner node - RaftAddress string `protobuf:"bytes,2,opt,name=raft_address,json=raftAddress,proto3" json:"raft_address,omitempty"` // Raft transport address of the learner - ServiceAddress string `protobuf:"bytes,3,opt,name=service_address,json=serviceAddress,proto3" json:"service_address,omitempty"` // Service API address of the learner - InstanceId []byte `protobuf:"bytes,4,opt,name=instance_id,json=instanceId,proto3" json:"instance_id,omitempty"` // 16-byte UUID identifying this peer's WAL/PVC incarnation (EN-1045); required - Capabilities []uint32 `protobuf:"fixed32,5,rep,packed,name=capabilities,proto3" json:"capabilities,omitempty"` // capabilities supported by this binary - CapabilityMembershipRevision uint64 `protobuf:"fixed64,6,opt,name=capability_membership_revision,json=capabilityMembershipRevision,proto3" json:"capability_membership_revision,omitempty"` // revision examined by the joiner, 0 lets leader bind current revision - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + NodeId uint64 `protobuf:"varint,1,opt,name=node_id,json=nodeId,proto3" json:"node_id,omitempty"` // Numeric ID for the new learner node + RaftAddress string `protobuf:"bytes,2,opt,name=raft_address,json=raftAddress,proto3" json:"raft_address,omitempty"` // Raft transport address of the learner + ServiceAddress string `protobuf:"bytes,3,opt,name=service_address,json=serviceAddress,proto3" json:"service_address,omitempty"` // Service API address of the learner + InstanceId []byte `protobuf:"bytes,4,opt,name=instance_id,json=instanceId,proto3" json:"instance_id,omitempty"` // 16-byte UUID identifying this peer's WAL/PVC incarnation (EN-1045); required + Capabilities []uint32 `protobuf:"fixed32,5,rep,packed,name=capabilities,proto3" json:"capabilities,omitempty"` // capabilities supported by this binary + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *JoinAsLearnerRequest) Reset() { @@ -248,13 +247,6 @@ func (x *JoinAsLearnerRequest) GetCapabilities() []uint32 { return nil } -func (x *JoinAsLearnerRequest) GetCapabilityMembershipRevision() uint64 { - if x != nil { - return x.CapabilityMembershipRevision - } - return 0 -} - type JoinAsLearnerResponse struct { state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields @@ -296,15 +288,14 @@ func (*JoinAsLearnerResponse) Descriptor() ([]byte, []int) { // It is used after a normal restart, where the node must not re-run the // learner-join path. type RefreshMembershipRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - NodeId uint64 `protobuf:"varint,1,opt,name=node_id,json=nodeId,proto3" json:"node_id,omitempty"` - RaftAddress string `protobuf:"bytes,2,opt,name=raft_address,json=raftAddress,proto3" json:"raft_address,omitempty"` - ServiceAddress string `protobuf:"bytes,3,opt,name=service_address,json=serviceAddress,proto3" json:"service_address,omitempty"` - InstanceId []byte `protobuf:"bytes,4,opt,name=instance_id,json=instanceId,proto3" json:"instance_id,omitempty"` - Capabilities []uint32 `protobuf:"fixed32,5,rep,packed,name=capabilities,proto3" json:"capabilities,omitempty"` - CapabilityMembershipRevision uint64 `protobuf:"fixed64,6,opt,name=capability_membership_revision,json=capabilityMembershipRevision,proto3" json:"capability_membership_revision,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + NodeId uint64 `protobuf:"varint,1,opt,name=node_id,json=nodeId,proto3" json:"node_id,omitempty"` + RaftAddress string `protobuf:"bytes,2,opt,name=raft_address,json=raftAddress,proto3" json:"raft_address,omitempty"` + ServiceAddress string `protobuf:"bytes,3,opt,name=service_address,json=serviceAddress,proto3" json:"service_address,omitempty"` + InstanceId []byte `protobuf:"bytes,4,opt,name=instance_id,json=instanceId,proto3" json:"instance_id,omitempty"` + Capabilities []uint32 `protobuf:"fixed32,5,rep,packed,name=capabilities,proto3" json:"capabilities,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *RefreshMembershipRequest) Reset() { @@ -372,13 +363,6 @@ func (x *RefreshMembershipRequest) GetCapabilities() []uint32 { return nil } -func (x *RefreshMembershipRequest) GetCapabilityMembershipRevision() uint64 { - if x != nil { - return x.CapabilityMembershipRevision - } - return 0 -} - type RefreshMembershipResponse struct { state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields @@ -428,24 +412,22 @@ const file_cluster_bootstrap_proto_rawDesc = "" + "\fraft_address\x18\x02 \x01(\tR\vraftAddress\x12'\n" + "\x0fservice_address\x18\x03 \x01(\tR\x0eserviceAddress\x12\x1f\n" + "\vinstance_id\x18\x04 \x01(\fR\n" + - "instanceId\"\x86\x02\n" + + "instanceId\"\xc0\x01\n" + "\x14JoinAsLearnerRequest\x12\x17\n" + "\anode_id\x18\x01 \x01(\x04R\x06nodeId\x12!\n" + "\fraft_address\x18\x02 \x01(\tR\vraftAddress\x12'\n" + "\x0fservice_address\x18\x03 \x01(\tR\x0eserviceAddress\x12\x1f\n" + "\vinstance_id\x18\x04 \x01(\fR\n" + "instanceId\x12\"\n" + - "\fcapabilities\x18\x05 \x03(\aR\fcapabilities\x12D\n" + - "\x1ecapability_membership_revision\x18\x06 \x01(\x06R\x1ccapabilityMembershipRevision\"\x17\n" + - "\x15JoinAsLearnerResponse\"\x8a\x02\n" + + "\fcapabilities\x18\x05 \x03(\aR\fcapabilities\"\x17\n" + + "\x15JoinAsLearnerResponse\"\xc4\x01\n" + "\x18RefreshMembershipRequest\x12\x17\n" + "\anode_id\x18\x01 \x01(\x04R\x06nodeId\x12!\n" + "\fraft_address\x18\x02 \x01(\tR\vraftAddress\x12'\n" + "\x0fservice_address\x18\x03 \x01(\tR\x0eserviceAddress\x12\x1f\n" + "\vinstance_id\x18\x04 \x01(\fR\n" + "instanceId\x12\"\n" + - "\fcapabilities\x18\x05 \x03(\aR\fcapabilities\x12D\n" + - "\x1ecapability_membership_revision\x18\x06 \x01(\x06R\x1ccapabilityMembershipRevision\"\x1b\n" + + "\fcapabilities\x18\x05 \x03(\aR\fcapabilities\"\x1b\n" + "\x19RefreshMembershipResponse2\xc2\x02\n" + "\x17ClusterBootstrapService\x12S\n" + "\bGetPeers\x12\".cluster_bootstrap.GetPeersRequest\x1a#.cluster_bootstrap.GetPeersResponse\x12b\n" + diff --git a/internal/proto/clusterbootstrappb/cluster_bootstrap_reader.pb.go b/internal/proto/clusterbootstrappb/cluster_bootstrap_reader.pb.go index ad2b4ec4b4..afb782846e 100644 --- a/internal/proto/clusterbootstrappb/cluster_bootstrap_reader.pb.go +++ b/internal/proto/clusterbootstrappb/cluster_bootstrap_reader.pb.go @@ -225,7 +225,6 @@ type JoinAsLearnerRequestReader interface { GetServiceAddress() string GetInstanceId() []byte GetCapabilities() []uint32 - GetCapabilityMembershipRevision() uint64 Mutate() *JoinAsLearnerRequest } @@ -251,10 +250,6 @@ func (r *joinAsLearnerRequestReadonly) GetCapabilities() []uint32 { return slices.Clone((*JoinAsLearnerRequest)(r).GetCapabilities()) } -func (r *joinAsLearnerRequestReadonly) GetCapabilityMembershipRevision() uint64 { - return (*JoinAsLearnerRequest)(r).GetCapabilityMembershipRevision() -} - func (r *joinAsLearnerRequestReadonly) Mutate() *JoinAsLearnerRequest { return (*JoinAsLearnerRequest)(r).CloneVT() } @@ -379,7 +374,6 @@ type RefreshMembershipRequestReader interface { GetServiceAddress() string GetInstanceId() []byte GetCapabilities() []uint32 - GetCapabilityMembershipRevision() uint64 Mutate() *RefreshMembershipRequest } @@ -405,10 +399,6 @@ func (r *refreshMembershipRequestReadonly) GetCapabilities() []uint32 { return slices.Clone((*RefreshMembershipRequest)(r).GetCapabilities()) } -func (r *refreshMembershipRequestReadonly) GetCapabilityMembershipRevision() uint64 { - return (*RefreshMembershipRequest)(r).GetCapabilityMembershipRevision() -} - func (r *refreshMembershipRequestReadonly) Mutate() *RefreshMembershipRequest { return (*RefreshMembershipRequest)(r).CloneVT() } diff --git a/internal/proto/clusterbootstrappb/cluster_bootstrap_vtproto.pb.go b/internal/proto/clusterbootstrappb/cluster_bootstrap_vtproto.pb.go index 10141d4484..547824d1ee 100644 --- a/internal/proto/clusterbootstrappb/cluster_bootstrap_vtproto.pb.go +++ b/internal/proto/clusterbootstrappb/cluster_bootstrap_vtproto.pb.go @@ -91,7 +91,6 @@ func (m *JoinAsLearnerRequest) CloneVT() *JoinAsLearnerRequest { r.NodeId = m.NodeId r.RaftAddress = m.RaftAddress r.ServiceAddress = m.ServiceAddress - r.CapabilityMembershipRevision = m.CapabilityMembershipRevision if rhs := m.InstanceId; rhs != nil { tmpBytes := make([]byte, len(rhs)) copy(tmpBytes, rhs) @@ -137,7 +136,6 @@ func (m *RefreshMembershipRequest) CloneVT() *RefreshMembershipRequest { r.NodeId = m.NodeId r.RaftAddress = m.RaftAddress r.ServiceAddress = m.ServiceAddress - r.CapabilityMembershipRevision = m.CapabilityMembershipRevision if rhs := m.InstanceId; rhs != nil { tmpBytes := make([]byte, len(rhs)) copy(tmpBytes, rhs) @@ -279,9 +277,6 @@ func (this *JoinAsLearnerRequest) EqualVT(that *JoinAsLearnerRequest) bool { return false } } - if this.CapabilityMembershipRevision != that.CapabilityMembershipRevision { - return false - } return string(this.unknownFields) == string(that.unknownFields) } @@ -335,9 +330,6 @@ func (this *RefreshMembershipRequest) EqualVT(that *RefreshMembershipRequest) bo return false } } - if this.CapabilityMembershipRevision != that.CapabilityMembershipRevision { - return false - } return string(this.unknownFields) == string(that.unknownFields) } @@ -531,12 +523,6 @@ func (m *JoinAsLearnerRequest) MarshalToSizedBufferVT(dAtA []byte) (int, error) i -= len(m.unknownFields) copy(dAtA[i:], m.unknownFields) } - if m.CapabilityMembershipRevision != 0 { - i -= 8 - binary.LittleEndian.PutUint64(dAtA[i:], uint64(m.CapabilityMembershipRevision)) - i-- - dAtA[i] = 0x31 - } if len(m.Capabilities) > 0 { for iNdEx := len(m.Capabilities) - 1; iNdEx >= 0; iNdEx-- { i -= 4 @@ -638,12 +624,6 @@ func (m *RefreshMembershipRequest) MarshalToSizedBufferVT(dAtA []byte) (int, err i -= len(m.unknownFields) copy(dAtA[i:], m.unknownFields) } - if m.CapabilityMembershipRevision != 0 { - i -= 8 - binary.LittleEndian.PutUint64(dAtA[i:], uint64(m.CapabilityMembershipRevision)) - i-- - dAtA[i] = 0x31 - } if len(m.Capabilities) > 0 { for iNdEx := len(m.Capabilities) - 1; iNdEx >= 0; iNdEx-- { i -= 4 @@ -790,9 +770,6 @@ func (m *JoinAsLearnerRequest) SizeVT() (n int) { if len(m.Capabilities) > 0 { n += 1 + protohelpers.SizeOfVarint(uint64(len(m.Capabilities)*4)) + len(m.Capabilities)*4 } - if m.CapabilityMembershipRevision != 0 { - n += 9 - } n += len(m.unknownFields) return n } @@ -831,9 +808,6 @@ func (m *RefreshMembershipRequest) SizeVT() (n int) { if len(m.Capabilities) > 0 { n += 1 + protohelpers.SizeOfVarint(uint64(len(m.Capabilities)*4)) + len(m.Capabilities)*4 } - if m.CapabilityMembershipRevision != 0 { - n += 9 - } n += len(m.unknownFields) return n } @@ -1350,16 +1324,6 @@ func (m *JoinAsLearnerRequest) UnmarshalVT(dAtA []byte) error { } else { return fmt.Errorf("proto: wrong wireType = %d for field Capabilities", wireType) } - case 6: - if wireType != 1 { - return fmt.Errorf("proto: wrong wireType = %d for field CapabilityMembershipRevision", wireType) - } - m.CapabilityMembershipRevision = 0 - if (iNdEx + 8) > l { - return io.ErrUnexpectedEOF - } - m.CapabilityMembershipRevision = uint64(binary.LittleEndian.Uint64(dAtA[iNdEx:])) - iNdEx += 8 default: iNdEx = preIndex skippy, err := protohelpers.Skip(dAtA[iNdEx:]) @@ -1631,16 +1595,6 @@ func (m *RefreshMembershipRequest) UnmarshalVT(dAtA []byte) error { } else { return fmt.Errorf("proto: wrong wireType = %d for field Capabilities", wireType) } - case 6: - if wireType != 1 { - return fmt.Errorf("proto: wrong wireType = %d for field CapabilityMembershipRevision", wireType) - } - m.CapabilityMembershipRevision = 0 - if (iNdEx + 8) > l { - return io.ErrUnexpectedEOF - } - m.CapabilityMembershipRevision = uint64(binary.LittleEndian.Uint64(dAtA[iNdEx:])) - iNdEx += 8 default: iNdEx = preIndex skippy, err := protohelpers.Skip(dAtA[iNdEx:]) diff --git a/internal/proto/commonpb/common_reader.pb.go b/internal/proto/commonpb/common_reader.pb.go index c3dfc18c29..8d3a2c7787 100644 --- a/internal/proto/commonpb/common_reader.pb.go +++ b/internal/proto/commonpb/common_reader.pb.go @@ -3524,7 +3524,6 @@ type ClusterPolicyReader interface { GetMetadataMaxCommandBytes() uint64 GetCapabilityActivationArmed() bool GetMinimumReplayProtocol() uint32 - GetCapabilityMembershipRevision() uint64 GetCapabilityActivations() CapabilityActivationListReader Mutate() *ClusterPolicy } @@ -3571,10 +3570,6 @@ func (r *clusterPolicyReadonly) GetMinimumReplayProtocol() uint32 { return (*ClusterPolicy)(r).GetMinimumReplayProtocol() } -func (r *clusterPolicyReadonly) GetCapabilityMembershipRevision() uint64 { - return (*ClusterPolicy)(r).GetCapabilityMembershipRevision() -} - func (r *clusterPolicyReadonly) GetCapabilityActivations() CapabilityActivationListReader { return NewCapabilityActivationListReader((*ClusterPolicy)(r).GetCapabilityActivations()) } diff --git a/internal/proto/commonpb/common_vtproto.pb.go b/internal/proto/commonpb/common_vtproto.pb.go index e12da78450..2cf0fd96f0 100644 --- a/internal/proto/commonpb/common_vtproto.pb.go +++ b/internal/proto/commonpb/common_vtproto.pb.go @@ -1275,7 +1275,6 @@ func (m *ClusterPolicy) CloneVT() *ClusterPolicy { r.MetadataMaxCommandBytes = m.MetadataMaxCommandBytes r.CapabilityActivationArmed = m.CapabilityActivationArmed r.MinimumReplayProtocol = m.MinimumReplayProtocol - r.CapabilityMembershipRevision = m.CapabilityMembershipRevision if rhs := m.CapabilityActivations; rhs != nil { tmpContainer := make([]*CapabilityActivation, len(rhs)) for k, v := range rhs { @@ -11118,9 +11117,6 @@ func (this *ClusterPolicy) EqualVT(that *ClusterPolicy) bool { if this.MinimumReplayProtocol != that.MinimumReplayProtocol { return false } - if this.CapabilityMembershipRevision != that.CapabilityMembershipRevision { - return false - } if len(this.CapabilityActivations) != len(that.CapabilityActivations) { return false } @@ -26572,12 +26568,6 @@ func (m *ClusterPolicy) MarshalToSizedBufferVT(dAtA []byte) (int, error) { dAtA[i] = 0x6a } } - if m.CapabilityMembershipRevision != 0 { - i -= 8 - binary.LittleEndian.PutUint64(dAtA[i:], uint64(m.CapabilityMembershipRevision)) - i-- - dAtA[i] = 0x61 - } if m.MinimumReplayProtocol != 0 { i = protohelpers.EncodeVarint(dAtA, i, uint64(m.MinimumReplayProtocol)) i-- @@ -46413,9 +46403,6 @@ func (m *ClusterPolicy) SizeVT() (n int) { if m.MinimumReplayProtocol != 0 { n += 1 + protohelpers.SizeOfVarint(uint64(m.MinimumReplayProtocol)) } - if m.CapabilityMembershipRevision != 0 { - n += 9 - } if len(m.CapabilityActivations) > 0 { for _, e := range m.CapabilityActivations { l = e.SizeVT() @@ -61576,16 +61563,6 @@ func (m *ClusterPolicy) UnmarshalVT(dAtA []byte) error { break } } - case 12: - if wireType != 1 { - return fmt.Errorf("proto: wrong wireType = %d for field CapabilityMembershipRevision", wireType) - } - m.CapabilityMembershipRevision = 0 - if (iNdEx + 8) > l { - return io.ErrUnexpectedEOF - } - m.CapabilityMembershipRevision = uint64(binary.LittleEndian.Uint64(dAtA[iNdEx:])) - iNdEx += 8 case 13: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field CapabilityActivations", wireType) diff --git a/internal/proto/raftcmdpb/raft_cmd.pb.go b/internal/proto/raftcmdpb/raft_cmd.pb.go index cb9b7847b5..c9e50c8a99 100644 --- a/internal/proto/raftcmdpb/raft_cmd.pb.go +++ b/internal/proto/raftcmdpb/raft_cmd.pb.go @@ -5584,10 +5584,9 @@ type PeerAddress struct { // Capability evidence is bound to this concrete (node_id, instance_id) // incarnation. It is persisted with the peer row so a restart cannot // accidentally reuse evidence from an older WAL/PVC incarnation. - Capabilities []uint32 `protobuf:"fixed32,5,rep,packed,name=capabilities,proto3" json:"capabilities,omitempty"` - CapabilityMembershipRevision uint64 `protobuf:"fixed64,6,opt,name=capability_membership_revision,json=capabilityMembershipRevision,proto3" json:"capability_membership_revision,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + Capabilities []uint32 `protobuf:"fixed32,5,rep,packed,name=capabilities,proto3" json:"capabilities,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *PeerAddress) Reset() { @@ -5655,13 +5654,6 @@ func (x *PeerAddress) GetCapabilities() []uint32 { return nil } -func (x *PeerAddress) GetCapabilityMembershipRevision() uint64 { - if x != nil { - return x.CapabilityMembershipRevision - } - return 0 -} - // RemovedMemberEntry is the persisted blacklist row keyed by // [ZoneClusterPersistent][SubGlobRemovedMembers][node_id BE 8][instance_id 16]. Written // by both the consensus and force removal paths, read by JoinAsLearner @@ -6168,15 +6160,14 @@ const file_raft_cmd_proto_rawDesc = "" + "\n" + "base_index\x18\x01 \x01(\x06R\tbaseIndex\"B\n" + "\x11CacheSnapshotMeta\x12-\n" + - "\x12current_generation\x18\x01 \x01(\x06R\x11currentGeneration\"\xfd\x01\n" + + "\x12current_generation\x18\x01 \x01(\x06R\x11currentGeneration\"\xb7\x01\n" + "\vPeerAddress\x12\x17\n" + "\anode_id\x18\x01 \x01(\x04R\x06nodeId\x12!\n" + "\fraft_address\x18\x02 \x01(\tR\vraftAddress\x12'\n" + "\x0fservice_address\x18\x03 \x01(\tR\x0eserviceAddress\x12\x1f\n" + "\vinstance_id\x18\x04 \x01(\fR\n" + "instanceId\x12\"\n" + - "\fcapabilities\x18\x05 \x03(\aR\fcapabilities\x12D\n" + - "\x1ecapability_membership_revision\x18\x06 \x01(\x06R\x1ccapabilityMembershipRevision\"\x85\x01\n" + + "\fcapabilities\x18\x05 \x03(\aR\fcapabilities\"\x85\x01\n" + "\x12RemovedMemberEntry\x12\x17\n" + "\anode_id\x18\x01 \x01(\x04R\x06nodeId\x12\x1f\n" + "\vinstance_id\x18\x02 \x01(\fR\n" + diff --git a/internal/proto/raftcmdpb/raft_cmd_reader.pb.go b/internal/proto/raftcmdpb/raft_cmd_reader.pb.go index 712a8a9719..89841e5195 100644 --- a/internal/proto/raftcmdpb/raft_cmd_reader.pb.go +++ b/internal/proto/raftcmdpb/raft_cmd_reader.pb.go @@ -5966,7 +5966,6 @@ type PeerAddressReader interface { GetServiceAddress() string GetInstanceId() []byte GetCapabilities() []uint32 - GetCapabilityMembershipRevision() uint64 Mutate() *PeerAddress } @@ -5992,10 +5991,6 @@ func (r *peerAddressReadonly) GetCapabilities() []uint32 { return slices.Clone((*PeerAddress)(r).GetCapabilities()) } -func (r *peerAddressReadonly) GetCapabilityMembershipRevision() uint64 { - return (*PeerAddress)(r).GetCapabilityMembershipRevision() -} - func (r *peerAddressReadonly) Mutate() *PeerAddress { return (*PeerAddress)(r).CloneVT() } diff --git a/internal/proto/raftcmdpb/raft_cmd_vtproto.pb.go b/internal/proto/raftcmdpb/raft_cmd_vtproto.pb.go index 5d2d0f5d57..76adedf891 100644 --- a/internal/proto/raftcmdpb/raft_cmd_vtproto.pb.go +++ b/internal/proto/raftcmdpb/raft_cmd_vtproto.pb.go @@ -2168,7 +2168,6 @@ func (m *PeerAddress) CloneVT() *PeerAddress { r.NodeId = m.NodeId r.RaftAddress = m.RaftAddress r.ServiceAddress = m.ServiceAddress - r.CapabilityMembershipRevision = m.CapabilityMembershipRevision if rhs := m.InstanceId; rhs != nil { tmpBytes := make([]byte, len(rhs)) copy(tmpBytes, rhs) @@ -5960,9 +5959,6 @@ func (this *PeerAddress) EqualVT(that *PeerAddress) bool { return false } } - if this.CapabilityMembershipRevision != that.CapabilityMembershipRevision { - return false - } return string(this.unknownFields) == string(that.unknownFields) } @@ -11487,12 +11483,6 @@ func (m *PeerAddress) MarshalToSizedBufferVT(dAtA []byte) (int, error) { i -= len(m.unknownFields) copy(dAtA[i:], m.unknownFields) } - if m.CapabilityMembershipRevision != 0 { - i -= 8 - binary.LittleEndian.PutUint64(dAtA[i:], uint64(m.CapabilityMembershipRevision)) - i-- - dAtA[i] = 0x31 - } if len(m.Capabilities) > 0 { for iNdEx := len(m.Capabilities) - 1; iNdEx >= 0; iNdEx-- { i -= 4 @@ -14240,9 +14230,6 @@ func (m *PeerAddress) SizeVT() (n int) { if len(m.Capabilities) > 0 { n += 1 + protohelpers.SizeOfVarint(uint64(len(m.Capabilities)*4)) + len(m.Capabilities)*4 } - if m.CapabilityMembershipRevision != 0 { - n += 9 - } n += len(m.unknownFields) return n } @@ -26700,16 +26687,6 @@ func (m *PeerAddress) UnmarshalVT(dAtA []byte) error { } else { return fmt.Errorf("proto: wrong wireType = %d for field Capabilities", wireType) } - case 6: - if wireType != 1 { - return fmt.Errorf("proto: wrong wireType = %d for field CapabilityMembershipRevision", wireType) - } - m.CapabilityMembershipRevision = 0 - if (iNdEx + 8) > l { - return io.ErrUnexpectedEOF - } - m.CapabilityMembershipRevision = uint64(binary.LittleEndian.Uint64(dAtA[iNdEx:])) - iNdEx += 8 default: iNdEx = preIndex skippy, err := protohelpers.Skip(dAtA[iNdEx:]) diff --git a/misc/proto/cluster_bootstrap.proto b/misc/proto/cluster_bootstrap.proto index 7a185771a3..c2b4483133 100644 --- a/misc/proto/cluster_bootstrap.proto +++ b/misc/proto/cluster_bootstrap.proto @@ -49,7 +49,6 @@ message JoinAsLearnerRequest { string service_address = 3; // Service API address of the learner bytes instance_id = 4; // 16-byte UUID identifying this peer's WAL/PVC incarnation (EN-1045); required repeated fixed32 capabilities = 5; // capabilities supported by this binary - fixed64 capability_membership_revision = 6; // revision examined by the joiner, 0 lets leader bind current revision } message JoinAsLearnerResponse {} @@ -64,7 +63,6 @@ message RefreshMembershipRequest { string service_address = 3; bytes instance_id = 4; repeated fixed32 capabilities = 5; - fixed64 capability_membership_revision = 6; } message RefreshMembershipResponse {} diff --git a/misc/proto/common.proto b/misc/proto/common.proto index c95a88de2f..823d5ae0ec 100644 --- a/misc/proto/common.proto +++ b/misc/proto/common.proto @@ -557,7 +557,6 @@ message ClusterPolicy { // the policy with their existing defaults. bool capability_activation_armed = 9; uint32 minimum_replay_protocol = 11; - fixed64 capability_membership_revision = 12; repeated CapabilityActivation capability_activations = 13; } diff --git a/misc/proto/raft_cmd.proto b/misc/proto/raft_cmd.proto index 21ddd3b12a..8cec3f05a6 100644 --- a/misc/proto/raft_cmd.proto +++ b/misc/proto/raft_cmd.proto @@ -843,7 +843,6 @@ message PeerAddress { // incarnation. It is persisted with the peer row so a restart cannot // accidentally reuse evidence from an older WAL/PVC incarnation. repeated fixed32 capabilities = 5; - fixed64 capability_membership_revision = 6; } // RemovedMemberEntry is the persisted blacklist row keyed by From a902528bf7a347cc38a65d8451b68647659e7fc8 Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Thu, 22 Oct 2026 15:22:33 +0200 Subject: [PATCH 012/127] refactor: remove unused replay protocol floor --- .../subsystems/consensus/README.md | 2 +- .../subsystems/consensus/capabilities.md | 11 +---- .../cluster_policy_reconciler_test.go | 26 ---------- internal/bootstrap/config_validation.go | 11 ----- internal/bootstrap/module.go | 5 -- .../processing/processor_cluster_policy.go | 15 ------ .../processor_cluster_policy_test.go | 49 ------------------- internal/infra/capabilities/capabilities.go | 42 +++------------- .../capabilities/capabilities_extra_test.go | 16 ------ .../infra/capabilities/capabilities_test.go | 1 - .../state/cluster_policy_recovery_test.go | 24 --------- internal/infra/state/decoded_entry.go | 32 ------------ internal/infra/state/decoded_entry_test.go | 29 ----------- internal/infra/state/recovery.go | 15 ------ internal/proto/commonpb/common_reader.pb.go | 5 -- internal/proto/commonpb/common_vtproto.pb.go | 31 ------------ misc/proto/common.proto | 1 - 17 files changed, 9 insertions(+), 306 deletions(-) diff --git a/docs/technical/architecture/subsystems/consensus/README.md b/docs/technical/architecture/subsystems/consensus/README.md index 432ce2c808..c3de13b9f5 100644 --- a/docs/technical/architecture/subsystems/consensus/README.md +++ b/docs/technical/architecture/subsystems/consensus/README.md @@ -13,7 +13,7 @@ The replication and ordering layer (`internal/infra/node`, `internal/infra/trans | [hybrid-logical-clock.md](hybrid-logical-clock.md) | Monotonic HLC timestamps across leader changes and clock skew. | | [removed-member-registry.md](removed-member-registry.md) | Universal member-identity invariant, committed-only peer routing, and replicated `(nodeID, instanceID)` set that prevents a removed member from silently rejoining and being auto-promoted; replay-safe per-event admission barriers, lookup costs, removal write ordering and durable restart states. | | [controlled-shutdown.md](controlled-shutdown.md) | Raft-ordered admission cutoff, local persistence confirmation, restart behavior, rolling-upgrade capability gate, and operator completion contract. | -| [capabilities.md](capabilities.md) | Stable capability declarations, persisted activation policy and replay floor, membership evidence, and mixed-version rollout boundaries. | +| [capabilities.md](capabilities.md) | Stable capability declarations, persisted activation policy, membership evidence, and mixed-version rollout boundaries. | ## Related diff --git a/docs/technical/architecture/subsystems/consensus/capabilities.md b/docs/technical/architecture/subsystems/consensus/capabilities.md index 0d62e9b4f7..09428f99eb 100644 --- a/docs/technical/architecture/subsystems/consensus/capabilities.md +++ b/docs/technical/architecture/subsystems/consensus/capabilities.md @@ -5,8 +5,7 @@ rolling upgrade. It is separate from the client-facing gRPC protocol revision. ## Wire contract -`common.ClusterPolicy` carries the replicated activation state and a monotonic -minimum replay protocol. +`common.ClusterPolicy` carries the replicated activation state. `raft.Proposal.required_capabilities` carries stable producer-owned capability IDs. The fields are optional and therefore backward-readable by binaries that predate this foundation. IDs are never reused; semantic dependencies must be @@ -31,18 +30,12 @@ capability is active and every member advertises it. Proposals with no declared capability remain unaffected. FSM apply never consults node-local capability flags. -The replay floor is monotonic, is bounded by the local replay protocol during -policy apply, and is checked immediately after decoding an entry or snapshot -that could require a newer protocol. This floor is intentionally not -`pkg/grpcprotocol.Version`. - ## Rollout and recovery Deploy barrier-aware binaries to every eligible member before arming an incompatible feature. An old leader that does not understand the barrier cannot be made safe by upgrading only the other nodes. Restart, snapshot install, -restore, and downgrade code must preserve the policy and floor; a downgrade -below the committed floor is refused. Membership changes racing activation are +restore, and downgrade code must preserve the policy. Membership changes racing activation are serialized at the Raft proposal boundary and must be revalidated immediately before `rawNode.Propose`, because `Node.Propose` queues work. Removing a member remains allowed; adding, promoting, or refreshing one without all active diff --git a/internal/bootstrap/cluster_policy_reconciler_test.go b/internal/bootstrap/cluster_policy_reconciler_test.go index a7b8b9f05e..94187306c4 100644 --- a/internal/bootstrap/cluster_policy_reconciler_test.go +++ b/internal/bootstrap/cluster_policy_reconciler_test.go @@ -14,7 +14,6 @@ import ( logging "github.com/formancehq/go-libs/v5/pkg/observe/log" "github.com/formancehq/ledger/v3/internal/domain" - "github.com/formancehq/ledger/v3/internal/infra/capabilities" "github.com/formancehq/ledger/v3/internal/infra/state" "github.com/formancehq/ledger/v3/internal/proto/commonpb" "github.com/formancehq/ledger/v3/internal/proto/servicepb" @@ -175,29 +174,4 @@ func TestReconcileClusterPolicy(t *testing.T) { require.Zero(t, admission.callCount(), "a newer applied revision must not be overwritten") }) - - t.Run("preserves the committed replay floor on a higher revision", func(t *testing.T) { - t.Parallel() - - store := newReconcilerTestStore(t) - cfg := validBaseConfig() - cfg.ClusterPolicyRevision++ - - batch := store.OpenWriteSession() - require.NoError(t, state.SaveClusterPolicy(batch, &commonpb.ClusterPolicy{ - Revision: cfg.ClusterPolicyRevision - 1, - QueryCheckpointLimit: cfg.QueryCheckpointLimit, - MinimumReplayProtocol: capabilities.CurrentReplayProtocol, - })) - require.NoError(t, batch.Commit()) - - admission := &fakeAdmission{} - reconcileClusterPolicy(context.Background(), admission, store, cfg, func() bool { return true }, logging.Testing()) - - require.Equal(t, 1, admission.callCount()) - admission.mu.Lock() - gotFloor := admission.lastPolicy.GetMinimumReplayProtocol() - admission.mu.Unlock() - require.Equal(t, capabilities.CurrentReplayProtocol, gotFloor) - }) } diff --git a/internal/bootstrap/config_validation.go b/internal/bootstrap/config_validation.go index 0346bbc6d8..6f18a7f14f 100644 --- a/internal/bootstrap/config_validation.go +++ b/internal/bootstrap/config_validation.go @@ -8,7 +8,6 @@ import ( logging "github.com/formancehq/go-libs/v5/pkg/observe/log" "github.com/formancehq/ledger/v3/internal/domain" - "github.com/formancehq/ledger/v3/internal/infra/capabilities" "github.com/formancehq/ledger/v3/internal/proto/commonpb" "github.com/formancehq/ledger/v3/internal/query" "github.com/formancehq/ledger/v3/internal/storage/dal" @@ -168,16 +167,6 @@ func validateCommittedMetadataLimits(store *dal.Store, cfg Config) error { return fmt.Errorf("reading committed cluster policy: %w", err) } - // The replay floor is checked during bootstrap, before the node opens a - // state machine or decodes historical entries. A binary that cannot execute - // the committed protocol must fail closed even when identity validation is - // run with --unsafe-skip-config-validation. - if applied != nil { - if err := capabilities.ValidateReplayFloor(applied.GetMinimumReplayProtocol(), capabilities.CurrentReplayProtocol); err != nil { - return fmt.Errorf("committed cluster policy requires an unsupported replay protocol: %w", err) - } - } - // No policy committed yet: the leader's reconciler commits the first one, // and admission holds business writes until it does. if applied.GetRevision() == 0 { diff --git a/internal/bootstrap/module.go b/internal/bootstrap/module.go index 9340bb1427..147a0da4b7 100644 --- a/internal/bootstrap/module.go +++ b/internal/bootstrap/module.go @@ -1739,11 +1739,6 @@ func reconcileClusterPolicy(ctx context.Context, admission ctrl.Admission, store MetadataMaxValueBytes: cfg.MetadataMaxValueBytes, MetadataMaxEntityBytes: cfg.MetadataMaxEntityBytes, MetadataMaxCommandBytes: cfg.MetadataMaxCommandBytes, - // The replay floor is monotonic and is not a node-local config default. - // Preserve the committed value when the reconciler proposes a higher - // policy revision; otherwise every ordinary config reconciliation would - // accidentally attempt to lower the floor back to zero. - MinimumReplayProtocol: applied.GetMinimumReplayProtocol(), } appliedRev := applied.GetRevision() diff --git a/internal/domain/processing/processor_cluster_policy.go b/internal/domain/processing/processor_cluster_policy.go index d8b5aec708..8e84bba187 100644 --- a/internal/domain/processing/processor_cluster_policy.go +++ b/internal/domain/processing/processor_cluster_policy.go @@ -4,7 +4,6 @@ import ( "fmt" "github.com/formancehq/ledger/v3/internal/domain" - "github.com/formancehq/ledger/v3/internal/infra/capabilities" "github.com/formancehq/ledger/v3/internal/proto/commonpb" "github.com/formancehq/ledger/v3/internal/proto/raftcmdpb" ) @@ -70,20 +69,6 @@ func applyClusterPolicy(newPolicy *commonpb.ClusterPolicy, scope Scope) (*common } } - // A policy may only raise the floor to a replay protocol this binary can - // execute. Rejecting it at the FSM boundary prevents a committed policy - // from making the current node unable to replay its own state after restart. - if err := capabilities.ValidateReplayFloor(newPolicy.GetMinimumReplayProtocol(), capabilities.CurrentReplayProtocol); err != nil { - return nil, &domain.ErrClusterPolicyInvalid{Detail: err.Error()} - } - - // The replay floor is a durable safety boundary, not a revision-scoped - // setting that can be rolled back by a later policy. Validate it before the - // revision switch so every replica rejects a downgrade deterministically. - if _, err := capabilities.AdvanceReplayFloor(current.GetMinimumReplayProtocol(), newPolicy.GetMinimumReplayProtocol()); err != nil { - return nil, &domain.ErrClusterPolicyInvalid{Detail: err.Error()} - } - switch { case newRev > appliedRev: scope.SetClusterPolicy(newPolicy) diff --git a/internal/domain/processing/processor_cluster_policy_test.go b/internal/domain/processing/processor_cluster_policy_test.go index 4fe1c98060..d6eccf0688 100644 --- a/internal/domain/processing/processor_cluster_policy_test.go +++ b/internal/domain/processing/processor_cluster_policy_test.go @@ -7,7 +7,6 @@ import ( "go.uber.org/mock/gomock" "github.com/formancehq/ledger/v3/internal/domain" - "github.com/formancehq/ledger/v3/internal/infra/capabilities" "github.com/formancehq/ledger/v3/internal/proto/commonpb" "github.com/formancehq/ledger/v3/internal/proto/raftcmdpb" ) @@ -102,54 +101,6 @@ func TestProcessSetClusterPolicy_LowerRevisionStale(t *testing.T) { require.Equal(t, uint64(5), stale.AppliedRevision) } -// A higher policy revision cannot lower the replay floor. The floor protects -// restart and restore paths, so accepting a rollback here would let a later -// policy make an old binary appear safe again. -func TestProcessSetClusterPolicy_ReplayFloorCannotDecrease(t *testing.T) { - t.Parallel() - - ctrl := gomock.NewController(t) - mockStore := NewMockScope(ctrl) - processor, err := NewRequestProcessor(nil, 0) - require.NoError(t, err) - - current := withMetadataLimits(&commonpb.ClusterPolicy{Revision: 2, MinimumReplayProtocol: capabilities.CurrentReplayProtocol, QueryCheckpointLimit: 1}) - mockStore.EXPECT().GetClusterPolicy().Return(current) - - proposed := withMetadataLimits(&commonpb.ClusterPolicy{Revision: 3, MinimumReplayProtocol: capabilities.CurrentReplayProtocol - 1, QueryCheckpointLimit: 1}) - _, procErr := processor.ProcessOrder(clusterPolicyOrder(proposed), mockStore) - - var invalid *domain.ErrClusterPolicyInvalid - require.ErrorAs(t, procErr, &invalid) - require.Contains(t, invalid.Detail, "replay protocol floor cannot decrease") -} - -// A policy cannot commit a replay floor newer than the protocol implemented -// by this binary: otherwise the node would accept the entry and then be unable -// to replay its own store after restart. -func TestProcessSetClusterPolicy_ReplayFloorCannotExceedLocalProtocol(t *testing.T) { - t.Parallel() - - ctrl := gomock.NewController(t) - mockStore := NewMockScope(ctrl) - processor, err := NewRequestProcessor(nil, 0) - require.NoError(t, err) - - current := withMetadataLimits(&commonpb.ClusterPolicy{Revision: 2, QueryCheckpointLimit: 1}) - mockStore.EXPECT().GetClusterPolicy().Return(current) - - proposed := withMetadataLimits(&commonpb.ClusterPolicy{ - Revision: 3, - MinimumReplayProtocol: capabilities.CurrentReplayProtocol + 1, - QueryCheckpointLimit: 1, - }) - _, procErr := processor.ProcessOrder(clusterPolicyOrder(proposed), mockStore) - - var invalid *domain.ErrClusterPolicyInvalid - require.ErrorAs(t, procErr, &invalid) - require.Contains(t, invalid.Detail, "persisted floor") -} - func TestProcessSetClusterPolicy_CapabilityActivationIsMonotonic(t *testing.T) { t.Parallel() diff --git a/internal/infra/capabilities/capabilities.go b/internal/infra/capabilities/capabilities.go index ae92d7787f..ba09544c48 100644 --- a/internal/infra/capabilities/capabilities.go +++ b/internal/infra/capabilities/capabilities.go @@ -23,13 +23,6 @@ func SupportedIDs() []ID { return nil } -// CurrentReplayProtocol is the replay encoding understood by this binary. -// It is intentionally independent from the client-facing gRPC protocol and -// from the capability IDs above. Once a policy raises its replay floor, a -// binary whose supported floor is lower must refuse to open the store rather -// than decode state it cannot execute deterministically. -const CurrentReplayProtocol uint32 = 1 - type MemberRole uint8 const ( @@ -46,9 +39,8 @@ type Member struct { } type State struct { - Armed bool - MinimumReplay uint32 - Activations map[ID]bool + Armed bool + Activations map[ID]bool } type Blocker struct { @@ -79,20 +71,11 @@ func ValidateMembers(members []Member) error { return nil } -func AdvanceReplayFloor(current, next uint32) (uint32, error) { - if next < current { - return current, fmt.Errorf("%w: replay protocol floor cannot decrease from %d to %d", ErrInvalidState, current, next) - } - - return next, nil -} - var ( - ErrInvalidState = errors.New("invalid capability state") - ErrInvalidDeclaration = errors.New("invalid capability declaration") - ErrMissingCapability = errors.New("proposal is missing a required capability declaration") - ErrCapabilityUnsupported = errors.New("capability is unsupported by a cluster member") - ErrReplayProtocolUnsupported = errors.New("replay protocol floor is unsupported") + ErrInvalidState = errors.New("invalid capability state") + ErrInvalidDeclaration = errors.New("invalid capability declaration") + ErrMissingCapability = errors.New("proposal is missing a required capability declaration") + ErrCapabilityUnsupported = errors.New("capability is unsupported by a cluster member") ) // RequiredCapabilities derives semantic protocol dependencies from the final @@ -125,18 +108,6 @@ func StampProposal(proposal *raftcmdpb.Proposal) error { return nil } -// ValidateReplayFloor checks the persisted replay floor before any other -// replicated state is decoded. A zero floor is the legacy policy and is -// accepted by every binary; newer floors are safe only when this binary knows -// how to decode and execute the corresponding entries. -func ValidateReplayFloor(required, supported uint32) error { - if required > supported { - return fmt.Errorf("%w: persisted floor %d exceeds supported protocol %d", ErrReplayProtocolUnsupported, required, supported) - } - - return nil -} - func (s State) Validate() error { if !s.Armed { return nil @@ -151,7 +122,6 @@ func FromPolicy(policy *commonpb.ClusterPolicy) State { return s } s.Armed = policy.GetCapabilityActivationArmed() - s.MinimumReplay = policy.GetMinimumReplayProtocol() for _, activation := range policy.GetCapabilityActivations() { if activation != nil { s.Activations[ID(activation.GetId())] = activation.GetActive() diff --git a/internal/infra/capabilities/capabilities_extra_test.go b/internal/infra/capabilities/capabilities_extra_test.go index 1eb3fc1ac6..b0c6df293d 100644 --- a/internal/infra/capabilities/capabilities_extra_test.go +++ b/internal/infra/capabilities/capabilities_extra_test.go @@ -10,19 +10,3 @@ func TestIncarnationValidation(t *testing.T) { require.Error(t, ValidateMembers([]Member{{NodeID: 4, InstanceID: []byte("a"), Role: Voter}, {NodeID: 4, InstanceID: []byte("b"), Role: Learner}})) require.NoError(t, ValidateMembers([]Member{{NodeID: 4, InstanceID: []byte("a"), Role: Voter}, {NodeID: 5, InstanceID: []byte("b"), Role: Learner}})) } - -func TestReplayFloorIsMonotonic(t *testing.T) { - floor, err := AdvanceReplayFloor(3, 4) - require.NoError(t, err) - require.Equal(t, uint32(4), floor) - _, err = AdvanceReplayFloor(floor, 2) - require.Error(t, err) -} - -func TestValidateReplayFloorRejectsUnsupportedDowngrade(t *testing.T) { - require.NoError(t, ValidateReplayFloor(0, 0)) - require.NoError(t, ValidateReplayFloor(CurrentReplayProtocol, CurrentReplayProtocol)) - - err := ValidateReplayFloor(CurrentReplayProtocol+1, CurrentReplayProtocol) - require.ErrorIs(t, err, ErrReplayProtocolUnsupported) -} diff --git a/internal/infra/capabilities/capabilities_test.go b/internal/infra/capabilities/capabilities_test.go index ef2a289bda..a1d07303e5 100644 --- a/internal/infra/capabilities/capabilities_test.go +++ b/internal/infra/capabilities/capabilities_test.go @@ -44,7 +44,6 @@ func TestStampProposalClearsFutureDeclarations(t *testing.T) { func TestFromPolicyRoundTrip(t *testing.T) { state := FromPolicy(&commonpb.ClusterPolicy{ CapabilityActivationArmed: true, - MinimumReplayProtocol: 4, CapabilityActivations: []*commonpb.CapabilityActivation{{Id: 99, Active: true}}, }) require.True(t, state.Armed) diff --git a/internal/infra/state/cluster_policy_recovery_test.go b/internal/infra/state/cluster_policy_recovery_test.go index ce9291ee71..f85dd11499 100644 --- a/internal/infra/state/cluster_policy_recovery_test.go +++ b/internal/infra/state/cluster_policy_recovery_test.go @@ -5,7 +5,6 @@ import ( "github.com/stretchr/testify/require" - "github.com/formancehq/ledger/v3/internal/infra/capabilities" "github.com/formancehq/ledger/v3/internal/proto/commonpb" ) @@ -33,29 +32,6 @@ func TestRecoverState_ClusterPolicyRoundtrip(t *testing.T) { "the persisted policy must be recovered verbatim into FSMState") } -// Recovery must inspect the persisted policy floor before hydrating the rest -// of FSMState. This models a downgrade opening a store written by a newer -// replay protocol and proves that state remains untouched on refusal. -func TestRecoverState_RejectsUnsupportedReplayFloorBeforeHydration(t *testing.T) { - t.Parallel() - - machine, store, _ := newTestMachine(t) - stateBefore := machine.State - - policy := &commonpb.ClusterPolicy{ - Revision: 4, - MinimumReplayProtocol: capabilities.CurrentReplayProtocol + 1, - QueryCheckpointLimit: 1, - } - batch := store.OpenWriteSession() - require.NoError(t, SaveClusterPolicy(batch, policy)) - require.NoError(t, batch.Commit()) - - err := NewRecovery(machine, store).RecoverState() - require.ErrorIs(t, err, capabilities.ErrReplayProtocolUnsupported) - require.Same(t, stateBefore, machine.State) -} - // NewFSMState seeds a non-nil revision-0 default so the apply path and the // readiness gate can read the policy before any is committed. func TestNewFSMState_ClusterPolicyDefault(t *testing.T) { diff --git a/internal/infra/state/decoded_entry.go b/internal/infra/state/decoded_entry.go index c9a5789303..46768f4189 100644 --- a/internal/infra/state/decoded_entry.go +++ b/internal/infra/state/decoded_entry.go @@ -5,7 +5,6 @@ import ( "go.etcd.io/raft/v3/raftpb" - "github.com/formancehq/ledger/v3/internal/infra/capabilities" "github.com/formancehq/ledger/v3/internal/proto/raftcmdpb" ) @@ -59,43 +58,12 @@ func DecodeEntries(entries []*raftpb.Entry) ([]DecodedEntry, error) { return nil, fmt.Errorf("unmarshaling proposal at raft index %d: %w", entries[i].GetIndex(), err) } - if err := validateReplayFloor(cmd); err != nil { - cmd.ReturnToVTPool() - ReleaseDecodedEntries(decoded[:i]) - - return nil, fmt.Errorf("validating proposal replay floor at raft index %d: %w", entries[i].GetIndex(), err) - } - decoded[i].Proposal = cmd } return decoded, nil } -// validateReplayFloor rejects a policy entry that this binary could commit -// but cannot later replay. It runs immediately after wire decoding, before the -// entry reaches checkpoint detection or FSM apply, so the unsupported floor -// cannot cause any partial state mutation. -func validateReplayFloor(proposal *raftcmdpb.Proposal) error { - for _, order := range proposal.GetOrders() { - policyOrder := order.GetSystemScoped().GetSetClusterPolicy() - if policyOrder == nil { - continue - } - - policy := policyOrder.GetPolicy() - if policy == nil { - continue - } - - if err := capabilities.ValidateReplayFloor(policy.GetMinimumReplayProtocol(), capabilities.CurrentReplayProtocol); err != nil { - return err - } - } - - return nil -} - // ReleaseDecodedEntries returns every non-nil Proposal in decoded back to // the VT pool. Safe to call with a nil or empty slice. After this call no // DecodedEntry.Proposal pointer in the slice may be accessed. diff --git a/internal/infra/state/decoded_entry_test.go b/internal/infra/state/decoded_entry_test.go index 58c031079d..ea9d5736c4 100644 --- a/internal/infra/state/decoded_entry_test.go +++ b/internal/infra/state/decoded_entry_test.go @@ -6,8 +6,6 @@ import ( "github.com/stretchr/testify/require" "go.etcd.io/raft/v3/raftpb" - "github.com/formancehq/ledger/v3/internal/infra/capabilities" - "github.com/formancehq/ledger/v3/internal/proto/commonpb" "github.com/formancehq/ledger/v3/internal/proto/raftcmdpb" ) @@ -130,33 +128,6 @@ func TestDecodeEntries(t *testing.T) { }) } -func TestDecodeEntriesRejectsUnsupportedReplayFloor(t *testing.T) { - t.Parallel() - - cmd := &raftcmdpb.Proposal{Orders: []*raftcmdpb.Order{ - { - Type: &raftcmdpb.Order_SystemScoped{ - SystemScoped: &raftcmdpb.SystemScopedOrder{ - Payload: &raftcmdpb.SystemScopedOrder_SetClusterPolicy{ - SetClusterPolicy: &raftcmdpb.SetClusterPolicyOrder{ - Policy: &commonpb.ClusterPolicy{ - Revision: 1, - MinimumReplayProtocol: capabilities.CurrentReplayProtocol + 1, - }, - }, - }, - }, - }, - }, - }} - data, err := cmd.MarshalVT() - require.NoError(t, err) - - decoded, err := DecodeEntries([]*raftpb.Entry{entWith(42, 1, raftpb.EntryNormal, data)}) - require.ErrorIs(t, err, capabilities.ErrReplayProtocolUnsupported) - require.Nil(t, decoded) -} - func TestReleaseDecodedEntries(t *testing.T) { t.Parallel() diff --git a/internal/infra/state/recovery.go b/internal/infra/state/recovery.go index 1fd379c8ba..6796098a9a 100644 --- a/internal/infra/state/recovery.go +++ b/internal/infra/state/recovery.go @@ -3,7 +3,6 @@ package state import ( "fmt" - "github.com/formancehq/ledger/v3/internal/infra/capabilities" "github.com/formancehq/ledger/v3/internal/query" "github.com/formancehq/ledger/v3/internal/storage/dal" ) @@ -55,20 +54,6 @@ func (r *Recovery) RecoverState() error { defer func() { _ = handle.Close() }() - // Read and validate the policy before hydrating any other FSM field. The - // policy is the only durable record that carries the replay floor, so this - // preflight is the recovery boundary that prevents a downgraded binary from - // decoding or executing state written by a newer replay protocol. - policy, err := query.ReadClusterPolicy(r.reader) - if err != nil { - return fmt.Errorf("reading cluster policy replay floor: %w", err) - } - if policy != nil { - if err := capabilities.ValidateReplayFloor(policy.GetMinimumReplayProtocol(), capabilities.CurrentReplayProtocol); err != nil { - return fmt.Errorf("recovery refused by replay protocol floor: %w", err) - } - } - // Phase 1: load a fresh FSMState in its entirety. Any error returns // before we touch the Machine. newState, err := LoadFSMStateFromStore(r.reader, handle) diff --git a/internal/proto/commonpb/common_reader.pb.go b/internal/proto/commonpb/common_reader.pb.go index 8d3a2c7787..f22e69a515 100644 --- a/internal/proto/commonpb/common_reader.pb.go +++ b/internal/proto/commonpb/common_reader.pb.go @@ -3523,7 +3523,6 @@ type ClusterPolicyReader interface { GetMetadataMaxEntityBytes() uint64 GetMetadataMaxCommandBytes() uint64 GetCapabilityActivationArmed() bool - GetMinimumReplayProtocol() uint32 GetCapabilityActivations() CapabilityActivationListReader Mutate() *ClusterPolicy } @@ -3566,10 +3565,6 @@ func (r *clusterPolicyReadonly) GetCapabilityActivationArmed() bool { return (*ClusterPolicy)(r).GetCapabilityActivationArmed() } -func (r *clusterPolicyReadonly) GetMinimumReplayProtocol() uint32 { - return (*ClusterPolicy)(r).GetMinimumReplayProtocol() -} - func (r *clusterPolicyReadonly) GetCapabilityActivations() CapabilityActivationListReader { return NewCapabilityActivationListReader((*ClusterPolicy)(r).GetCapabilityActivations()) } diff --git a/internal/proto/commonpb/common_vtproto.pb.go b/internal/proto/commonpb/common_vtproto.pb.go index 2cf0fd96f0..2c8a21a34c 100644 --- a/internal/proto/commonpb/common_vtproto.pb.go +++ b/internal/proto/commonpb/common_vtproto.pb.go @@ -1274,7 +1274,6 @@ func (m *ClusterPolicy) CloneVT() *ClusterPolicy { r.MetadataMaxEntityBytes = m.MetadataMaxEntityBytes r.MetadataMaxCommandBytes = m.MetadataMaxCommandBytes r.CapabilityActivationArmed = m.CapabilityActivationArmed - r.MinimumReplayProtocol = m.MinimumReplayProtocol if rhs := m.CapabilityActivations; rhs != nil { tmpContainer := make([]*CapabilityActivation, len(rhs)) for k, v := range rhs { @@ -11114,9 +11113,6 @@ func (this *ClusterPolicy) EqualVT(that *ClusterPolicy) bool { if this.CapabilityActivationArmed != that.CapabilityActivationArmed { return false } - if this.MinimumReplayProtocol != that.MinimumReplayProtocol { - return false - } if len(this.CapabilityActivations) != len(that.CapabilityActivations) { return false } @@ -26568,11 +26564,6 @@ func (m *ClusterPolicy) MarshalToSizedBufferVT(dAtA []byte) (int, error) { dAtA[i] = 0x6a } } - if m.MinimumReplayProtocol != 0 { - i = protohelpers.EncodeVarint(dAtA, i, uint64(m.MinimumReplayProtocol)) - i-- - dAtA[i] = 0x58 - } if m.CapabilityActivationArmed { i-- if m.CapabilityActivationArmed { @@ -46400,9 +46391,6 @@ func (m *ClusterPolicy) SizeVT() (n int) { if m.CapabilityActivationArmed { n += 2 } - if m.MinimumReplayProtocol != 0 { - n += 1 + protohelpers.SizeOfVarint(uint64(m.MinimumReplayProtocol)) - } if len(m.CapabilityActivations) > 0 { for _, e := range m.CapabilityActivations { l = e.SizeVT() @@ -61544,25 +61532,6 @@ func (m *ClusterPolicy) UnmarshalVT(dAtA []byte) error { } } m.CapabilityActivationArmed = bool(v != 0) - case 11: - if wireType != 0 { - return fmt.Errorf("proto: wrong wireType = %d for field MinimumReplayProtocol", wireType) - } - m.MinimumReplayProtocol = 0 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return protohelpers.ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - m.MinimumReplayProtocol |= uint32(b&0x7F) << shift - if b < 0x80 { - break - } - } case 13: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field CapabilityActivations", wireType) diff --git a/misc/proto/common.proto b/misc/proto/common.proto index 823d5ae0ec..1b3494e120 100644 --- a/misc/proto/common.proto +++ b/misc/proto/common.proto @@ -556,7 +556,6 @@ message ClusterPolicy { // fields are optional so nodes that predate the capability foundation decode // the policy with their existing defaults. bool capability_activation_armed = 9; - uint32 minimum_replay_protocol = 11; repeated CapabilityActivation capability_activations = 13; } From 90aeeb611303a46feddd1587a78163a0b142b89f Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Thu, 22 Oct 2026 16:09:57 +0200 Subject: [PATCH 013/127] fix: close capability admission races --- internal/bootstrap/module.go | 35 +++++++++------- .../membership/confchange_identity_test.go | 30 ++++++++++++++ internal/infra/membership/membership.go | 41 ++++++++++++++++--- internal/infra/state/machine.go | 17 +++++++- 4 files changed, 102 insertions(+), 21 deletions(-) diff --git a/internal/bootstrap/module.go b/internal/bootstrap/module.go index 147a0da4b7..e1dd729962 100644 --- a/internal/bootstrap/module.go +++ b/internal/bootstrap/module.go @@ -309,6 +309,27 @@ func Module() fx.Option { return nil, err } + publishCapabilityGate := func(policy *commonpb.ClusterPolicy) { + required := make([]uint32, 0, len(policy.GetCapabilityActivations())) + for _, activation := range policy.GetCapabilityActivations() { + if activation != nil && activation.GetActive() { + required = append(required, activation.GetId()) + } + } + slices.Sort(required) + required = slices.Compact(required) + if err := membership.SetCapabilityGate(policy.GetCapabilityActivationArmed(), required); err != nil { + logger.WithFields(map[string]any{"error": err}).Errorf("Invalid capability gate in committed cluster policy") + } + } + membership.SetCapabilityPolicyProvider(func() *commonpb.ClusterPolicy { + return m.State.ClusterPolicy + }) + m.SetCapabilityPolicyCommitHandler(publishCapabilityGate) + if m.State.ClusterPolicy != nil { + publishCapabilityGate(m.State.ClusterPolicy) + } + logger.WithFields(map[string]any{ "duration": time.Since(machineStart).String(), }).Infof("FSM Machine created") @@ -648,20 +669,6 @@ func Module() fx.Option { if !raftNode.IsLeader() { return } - if policy, err := query.ReadClusterPolicy(store); err == nil { - required := make([]uint32, 0, len(policy.GetCapabilityActivations())) - for _, activation := range policy.GetCapabilityActivations() { - if activation != nil && activation.GetActive() { - required = append(required, activation.GetId()) - } - } - slices.Sort(required) - if err := raftNode.SetCapabilityGate(policy.GetCapabilityActivationArmed(), required); err != nil { - logger.WithFields(map[string]any{"error": err}).Errorf("Invalid capability gate in committed cluster policy") - - return - } - } if err := proposeAuditKeyIfNeeded(ctx, raftNode, builder, store); err != nil { logger.WithFields(map[string]any{"error": err}).Errorf("Audit key initialization failed; will retry") diff --git a/internal/infra/membership/confchange_identity_test.go b/internal/infra/membership/confchange_identity_test.go index 4119d71f71..6b78d4e03f 100644 --- a/internal/infra/membership/confchange_identity_test.go +++ b/internal/infra/membership/confchange_identity_test.go @@ -6,6 +6,8 @@ import ( "github.com/stretchr/testify/require" "go.etcd.io/raft/v3/raftpb" "google.golang.org/protobuf/proto" + + "github.com/formancehq/ledger/v3/internal/proto/commonpb" ) func TestValidateCapabilityEvidenceRequiresActiveCapabilities(t *testing.T) { @@ -20,6 +22,34 @@ func TestValidateCapabilityEvidenceRequiresActiveCapabilities(t *testing.T) { require.NoError(t, ValidateCapabilityEvidence(&ConfChangeContext{InstanceID: bytesN(0x42)}, false, nil)) } +func TestWriteConfChangeRejectsCapabilitiesMissingFromCommittedPolicy(t *testing.T) { + m := newTestMembership(t) + m.SetCapabilityPolicyProvider(func() *commonpb.ClusterPolicy { + return &commonpb.ClusterPolicy{ + CapabilityActivationArmed: true, + CapabilityActivations: []*commonpb.CapabilityActivation{{Id: 7, Active: true}}, + } + }) + + data, err := MarshalConfChangeContext(ConfChangeContext{ + RaftAddress: "peer:7777", + ServiceAddress: "peer:8888", + InstanceID: fixedInstanceID(1), + }) + require.NoError(t, err) + cc := &raftpb.ConfChangeV2{ + Changes: []*raftpb.ConfChangeSingle{{Type: new(raftpb.ConfChangeAddLearnerNode), NodeId: proto.Uint64(9)}}, + Context: data, + } + encoded, err := proto.Marshal(cc) + require.NoError(t, err) + + session := m.store.OpenWriteSession() + err = m.WriteConfChange(&raftpb.Entry{Type: new(raftpb.EntryConfChangeV2), Data: encoded}, session) + require.ErrorContains(t, err, "capability 7 is required") + require.NoError(t, session.Commit()) +} + func TestWriteConfChangeRejectsIncompleteRegistration(t *testing.T) { t.Parallel() for _, kind := range []raftpb.ConfChangeType{raftpb.ConfChangeAddNode, raftpb.ConfChangeAddLearnerNode, raftpb.ConfChangeUpdateNode} { diff --git a/internal/infra/membership/membership.go b/internal/infra/membership/membership.go index 010f72f3af..fe82c0a776 100644 --- a/internal/infra/membership/membership.go +++ b/internal/infra/membership/membership.go @@ -12,6 +12,7 @@ import ( logging "github.com/formancehq/go-libs/v5/pkg/observe/log" + "github.com/formancehq/ledger/v3/internal/proto/commonpb" "github.com/formancehq/ledger/v3/internal/proto/raftcmdpb" "github.com/formancehq/ledger/v3/internal/storage/dal" ) @@ -73,8 +74,9 @@ type Membership struct { // It is intentionally kept beside the membership cache so Add/Promote // admission observes one lock-consistent view of active requirements and // member identity evidence. - capabilityArmed bool - requiredCapabilities []uint32 + capabilityArmed bool + requiredCapabilities []uint32 + capabilityPolicyProvider func() *commonpb.ClusterPolicy // started gates transport / service-pool wiring. Until Start fires // (OnStart hook, after the local Raft gRPC server is listening), // Set / Remove / Register / Rehydrate mutate only the cache — the @@ -261,6 +263,15 @@ func (m *Membership) SetCapabilityGate(armed bool, required []uint32) error { return nil } +// SetCapabilityPolicyProvider installs the authoritative committed-policy +// view used while applying a ConfChange. It is called only by the FSM apply +// path, where the policy and membership entries are processed in Raft order. +func (m *Membership) SetCapabilityPolicyProvider(provider func() *commonpb.ClusterPolicy) { + m.mu.Lock() + defer m.mu.Unlock() + m.capabilityPolicyProvider = provider +} + // CapabilityGate returns the current committed activation gate. func (m *Membership) CapabilityGate() (armed bool, required []uint32) { m.mu.RLock() @@ -269,10 +280,9 @@ func (m *Membership) CapabilityGate() (armed bool, required []uint32) { return m.capabilityArmed, append([]uint32(nil), m.requiredCapabilities...) } -// ValidateTransitionEvidence checks a registration context against the -// committed gate while holding the same membership lock used to publish the -// gate. This closes the race where activation changes between an admission -// read and rawNode.Propose. +// ValidateTransitionEvidence checks a registration context against the latest +// locally published gate. The FSM write path revalidates the same evidence +// against the committed policy, which closes races with policy application. func (m *Membership) ValidateTransitionEvidence(ctx *ConfChangeContext) error { m.mu.RLock() armed, required := m.capabilityArmed, append([]uint32(nil), m.requiredCapabilities...) @@ -656,6 +666,25 @@ func (m *Membership) WriteConfChange(entry *raftpb.Entry, session *dal.WriteSess if err := ValidateCapabilities(ctx.Capabilities); err != nil { return fmt.Errorf("invalid capability evidence for peer %d: %w", nodeID, err) } + m.mu.RLock() + provider := m.capabilityPolicyProvider + m.mu.RUnlock() + if provider != nil { + policy := provider() + if policy == nil { + return fmt.Errorf("invalid capability evidence for peer %d: missing committed cluster policy", nodeID) + } + required := make([]uint32, 0, len(policy.GetCapabilityActivations())) + for _, activation := range policy.GetCapabilityActivations() { + if activation != nil && activation.GetActive() { + required = append(required, activation.GetId()) + } + } + slices.Sort(required) + if err := ValidateCapabilityEvidence(ctx, policy.GetCapabilityActivationArmed(), required); err != nil { + return fmt.Errorf("invalid capability evidence for peer %d: %w", nodeID, err) + } + } if err := session.SetProto(peerKey(nodeID), &raftcmdpb.PeerAddress{ NodeId: nodeID, RaftAddress: ctx.RaftAddress, diff --git a/internal/infra/state/machine.go b/internal/infra/state/machine.go index cd647fcbc5..b53e1fdfe8 100644 --- a/internal/infra/state/machine.go +++ b/internal/infra/state/machine.go @@ -159,7 +159,8 @@ type Machine struct { // cluster membership atomic with FSM state and makes the spool/WAL // replay path naturally idempotent — every replay of the entry // re-asserts the membership row. EN-1413. - confChangeHandler func(entry *raftpb.Entry, session *dal.WriteSession) error + confChangeHandler func(entry *raftpb.Entry, session *dal.WriteSession) error + capabilityPolicyCommitHandler func(*commonpb.ClusterPolicy) } // NewMachine constructs the hot-path FSM. It composes pre-built sub-objects @@ -799,6 +800,9 @@ func (fsm *Machine) PrepareDecodedEntries(ctx context.Context, sessions dal.Writ bloomReplacementEpoch: bloomReplacementEpoch, entryCount: len(decoded), } + if fsm.State.ClusterPolicy != nil { + pb.capabilityPolicy = fsm.State.ClusterPolicy.CloneVT() + } // Capture sentinel data before releasing the lock. if fsm.sentinelMode { @@ -836,6 +840,9 @@ func (fsm *Machine) CommitPreparedBatch(ctx context.Context, pb *PreparedBatch) if pb.bloomReplacement { fsm.BloomFilters.ConfirmPersistedReplacement(pb.bloomReplacementEpoch) } + if fsm.capabilityPolicyCommitHandler != nil && pb.capabilityPolicy != nil { + fsm.capabilityPolicyCommitHandler(pb.capabilityPolicy) + } fsm.batchCommitHistogram.Record(ctx, time.Since(commitStart).Seconds()) @@ -2080,10 +2087,18 @@ type PreparedBatch struct { sentinelLedgerNames []string sentinelDeletedLedgerNames map[string]struct{} sentinelTracer *SentinelTracer + capabilityPolicy *commonpb.ClusterPolicy entryCount int } +// SetCapabilityPolicyCommitHandler installs the post-commit hook used to +// publish the authoritative membership admission gate. The callback runs only +// after the corresponding FSM batch has committed. +func (fsm *Machine) SetCapabilityPolicyCommitHandler(handler func(*commonpb.ClusterPolicy)) { + fsm.capabilityPolicyCommitHandler = handler +} + // Close cancels the uncommitted batch. Must be called if CommitPreparedBatch // will not be called (e.g. on error in the caller). func (pb *PreparedBatch) Close() { From 500ba973a9bcd584d3edb68eb7d2914bcb13e250 Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Thu, 22 Oct 2026 16:28:12 +0200 Subject: [PATCH 014/127] refactor: remove capability activation arming --- .../subsystems/consensus/capabilities.md | 11 +++--- .../admission/capability_policy_test.go | 8 +--- internal/bootstrap/module.go | 2 +- .../processing/processor_cluster_policy.go | 3 -- .../processor_cluster_policy_test.go | 24 ------------ internal/infra/capabilities/capabilities.go | 9 ----- .../infra/capabilities/capabilities_test.go | 12 +++--- internal/infra/membership/confchange.go | 10 ++--- .../membership/confchange_identity_test.go | 10 ++--- internal/infra/membership/membership.go | 14 +++---- internal/infra/node/node.go | 6 +-- internal/infra/plan/capability_policy_test.go | 4 +- internal/proto/commonpb/common_reader.pb.go | 5 --- internal/proto/commonpb/common_vtproto.pb.go | 37 ------------------- misc/proto/common.proto | 5 +-- 15 files changed, 31 insertions(+), 129 deletions(-) diff --git a/docs/technical/architecture/subsystems/consensus/capabilities.md b/docs/technical/architecture/subsystems/consensus/capabilities.md index 09428f99eb..17d73dac4d 100644 --- a/docs/technical/architecture/subsystems/consensus/capabilities.md +++ b/docs/technical/architecture/subsystems/consensus/capabilities.md @@ -25,10 +25,9 @@ from the old one. Reachability and support are separate: an unreachable member blocks activation just as an incapable member does. Once a capability is active, every new membership announcement must include that capability. -While armed, a proposal that declares a capability is admitted only when that -capability is active and every member advertises it. Proposals with no declared -capability remain unaffected. FSM apply never consults node-local capability -flags. +A proposal that declares a capability is admitted only when that capability is +active and every member advertises it. Proposals with no declared capability +remain unaffected. FSM apply never consults node-local capability flags. ## Rollout and recovery @@ -66,5 +65,5 @@ uses `JoinAsLearner` and `ConfChangeAddLearnerNode`. The leader's existing cluster-policy reconciler does not activate a capability automatically. A future rollout can add an explicit `CapabilityActivation` entry once every current voter and learner has refreshed its membership proof. -Until then the mechanism remains disarmed and this binary declares no protocol -feature. +Until then the policy contains no active capability and this binary declares no +protocol feature. diff --git a/internal/application/admission/capability_policy_test.go b/internal/application/admission/capability_policy_test.go index a7b3e9e5aa..f26e317b97 100644 --- a/internal/application/admission/capability_policy_test.go +++ b/internal/application/admission/capability_policy_test.go @@ -61,9 +61,7 @@ func TestCapabilityPolicyProposerBlocksUnknownCapabilityAtFinalBoundary(t *testi proposer := capabilityPolicyProposer{ delegate: delegate, readPolicy: func() (*commonpb.ClusterPolicy, error) { - return &commonpb.ClusterPolicy{ - CapabilityActivationArmed: true, - }, nil + return &commonpb.ClusterPolicy{}, nil }, businessWrite: true, } @@ -80,9 +78,7 @@ func TestCapabilityPolicyProposerAllowsUnrelatedWrite(t *testing.T) { proposer := capabilityPolicyProposer{ delegate: delegate, readPolicy: func() (*commonpb.ClusterPolicy, error) { - return &commonpb.ClusterPolicy{ - CapabilityActivationArmed: true, - }, nil + return &commonpb.ClusterPolicy{}, nil }, businessWrite: true, } diff --git a/internal/bootstrap/module.go b/internal/bootstrap/module.go index e1dd729962..1b06df663b 100644 --- a/internal/bootstrap/module.go +++ b/internal/bootstrap/module.go @@ -318,7 +318,7 @@ func Module() fx.Option { } slices.Sort(required) required = slices.Compact(required) - if err := membership.SetCapabilityGate(policy.GetCapabilityActivationArmed(), required); err != nil { + if err := membership.SetCapabilityGate(required); err != nil { logger.WithFields(map[string]any{"error": err}).Errorf("Invalid capability gate in committed cluster policy") } } diff --git a/internal/domain/processing/processor_cluster_policy.go b/internal/domain/processing/processor_cluster_policy.go index 8e84bba187..915446a743 100644 --- a/internal/domain/processing/processor_cluster_policy.go +++ b/internal/domain/processing/processor_cluster_policy.go @@ -46,9 +46,6 @@ func applyClusterPolicy(newPolicy *commonpb.ClusterPolicy, scope Scope) (*common current := scope.GetClusterPolicy() appliedRev := current.GetRevision() newRev := newPolicy.GetRevision() - if current.GetCapabilityActivationArmed() && !newPolicy.GetCapabilityActivationArmed() { - return nil, &domain.ErrClusterPolicyInvalid{Detail: "capability activation cannot be disarmed"} - } active := make(map[uint32]struct{}) for _, activation := range current.GetCapabilityActivations() { if activation != nil && activation.GetActive() { diff --git a/internal/domain/processing/processor_cluster_policy_test.go b/internal/domain/processing/processor_cluster_policy_test.go index d6eccf0688..a83f8bafea 100644 --- a/internal/domain/processing/processor_cluster_policy_test.go +++ b/internal/domain/processing/processor_cluster_policy_test.go @@ -101,30 +101,6 @@ func TestProcessSetClusterPolicy_LowerRevisionStale(t *testing.T) { require.Equal(t, uint64(5), stale.AppliedRevision) } -func TestProcessSetClusterPolicy_CapabilityActivationIsMonotonic(t *testing.T) { - t.Parallel() - - ctrl := gomock.NewController(t) - mockStore := NewMockScope(ctrl) - processor, err := NewRequestProcessor(nil, 0) - require.NoError(t, err) - - current := withMetadataLimits(&commonpb.ClusterPolicy{ - Revision: 2, - CapabilityActivationArmed: true, - CapabilityActivations: []*commonpb.CapabilityActivation{{Id: 7, Active: true}}, - QueryCheckpointLimit: 1, - }) - mockStore.EXPECT().GetClusterPolicy().Return(current) - - proposed := withMetadataLimits(&commonpb.ClusterPolicy{Revision: 3, QueryCheckpointLimit: 1}) - _, procErr := processor.ProcessOrder(clusterPolicyOrder(proposed), mockStore) - - var invalid *domain.ErrClusterPolicyInvalid - require.ErrorAs(t, procErr, &invalid) - require.Contains(t, invalid.Detail, "cannot be disarmed") -} - func TestProcessSetClusterPolicy_CapabilityCannotDisappear(t *testing.T) { t.Parallel() diff --git a/internal/infra/capabilities/capabilities.go b/internal/infra/capabilities/capabilities.go index ba09544c48..1dc22e7626 100644 --- a/internal/infra/capabilities/capabilities.go +++ b/internal/infra/capabilities/capabilities.go @@ -39,7 +39,6 @@ type Member struct { } type State struct { - Armed bool Activations map[ID]bool } @@ -109,10 +108,6 @@ func StampProposal(proposal *raftcmdpb.Proposal) error { } func (s State) Validate() error { - if !s.Armed { - return nil - } - return nil } @@ -121,7 +116,6 @@ func FromPolicy(policy *commonpb.ClusterPolicy) State { if policy == nil { return s } - s.Armed = policy.GetCapabilityActivationArmed() for _, activation := range policy.GetCapabilityActivations() { if activation != nil { s.Activations[ID(activation.GetId())] = activation.GetActive() @@ -150,9 +144,6 @@ func Evaluate(state State, required []ID, _ bool, members []Member) Decision { if err := state.Validate(); err != nil { return Decision{Reason: err.Error()} } - if !state.Armed { - return Decision{Allowed: true} - } required, err := Normalize(required) if err != nil { return Decision{Reason: err.Error()} diff --git a/internal/infra/capabilities/capabilities_test.go b/internal/infra/capabilities/capabilities_test.go index a1d07303e5..0d94d16b36 100644 --- a/internal/infra/capabilities/capabilities_test.go +++ b/internal/infra/capabilities/capabilities_test.go @@ -9,12 +9,12 @@ import ( "github.com/formancehq/ledger/v3/internal/proto/raftcmdpb" ) -func armedState() State { - return State{Armed: true, Activations: map[ID]bool{}} +func capabilityState() State { + return State{Activations: map[ID]bool{}} } func TestEvaluateIncludesVotersAndLearners(t *testing.T) { - state := armedState() + state := capabilityState() state.Activations[99] = true decision := Evaluate(state, []ID{99}, true, []Member{ {NodeID: 1, Role: Voter, Reachable: true, Capabilities: []ID{99}}, @@ -25,7 +25,7 @@ func TestEvaluateIncludesVotersAndLearners(t *testing.T) { } func TestEvaluateAllowsUnrelatedWriteWhenNoFeatureIsDeclared(t *testing.T) { - decision := Evaluate(armedState(), nil, true, []Member{{NodeID: 1, Role: Voter, Reachable: true}}) + decision := Evaluate(capabilityState(), nil, true, []Member{{NodeID: 1, Role: Voter, Reachable: true}}) require.True(t, decision.Allowed) } @@ -43,10 +43,8 @@ func TestStampProposalClearsFutureDeclarations(t *testing.T) { func TestFromPolicyRoundTrip(t *testing.T) { state := FromPolicy(&commonpb.ClusterPolicy{ - CapabilityActivationArmed: true, - CapabilityActivations: []*commonpb.CapabilityActivation{{Id: 99, Active: true}}, + CapabilityActivations: []*commonpb.CapabilityActivation{{Id: 99, Active: true}}, }) - require.True(t, state.Armed) require.True(t, state.Activations[99]) require.NoError(t, state.Validate()) } diff --git a/internal/infra/membership/confchange.go b/internal/infra/membership/confchange.go index f834d3dfeb..74e05482ed 100644 --- a/internal/infra/membership/confchange.go +++ b/internal/infra/membership/confchange.go @@ -42,19 +42,15 @@ func ValidateCapabilities(ids []uint32) error { } // ValidateCapabilityEvidence validates the proof attached to a membership -// transition. Unarmed clusters retain backwards-compatible empty evidence; -// once the capability barrier is armed, every Add/Update/Promote transition -// must announce all currently active capabilities. -func ValidateCapabilityEvidence(ctx *ConfChangeContext, armed bool, required []uint32) error { +// transition against all capabilities currently active in the committed +// policy. An empty policy keeps membership admission backwards-compatible. +func ValidateCapabilityEvidence(ctx *ConfChangeContext, required []uint32) error { if ctx == nil { return errors.New("missing capability membership evidence") } if err := ValidateCapabilities(ctx.Capabilities); err != nil { return fmt.Errorf("invalid capability evidence: %w", err) } - if !armed { - return nil - } for _, id := range required { if !slices.Contains(ctx.Capabilities, id) { return fmt.Errorf("capability %d is required by the active policy", id) diff --git a/internal/infra/membership/confchange_identity_test.go b/internal/infra/membership/confchange_identity_test.go index 6b78d4e03f..56e2201b74 100644 --- a/internal/infra/membership/confchange_identity_test.go +++ b/internal/infra/membership/confchange_identity_test.go @@ -16,18 +16,16 @@ func TestValidateCapabilityEvidenceRequiresActiveCapabilities(t *testing.T) { Capabilities: []uint32{1, 2}, } - require.NoError(t, ValidateCapabilityEvidence(ctx, true, []uint32{1, 2})) - require.ErrorContains(t, ValidateCapabilityEvidence(ctx, true, []uint32{1, 3}), "capability 3") - require.NoError(t, ValidateCapabilityEvidence(&ConfChangeContext{InstanceID: bytesN(0x42)}, true, nil)) - require.NoError(t, ValidateCapabilityEvidence(&ConfChangeContext{InstanceID: bytesN(0x42)}, false, nil)) + require.NoError(t, ValidateCapabilityEvidence(ctx, []uint32{1, 2})) + require.ErrorContains(t, ValidateCapabilityEvidence(ctx, []uint32{1, 3}), "capability 3") + require.NoError(t, ValidateCapabilityEvidence(&ConfChangeContext{InstanceID: bytesN(0x42)}, nil)) } func TestWriteConfChangeRejectsCapabilitiesMissingFromCommittedPolicy(t *testing.T) { m := newTestMembership(t) m.SetCapabilityPolicyProvider(func() *commonpb.ClusterPolicy { return &commonpb.ClusterPolicy{ - CapabilityActivationArmed: true, - CapabilityActivations: []*commonpb.CapabilityActivation{{Id: 7, Active: true}}, + CapabilityActivations: []*commonpb.CapabilityActivation{{Id: 7, Active: true}}, } }) diff --git a/internal/infra/membership/membership.go b/internal/infra/membership/membership.go index fe82c0a776..d120b36f37 100644 --- a/internal/infra/membership/membership.go +++ b/internal/infra/membership/membership.go @@ -74,7 +74,6 @@ type Membership struct { // It is intentionally kept beside the membership cache so Add/Promote // admission observes one lock-consistent view of active requirements and // member identity evidence. - capabilityArmed bool requiredCapabilities []uint32 capabilityPolicyProvider func() *commonpb.ClusterPolicy // started gates transport / service-pool wiring. Until Start fires @@ -250,14 +249,13 @@ func (m *Membership) SnapshotDonorCandidates() []uint64 { // SetCapabilityGate publishes the committed activation gate used by // membership transitions. The required IDs are copied and must be canonical. -func (m *Membership) SetCapabilityGate(armed bool, required []uint32) error { +func (m *Membership) SetCapabilityGate(required []uint32) error { if err := ValidateCapabilities(required); err != nil { return err } m.mu.Lock() defer m.mu.Unlock() - m.capabilityArmed = armed m.requiredCapabilities = append([]uint32(nil), required...) return nil @@ -273,11 +271,11 @@ func (m *Membership) SetCapabilityPolicyProvider(provider func() *commonpb.Clust } // CapabilityGate returns the current committed activation gate. -func (m *Membership) CapabilityGate() (armed bool, required []uint32) { +func (m *Membership) CapabilityGate() (required []uint32) { m.mu.RLock() defer m.mu.RUnlock() - return m.capabilityArmed, append([]uint32(nil), m.requiredCapabilities...) + return append([]uint32(nil), m.requiredCapabilities...) } // ValidateTransitionEvidence checks a registration context against the latest @@ -285,10 +283,10 @@ func (m *Membership) CapabilityGate() (armed bool, required []uint32) { // against the committed policy, which closes races with policy application. func (m *Membership) ValidateTransitionEvidence(ctx *ConfChangeContext) error { m.mu.RLock() - armed, required := m.capabilityArmed, append([]uint32(nil), m.requiredCapabilities...) + required := append([]uint32(nil), m.requiredCapabilities...) m.mu.RUnlock() - return ValidateCapabilityEvidence(ctx, armed, required) + return ValidateCapabilityEvidence(ctx, required) } // GetInstanceID returns the peer's 16-byte identity UUID from the in-memory @@ -681,7 +679,7 @@ func (m *Membership) WriteConfChange(entry *raftpb.Entry, session *dal.WriteSess } } slices.Sort(required) - if err := ValidateCapabilityEvidence(ctx, policy.GetCapabilityActivationArmed(), required); err != nil { + if err := ValidateCapabilityEvidence(ctx, required); err != nil { return fmt.Errorf("invalid capability evidence for peer %d: %w", nodeID, err) } } diff --git a/internal/infra/node/node.go b/internal/infra/node/node.go index 9c1e3c41e4..39ad210d18 100644 --- a/internal/infra/node/node.go +++ b/internal/infra/node/node.go @@ -3178,7 +3178,7 @@ func (node *Node) AddLearner(ctx context.Context, nodeID uint64, raftAddr, servi // AddLearnerWithCapabilities proposes a learner registration carrying the // caller's capability proof. The proof is persisted with the peer identity and -// is required once the capability activation gate is armed. +// is required by the active capability policy. func (node *Node) AddLearnerWithCapabilities(ctx context.Context, nodeID uint64, raftAddr, serviceAddr string, instanceID []byte, capabilityIDs []uint32) error { return node.addLearnerWithCapabilities(ctx, nodeID, raftAddr, serviceAddr, instanceID, capabilityIDs, false) } @@ -3297,8 +3297,8 @@ func (node *Node) RefreshMembership(ctx context.Context, nodeID uint64, raftAddr // SetCapabilityGate updates the leader-side membership admission gate from a // committed ClusterPolicy projection. -func (node *Node) SetCapabilityGate(armed bool, required []uint32) error { - return node.membership.SetCapabilityGate(armed, required) +func (node *Node) SetCapabilityGate(required []uint32) error { + return node.membership.SetCapabilityGate(required) } // addLearner is kept as a compatibility shim for package-local tests and diff --git a/internal/infra/plan/capability_policy_test.go b/internal/infra/plan/capability_policy_test.go index 48fe4fc7c0..331af71b5e 100644 --- a/internal/infra/plan/capability_policy_test.go +++ b/internal/infra/plan/capability_policy_test.go @@ -37,9 +37,7 @@ func capabilityPolicyProposal(t *testing.T) *node.Proposal { func TestCapabilityPolicyProposerBlocksInactiveCapability(t *testing.T) { delegate := new(capabilityPolicyTestProposer) proposer := NewCapabilityPolicyProposer(delegate, func() (*commonpb.ClusterPolicy, error) { - return &commonpb.ClusterPolicy{ - CapabilityActivationArmed: true, - }, nil + return &commonpb.ClusterPolicy{}, nil }, nil, false) _, err := proposer.Propose(context.Background(), capabilityPolicyProposal(t)) diff --git a/internal/proto/commonpb/common_reader.pb.go b/internal/proto/commonpb/common_reader.pb.go index f22e69a515..e78ae7e47e 100644 --- a/internal/proto/commonpb/common_reader.pb.go +++ b/internal/proto/commonpb/common_reader.pb.go @@ -3522,7 +3522,6 @@ type ClusterPolicyReader interface { GetMetadataMaxValueBytes() uint64 GetMetadataMaxEntityBytes() uint64 GetMetadataMaxCommandBytes() uint64 - GetCapabilityActivationArmed() bool GetCapabilityActivations() CapabilityActivationListReader Mutate() *ClusterPolicy } @@ -3561,10 +3560,6 @@ func (r *clusterPolicyReadonly) GetMetadataMaxCommandBytes() uint64 { return (*ClusterPolicy)(r).GetMetadataMaxCommandBytes() } -func (r *clusterPolicyReadonly) GetCapabilityActivationArmed() bool { - return (*ClusterPolicy)(r).GetCapabilityActivationArmed() -} - func (r *clusterPolicyReadonly) GetCapabilityActivations() CapabilityActivationListReader { return NewCapabilityActivationListReader((*ClusterPolicy)(r).GetCapabilityActivations()) } diff --git a/internal/proto/commonpb/common_vtproto.pb.go b/internal/proto/commonpb/common_vtproto.pb.go index 2c8a21a34c..3d585858c1 100644 --- a/internal/proto/commonpb/common_vtproto.pb.go +++ b/internal/proto/commonpb/common_vtproto.pb.go @@ -1273,7 +1273,6 @@ func (m *ClusterPolicy) CloneVT() *ClusterPolicy { r.MetadataMaxValueBytes = m.MetadataMaxValueBytes r.MetadataMaxEntityBytes = m.MetadataMaxEntityBytes r.MetadataMaxCommandBytes = m.MetadataMaxCommandBytes - r.CapabilityActivationArmed = m.CapabilityActivationArmed if rhs := m.CapabilityActivations; rhs != nil { tmpContainer := make([]*CapabilityActivation, len(rhs)) for k, v := range rhs { @@ -11110,9 +11109,6 @@ func (this *ClusterPolicy) EqualVT(that *ClusterPolicy) bool { if this.MetadataMaxCommandBytes != that.MetadataMaxCommandBytes { return false } - if this.CapabilityActivationArmed != that.CapabilityActivationArmed { - return false - } if len(this.CapabilityActivations) != len(that.CapabilityActivations) { return false } @@ -26564,16 +26560,6 @@ func (m *ClusterPolicy) MarshalToSizedBufferVT(dAtA []byte) (int, error) { dAtA[i] = 0x6a } } - if m.CapabilityActivationArmed { - i-- - if m.CapabilityActivationArmed { - dAtA[i] = 1 - } else { - dAtA[i] = 0 - } - i-- - dAtA[i] = 0x48 - } if m.MetadataMaxCommandBytes != 0 { i -= 8 binary.LittleEndian.PutUint64(dAtA[i:], uint64(m.MetadataMaxCommandBytes)) @@ -46388,9 +46374,6 @@ func (m *ClusterPolicy) SizeVT() (n int) { if m.MetadataMaxCommandBytes != 0 { n += 9 } - if m.CapabilityActivationArmed { - n += 2 - } if len(m.CapabilityActivations) > 0 { for _, e := range m.CapabilityActivations { l = e.SizeVT() @@ -61512,26 +61495,6 @@ func (m *ClusterPolicy) UnmarshalVT(dAtA []byte) error { } m.MetadataMaxCommandBytes = uint64(binary.LittleEndian.Uint64(dAtA[iNdEx:])) iNdEx += 8 - case 9: - if wireType != 0 { - return fmt.Errorf("proto: wrong wireType = %d for field CapabilityActivationArmed", wireType) - } - var v int - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return protohelpers.ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - v |= int(b&0x7F) << shift - if b < 0x80 { - break - } - } - m.CapabilityActivationArmed = bool(v != 0) case 13: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field CapabilityActivations", wireType) diff --git a/misc/proto/common.proto b/misc/proto/common.proto index 1b3494e120..f2166b6270 100644 --- a/misc/proto/common.proto +++ b/misc/proto/common.proto @@ -552,10 +552,7 @@ message ClusterPolicy { fixed64 metadata_max_value_bytes = 6; fixed64 metadata_max_entity_bytes = 7; fixed64 metadata_max_command_bytes = 8; -// Capability activation is replicated policy, never a node-local flag. The -// fields are optional so nodes that predate the capability foundation decode -// the policy with their existing defaults. - bool capability_activation_armed = 9; + // Capability activation is replicated policy, never a node-local flag. repeated CapabilityActivation capability_activations = 13; } From 6b66f76153be860cd81957dbe8a39483812e4a50 Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Thu, 22 Oct 2026 16:32:34 +0200 Subject: [PATCH 015/127] docs: define semantic capability behavior --- .../subsystems/consensus/capabilities.md | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/docs/technical/architecture/subsystems/consensus/capabilities.md b/docs/technical/architecture/subsystems/consensus/capabilities.md index 17d73dac4d..a9cb15006a 100644 --- a/docs/technical/architecture/subsystems/consensus/capabilities.md +++ b/docs/technical/architecture/subsystems/consensus/capabilities.md @@ -29,6 +29,34 @@ A proposal that declares a capability is admitted only when that capability is active and every member advertises it. Proposals with no declared capability remain unaffected. FSM apply never consults node-local capability flags. +## Capabilities that change FSM semantics + +A capability can describe a protocol or semantic behavior, not only a payload +feature. For example, `MirrorIngestRejectInvalidAssetV2` can identify the +behavior that rejects an asset which an older FSM accepted. + +The capability has two roles in this case: + +1. The replicated `ClusterPolicy` controls whether the cluster may accept the + behavior. +2. The order declares the capability that selects the behavior during FSM + apply. + +The order must carry the semantic marker explicitly: + +```text +old order: required_capabilities = [] +new order: required_capabilities = [MirrorIngestRejectInvalidAssetV2] +``` + +The FSM must use the order's declaration to select the behavior. It must not +infer the behavior from the current policy alone. An old order can be replayed +after the capability becomes active, and it must keep its old semantics. + +If a producer cannot mark the new orders, the change must remain backward +compatible or add an explicit semantic version to the order. A capability by +itself is an admission barrier; it is not a historical replay version. + ## Rollout and recovery Deploy barrier-aware binaries to every eligible member before arming an From 0b66f7dfb8636f358977dc3317aaf34ec83f75bd Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Thu, 22 Oct 2026 16:35:21 +0200 Subject: [PATCH 016/127] docs: add protocol capability rollout workflow --- .../subsystems/consensus/capabilities.md | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/docs/technical/architecture/subsystems/consensus/capabilities.md b/docs/technical/architecture/subsystems/consensus/capabilities.md index a9cb15006a..c7562c35e2 100644 --- a/docs/technical/architecture/subsystems/consensus/capabilities.md +++ b/docs/technical/architecture/subsystems/consensus/capabilities.md @@ -57,6 +57,35 @@ If a producer cannot mark the new orders, the change must remain backward compatible or add an explicit semantic version to the order. A capability by itself is an admission barrier; it is not a historical replay version. +## Adding a protocol-breaking capability + +Use this workflow when a change can make an order apply differently on two +binary versions. + +1. Name the semantic contract. Describe the old behavior, the new behavior, + and the orders that use each behavior. +2. Allocate a new capability ID. Never reuse an ID for a different contract. +3. Add the capability to the binary registry and derive it from the final + proposal in `RequiredCapabilities`. +4. Stamp the capability on every new order that needs the new behavior. Do not + infer it from the current policy during FSM apply. +5. Keep the old behavior for orders that do not carry the capability. Add + replay tests that apply both order forms with the new binary. +6. Add admission tests for an inactive capability, an active capability, and a + member that does not advertise it. +7. Deploy the compatible binary to all current members. Let each restarted + member refresh its membership evidence before activating the capability. +8. Activate the capability through the replicated `ClusterPolicy`. The FSM + rejects a new member or membership refresh that omits an active capability. +9. Keep the capability active after activation. The policy processor rejects + removal of an active capability. +10. Update this document in the same change. Record the capability ID, the + semantic contract, the replay rule, and the rollout order. + +Do not add a capability for a bug fix that restores the intended behavior when +the old behavior was invalid. Add one when valid historical orders need to keep +their old behavior while new orders use a different rule. + ## Rollout and recovery Deploy barrier-aware binaries to every eligible member before arming an From 773bd5b3a02cba7c8e58a4b791d5359470b7a076 Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Thu, 22 Oct 2026 16:54:00 +0200 Subject: [PATCH 017/127] fix(consensus): preserve capability evidence during bootstrap --- cmd/server/server.go | 1 + internal/adapter/grpc/server_bootstrap.go | 3 +- internal/application/membership/membership.go | 18 +++-- .../membership/membership_node_test.go | 19 +++++ internal/infra/node/config.go | 7 +- .../infra/node/membership_helpers_test.go | 2 + internal/infra/node/node.go | 5 +- .../cluster_bootstrap.pb.go | 13 +++- .../cluster_bootstrap_reader.pb.go | 5 ++ .../cluster_bootstrap_vtproto.pb.go | 78 +++++++++++++++++++ misc/proto/cluster_bootstrap.proto | 1 + 11 files changed, 139 insertions(+), 13 deletions(-) diff --git a/cmd/server/server.go b/cmd/server/server.go index da1916685a..f8c2237ebf 100644 --- a/cmd/server/server.go +++ b/cmd/server/server.go @@ -873,6 +873,7 @@ func discoverPeersFromCluster(raftAddr string, tlsCfg bootstrap.TLSConfig, clust Address: p.GetRaftAddress(), ServiceAddress: p.GetServiceAddress(), InstanceID: p.GetInstanceId(), + Capabilities: append([]uint32(nil), p.GetCapabilities()...), }) } diff --git a/internal/adapter/grpc/server_bootstrap.go b/internal/adapter/grpc/server_bootstrap.go index 900f6b6a5b..b3085e9246 100644 --- a/internal/adapter/grpc/server_bootstrap.go +++ b/internal/adapter/grpc/server_bootstrap.go @@ -134,6 +134,7 @@ func (impl *ClusterBootstrapServiceServerImpl) GetPeers(ctx context.Context, req RaftAddress: p.RaftAddress, ServiceAddress: p.ServiceAddress, InstanceId: p.InstanceID, + Capabilities: p.Capabilities, }) } @@ -223,7 +224,7 @@ func (impl *ClusterBootstrapServiceServerImpl) JoinAsLearner(ctx context.Context req.GetNodeId(), req.GetInstanceId()) } - if err := impl.membership.JoinAsLearner(ctx, req.GetNodeId(), req.GetRaftAddress(), req.GetServiceAddress(), req.GetInstanceId()); err != nil { + if err := impl.membership.JoinAsLearnerWithCapabilities(ctx, req.GetNodeId(), req.GetRaftAddress(), req.GetServiceAddress(), req.GetInstanceId(), req.GetCapabilities()); err != nil { // EN-1436: a JoinAsLearner call reaches the leader only when the caller // has no CLUSTER_JOINED marker on its WAL (see bootstrap/module.go // tryAddLearner). If the leader's Progress already carries this diff --git a/internal/application/membership/membership.go b/internal/application/membership/membership.go index 692205bb23..6879c1862b 100644 --- a/internal/application/membership/membership.go +++ b/internal/application/membership/membership.go @@ -50,6 +50,7 @@ type Peer struct { RaftAddress string ServiceAddress string InstanceID []byte + Capabilities []uint32 } // Service is the single owner of cluster membership operations. @@ -90,7 +91,7 @@ func (s *Service) IsRemoved(nodeID uint64, instanceID []byte) (bool, error) { // Returns node.ErrNodeAlreadyInCluster on idempotent retries; adapters // map that to their transport-specific "already-exists" status. func (s *Service) AddLearner(ctx context.Context, nodeID uint64, raftAddr, serviceAddr string, instanceID []byte) error { - return s.addLearner(ctx, nodeID, raftAddr, serviceAddr, instanceID, false) + return s.addLearner(ctx, nodeID, raftAddr, serviceAddr, instanceID, nil, false) } // JoinAsLearner registers a node that is booting with an empty WAL and no @@ -98,7 +99,13 @@ func (s *Service) AddLearner(ctx context.Context, nodeID uint64, raftAddr, servi // AddLearner operation preserves EN-1436's stale-progress fail-fast without // inferring caller intent from a missing instanceID sentinel. func (s *Service) JoinAsLearner(ctx context.Context, nodeID uint64, raftAddr, serviceAddr string, instanceID []byte) error { - return s.addLearner(ctx, nodeID, raftAddr, serviceAddr, instanceID, true) + return s.JoinAsLearnerWithCapabilities(ctx, nodeID, raftAddr, serviceAddr, instanceID, nil) +} + +// JoinAsLearnerWithCapabilities is the bootstrap path carrying the joining +// binary's capability evidence through the committed ConfChange. +func (s *Service) JoinAsLearnerWithCapabilities(ctx context.Context, nodeID uint64, raftAddr, serviceAddr string, instanceID []byte, capabilityIDs []uint32) error { + return s.addLearner(ctx, nodeID, raftAddr, serviceAddr, instanceID, capabilityIDs, true) } // RefreshMembership updates the durable identity, endpoints and capability @@ -116,7 +123,7 @@ func (s *Service) RefreshMembership(ctx context.Context, nodeID uint64, raftAddr return nil } -func (s *Service) addLearner(ctx context.Context, nodeID uint64, raftAddr, serviceAddr string, instanceID []byte, bootJoin bool) error { +func (s *Service) addLearner(ctx context.Context, nodeID uint64, raftAddr, serviceAddr string, instanceID []byte, capabilityIDs []uint32, bootJoin bool) error { if err := validatePeer(nodeID, raftAddr, serviceAddr, instanceID); err != nil { return err } @@ -134,9 +141,9 @@ func (s *Service) addLearner(ctx context.Context, nodeID uint64, raftAddr, servi var err error if bootJoin { - err = s.node.JoinAsLearner(ctx, nodeID, raftAddr, serviceAddr, instanceID) + err = s.node.JoinAsLearnerWithCapabilities(ctx, nodeID, raftAddr, serviceAddr, instanceID, capabilityIDs) } else { - err = s.node.AddLearner(ctx, nodeID, raftAddr, serviceAddr, instanceID) + err = s.node.AddLearnerWithCapabilities(ctx, nodeID, raftAddr, serviceAddr, instanceID, capabilityIDs) } if err != nil { return fmt.Errorf("adding learner: %w", err) @@ -214,6 +221,7 @@ func (s *Service) ListPeers(ctx context.Context) ([]Peer, error) { RaftAddress: raftAddr, ServiceAddress: serviceAddr, InstanceID: peer.InstanceID, + Capabilities: append([]uint32(nil), peer.Capabilities...), }) } diff --git a/internal/application/membership/membership_node_test.go b/internal/application/membership/membership_node_test.go index 5525329b61..c401b7d95d 100644 --- a/internal/application/membership/membership_node_test.go +++ b/internal/application/membership/membership_node_test.go @@ -199,3 +199,22 @@ func TestService_ListPeersReturnsCompleteIndependentSnapshot(t *testing.T) { require.ElementsMatch(t, want, fresh, "caller mutation must not corrupt a later discovery response") }) } + +func TestService_JoinAsLearnerWithCapabilitiesPersistsEvidence(t *testing.T) { + t.Parallel() + + s := newMembershipServiceNode(t) + ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second) + defer cancel() + + require.NoError(t, s.JoinAsLearnerWithCapabilities(ctx, 2, "peer:7777", "peer:8888", []byte("peer-instance-id"), []uint32{7, 9})) + + peer := s.infraMembership.PeerAddresses()[2] + require.Equal(t, []uint32{7, 9}, peer.Capabilities) + peers, err := s.ListPeers(ctx) + require.NoError(t, err) + require.Contains(t, peers, Peer{ + ID: 2, RaftAddress: "peer:7777", ServiceAddress: "peer:8888", + InstanceID: []byte("peer-instance-id"), Capabilities: []uint32{7, 9}, + }) +} diff --git a/internal/infra/node/config.go b/internal/infra/node/config.go index f91fbe6d59..4c5b91c1c1 100644 --- a/internal/infra/node/config.go +++ b/internal/infra/node/config.go @@ -177,7 +177,8 @@ func (cfg *NodeConfig) SetDefaults() { type Peer struct { ID uint64 - Address string // Raft transport address - ServiceAddress string // Service API address (for request forwarding) - InstanceID []byte // 16-byte WAL/PVC incarnation identity + Address string // Raft transport address + ServiceAddress string // Service API address (for request forwarding) + InstanceID []byte // 16-byte WAL/PVC incarnation identity + Capabilities []uint32 // capabilities supported by this binary } diff --git a/internal/infra/node/membership_helpers_test.go b/internal/infra/node/membership_helpers_test.go index 21c7da255f..e38aa6f99a 100644 --- a/internal/infra/node/membership_helpers_test.go +++ b/internal/infra/node/membership_helpers_test.go @@ -70,6 +70,7 @@ func TestRegisterInitialPeersPersistsInstanceIDs(t *testing.T) { Address: "peer:7777", ServiceAddress: "peer:8888", InstanceID: instanceID, + Capabilities: []uint32{7, 9}, }}, }, true) require.NoError(t, err) @@ -80,6 +81,7 @@ func TestRegisterInitialPeersPersistsInstanceIDs(t *testing.T) { storedSelf, ok := m.GetInstanceID(42) require.True(t, ok) require.Equal(t, selfInstanceID, storedSelf) + require.Equal(t, []uint32{7, 9}, m.PeerAddresses()[1].Capabilities) } func TestRegisterInitialPeersRejectsMissingInstanceID(t *testing.T) { diff --git a/internal/infra/node/node.go b/internal/infra/node/node.go index 39ad210d18..e015d6bce3 100644 --- a/internal/infra/node/node.go +++ b/internal/infra/node/node.go @@ -2684,6 +2684,7 @@ func (node *Node) GetConfiguredPeers(ctx context.Context) ([]Peer, error) { Address: address.RaftAddress, ServiceAddress: address.ServiceAddress, InstanceID: bytes.Clone(address.InstanceID), + Capabilities: append([]uint32(nil), address.Capabilities...), }) } @@ -4023,7 +4024,7 @@ func initialJoinVoters(peers []Peer, selfID uint64) []uint64 { // through WriteConfChange). func registerInitialPeers(m *membership.Membership, cfg NodeConfig, includeSelf bool) error { if includeSelf { - if err := m.Register(cfg.NodeID, cfg.AdvertiseAddr, cfg.ServiceAdvertiseAddr, cfg.InstanceID); err != nil { + if err := m.RegisterWithEvidence(cfg.NodeID, cfg.AdvertiseAddr, cfg.ServiceAdvertiseAddr, cfg.InstanceID, capabilityIDsAsUint32(capabilities.SupportedIDs())); err != nil { return fmt.Errorf("persisting self in peer store: %w", err) } } @@ -4031,7 +4032,7 @@ func registerInitialPeers(m *membership.Membership, cfg NodeConfig, includeSelf // Peer discovery carries the durable identity of every configured member; // never create an address-only membership row. for _, p := range cfg.Peers { - if err := m.Register(p.ID, p.Address, p.ServiceAddress, p.InstanceID); err != nil { + if err := m.RegisterWithEvidence(p.ID, p.Address, p.ServiceAddress, p.InstanceID, p.Capabilities); err != nil { return fmt.Errorf("persisting initial peer %d: %w", p.ID, err) } } diff --git a/internal/proto/clusterbootstrappb/cluster_bootstrap.pb.go b/internal/proto/clusterbootstrappb/cluster_bootstrap.pb.go index fb87fe693a..76ae2b342f 100644 --- a/internal/proto/clusterbootstrappb/cluster_bootstrap.pb.go +++ b/internal/proto/clusterbootstrappb/cluster_bootstrap.pb.go @@ -109,6 +109,7 @@ type PeerInfo struct { RaftAddress string `protobuf:"bytes,2,opt,name=raft_address,json=raftAddress,proto3" json:"raft_address,omitempty"` ServiceAddress string `protobuf:"bytes,3,opt,name=service_address,json=serviceAddress,proto3" json:"service_address,omitempty"` InstanceId []byte `protobuf:"bytes,4,opt,name=instance_id,json=instanceId,proto3" json:"instance_id,omitempty"` // 16-byte UUID identifying this peer's WAL/PVC incarnation; required + Capabilities []uint32 `protobuf:"fixed32,5,rep,packed,name=capabilities,proto3" json:"capabilities,omitempty"` // capabilities supported by this binary unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -171,6 +172,13 @@ func (x *PeerInfo) GetInstanceId() []byte { return nil } +func (x *PeerInfo) GetCapabilities() []uint32 { + if x != nil { + return x.Capabilities + } + return nil +} + type JoinAsLearnerRequest struct { state protoimpl.MessageState `protogen:"open.v1"` NodeId uint64 `protobuf:"varint,1,opt,name=node_id,json=nodeId,proto3" json:"node_id,omitempty"` // Numeric ID for the new learner node @@ -406,13 +414,14 @@ const file_cluster_bootstrap_proto_rawDesc = "" + "\x17cluster_bootstrap.proto\x12\x11cluster_bootstrap\"\x11\n" + "\x0fGetPeersRequest\"E\n" + "\x10GetPeersResponse\x121\n" + - "\x05peers\x18\x01 \x03(\v2\x1b.cluster_bootstrap.PeerInfoR\x05peers\"\x87\x01\n" + + "\x05peers\x18\x01 \x03(\v2\x1b.cluster_bootstrap.PeerInfoR\x05peers\"\xab\x01\n" + "\bPeerInfo\x12\x0e\n" + "\x02id\x18\x01 \x01(\x04R\x02id\x12!\n" + "\fraft_address\x18\x02 \x01(\tR\vraftAddress\x12'\n" + "\x0fservice_address\x18\x03 \x01(\tR\x0eserviceAddress\x12\x1f\n" + "\vinstance_id\x18\x04 \x01(\fR\n" + - "instanceId\"\xc0\x01\n" + + "instanceId\x12\"\n" + + "\fcapabilities\x18\x05 \x03(\aR\fcapabilities\"\xc0\x01\n" + "\x14JoinAsLearnerRequest\x12\x17\n" + "\anode_id\x18\x01 \x01(\x04R\x06nodeId\x12!\n" + "\fraft_address\x18\x02 \x01(\tR\vraftAddress\x12'\n" + diff --git a/internal/proto/clusterbootstrappb/cluster_bootstrap_reader.pb.go b/internal/proto/clusterbootstrappb/cluster_bootstrap_reader.pb.go index afb782846e..f52cbfadf3 100644 --- a/internal/proto/clusterbootstrappb/cluster_bootstrap_reader.pb.go +++ b/internal/proto/clusterbootstrappb/cluster_bootstrap_reader.pb.go @@ -144,6 +144,7 @@ type PeerInfoReader interface { GetRaftAddress() string GetServiceAddress() string GetInstanceId() []byte + GetCapabilities() []uint32 Mutate() *PeerInfo } @@ -165,6 +166,10 @@ func (r *peerInfoReadonly) GetInstanceId() []byte { return bytes.Clone((*PeerInfo)(r).GetInstanceId()) } +func (r *peerInfoReadonly) GetCapabilities() []uint32 { + return slices.Clone((*PeerInfo)(r).GetCapabilities()) +} + func (r *peerInfoReadonly) Mutate() *PeerInfo { return (*PeerInfo)(r).CloneVT() } diff --git a/internal/proto/clusterbootstrappb/cluster_bootstrap_vtproto.pb.go b/internal/proto/clusterbootstrappb/cluster_bootstrap_vtproto.pb.go index 547824d1ee..0fd577baa8 100644 --- a/internal/proto/clusterbootstrappb/cluster_bootstrap_vtproto.pb.go +++ b/internal/proto/clusterbootstrappb/cluster_bootstrap_vtproto.pb.go @@ -72,6 +72,11 @@ func (m *PeerInfo) CloneVT() *PeerInfo { copy(tmpBytes, rhs) r.InstanceId = tmpBytes } + if rhs := m.Capabilities; rhs != nil { + tmpContainer := make([]uint32, len(rhs)) + copy(tmpContainer, rhs) + r.Capabilities = tmpContainer + } if len(m.unknownFields) > 0 { r.unknownFields = make([]byte, len(m.unknownFields)) copy(r.unknownFields, m.unknownFields) @@ -240,6 +245,15 @@ func (this *PeerInfo) EqualVT(that *PeerInfo) bool { if string(this.InstanceId) != string(that.InstanceId) { return false } + if len(this.Capabilities) != len(that.Capabilities) { + return false + } + for i, vx := range this.Capabilities { + vy := that.Capabilities[i] + if vx != vy { + return false + } + } return string(this.unknownFields) == string(that.unknownFields) } @@ -464,6 +478,15 @@ func (m *PeerInfo) MarshalToSizedBufferVT(dAtA []byte) (int, error) { i -= len(m.unknownFields) copy(dAtA[i:], m.unknownFields) } + if len(m.Capabilities) > 0 { + for iNdEx := len(m.Capabilities) - 1; iNdEx >= 0; iNdEx-- { + i -= 4 + binary.LittleEndian.PutUint32(dAtA[i:], uint32(m.Capabilities[iNdEx])) + } + i = protohelpers.EncodeVarint(dAtA, i, uint64(len(m.Capabilities)*4)) + i-- + dAtA[i] = 0x2a + } if len(m.InstanceId) > 0 { i -= len(m.InstanceId) copy(dAtA[i:], m.InstanceId) @@ -742,6 +765,9 @@ func (m *PeerInfo) SizeVT() (n int) { if l > 0 { n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) } + if len(m.Capabilities) > 0 { + n += 1 + protohelpers.SizeOfVarint(uint64(len(m.Capabilities)*4)) + len(m.Capabilities)*4 + } n += len(m.unknownFields) return n } @@ -1104,6 +1130,58 @@ func (m *PeerInfo) UnmarshalVT(dAtA []byte) error { m.InstanceId = []byte{} } iNdEx = postIndex + case 5: + if wireType == 5 { + var v uint32 + if (iNdEx + 4) > l { + return io.ErrUnexpectedEOF + } + v = uint32(binary.LittleEndian.Uint32(dAtA[iNdEx:])) + iNdEx += 4 + m.Capabilities = append(m.Capabilities, v) + } else if wireType == 2 { + var packedLen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + packedLen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if packedLen < 0 { + return protohelpers.ErrInvalidLength + } + postIndex := iNdEx + packedLen + if postIndex < 0 { + return protohelpers.ErrInvalidLength + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + var elementCount int + elementCount = packedLen / 4 + if elementCount != 0 && len(m.Capabilities) == 0 { + m.Capabilities = make([]uint32, 0, elementCount) + } + for iNdEx < postIndex { + var v uint32 + if (iNdEx + 4) > l { + return io.ErrUnexpectedEOF + } + v = uint32(binary.LittleEndian.Uint32(dAtA[iNdEx:])) + iNdEx += 4 + m.Capabilities = append(m.Capabilities, v) + } + } else { + return fmt.Errorf("proto: wrong wireType = %d for field Capabilities", wireType) + } default: iNdEx = preIndex skippy, err := protohelpers.Skip(dAtA[iNdEx:]) diff --git a/misc/proto/cluster_bootstrap.proto b/misc/proto/cluster_bootstrap.proto index c2b4483133..b0c1a32ef3 100644 --- a/misc/proto/cluster_bootstrap.proto +++ b/misc/proto/cluster_bootstrap.proto @@ -41,6 +41,7 @@ message PeerInfo { string raft_address = 2; string service_address = 3; bytes instance_id = 4; // 16-byte UUID identifying this peer's WAL/PVC incarnation; required + repeated fixed32 capabilities = 5; // capabilities supported by this binary } message JoinAsLearnerRequest { From 99aba29c40293ab3c5a7bc1afa256efc5a026191 Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Thu, 22 Oct 2026 17:14:15 +0200 Subject: [PATCH 018/127] feat(consensus): auto-activate converged capabilities --- .../subsystems/consensus/capabilities.md | 13 +++-- .../bootstrap/cluster_policy_reconciler.go | 24 ++++++++- .../cluster_policy_reconciler_test.go | 10 ++-- internal/bootstrap/module.go | 49 ++++++++++++++++--- internal/infra/capabilities/capabilities.go | 29 +++++++++++ .../infra/capabilities/capabilities_test.go | 17 +++++++ internal/infra/node/node.go | 7 +++ internal/infra/node/observer.go | 5 ++ 8 files changed, 137 insertions(+), 17 deletions(-) diff --git a/docs/technical/architecture/subsystems/consensus/capabilities.md b/docs/technical/architecture/subsystems/consensus/capabilities.md index c7562c35e2..018ff4592e 100644 --- a/docs/technical/architecture/subsystems/consensus/capabilities.md +++ b/docs/technical/architecture/subsystems/consensus/capabilities.md @@ -119,8 +119,11 @@ Raft and applied to every member's peer store; a local restart-time upsert is not sufficient to activate a capability for admission. A fresh WAL instead uses `JoinAsLearner` and `ConfChangeAddLearnerNode`. -The leader's existing cluster-policy reconciler does not activate a capability -automatically. A future rollout can add an explicit `CapabilityActivation` -entry once every current voter and learner has refreshed its membership proof. -Until then the policy contains no active capability and this binary declares no -protocol feature. +The cluster-policy reconciler activates a capability automatically once every +current voter and learner has refreshed membership with that capability. The +candidate set is the intersection of the local binary registry and all +committed membership evidence; an empty or incomplete membership set never +activates anything. Membership commits trigger reconciliation immediately, and +the periodic retry remains only as a self-healing fallback for leadership or +proposal failures. Once committed, an activation is monotonic and a new member +that does not advertise it is rejected. diff --git a/internal/bootstrap/cluster_policy_reconciler.go b/internal/bootstrap/cluster_policy_reconciler.go index ec2e046940..e5440e9bb1 100644 --- a/internal/bootstrap/cluster_policy_reconciler.go +++ b/internal/bootstrap/cluster_policy_reconciler.go @@ -23,6 +23,7 @@ const clusterPolicyReconcileInterval = 1 * time.Second type ClusterPolicyReconciler struct { reconcile func(context.Context) interval time.Duration + trigger chan struct{} w worker.Worker } @@ -33,16 +34,37 @@ func NewClusterPolicyReconciler(reconcile func(context.Context)) *ClusterPolicyR return &ClusterPolicyReconciler{ reconcile: reconcile, interval: clusterPolicyReconcileInterval, + trigger: make(chan struct{}, 1), w: worker.New(), } } +// Trigger asks the running reconciler to converge immediately. The signal is +// coalesced because several ConfChanges may be committed in one Ready batch. +func (r *ClusterPolicyReconciler) Trigger() { + select { + case r.trigger <- struct{}{}: + default: + } +} + // Start runs an immediate reconciliation, then one per interval, until Stop. func (r *ClusterPolicyReconciler) Start() { r.w.Run(func(stop <-chan struct{}) { ctx := worker.ContextFromStop(stop) r.reconcile(ctx) - worker.RunTicker(stop, r.interval, func() { r.reconcile(ctx) }) + ticker := time.NewTicker(r.interval) + defer ticker.Stop() + for { + select { + case <-stop: + return + case <-ticker.C: + r.reconcile(ctx) + case <-r.trigger: + r.reconcile(ctx) + } + } }) } diff --git a/internal/bootstrap/cluster_policy_reconciler_test.go b/internal/bootstrap/cluster_policy_reconciler_test.go index 94187306c4..81a7109ac7 100644 --- a/internal/bootstrap/cluster_policy_reconciler_test.go +++ b/internal/bootstrap/cluster_policy_reconciler_test.go @@ -102,7 +102,7 @@ func TestReconcileClusterPolicy(t *testing.T) { admission := &fakeAdmission{} reconcileClusterPolicy(context.Background(), admission, newReconcilerTestStore(t), validBaseConfig(), - func() bool { return false }, logging.Testing()) + func() bool { return false }, nil, logging.Testing()) require.Zero(t, admission.callCount(), "a non-leader must not propose") }) @@ -112,7 +112,7 @@ func TestReconcileClusterPolicy(t *testing.T) { admission := &fakeAdmission{} reconcileClusterPolicy(context.Background(), admission, newReconcilerTestStore(t), validBaseConfig(), - func() bool { return true }, logging.Testing()) + func() bool { return true }, nil, logging.Testing()) require.Equal(t, 1, admission.callCount(), "a leader with no committed policy must propose") }) @@ -132,7 +132,7 @@ func TestReconcileClusterPolicy(t *testing.T) { require.NoError(t, batch.Commit()) admission := &fakeAdmission{} - reconcileClusterPolicy(context.Background(), admission, store, cfg, func() bool { return true }, logging.Testing()) + reconcileClusterPolicy(context.Background(), admission, store, cfg, func() bool { return true }, nil, logging.Testing()) require.Zero(t, admission.callCount(), "an already-applied revision must not be re-proposed") }) @@ -151,7 +151,7 @@ func TestReconcileClusterPolicy(t *testing.T) { require.NoError(t, batch.Commit()) admission := &fakeAdmission{} - reconcileClusterPolicy(context.Background(), admission, store, cfg, func() bool { return true }, logging.Testing()) + reconcileClusterPolicy(context.Background(), admission, store, cfg, func() bool { return true }, nil, logging.Testing()) require.Zero(t, admission.callCount(), "a same-revision/different-payload conflict must not be re-proposed") }) @@ -170,7 +170,7 @@ func TestReconcileClusterPolicy(t *testing.T) { require.NoError(t, batch.Commit()) admission := &fakeAdmission{} - reconcileClusterPolicy(context.Background(), admission, store, cfg, func() bool { return true }, logging.Testing()) + reconcileClusterPolicy(context.Background(), admission, store, cfg, func() bool { return true }, nil, logging.Testing()) require.Zero(t, admission.callCount(), "a newer applied revision must not be overwritten") }) diff --git a/internal/bootstrap/module.go b/internal/bootstrap/module.go index 1b06df663b..89a24c959a 100644 --- a/internal/bootstrap/module.go +++ b/internal/bootstrap/module.go @@ -674,7 +674,7 @@ func Module() fx.Option { return } - reconcileClusterPolicy(ctx, admission, store, cfg, raftNode.IsLeader, logger) + reconcileClusterPolicy(ctx, admission, store, cfg, raftNode.IsLeader, raftNode.CapabilityMembers, logger) }) }, ), @@ -903,12 +903,13 @@ func Module() fx.Option { RequestShutdown: shutdownRequester(shutdowner), })) }, - // Wire Observer: handle LeadershipChange and LeaderReady events. - // ConfChange events are no longer dispatched here — Membership - // owns the transport / service-pool wiring and reacts directly - // to peer mutations (EN-1413). + // Wire Observer: handle leadership, readiness and capability + // membership convergence events. Membership still owns transport + // and service-pool wiring; the capability event only wakes the + // policy reconciler. func( n *node.Node, + reconciler *ClusterPolicyReconciler, store *dal.Store, builder *plan.Builder, cfg Config, @@ -920,6 +921,8 @@ func Module() fx.Option { ) { n.SetObserver(node.NewObserver(func(event any) { switch e := event.(type) { + case node.CapabilityMembershipChangedEvent: + reconciler.Trigger() case node.LeadershipChangeEvent: // Disk evidence is leader-term local. Invalidate it inline, // before admission can observe the new leadership term. @@ -1725,7 +1728,7 @@ func proposeClusterConfigIfNeeded(parent context.Context, n plan.Proposer, build // through AdmitClusterPolicy as an operational SetClusterPolicyProposal. // The write-readiness gate exempts this non-business proposal so it is never // blocked by its own cluster-policy gate. -func reconcileClusterPolicy(ctx context.Context, admission ctrl.Admission, store *dal.Store, cfg Config, isLeader func() bool, logger logging.Logger) { +func reconcileClusterPolicy(ctx context.Context, admission ctrl.Admission, store *dal.Store, cfg Config, isLeader func() bool, readMembers func() []capabilities.Member, logger logging.Logger) { if !isLeader() { return } @@ -1747,8 +1750,42 @@ func reconcileClusterPolicy(ctx context.Context, admission ctrl.Admission, store MetadataMaxEntityBytes: cfg.MetadataMaxEntityBytes, MetadataMaxCommandBytes: cfg.MetadataMaxCommandBytes, } + activationChanged := false + if applied != nil { + desired.CapabilityActivations = make([]*commonpb.CapabilityActivation, 0, len(applied.GetCapabilityActivations())) + for _, activation := range applied.GetCapabilityActivations() { + desired.CapabilityActivations = append(desired.CapabilityActivations, activation.CloneVT()) + } + } + if readMembers != nil { + ready, err := capabilities.Ready(capabilities.SupportedIDs(), readMembers()) + if err != nil { + logger.WithFields(map[string]any{"error": err}).Errorf("Computing converged capability set") + return + } + for _, id := range ready { + found := false + for _, activation := range desired.CapabilityActivations { + if activation != nil && activation.GetId() == uint32(id) { + if !activation.GetActive() { + activation.Active = true + activationChanged = true + } + found = true + break + } + } + if !found { + desired.CapabilityActivations = append(desired.CapabilityActivations, &commonpb.CapabilityActivation{Id: uint32(id), Active: true}) + activationChanged = true + } + } + } appliedRev := applied.GetRevision() + if activationChanged && desired.GetRevision() <= appliedRev { + desired.Revision = appliedRev + 1 + } switch { case appliedRev > desired.GetRevision(): diff --git a/internal/infra/capabilities/capabilities.go b/internal/infra/capabilities/capabilities.go index 1dc22e7626..292b57c093 100644 --- a/internal/infra/capabilities/capabilities.go +++ b/internal/infra/capabilities/capabilities.go @@ -137,6 +137,35 @@ func Normalize(ids []ID) ([]ID, error) { return result, nil } +// Ready returns the capabilities implemented by this binary and acknowledged +// by every current cluster member. Membership evidence is the acknowledgement +// recorded by JoinAsLearner/RefreshMembership; an empty member set is never +// considered ready. +func Ready(supported []ID, members []Member) ([]ID, error) { + if len(members) == 0 { + return nil, nil + } + supported, err := Normalize(supported) + if err != nil { + return nil, err + } + ready := make([]ID, 0, len(supported)) + for _, id := range supported { + acknowledged := true + for _, member := range members { + if !slices.Contains(member.Capabilities, id) { + acknowledged = false + break + } + } + if acknowledged { + ready = append(ready, id) + } + } + + return ready, nil +} + // Evaluate is pure. Reachability is reported separately from support: a // temporarily unreachable voter is still a blocker for activation, while a // learner is included because it must replay every committed entry. diff --git a/internal/infra/capabilities/capabilities_test.go b/internal/infra/capabilities/capabilities_test.go index 0d94d16b36..2afe65bd49 100644 --- a/internal/infra/capabilities/capabilities_test.go +++ b/internal/infra/capabilities/capabilities_test.go @@ -48,3 +48,20 @@ func TestFromPolicyRoundTrip(t *testing.T) { require.True(t, state.Activations[99]) require.NoError(t, state.Validate()) } + +func TestReadyReturnsIntersectionAcrossMembers(t *testing.T) { + members := []Member{ + {NodeID: 1, Role: Voter, Capabilities: []ID{1, 2}}, + {NodeID: 2, Role: Learner, Capabilities: []ID{1}}, + } + + ready, err := Ready([]ID{2, 1}, members) + require.NoError(t, err) + require.Equal(t, []ID{1}, ready) +} + +func TestReadyDoesNotActivateAnEmptyCluster(t *testing.T) { + ready, err := Ready([]ID{1}, nil) + require.NoError(t, err) + require.Empty(t, ready) +} diff --git a/internal/infra/node/node.go b/internal/infra/node/node.go index e015d6bce3..ff1bd04c01 100644 --- a/internal/infra/node/node.go +++ b/internal/infra/node/node.go @@ -1738,6 +1738,7 @@ func (node *Node) finishReady(result readyResult, stop chan struct{}) error { // callers waiting on ConfChange commit (AddLearner, PromoteLearner, etc.) // don't resume before the WAL is consistent. var pendingFutures []committedConfChangeFuture + capabilityMembershipChanged := false for _, committed := range result.confChanges { cc := committed.change @@ -1789,6 +1790,7 @@ func (node *Node) finishReady(result readyResult, stop chan struct{}) error { if err != nil { return err } + capabilityMembershipChanged = true pending, err := node.takePendingConfChange(cc, committed.index) if err != nil { @@ -1807,6 +1809,11 @@ func (node *Node) finishReady(result readyResult, stop chan struct{}) error { }) } } + if capabilityMembershipChanged { + if node.observer != nil { + node.observer.Emit(CapabilityMembershipChangedEvent{}) + } + } // If the ConfState changed (e.g. a learner was added), update the WAL // snapshot's ConfState immediately. Without this, etcd/raft would send diff --git a/internal/infra/node/observer.go b/internal/infra/node/observer.go index ecdc3bc28f..5a8f1a3a05 100644 --- a/internal/infra/node/observer.go +++ b/internal/infra/node/observer.go @@ -18,6 +18,11 @@ type LeaderReadyEvent struct { Context context.Context } +// CapabilityMembershipChangedEvent is emitted after a committed membership +// change has updated the local capability evidence cache. A leader can use it +// to immediately reconcile capability activations without polling the cache. +type CapabilityMembershipChangedEvent struct{} + // EventHandler is a callback invoked synchronously for each emitted event. // Consumers use a type switch on the event to handle concrete types. type EventHandler func(event any) From 542c8920c0045909c3571ca4ca48b4bcadece762 Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Thu, 22 Oct 2026 17:19:43 +0200 Subject: [PATCH 019/127] test(consensus): guard capability rollout contracts --- .../cluster_policy_reconciler_test.go | 77 +++++++++++++++++++ internal/bootstrap/module.go | 6 +- scripts/agent-check-pr | 11 +++ scripts/agentcheckpr/selector_test.go | 5 ++ scripts/check-capability-contract | 34 ++++++++ 5 files changed, 132 insertions(+), 1 deletion(-) create mode 100755 scripts/check-capability-contract diff --git a/internal/bootstrap/cluster_policy_reconciler_test.go b/internal/bootstrap/cluster_policy_reconciler_test.go index 81a7109ac7..72e2cd9932 100644 --- a/internal/bootstrap/cluster_policy_reconciler_test.go +++ b/internal/bootstrap/cluster_policy_reconciler_test.go @@ -14,6 +14,7 @@ import ( logging "github.com/formancehq/go-libs/v5/pkg/observe/log" "github.com/formancehq/ledger/v3/internal/domain" + "github.com/formancehq/ledger/v3/internal/infra/capabilities" "github.com/formancehq/ledger/v3/internal/infra/state" "github.com/formancehq/ledger/v3/internal/proto/commonpb" "github.com/formancehq/ledger/v3/internal/proto/servicepb" @@ -47,6 +48,29 @@ func TestClusterPolicyReconciler_TicksUntilStopped(t *testing.T) { }) } +func TestClusterPolicyReconciler_TriggerIsCoalescedAndRaceSafe(t *testing.T) { + t.Parallel() + + var count atomic.Int32 + r := NewClusterPolicyReconciler(func(context.Context) { count.Add(1) }) + r.interval = time.Hour + r.Start() + require.Eventually(t, func() bool { return count.Load() >= 1 }, time.Second, time.Millisecond) + + var wg sync.WaitGroup + for range 100 { + wg.Add(1) + go func() { + defer wg.Done() + r.Trigger() + }() + } + wg.Wait() + require.Eventually(t, func() bool { return count.Load() >= 2 }, time.Second, time.Millisecond, + "a membership event must wake reconciliation without waiting for the ticker") + r.Stop() +} + type fakeAdmission struct { mu sync.Mutex calls int @@ -175,3 +199,56 @@ func TestReconcileClusterPolicy(t *testing.T) { require.Zero(t, admission.callCount(), "a newer applied revision must not be overwritten") }) } + +func TestReconcileClusterPolicyAutoActivatesOnlyAfterConvergence(t *testing.T) { + t.Parallel() + + cfg := validBaseConfig() + store := newReconcilerTestStore(t) + batch := store.OpenWriteSession() + require.NoError(t, state.SaveClusterPolicy(batch, &commonpb.ClusterPolicy{ + Revision: cfg.ClusterPolicyRevision, + IdempotencyTtlMicros: uint64(cfg.IdempotencyTTL.Microseconds()), + QueryCheckpointLimit: cfg.QueryCheckpointLimit, + })) + require.NoError(t, batch.Commit()) + + readMembers := func() []capabilities.Member { + return []capabilities.Member{ + {NodeID: 1, Role: capabilities.Voter, Capabilities: []capabilities.ID{7}}, + {NodeID: 2, Role: capabilities.Learner, Capabilities: []capabilities.ID{7}}, + } + } + admission := &fakeAdmission{} + reconcileClusterPolicyWithRegistry(context.Background(), admission, store, cfg, + func() bool { return true }, func() []capabilities.ID { return []capabilities.ID{7} }, readMembers, logging.Testing()) + + require.Equal(t, 1, admission.callCount()) + require.Equal(t, uint64(cfg.ClusterPolicyRevision+1), admission.lastPolicy.GetRevision()) + require.Equal(t, []*commonpb.CapabilityActivation{{Id: 7, Active: true}}, admission.lastPolicy.GetCapabilityActivations()) +} + +func TestReconcileClusterPolicyDoesNotActivateBeforeConvergence(t *testing.T) { + t.Parallel() + + cfg := validBaseConfig() + store := newReconcilerTestStore(t) + batch := store.OpenWriteSession() + require.NoError(t, state.SaveClusterPolicy(batch, &commonpb.ClusterPolicy{ + Revision: cfg.ClusterPolicyRevision, + IdempotencyTtlMicros: uint64(cfg.IdempotencyTTL.Microseconds()), + QueryCheckpointLimit: cfg.QueryCheckpointLimit, + })) + require.NoError(t, batch.Commit()) + + admission := &fakeAdmission{} + reconcileClusterPolicyWithRegistry(context.Background(), admission, store, cfg, + func() bool { return true }, func() []capabilities.ID { return []capabilities.ID{7} }, func() []capabilities.Member { + return []capabilities.Member{ + {NodeID: 1, Role: capabilities.Voter, Capabilities: []capabilities.ID{7}}, + {NodeID: 2, Role: capabilities.Learner}, + } + }, logging.Testing()) + + require.Zero(t, admission.callCount()) +} diff --git a/internal/bootstrap/module.go b/internal/bootstrap/module.go index 89a24c959a..b3909e92fd 100644 --- a/internal/bootstrap/module.go +++ b/internal/bootstrap/module.go @@ -1729,6 +1729,10 @@ func proposeClusterConfigIfNeeded(parent context.Context, n plan.Proposer, build // The write-readiness gate exempts this non-business proposal so it is never // blocked by its own cluster-policy gate. func reconcileClusterPolicy(ctx context.Context, admission ctrl.Admission, store *dal.Store, cfg Config, isLeader func() bool, readMembers func() []capabilities.Member, logger logging.Logger) { + reconcileClusterPolicyWithRegistry(ctx, admission, store, cfg, isLeader, capabilities.SupportedIDs, readMembers, logger) +} + +func reconcileClusterPolicyWithRegistry(ctx context.Context, admission ctrl.Admission, store *dal.Store, cfg Config, isLeader func() bool, readSupported func() []capabilities.ID, readMembers func() []capabilities.Member, logger logging.Logger) { if !isLeader() { return } @@ -1758,7 +1762,7 @@ func reconcileClusterPolicy(ctx context.Context, admission ctrl.Admission, store } } if readMembers != nil { - ready, err := capabilities.Ready(capabilities.SupportedIDs(), readMembers()) + ready, err := capabilities.Ready(readSupported(), readMembers()) if err != nil { logger.WithFields(map[string]any{"error": err}).Errorf("Computing converged capability set") return diff --git a/scripts/agent-check-pr b/scripts/agent-check-pr index bff0426ef1..3584016aaa 100755 --- a/scripts/agent-check-pr +++ b/scripts/agent-check-pr @@ -79,6 +79,7 @@ needs_schemathesis=false needs_operator=false needs_protocol=false needs_proto_lint=false +needs_capability_contract=false has_change=false # Gate selection runs under the host bash, before the Nix re-exec below; macOS # ships bash 3.2, where "${arr[@]}" on an empty array is an unbound-variable @@ -102,6 +103,10 @@ classify_path() { # Service contract or revision edits are classified against the base by # scripts/checkprotocolversion, independently of the gates below. case "$path" in + misc/proto/common.proto|misc/proto/raft_cmd.proto) + needs_protocol=true + needs_capability_contract=true + ;; misc/proto/*|pkg/grpcprotocol/*) needs_protocol=true ;; esac case "$path" in @@ -243,6 +248,9 @@ fi if [[ "$needs_proto_lint" == true ]]; then gates+=(proto-lint) fi +if [[ "$needs_capability_contract" == true ]]; then + gates+=(capability-contract) +fi if [[ "$mode" == list ]]; then printf '%s\n' "${gates[@]}" @@ -307,6 +315,9 @@ if [[ "$mode" == normalize ]]; then fi echo "==> agent-check-pr: selected local gates: ${gates[*]}" +if [[ "$needs_capability_contract" == true ]]; then + bash "$SCRIPT_DIR/check-capability-contract" "$base_sha" +fi if [[ "$needs_protocol" == true ]]; then # Cheap and decisive: run before the long suites. PROTOCOL_BASE_SHA="$base_sha" just check-protocol-version diff --git a/scripts/agentcheckpr/selector_test.go b/scripts/agentcheckpr/selector_test.go index d0b1901bb6..e70995ac1a 100644 --- a/scripts/agentcheckpr/selector_test.go +++ b/scripts/agentcheckpr/selector_test.go @@ -83,6 +83,11 @@ func TestSelectsLocalValidationGatesFromCompleteDiff(t *testing.T) { paths: []string{"tools/buf-plugin-ledger/main.go"}, expected: []string{"pre-commit", "agent-check", "test-tooling", "proto-lint"}, }, + { + name: "Raft protocol change requires capability contract", + paths: []string{"misc/proto/common.proto"}, + expected: []string{"agent-check-full", "test-e2e", "protocol-revision", "capability-contract"}, + }, { name: "protocol revision only", paths: []string{"pkg/grpcprotocol/protocol.go"}, diff --git a/scripts/check-capability-contract b/scripts/check-capability-contract new file mode 100755 index 0000000000..970f7e62b9 --- /dev/null +++ b/scripts/check-capability-contract @@ -0,0 +1,34 @@ +#!/usr/bin/env bash +set -euo pipefail + +base_sha=${1:-${AI_REVIEW_BASE_SHA:-}} +if [[ -z "$base_sha" ]]; then + echo "check-capability-contract: review base SHA is required" >&2 + exit 1 +fi + +mapfile -t changed < <( + git diff --name-only "$base_sha" -- + git ls-files --others --exclude-standard +) +protocol_changed=false +contract_changed=false +for path in "${changed[@]}"; do + case "$path" in + misc/proto/common.proto|misc/proto/raft_cmd.proto) + protocol_changed=true + ;; + internal/infra/capabilities/*|docs/technical/architecture/subsystems/consensus/capabilities.md) + contract_changed=true + ;; + esac +done + +if [[ "$protocol_changed" == true && "$contract_changed" != true ]]; then + echo "CAPABILITY_CONTRACT=FAIL" >&2 + echo "A Raft/FSM protocol schema changed without a capability registry or consensus capability-document change." >&2 + echo "If the change is backward-compatible, document why in the capability document; otherwise add an ID, derivation, admission/replay tests, and rollout documentation." >&2 + exit 1 +fi + +echo "CAPABILITY_CONTRACT=PASS" From c840bacd87a4994e17f08dba2953403713bf8969 Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Thu, 22 Oct 2026 17:29:28 +0200 Subject: [PATCH 020/127] fix(consensus): close capability rollout review gaps --- .../subsystems/consensus/capabilities.md | 14 +++++++++++--- internal/infra/capabilities/capabilities.go | 6 ++++++ .../infra/capabilities/capabilities_test.go | 19 +++++++++++++++++++ scripts/agent-check-pr | 11 +++++++++++ scripts/agentcheckpr/selector_test.go | 5 +++++ scripts/check-capability-contract | 5 +++++ 6 files changed, 57 insertions(+), 3 deletions(-) diff --git a/docs/technical/architecture/subsystems/consensus/capabilities.md b/docs/technical/architecture/subsystems/consensus/capabilities.md index 018ff4592e..e43fe8d015 100644 --- a/docs/technical/architecture/subsystems/consensus/capabilities.md +++ b/docs/technical/architecture/subsystems/consensus/capabilities.md @@ -21,13 +21,21 @@ that depends on a capability but omits its declaration is invalid. The pure decision contract in `internal/infra/capabilities` evaluates every current voter and learner. A member is represented by `(node_id, instance_id)`; an identity refresh is a new incarnation and cannot silently inherit evidence -from the old one. Reachability and support are separate: an unreachable member -blocks activation just as an incapable member does. Once a capability is -active, every new membership announcement must include that capability. +from the old one. Capability activation uses committed support evidence, not a +live health poll: a member that has already acknowledged the capability may be +temporarily unreachable when the activation is committed. Reachability still +blocks admission of capability-dependent writes until that member is reachable +again. Once a capability is active, every new membership announcement must +include that capability. A proposal that declares a capability is admitted only when that capability is active and every member advertises it. Proposals with no declared capability remain unaffected. FSM apply never consults node-local capability flags. +Membership evidence is an authenticated cluster fact, not a proof of code +inspection: a node can only cause activation for IDs in the leader's local +registry, and malformed (zero, duplicate, or unsorted) declarations are +rejected. Operators must therefore treat a compromised binary as outside the +trust model of this barrier. ## Capabilities that change FSM semantics diff --git a/internal/infra/capabilities/capabilities.go b/internal/infra/capabilities/capabilities.go index 292b57c093..7883a378f1 100644 --- a/internal/infra/capabilities/capabilities.go +++ b/internal/infra/capabilities/capabilities.go @@ -64,6 +64,9 @@ func ValidateMembers(members []Member) error { if previous, ok := seen[member.NodeID]; ok && !bytes.Equal(previous, identity) { return fmt.Errorf("%w: node %d has multiple incarnations", ErrInvalidState, member.NodeID) } + if _, err := Normalize(member.Capabilities); err != nil { + return fmt.Errorf("%w: node %d has invalid capability evidence: %w", ErrInvalidState, member.NodeID, err) + } seen[member.NodeID] = identity } @@ -149,6 +152,9 @@ func Ready(supported []ID, members []Member) ([]ID, error) { if err != nil { return nil, err } + if err := ValidateMembers(members); err != nil { + return nil, err + } ready := make([]ID, 0, len(supported)) for _, id := range supported { acknowledged := true diff --git a/internal/infra/capabilities/capabilities_test.go b/internal/infra/capabilities/capabilities_test.go index 2afe65bd49..4e7ce06481 100644 --- a/internal/infra/capabilities/capabilities_test.go +++ b/internal/infra/capabilities/capabilities_test.go @@ -65,3 +65,22 @@ func TestReadyDoesNotActivateAnEmptyCluster(t *testing.T) { require.NoError(t, err) require.Empty(t, ready) } + +func TestReadyRejectsMalformedMemberEvidence(t *testing.T) { + _, err := Ready([]ID{1}, []Member{{ + NodeID: 1, + Role: Voter, + Capabilities: []ID{1, 1}, + }}) + require.ErrorContains(t, err, "invalid capability evidence") +} + +func TestReadyIgnoresUnsupportedAdvertisedIDs(t *testing.T) { + ready, err := Ready([]ID{1}, []Member{{ + NodeID: 1, + Role: Voter, + Capabilities: []ID{1, 99}, + }}) + require.NoError(t, err) + require.Equal(t, []ID{1}, ready) +} diff --git a/scripts/agent-check-pr b/scripts/agent-check-pr index 3584016aaa..0fe460e55d 100755 --- a/scripts/agent-check-pr +++ b/scripts/agent-check-pr @@ -113,6 +113,17 @@ classify_path() { misc/proto/*|tools/buf-plugin-ledger/*) needs_proto_lint=true ;; esac + # A protocol-semantic change does not have to alter the protobuf schema. + # Require the capability contract gate for production FSM/admission code + # as well, while allowing focused tests to evolve independently. + case "$path" in + internal/domain/processing/*.go|internal/application/admission/*.go|internal/infra/plan/*.go|internal/infra/membership/*.go|internal/infra/node/*.go) + if [[ "$path" != *_test.go ]]; then + needs_capability_contract=true + fi + ;; + esac + case "$status" in A|D|M) ;; *) needs_full=true ;; diff --git a/scripts/agentcheckpr/selector_test.go b/scripts/agentcheckpr/selector_test.go index e70995ac1a..a2972a220c 100644 --- a/scripts/agentcheckpr/selector_test.go +++ b/scripts/agentcheckpr/selector_test.go @@ -88,6 +88,11 @@ func TestSelectsLocalValidationGatesFromCompleteDiff(t *testing.T) { paths: []string{"misc/proto/common.proto"}, expected: []string{"agent-check-full", "test-e2e", "protocol-revision", "capability-contract"}, }, + { + name: "FSM behavior change requires capability contract", + paths: []string{"internal/domain/processing/processor.go"}, + expected: []string{"agent-check-full", "capability-contract"}, + }, { name: "protocol revision only", paths: []string{"pkg/grpcprotocol/protocol.go"}, diff --git a/scripts/check-capability-contract b/scripts/check-capability-contract index 970f7e62b9..0c3c653b2e 100755 --- a/scripts/check-capability-contract +++ b/scripts/check-capability-contract @@ -18,6 +18,11 @@ for path in "${changed[@]}"; do misc/proto/common.proto|misc/proto/raft_cmd.proto) protocol_changed=true ;; + internal/domain/processing/*.go|internal/application/admission/*.go|internal/infra/plan/*.go|internal/infra/membership/*.go|internal/infra/node/*.go) + if [[ "$path" != *_test.go ]]; then + protocol_changed=true + fi + ;; internal/infra/capabilities/*|docs/technical/architecture/subsystems/consensus/capabilities.md) contract_changed=true ;; From 179248226a1aa967e75146340cc4d618160a1af4 Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Thu, 22 Oct 2026 17:33:28 +0200 Subject: [PATCH 021/127] refactor(consensus): make node learner join capability-aware --- internal/application/membership/membership.go | 2 +- internal/infra/node/add_learner_classify_test.go | 2 +- internal/infra/node/node.go | 11 +++-------- 3 files changed, 5 insertions(+), 10 deletions(-) diff --git a/internal/application/membership/membership.go b/internal/application/membership/membership.go index 6879c1862b..045afbbb18 100644 --- a/internal/application/membership/membership.go +++ b/internal/application/membership/membership.go @@ -141,7 +141,7 @@ func (s *Service) addLearner(ctx context.Context, nodeID uint64, raftAddr, servi var err error if bootJoin { - err = s.node.JoinAsLearnerWithCapabilities(ctx, nodeID, raftAddr, serviceAddr, instanceID, capabilityIDs) + err = s.node.JoinAsLearner(ctx, nodeID, raftAddr, serviceAddr, instanceID, capabilityIDs) } else { err = s.node.AddLearnerWithCapabilities(ctx, nodeID, raftAddr, serviceAddr, instanceID, capabilityIDs) } diff --git a/internal/infra/node/add_learner_classify_test.go b/internal/infra/node/add_learner_classify_test.go index 48ecbca310..80f5ce504a 100644 --- a/internal/infra/node/add_learner_classify_test.go +++ b/internal/infra/node/add_learner_classify_test.go @@ -12,7 +12,7 @@ func TestAddLearnerPathsRejectInvalidInstanceIDBeforeRaftAccess(t *testing.T) { n := &Node{} require.ErrorContains(t, n.AddLearner(context.Background(), 1, "r:1", "s:1", nil), "instance_id must be 16 bytes") - require.ErrorContains(t, n.JoinAsLearner(context.Background(), 1, "r:1", "s:1", []byte("short")), "instance_id must be 16 bytes") + require.ErrorContains(t, n.JoinAsLearner(context.Background(), 1, "r:1", "s:1", []byte("short"), nil), "instance_id must be 16 bytes") } // TestClassifyExistingLearner locks in the EN-1436 fail-fast decision. diff --git a/internal/infra/node/node.go b/internal/infra/node/node.go index ff1bd04c01..92fab63483 100644 --- a/internal/infra/node/node.go +++ b/internal/infra/node/node.go @@ -3218,14 +3218,9 @@ func (node *Node) WithStableDrainMembership(ctx context.Context, fn func() error // JoinAsLearner is the bootstrap admission path for a process with an empty // WAL. It deliberately keeps EN-1436's stricter stale-progress handling -// separate from administrative AddLearner retries. -func (node *Node) JoinAsLearner(ctx context.Context, nodeID uint64, raftAddr, serviceAddr string, instanceID []byte) error { - return node.JoinAsLearnerWithCapabilities(ctx, nodeID, raftAddr, serviceAddr, instanceID, nil) -} - -// JoinAsLearnerWithCapabilities is the bootstrap admission variant carrying -// the joining node's capability proof. -func (node *Node) JoinAsLearnerWithCapabilities(ctx context.Context, nodeID uint64, raftAddr, serviceAddr string, instanceID []byte, capabilityIDs []uint32) error { +// separate from administrative AddLearner retries and carries the joining +// node's capability evidence. +func (node *Node) JoinAsLearner(ctx context.Context, nodeID uint64, raftAddr, serviceAddr string, instanceID []byte, capabilityIDs []uint32) error { return node.addLearnerWithCapabilities(ctx, nodeID, raftAddr, serviceAddr, instanceID, capabilityIDs, true) } From 85bda994d55f111e2a56f6b6be8e07fb0dc7038a Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Thu, 22 Oct 2026 17:52:39 +0200 Subject: [PATCH 022/127] refactor(consensus): pass capability metadata without decode --- .../subsystems/consensus/capabilities.md | 10 ++- internal/application/admission/admission.go | 19 +---- .../admission/capability_policy_test.go | 7 +- .../cluster_policy_reconciler_test.go | 10 +-- internal/bootstrap/module.go | 4 +- .../bootstrap/node_hook_commit_drain_test.go | 8 +- internal/infra/capabilities/capabilities.go | 83 ++++++------------- .../infra/capabilities/capabilities_test.go | 32 +++---- .../node/force_remove_durability_test.go | 4 +- internal/infra/node/node.go | 8 +- internal/infra/node/proposal.go | 32 ++++--- internal/infra/plan/capability_policy.go | 15 +--- internal/infra/plan/capability_policy_test.go | 2 +- internal/infra/plan/runner.go | 29 ++----- 14 files changed, 93 insertions(+), 170 deletions(-) diff --git a/docs/technical/architecture/subsystems/consensus/capabilities.md b/docs/technical/architecture/subsystems/consensus/capabilities.md index e43fe8d015..f0ed1412a6 100644 --- a/docs/technical/architecture/subsystems/consensus/capabilities.md +++ b/docs/technical/architecture/subsystems/consensus/capabilities.md @@ -11,10 +11,12 @@ IDs. The fields are optional and therefore backward-readable by binaries that predate this foundation. IDs are never reused; semantic dependencies must be declared even when they do not add bytes to a proposal. -The final serialized proposal is the source of truth. A producer must rebuild -its plan first, then validate that the declared IDs exactly match -`required_capabilities`, immediately before enqueueing the proposal. A payload -that depends on a capability but omits its declaration is invalid. +The producer must rebuild its plan first, derive the capabilities from that +final plan, stamp `required_capabilities`, and pass the typed result alongside +the serialized proposal. Admission reuses that result without decoding the +proposal a second time. A payload that depends on a capability but omits its +declaration is invalid; the shared builder and stamping helper are the +enforcement boundary for producers. ## Admission and membership diff --git a/internal/application/admission/admission.go b/internal/application/admission/admission.go index 6ed7a72c53..88e0df0391 100644 --- a/internal/application/admission/admission.go +++ b/internal/application/admission/admission.go @@ -65,14 +65,7 @@ func (p capabilityPolicyProposer) Propose(ctx context.Context, proposal *node.Pr return nil, err } - final := new(raftcmdpb.Proposal) - if err := final.UnmarshalVT(proposal.Data()); err != nil { - return nil, fmt.Errorf("decode proposal for capability admission: %w", err) - } - required := make([]capabilities.ID, len(final.GetRequiredCapabilities())) - for i, id := range final.GetRequiredCapabilities() { - required[i] = capabilities.ID(id) - } + required := proposal.RequiredCapabilities() var members []capabilities.Member if p.readMembers != nil { members = p.readMembers() @@ -1057,20 +1050,16 @@ func (a *Admission) Barrier(ctx context.Context) (uint64, error) { // capability declaration before serialization. The no-op command created by // commands.NewCommand carries an empty ExecutionPlan, which is itself a // protocol dependency for replaying the envelope. - if err := capabilities.StampProposal(cmd); err != nil { - return 0, fmt.Errorf("stamping barrier capabilities: %w", err) - } - requiredCapabilities, err := capabilities.RequiredCapabilities(cmd) + required, err := capabilities.PrepareProposal(cmd) if err != nil { - return 0, fmt.Errorf("deriving barrier capabilities: %w", err) + return 0, fmt.Errorf("stamping barrier capabilities: %w", err) } proposalData, err := a.marshalCommand(ctx, cmd) if err != nil { return 0, fmt.Errorf("marshaling barrier command: %w", err) } - proposal := node.NewProposal(cmd.GetId(), proposalData) - proposal.RequireCapabilities(requiredCapabilities...) + proposal := node.NewProposal(cmd.GetId(), proposalData, required) // Lock the tracker to serialize the Increment with guarded proposals, // preventing preload boundary mismatches in the FSM. diff --git a/internal/application/admission/capability_policy_test.go b/internal/application/admission/capability_policy_test.go index f26e317b97..f2156cc1a9 100644 --- a/internal/application/admission/capability_policy_test.go +++ b/internal/application/admission/capability_policy_test.go @@ -31,9 +31,6 @@ type barrierTestProposer struct{} func (barrierTestProposer) InitialIndex() uint64 { return 1 } func (barrierTestProposer) Propose(_ context.Context, proposal *node.Proposal) (*futures.Future[state.ApplyResult], error) { - if err := capabilities.ValidateFinalProposal(proposal.Data(), proposal.DeclaredCapabilities()); err != nil { - return nil, err - } proposal.Resolve(nil, nil) future := futures.New[state.ApplyResult]() future.Resolve(state.ApplyResult{AppliedIndex: 1}, nil) @@ -67,7 +64,7 @@ func TestCapabilityPolicyProposerBlocksUnknownCapabilityAtFinalBoundary(t *testi } data, err := (&raftcmdpb.Proposal{RequiredCapabilities: []uint32{99}}).MarshalVT() require.NoError(t, err) - _, err = proposer.Propose(context.Background(), node.NewProposal(1, data)) + _, err = proposer.Propose(context.Background(), node.NewProposal(1, data, []capabilities.CapabilityID{99})) require.Error(t, err) require.ErrorIs(t, err, capabilities.ErrCapabilityUnsupported) require.False(t, delegate.called) @@ -84,7 +81,7 @@ func TestCapabilityPolicyProposerAllowsUnrelatedWrite(t *testing.T) { } data, err := (&raftcmdpb.Proposal{}).MarshalVT() require.NoError(t, err) - _, err = proposer.Propose(context.Background(), node.NewProposal(1, data)) + _, err = proposer.Propose(context.Background(), node.NewProposal(1, data, nil)) require.NoError(t, err) require.True(t, delegate.called) } diff --git a/internal/bootstrap/cluster_policy_reconciler_test.go b/internal/bootstrap/cluster_policy_reconciler_test.go index 72e2cd9932..a40b073da4 100644 --- a/internal/bootstrap/cluster_policy_reconciler_test.go +++ b/internal/bootstrap/cluster_policy_reconciler_test.go @@ -215,13 +215,13 @@ func TestReconcileClusterPolicyAutoActivatesOnlyAfterConvergence(t *testing.T) { readMembers := func() []capabilities.Member { return []capabilities.Member{ - {NodeID: 1, Role: capabilities.Voter, Capabilities: []capabilities.ID{7}}, - {NodeID: 2, Role: capabilities.Learner, Capabilities: []capabilities.ID{7}}, + {NodeID: 1, Role: capabilities.Voter, Capabilities: []capabilities.CapabilityID{7}}, + {NodeID: 2, Role: capabilities.Learner, Capabilities: []capabilities.CapabilityID{7}}, } } admission := &fakeAdmission{} reconcileClusterPolicyWithRegistry(context.Background(), admission, store, cfg, - func() bool { return true }, func() []capabilities.ID { return []capabilities.ID{7} }, readMembers, logging.Testing()) + func() bool { return true }, func() []capabilities.CapabilityID { return []capabilities.CapabilityID{7} }, readMembers, logging.Testing()) require.Equal(t, 1, admission.callCount()) require.Equal(t, uint64(cfg.ClusterPolicyRevision+1), admission.lastPolicy.GetRevision()) @@ -243,9 +243,9 @@ func TestReconcileClusterPolicyDoesNotActivateBeforeConvergence(t *testing.T) { admission := &fakeAdmission{} reconcileClusterPolicyWithRegistry(context.Background(), admission, store, cfg, - func() bool { return true }, func() []capabilities.ID { return []capabilities.ID{7} }, func() []capabilities.Member { + func() bool { return true }, func() []capabilities.CapabilityID { return []capabilities.CapabilityID{7} }, func() []capabilities.Member { return []capabilities.Member{ - {NodeID: 1, Role: capabilities.Voter, Capabilities: []capabilities.ID{7}}, + {NodeID: 1, Role: capabilities.Voter, Capabilities: []capabilities.CapabilityID{7}}, {NodeID: 2, Role: capabilities.Learner}, } }, logging.Testing()) diff --git a/internal/bootstrap/module.go b/internal/bootstrap/module.go index b3909e92fd..852141d7c1 100644 --- a/internal/bootstrap/module.go +++ b/internal/bootstrap/module.go @@ -1598,7 +1598,7 @@ func tryRefreshMembership(ctx context.Context, cfg Config, tlsCfg TLSConfig, log } } -func capabilityIDsAsUint32(ids []capabilities.ID) []uint32 { +func capabilityIDsAsUint32(ids []capabilities.CapabilityID) []uint32 { result := make([]uint32, len(ids)) for i, id := range ids { result[i] = uint32(id) @@ -1732,7 +1732,7 @@ func reconcileClusterPolicy(ctx context.Context, admission ctrl.Admission, store reconcileClusterPolicyWithRegistry(ctx, admission, store, cfg, isLeader, capabilities.SupportedIDs, readMembers, logger) } -func reconcileClusterPolicyWithRegistry(ctx context.Context, admission ctrl.Admission, store *dal.Store, cfg Config, isLeader func() bool, readSupported func() []capabilities.ID, readMembers func() []capabilities.Member, logger logging.Logger) { +func reconcileClusterPolicyWithRegistry(ctx context.Context, admission ctrl.Admission, store *dal.Store, cfg Config, isLeader func() bool, readSupported func() []capabilities.CapabilityID, readMembers func() []capabilities.Member, logger logging.Logger) { if !isLeader() { return } diff --git a/internal/bootstrap/node_hook_commit_drain_test.go b/internal/bootstrap/node_hook_commit_drain_test.go index 4a4fd4feee..09fae28dfc 100644 --- a/internal/bootstrap/node_hook_commit_drain_test.go +++ b/internal/bootstrap/node_hook_commit_drain_test.go @@ -100,13 +100,11 @@ func TestNodeHookStopCancellationRetainsRealCommitDrain(t *testing.T) { command.ExecutionPlan = &raftcmdpb.ExecutionPlan{Attributes: []*raftcmdpb.AttributeCoverage{{ Id: &raftcmdpb.AttributeID{Id: ledgerKey[:]}, AttrCode: uint32(dal.SubAttrLedger), }}} - require.NoError(t, capabilities.StampProposal(command)) - data, err := command.MarshalVT() + required, err := capabilities.PrepareProposal(command) require.NoError(t, err) - required, err := capabilities.RequiredCapabilities(command) + data, err := command.MarshalVT() require.NoError(t, err) - proposal := node.NewProposal(command.GetId(), data) - proposal.RequireCapabilities(required...) + proposal := node.NewProposal(command.GetId(), data, required) future, err := n.Propose(ctx, proposal) require.NoError(t, err) select { diff --git a/internal/infra/capabilities/capabilities.go b/internal/infra/capabilities/capabilities.go index 7883a378f1..fe44dca3ab 100644 --- a/internal/infra/capabilities/capabilities.go +++ b/internal/infra/capabilities/capabilities.go @@ -14,12 +14,14 @@ import ( "github.com/formancehq/ledger/v3/internal/proto/raftcmdpb" ) -type ID uint32 +// CapabilityID is the stable numeric identity of a consensus capability. +// The wire representation remains uint32 at protobuf and Raft JSON boundaries. +type CapabilityID uint32 // SupportedIDs is the immutable capability set implemented by this binary. // No protocol feature is activated yet; this empty registry keeps the // membership-evidence plumbing ready for the first real capability. -func SupportedIDs() []ID { +func SupportedIDs() []CapabilityID { return nil } @@ -35,11 +37,11 @@ type Member struct { InstanceID []byte Role MemberRole Reachable bool - Capabilities []ID + Capabilities []CapabilityID } type State struct { - Activations map[ID]bool + Activations map[CapabilityID]bool } type Blocker struct { @@ -85,7 +87,7 @@ var ( // contract so a producer cannot make a feature-specific payload appear safe by // omitting required_capabilities. Predicates inspect only replicated fields, // which makes the result deterministic on every member. -func RequiredCapabilities(proposal *raftcmdpb.Proposal) ([]ID, error) { +func RequiredCapabilities(proposal *raftcmdpb.Proposal) ([]CapabilityID, error) { if proposal == nil { return nil, fmt.Errorf("%w: nil proposal", ErrInvalidDeclaration) } @@ -98,16 +100,26 @@ func RequiredCapabilities(proposal *raftcmdpb.Proposal) ([]ID, error) { // execution plan and immediately before marshaling; repeated calls are safe on // retry and rebuild paths. func StampProposal(proposal *raftcmdpb.Proposal) error { + _, err := PrepareProposal(proposal) + + return err +} + +// PrepareProposal derives and stamps the semantic capability declaration and +// returns the typed result for the caller to pass alongside the serialized +// proposal. Keeping the result avoids decoding the same proposal again during +// admission. +func PrepareProposal(proposal *raftcmdpb.Proposal) ([]CapabilityID, error) { required, err := RequiredCapabilities(proposal) if err != nil { - return err + return nil, err } proposal.RequiredCapabilities = make([]uint32, len(required)) for i, id := range required { proposal.RequiredCapabilities[i] = uint32(id) } - return nil + return required, nil } func (s State) Validate() error { @@ -115,20 +127,20 @@ func (s State) Validate() error { } func FromPolicy(policy *commonpb.ClusterPolicy) State { - s := State{Activations: make(map[ID]bool)} + s := State{Activations: make(map[CapabilityID]bool)} if policy == nil { return s } for _, activation := range policy.GetCapabilityActivations() { if activation != nil { - s.Activations[ID(activation.GetId())] = activation.GetActive() + s.Activations[CapabilityID(activation.GetId())] = activation.GetActive() } } return s } -func Normalize(ids []ID) ([]ID, error) { +func Normalize(ids []CapabilityID) ([]CapabilityID, error) { result := slices.Clone(ids) slices.Sort(result) for i := 1; i < len(result); i++ { @@ -144,7 +156,7 @@ func Normalize(ids []ID) ([]ID, error) { // by every current cluster member. Membership evidence is the acknowledgement // recorded by JoinAsLearner/RefreshMembership; an empty member set is never // considered ready. -func Ready(supported []ID, members []Member) ([]ID, error) { +func Ready(supported []CapabilityID, members []Member) ([]CapabilityID, error) { if len(members) == 0 { return nil, nil } @@ -155,7 +167,7 @@ func Ready(supported []ID, members []Member) ([]ID, error) { if err := ValidateMembers(members); err != nil { return nil, err } - ready := make([]ID, 0, len(supported)) + ready := make([]CapabilityID, 0, len(supported)) for _, id := range supported { acknowledged := true for _, member := range members { @@ -175,7 +187,7 @@ func Ready(supported []ID, members []Member) ([]ID, error) { // Evaluate is pure. Reachability is reported separately from support: a // temporarily unreachable voter is still a blocker for activation, while a // learner is included because it must replay every committed entry. -func Evaluate(state State, required []ID, _ bool, members []Member) Decision { +func Evaluate(state State, required []CapabilityID, _ bool, members []Member) Decision { if err := state.Validate(); err != nil { return Decision{Reason: err.Error()} } @@ -209,48 +221,3 @@ func Evaluate(state State, required []ID, _ bool, members []Member) Decision { return Decision{Allowed: true, Blockers: blockers} } - -// ValidateFinalProposal proves that the declaration belongs to the bytes that -// will be proposed. Callers must invoke it after rebuilding an execution plan. -func ValidateFinalProposal(data []byte, declared []ID) error { - proposal := new(raftcmdpb.Proposal) - if err := proposal.UnmarshalVT(data); err != nil { - return fmt.Errorf("decode final proposal: %w", err) - } - expected, err := RequiredCapabilities(proposal) - if err != nil { - return err - } - wire := make([]ID, len(proposal.GetRequiredCapabilities())) - for i, id := range proposal.GetRequiredCapabilities() { - wire[i] = ID(id) - } - wire, err = Normalize(wire) - if err != nil { - return err - } - declared, err = Normalize(declared) - if err != nil { - return err - } - if !bytes.Equal(idsBytes(wire), idsBytes(expected)) { - return fmt.Errorf("%w: wire declaration does not match semantic payload", ErrMissingCapability) - } - if !bytes.Equal(idsBytes(wire), idsBytes(declared)) { - return fmt.Errorf("%w: declaration does not match final payload", ErrMissingCapability) - } - - return nil -} - -func idsBytes(ids []ID) []byte { - result := make([]byte, len(ids)*4) - for i, id := range ids { - result[i*4] = byte(id) - result[i*4+1] = byte(id >> 8) - result[i*4+2] = byte(id >> 16) - result[i*4+3] = byte(id >> 24) - } - - return result -} diff --git a/internal/infra/capabilities/capabilities_test.go b/internal/infra/capabilities/capabilities_test.go index 4e7ce06481..36bfe00a83 100644 --- a/internal/infra/capabilities/capabilities_test.go +++ b/internal/infra/capabilities/capabilities_test.go @@ -10,14 +10,14 @@ import ( ) func capabilityState() State { - return State{Activations: map[ID]bool{}} + return State{Activations: map[CapabilityID]bool{}} } func TestEvaluateIncludesVotersAndLearners(t *testing.T) { state := capabilityState() state.Activations[99] = true - decision := Evaluate(state, []ID{99}, true, []Member{ - {NodeID: 1, Role: Voter, Reachable: true, Capabilities: []ID{99}}, + decision := Evaluate(state, []CapabilityID{99}, true, []Member{ + {NodeID: 1, Role: Voter, Reachable: true, Capabilities: []CapabilityID{99}}, {NodeID: 2, Role: Learner, Reachable: true}, }) require.False(t, decision.Allowed) @@ -29,12 +29,6 @@ func TestEvaluateAllowsUnrelatedWriteWhenNoFeatureIsDeclared(t *testing.T) { require.True(t, decision.Allowed) } -func TestValidateFinalProposalHasNoCurrentDependencies(t *testing.T) { - data, err := (&raftcmdpb.Proposal{ExecutionPlan: &raftcmdpb.ExecutionPlan{}}).MarshalVT() - require.NoError(t, err) - require.NoError(t, ValidateFinalProposal(data, nil)) -} - func TestStampProposalClearsFutureDeclarations(t *testing.T) { proposal := &raftcmdpb.Proposal{RequiredCapabilities: []uint32{99}} require.NoError(t, StampProposal(proposal)) @@ -51,36 +45,36 @@ func TestFromPolicyRoundTrip(t *testing.T) { func TestReadyReturnsIntersectionAcrossMembers(t *testing.T) { members := []Member{ - {NodeID: 1, Role: Voter, Capabilities: []ID{1, 2}}, - {NodeID: 2, Role: Learner, Capabilities: []ID{1}}, + {NodeID: 1, Role: Voter, Capabilities: []CapabilityID{1, 2}}, + {NodeID: 2, Role: Learner, Capabilities: []CapabilityID{1}}, } - ready, err := Ready([]ID{2, 1}, members) + ready, err := Ready([]CapabilityID{2, 1}, members) require.NoError(t, err) - require.Equal(t, []ID{1}, ready) + require.Equal(t, []CapabilityID{1}, ready) } func TestReadyDoesNotActivateAnEmptyCluster(t *testing.T) { - ready, err := Ready([]ID{1}, nil) + ready, err := Ready([]CapabilityID{1}, nil) require.NoError(t, err) require.Empty(t, ready) } func TestReadyRejectsMalformedMemberEvidence(t *testing.T) { - _, err := Ready([]ID{1}, []Member{{ + _, err := Ready([]CapabilityID{1}, []Member{{ NodeID: 1, Role: Voter, - Capabilities: []ID{1, 1}, + Capabilities: []CapabilityID{1, 1}, }}) require.ErrorContains(t, err, "invalid capability evidence") } func TestReadyIgnoresUnsupportedAdvertisedIDs(t *testing.T) { - ready, err := Ready([]ID{1}, []Member{{ + ready, err := Ready([]CapabilityID{1}, []Member{{ NodeID: 1, Role: Voter, - Capabilities: []ID{1, 99}, + Capabilities: []CapabilityID{1, 99}, }}) require.NoError(t, err) - require.Equal(t, []ID{1}, ready) + require.Equal(t, []CapabilityID{1}, ready) } diff --git a/internal/infra/node/force_remove_durability_test.go b/internal/infra/node/force_remove_durability_test.go index e58e4a23e0..e335dca7be 100644 --- a/internal/infra/node/force_remove_durability_test.go +++ b/internal/infra/node/force_remove_durability_test.go @@ -438,7 +438,7 @@ func TestForceRemoveNodeDurabilityFailureContract(t *testing.T) { require.ErrorIs(t, runErr, injected) _, err = h.n.GetClusterState(context.Background()) require.ErrorIs(t, err, injected) - _, err = h.n.Propose(context.Background(), NewProposal(100, []byte("must not be admitted"))) + _, err = h.n.Propose(context.Background(), NewProposal(100, []byte("must not be admitted"), nil)) require.ErrorIs(t, err, injected) _, restartedVoters := h.restartedState(t) @@ -497,7 +497,7 @@ func TestForceRemoveNodeDurabilityFailureContract(t *testing.T) { synctest.Wait() require.Len(t, h.n.clusterCommandCh, 1) - proposal := NewProposal(101, []byte("queued before terminal publication")) + proposal := NewProposal(101, []byte("queued before terminal publication"), nil) trackerBefore := h.n.indexTracker.Next() h.n.indexTracker.Lock() fsmFuture, err := h.n.Propose(context.Background(), proposal) diff --git a/internal/infra/node/node.go b/internal/infra/node/node.go index 92fab63483..ecd347e5cd 100644 --- a/internal/infra/node/node.go +++ b/internal/infra/node/node.go @@ -98,7 +98,7 @@ var ( ErrNodeSyncing = errors.New("node is syncing") ) -func capabilityIDsAsUint32(ids []capabilities.ID) []uint32 { +func capabilityIDsAsUint32(ids []capabilities.CapabilityID) []uint32 { result := make([]uint32, len(ids)) for i, id := range ids { result[i] = uint32(id) @@ -2468,7 +2468,7 @@ func (node *Node) handleProposal(p *Proposal) { } } if err == nil { - if validationErr := capabilities.ValidateFinalProposal(p.data, p.declaredCapabilities); validationErr != nil { + if validationErr := capabilities.ValidateFinalProposal(p.data, p.requiredCapabilities); validationErr != nil { err = validationErr } else { err = node.rawNode.Propose(p.data) @@ -2592,9 +2592,9 @@ func (node *Node) CapabilityMembers() []capabilities.Member { member := capabilities.Member{NodeID: id, Role: role, Reachable: ok && peer.RaftAddress != "" && peer.ServiceAddress != ""} if ok { member.InstanceID = peer.InstanceID - member.Capabilities = make([]capabilities.ID, len(peer.Capabilities)) + member.Capabilities = make([]capabilities.CapabilityID, len(peer.Capabilities)) for i, capabilityID := range peer.Capabilities { - member.Capabilities[i] = capabilities.ID(capabilityID) + member.Capabilities[i] = capabilities.CapabilityID(capabilityID) } } result = append(result, member) diff --git a/internal/infra/node/proposal.go b/internal/infra/node/proposal.go index 90fe9cef4c..10135629d2 100644 --- a/internal/infra/node/proposal.go +++ b/internal/infra/node/proposal.go @@ -1,8 +1,6 @@ package node import ( - "slices" - "github.com/formancehq/ledger/v3/internal/infra/capabilities" "github.com/formancehq/ledger/v3/internal/pkg/futures" ) @@ -16,25 +14,16 @@ type Proposal struct { releaseMarker bool membershipBarrier bool // expectedTerm fences a drain preflight against a later leadership term. - expectedTerm uint64 - declaredCapabilities []capabilities.ID - -// RequireCapabilities records the producer declaration that must match the -// final serialized proposal. It is intentionally separate from the bytes so a -// plan rebuild cannot silently retain stale capability metadata. -func (p *Proposal) RequireCapabilities(ids ...capabilities.ID) { - p.declaredCapabilities = slices.Clone(ids) -} - -func (p *Proposal) DeclaredCapabilities() []capabilities.ID { - return slices.Clone(p.declaredCapabilities) + expectedTerm uint64 + requiredCapabilities []capabilities.CapabilityID } -func NewProposal(commandID uint64, data []byte) *Proposal { +func NewProposal(commandID uint64, data []byte, required []capabilities.CapabilityID) *Proposal { return &Proposal{ - commandID: commandID, - data: data, - Future: futures.New[any](), + commandID: commandID, + data: data, + requiredCapabilities: append([]capabilities.CapabilityID(nil), required...), + Future: futures.New[any](), } } @@ -42,3 +31,10 @@ func NewProposal(commandID uint64, data []byte) *Proposal { func (p *Proposal) Data() []byte { return p.data } + +// RequiredCapabilities returns the capabilities derived by the producer from +// the final command before it was serialized. Admission reuses this typed +// value instead of decoding the serialized proposal a second time. +func (p *Proposal) RequiredCapabilities() []capabilities.CapabilityID { + return append([]capabilities.CapabilityID(nil), p.requiredCapabilities...) +} diff --git a/internal/infra/plan/capability_policy.go b/internal/infra/plan/capability_policy.go index 1bce531f69..ed2b38340d 100644 --- a/internal/infra/plan/capability_policy.go +++ b/internal/infra/plan/capability_policy.go @@ -9,12 +9,11 @@ import ( "github.com/formancehq/ledger/v3/internal/infra/state" "github.com/formancehq/ledger/v3/internal/pkg/futures" "github.com/formancehq/ledger/v3/internal/proto/commonpb" - "github.com/formancehq/ledger/v3/internal/proto/raftcmdpb" ) // CapabilityPolicyProposer rechecks the committed capability policy against -// the final serialized proposal immediately before handing it to the Raft -// proposer. +// the capabilities derived by the producer from the final proposal immediately +// before handing it to the Raft proposer. type CapabilityPolicyProposer struct { delegate Proposer readPolicy func() (*commonpb.ClusterPolicy, error) @@ -50,15 +49,7 @@ func (p CapabilityPolicyProposer) Propose(ctx context.Context, proposal *node.Pr return nil, err } - final := new(raftcmdpb.Proposal) - if err := final.UnmarshalVT(proposal.Data()); err != nil { - return nil, fmt.Errorf("decode proposal for capability admission: %w", err) - } - - required := make([]capabilities.ID, len(final.GetRequiredCapabilities())) - for i, id := range final.GetRequiredCapabilities() { - required[i] = capabilities.ID(id) - } + required := proposal.RequiredCapabilities() var members []capabilities.Member if p.readMembers != nil { members = p.readMembers() diff --git a/internal/infra/plan/capability_policy_test.go b/internal/infra/plan/capability_policy_test.go index 331af71b5e..a94421ada6 100644 --- a/internal/infra/plan/capability_policy_test.go +++ b/internal/infra/plan/capability_policy_test.go @@ -31,7 +31,7 @@ func capabilityPolicyProposal(t *testing.T) *node.Proposal { }).MarshalVT() require.NoError(t, err) - return node.NewProposal(1, data) + return node.NewProposal(1, data, []capabilities.CapabilityID{99}) } func TestCapabilityPolicyProposerBlocksInactiveCapability(t *testing.T) { diff --git a/internal/infra/plan/runner.go b/internal/infra/plan/runner.go index 1ed836ac41..8fd3e376cf 100644 --- a/internal/infra/plan/runner.go +++ b/internal/infra/plan/runner.go @@ -154,7 +154,7 @@ func (p *Builder) Run( } // Guarantee a unique command ID. The applier keys FSM futures by - // `cmd.Id` (`node.NewProposal(cmd.GetId(), data)` below); two in-flight + // `cmd.Id` (`node.NewProposal(cmd.GetId(), data, required)` below); two in-flight // proposals sharing Id=0 would overwrite each other in // `Applier.StoreFuture`, hanging one waiter and routing the apply // result to the wrong caller. Several technical callers build @@ -185,7 +185,8 @@ func (p *Builder) Run( } cmd.ExecutionPlan = build.ExecutionPlan - if err := capabilities.StampProposal(cmd); err != nil { + required, err := capabilities.PrepareProposal(cmd) + if err != nil { build.ReleaseLoaders() return nil, fmt.Errorf("%w: %w", ErrMarshalProposal, err) @@ -231,7 +232,8 @@ func (p *Builder) Run( // possibly values. result.Rebuilt = true cmd.ExecutionPlan = updatedPreloads - if err := capabilities.StampProposal(cmd); err != nil { + required, err = capabilities.PrepareProposal(cmd) + if err != nil { guard.ReleaseAll() return nil, fmt.Errorf("%w (under guard): %w", ErrMarshalProposal, err) @@ -255,14 +257,7 @@ func (p *Builder) Run( data = AppendProposalPredictedIndex(data, cmd.GetPredictedIndex()) } - proposal := node.NewProposal(cmd.GetId(), data) - required, err := capabilities.RequiredCapabilities(cmd) - if err != nil { - guard.ReleaseAll() - - return nil, fmt.Errorf("%w: %w", ErrMarshalProposal, err) - } - proposal.RequireCapabilities(required...) + proposal := node.NewProposal(cmd.GetId(), data, required) result.ProposeStartTime = time.Now() fsmFuture, err := proposer.Propose(ctx, proposal) @@ -365,7 +360,8 @@ func (p *Builder) runWithoutPreload( } else { cmd.ExecutionPlan = nil } - if err := capabilities.StampProposal(cmd); err != nil { + required, err := capabilities.PrepareProposal(cmd) + if err != nil { build.ReleaseLoaders() return nil, fmt.Errorf("%w: %w", ErrMarshalProposal, err) @@ -393,14 +389,7 @@ func (p *Builder) runWithoutPreload( cmd.PredictedIndex = p.TrackerNext() data = AppendProposalPredictedIndex(data, cmd.GetPredictedIndex()) - proposal := node.NewProposal(cmd.GetId(), data) - required, err := capabilities.RequiredCapabilities(cmd) - if err != nil { - guard.ReleaseLoaders() - - return nil, fmt.Errorf("%w: %w", ErrMarshalProposal, err) - } - proposal.RequireCapabilities(required...) + proposal := node.NewProposal(cmd.GetId(), data, required) proposeStart := time.Now() fsmFuture, err := proposer.Propose(ctx, proposal) From 796ad6bc5eae811b1931c2c4d06b231d1204f5f3 Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Thu, 22 Oct 2026 18:24:41 +0200 Subject: [PATCH 023/127] chore(ledger): apply Go formatter output --- internal/bootstrap/cluster_policy_reconciler_test.go | 6 ++---- internal/bootstrap/module.go | 4 +++- internal/infra/capabilities/capabilities.go | 1 + 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/internal/bootstrap/cluster_policy_reconciler_test.go b/internal/bootstrap/cluster_policy_reconciler_test.go index a40b073da4..7e4f9222b8 100644 --- a/internal/bootstrap/cluster_policy_reconciler_test.go +++ b/internal/bootstrap/cluster_policy_reconciler_test.go @@ -59,11 +59,9 @@ func TestClusterPolicyReconciler_TriggerIsCoalescedAndRaceSafe(t *testing.T) { var wg sync.WaitGroup for range 100 { - wg.Add(1) - go func() { - defer wg.Done() + wg.Go(func() { r.Trigger() - }() + }) } wg.Wait() require.Eventually(t, func() bool { return count.Load() >= 2 }, time.Second, time.Millisecond, diff --git a/internal/bootstrap/module.go b/internal/bootstrap/module.go index 852141d7c1..adcab45d8d 100644 --- a/internal/bootstrap/module.go +++ b/internal/bootstrap/module.go @@ -1765,17 +1765,19 @@ func reconcileClusterPolicyWithRegistry(ctx context.Context, admission ctrl.Admi ready, err := capabilities.Ready(readSupported(), readMembers()) if err != nil { logger.WithFields(map[string]any{"error": err}).Errorf("Computing converged capability set") + return } for _, id := range ready { found := false - for _, activation := range desired.CapabilityActivations { + for _, activation := range desired.GetCapabilityActivations() { if activation != nil && activation.GetId() == uint32(id) { if !activation.GetActive() { activation.Active = true activationChanged = true } found = true + break } } diff --git a/internal/infra/capabilities/capabilities.go b/internal/infra/capabilities/capabilities.go index fe44dca3ab..eff5673c2b 100644 --- a/internal/infra/capabilities/capabilities.go +++ b/internal/infra/capabilities/capabilities.go @@ -173,6 +173,7 @@ func Ready(supported []CapabilityID, members []Member) ([]CapabilityID, error) { for _, member := range members { if !slices.Contains(member.Capabilities, id) { acknowledged = false + break } } From 07fcb7d9ded142816851ea480d796b3fd8db0318 Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Thu, 22 Oct 2026 18:37:06 +0200 Subject: [PATCH 024/127] fix(consensus): centralize prepared proposal construction --- internal/application/admission/admission.go | 2 +- .../admission/capability_policy_test.go | 4 +- .../cluster_policy_reconciler_test.go | 64 +------------ internal/bootstrap/module.go | 96 +++++-------------- .../bootstrap/node_hook_commit_drain_test.go | 2 +- .../node/force_remove_durability_test.go | 4 +- internal/infra/node/proposal.go | 9 +- internal/infra/plan/capability_policy_test.go | 2 +- internal/infra/plan/runner.go | 6 +- 9 files changed, 46 insertions(+), 143 deletions(-) diff --git a/internal/application/admission/admission.go b/internal/application/admission/admission.go index 88e0df0391..6787a52828 100644 --- a/internal/application/admission/admission.go +++ b/internal/application/admission/admission.go @@ -1059,7 +1059,7 @@ func (a *Admission) Barrier(ctx context.Context) (uint64, error) { return 0, fmt.Errorf("marshaling barrier command: %w", err) } - proposal := node.NewProposal(cmd.GetId(), proposalData, required) + proposal := node.NewPreparedProposal(cmd.GetId(), proposalData, required) // Lock the tracker to serialize the Increment with guarded proposals, // preventing preload boundary mismatches in the FSM. diff --git a/internal/application/admission/capability_policy_test.go b/internal/application/admission/capability_policy_test.go index f2156cc1a9..1d573a746b 100644 --- a/internal/application/admission/capability_policy_test.go +++ b/internal/application/admission/capability_policy_test.go @@ -64,7 +64,7 @@ func TestCapabilityPolicyProposerBlocksUnknownCapabilityAtFinalBoundary(t *testi } data, err := (&raftcmdpb.Proposal{RequiredCapabilities: []uint32{99}}).MarshalVT() require.NoError(t, err) - _, err = proposer.Propose(context.Background(), node.NewProposal(1, data, []capabilities.CapabilityID{99})) + _, err = proposer.Propose(context.Background(), node.NewPreparedProposal(1, data, []capabilities.CapabilityID{99})) require.Error(t, err) require.ErrorIs(t, err, capabilities.ErrCapabilityUnsupported) require.False(t, delegate.called) @@ -81,7 +81,7 @@ func TestCapabilityPolicyProposerAllowsUnrelatedWrite(t *testing.T) { } data, err := (&raftcmdpb.Proposal{}).MarshalVT() require.NoError(t, err) - _, err = proposer.Propose(context.Background(), node.NewProposal(1, data, nil)) + _, err = proposer.Propose(context.Background(), node.NewPreparedProposal(1, data, nil)) require.NoError(t, err) require.True(t, delegate.called) } diff --git a/internal/bootstrap/cluster_policy_reconciler_test.go b/internal/bootstrap/cluster_policy_reconciler_test.go index 7e4f9222b8..1039046e25 100644 --- a/internal/bootstrap/cluster_policy_reconciler_test.go +++ b/internal/bootstrap/cluster_policy_reconciler_test.go @@ -14,7 +14,6 @@ import ( logging "github.com/formancehq/go-libs/v5/pkg/observe/log" "github.com/formancehq/ledger/v3/internal/domain" - "github.com/formancehq/ledger/v3/internal/infra/capabilities" "github.com/formancehq/ledger/v3/internal/infra/state" "github.com/formancehq/ledger/v3/internal/proto/commonpb" "github.com/formancehq/ledger/v3/internal/proto/servicepb" @@ -124,7 +123,7 @@ func TestReconcileClusterPolicy(t *testing.T) { admission := &fakeAdmission{} reconcileClusterPolicy(context.Background(), admission, newReconcilerTestStore(t), validBaseConfig(), - func() bool { return false }, nil, logging.Testing()) + func() bool { return false }, logging.Testing()) require.Zero(t, admission.callCount(), "a non-leader must not propose") }) @@ -134,7 +133,7 @@ func TestReconcileClusterPolicy(t *testing.T) { admission := &fakeAdmission{} reconcileClusterPolicy(context.Background(), admission, newReconcilerTestStore(t), validBaseConfig(), - func() bool { return true }, nil, logging.Testing()) + func() bool { return true }, logging.Testing()) require.Equal(t, 1, admission.callCount(), "a leader with no committed policy must propose") }) @@ -154,7 +153,7 @@ func TestReconcileClusterPolicy(t *testing.T) { require.NoError(t, batch.Commit()) admission := &fakeAdmission{} - reconcileClusterPolicy(context.Background(), admission, store, cfg, func() bool { return true }, nil, logging.Testing()) + reconcileClusterPolicy(context.Background(), admission, store, cfg, func() bool { return true }, logging.Testing()) require.Zero(t, admission.callCount(), "an already-applied revision must not be re-proposed") }) @@ -173,7 +172,7 @@ func TestReconcileClusterPolicy(t *testing.T) { require.NoError(t, batch.Commit()) admission := &fakeAdmission{} - reconcileClusterPolicy(context.Background(), admission, store, cfg, func() bool { return true }, nil, logging.Testing()) + reconcileClusterPolicy(context.Background(), admission, store, cfg, func() bool { return true }, logging.Testing()) require.Zero(t, admission.callCount(), "a same-revision/different-payload conflict must not be re-proposed") }) @@ -192,61 +191,8 @@ func TestReconcileClusterPolicy(t *testing.T) { require.NoError(t, batch.Commit()) admission := &fakeAdmission{} - reconcileClusterPolicy(context.Background(), admission, store, cfg, func() bool { return true }, nil, logging.Testing()) + reconcileClusterPolicy(context.Background(), admission, store, cfg, func() bool { return true }, logging.Testing()) require.Zero(t, admission.callCount(), "a newer applied revision must not be overwritten") }) } - -func TestReconcileClusterPolicyAutoActivatesOnlyAfterConvergence(t *testing.T) { - t.Parallel() - - cfg := validBaseConfig() - store := newReconcilerTestStore(t) - batch := store.OpenWriteSession() - require.NoError(t, state.SaveClusterPolicy(batch, &commonpb.ClusterPolicy{ - Revision: cfg.ClusterPolicyRevision, - IdempotencyTtlMicros: uint64(cfg.IdempotencyTTL.Microseconds()), - QueryCheckpointLimit: cfg.QueryCheckpointLimit, - })) - require.NoError(t, batch.Commit()) - - readMembers := func() []capabilities.Member { - return []capabilities.Member{ - {NodeID: 1, Role: capabilities.Voter, Capabilities: []capabilities.CapabilityID{7}}, - {NodeID: 2, Role: capabilities.Learner, Capabilities: []capabilities.CapabilityID{7}}, - } - } - admission := &fakeAdmission{} - reconcileClusterPolicyWithRegistry(context.Background(), admission, store, cfg, - func() bool { return true }, func() []capabilities.CapabilityID { return []capabilities.CapabilityID{7} }, readMembers, logging.Testing()) - - require.Equal(t, 1, admission.callCount()) - require.Equal(t, uint64(cfg.ClusterPolicyRevision+1), admission.lastPolicy.GetRevision()) - require.Equal(t, []*commonpb.CapabilityActivation{{Id: 7, Active: true}}, admission.lastPolicy.GetCapabilityActivations()) -} - -func TestReconcileClusterPolicyDoesNotActivateBeforeConvergence(t *testing.T) { - t.Parallel() - - cfg := validBaseConfig() - store := newReconcilerTestStore(t) - batch := store.OpenWriteSession() - require.NoError(t, state.SaveClusterPolicy(batch, &commonpb.ClusterPolicy{ - Revision: cfg.ClusterPolicyRevision, - IdempotencyTtlMicros: uint64(cfg.IdempotencyTTL.Microseconds()), - QueryCheckpointLimit: cfg.QueryCheckpointLimit, - })) - require.NoError(t, batch.Commit()) - - admission := &fakeAdmission{} - reconcileClusterPolicyWithRegistry(context.Background(), admission, store, cfg, - func() bool { return true }, func() []capabilities.CapabilityID { return []capabilities.CapabilityID{7} }, func() []capabilities.Member { - return []capabilities.Member{ - {NodeID: 1, Role: capabilities.Voter, Capabilities: []capabilities.CapabilityID{7}}, - {NodeID: 2, Role: capabilities.Learner}, - } - }, logging.Testing()) - - require.Zero(t, admission.callCount()) -} diff --git a/internal/bootstrap/module.go b/internal/bootstrap/module.go index adcab45d8d..e64473316b 100644 --- a/internal/bootstrap/module.go +++ b/internal/bootstrap/module.go @@ -309,27 +309,6 @@ func Module() fx.Option { return nil, err } - publishCapabilityGate := func(policy *commonpb.ClusterPolicy) { - required := make([]uint32, 0, len(policy.GetCapabilityActivations())) - for _, activation := range policy.GetCapabilityActivations() { - if activation != nil && activation.GetActive() { - required = append(required, activation.GetId()) - } - } - slices.Sort(required) - required = slices.Compact(required) - if err := membership.SetCapabilityGate(required); err != nil { - logger.WithFields(map[string]any{"error": err}).Errorf("Invalid capability gate in committed cluster policy") - } - } - membership.SetCapabilityPolicyProvider(func() *commonpb.ClusterPolicy { - return m.State.ClusterPolicy - }) - m.SetCapabilityPolicyCommitHandler(publishCapabilityGate) - if m.State.ClusterPolicy != nil { - publishCapabilityGate(m.State.ClusterPolicy) - } - logger.WithFields(map[string]any{ "duration": time.Since(machineStart).String(), }).Infof("FSM Machine created") @@ -669,12 +648,26 @@ func Module() fx.Option { if !raftNode.IsLeader() { return } + if policy, err := query.ReadClusterPolicy(store); err == nil { + required := make([]uint32, 0, len(policy.GetCapabilityActivations())) + for _, activation := range policy.GetCapabilityActivations() { + if activation != nil && activation.GetActive() { + required = append(required, activation.GetId()) + } + } + slices.Sort(required) + if err := raftNode.SetCapabilityGate(required); err != nil { + logger.WithFields(map[string]any{"error": err}).Errorf("Invalid capability gate in committed cluster policy") + + return + } + } if err := proposeAuditKeyIfNeeded(ctx, raftNode, builder, store); err != nil { logger.WithFields(map[string]any{"error": err}).Errorf("Audit key initialization failed; will retry") return } - reconcileClusterPolicy(ctx, admission, store, cfg, raftNode.IsLeader, raftNode.CapabilityMembers, logger) + reconcileClusterPolicy(ctx, admission, store, cfg, raftNode.IsLeader, logger) }) }, ), @@ -903,13 +896,12 @@ func Module() fx.Option { RequestShutdown: shutdownRequester(shutdowner), })) }, - // Wire Observer: handle leadership, readiness and capability - // membership convergence events. Membership still owns transport - // and service-pool wiring; the capability event only wakes the - // policy reconciler. + // Wire Observer: handle LeadershipChange and LeaderReady events. + // ConfChange events are no longer dispatched here — Membership + // owns the transport / service-pool wiring and reacts directly + // to peer mutations (EN-1413). func( n *node.Node, - reconciler *ClusterPolicyReconciler, store *dal.Store, builder *plan.Builder, cfg Config, @@ -921,8 +913,6 @@ func Module() fx.Option { ) { n.SetObserver(node.NewObserver(func(event any) { switch e := event.(type) { - case node.CapabilityMembershipChangedEvent: - reconciler.Trigger() case node.LeadershipChangeEvent: // Disk evidence is leader-term local. Invalidate it inline, // before admission can observe the new leadership term. @@ -1725,14 +1715,10 @@ func proposeClusterConfigIfNeeded(parent context.Context, n plan.Proposer, build // revision. It gates on leadership itself and is safe to call repeatedly: the // ClusterPolicyReconciler invokes it on a ticker so a transient proposal failure // self-heals on the next tick. Unlike the cluster config, the policy flows -// through AdmitClusterPolicy as an operational SetClusterPolicyProposal. -// The write-readiness gate exempts this non-business proposal so it is never -// blocked by its own cluster-policy gate. -func reconcileClusterPolicy(ctx context.Context, admission ctrl.Admission, store *dal.Store, cfg Config, isLeader func() bool, readMembers func() []capabilities.Member, logger logging.Logger) { - reconcileClusterPolicyWithRegistry(ctx, admission, store, cfg, isLeader, capabilities.SupportedIDs, readMembers, logger) -} - -func reconcileClusterPolicyWithRegistry(ctx context.Context, admission ctrl.Admission, store *dal.Store, cfg Config, isLeader func() bool, readSupported func() []capabilities.CapabilityID, readMembers func() []capabilities.Member, logger logging.Logger) { +// through AdmitClusterPolicy as an internal audited SetClusterPolicy order so +// the checker can re-derive it; the write-readiness gate exempts this +// non-business proposal so it is never blocked by its own cluster-policy gate. +func reconcileClusterPolicy(ctx context.Context, admission ctrl.Admission, store *dal.Store, cfg Config, isLeader func() bool, logger logging.Logger) { if !isLeader() { return } @@ -1754,44 +1740,8 @@ func reconcileClusterPolicyWithRegistry(ctx context.Context, admission ctrl.Admi MetadataMaxEntityBytes: cfg.MetadataMaxEntityBytes, MetadataMaxCommandBytes: cfg.MetadataMaxCommandBytes, } - activationChanged := false - if applied != nil { - desired.CapabilityActivations = make([]*commonpb.CapabilityActivation, 0, len(applied.GetCapabilityActivations())) - for _, activation := range applied.GetCapabilityActivations() { - desired.CapabilityActivations = append(desired.CapabilityActivations, activation.CloneVT()) - } - } - if readMembers != nil { - ready, err := capabilities.Ready(readSupported(), readMembers()) - if err != nil { - logger.WithFields(map[string]any{"error": err}).Errorf("Computing converged capability set") - - return - } - for _, id := range ready { - found := false - for _, activation := range desired.GetCapabilityActivations() { - if activation != nil && activation.GetId() == uint32(id) { - if !activation.GetActive() { - activation.Active = true - activationChanged = true - } - found = true - - break - } - } - if !found { - desired.CapabilityActivations = append(desired.CapabilityActivations, &commonpb.CapabilityActivation{Id: uint32(id), Active: true}) - activationChanged = true - } - } - } appliedRev := applied.GetRevision() - if activationChanged && desired.GetRevision() <= appliedRev { - desired.Revision = appliedRev + 1 - } switch { case appliedRev > desired.GetRevision(): diff --git a/internal/bootstrap/node_hook_commit_drain_test.go b/internal/bootstrap/node_hook_commit_drain_test.go index 09fae28dfc..c97a07632d 100644 --- a/internal/bootstrap/node_hook_commit_drain_test.go +++ b/internal/bootstrap/node_hook_commit_drain_test.go @@ -104,7 +104,7 @@ func TestNodeHookStopCancellationRetainsRealCommitDrain(t *testing.T) { require.NoError(t, err) data, err := command.MarshalVT() require.NoError(t, err) - proposal := node.NewProposal(command.GetId(), data, required) + proposal := node.NewPreparedProposal(command.GetId(), data, required) future, err := n.Propose(ctx, proposal) require.NoError(t, err) select { diff --git a/internal/infra/node/force_remove_durability_test.go b/internal/infra/node/force_remove_durability_test.go index e335dca7be..dcc5df907a 100644 --- a/internal/infra/node/force_remove_durability_test.go +++ b/internal/infra/node/force_remove_durability_test.go @@ -438,7 +438,7 @@ func TestForceRemoveNodeDurabilityFailureContract(t *testing.T) { require.ErrorIs(t, runErr, injected) _, err = h.n.GetClusterState(context.Background()) require.ErrorIs(t, err, injected) - _, err = h.n.Propose(context.Background(), NewProposal(100, []byte("must not be admitted"), nil)) + _, err = h.n.Propose(context.Background(), newProposal(100, []byte("must not be admitted"), nil)) require.ErrorIs(t, err, injected) _, restartedVoters := h.restartedState(t) @@ -497,7 +497,7 @@ func TestForceRemoveNodeDurabilityFailureContract(t *testing.T) { synctest.Wait() require.Len(t, h.n.clusterCommandCh, 1) - proposal := NewProposal(101, []byte("queued before terminal publication"), nil) + proposal := newProposal(101, []byte("queued before terminal publication"), nil) trackerBefore := h.n.indexTracker.Next() h.n.indexTracker.Lock() fsmFuture, err := h.n.Propose(context.Background(), proposal) diff --git a/internal/infra/node/proposal.go b/internal/infra/node/proposal.go index 10135629d2..d66d7605a8 100644 --- a/internal/infra/node/proposal.go +++ b/internal/infra/node/proposal.go @@ -18,7 +18,7 @@ type Proposal struct { requiredCapabilities []capabilities.CapabilityID } -func NewProposal(commandID uint64, data []byte, required []capabilities.CapabilityID) *Proposal { +func newProposal(commandID uint64, data []byte, required []capabilities.CapabilityID) *Proposal { return &Proposal{ commandID: commandID, data: data, @@ -27,6 +27,13 @@ func NewProposal(commandID uint64, data []byte, required []capabilities.Capabili } } +// NewPreparedProposal is the only public proposal factory. Callers must pass +// the capability list returned by capabilities.PrepareProposal for the same +// command before serialization; the raw constructor remains package-private. +func NewPreparedProposal(commandID uint64, data []byte, required []capabilities.CapabilityID) *Proposal { + return newProposal(commandID, data, required) +} + // Data returns the serialized proposal data. func (p *Proposal) Data() []byte { return p.data diff --git a/internal/infra/plan/capability_policy_test.go b/internal/infra/plan/capability_policy_test.go index a94421ada6..e37534a7a4 100644 --- a/internal/infra/plan/capability_policy_test.go +++ b/internal/infra/plan/capability_policy_test.go @@ -31,7 +31,7 @@ func capabilityPolicyProposal(t *testing.T) *node.Proposal { }).MarshalVT() require.NoError(t, err) - return node.NewProposal(1, data, []capabilities.CapabilityID{99}) + return node.NewPreparedProposal(1, data, []capabilities.CapabilityID{99}) } func TestCapabilityPolicyProposerBlocksInactiveCapability(t *testing.T) { diff --git a/internal/infra/plan/runner.go b/internal/infra/plan/runner.go index 8fd3e376cf..1afb33a078 100644 --- a/internal/infra/plan/runner.go +++ b/internal/infra/plan/runner.go @@ -154,7 +154,7 @@ func (p *Builder) Run( } // Guarantee a unique command ID. The applier keys FSM futures by - // `cmd.Id` (`node.NewProposal(cmd.GetId(), data, required)` below); two in-flight + // `cmd.Id` (`node.NewPreparedProposal(cmd.GetId(), data, required)` below); two in-flight // proposals sharing Id=0 would overwrite each other in // `Applier.StoreFuture`, hanging one waiter and routing the apply // result to the wrong caller. Several technical callers build @@ -257,7 +257,7 @@ func (p *Builder) Run( data = AppendProposalPredictedIndex(data, cmd.GetPredictedIndex()) } - proposal := node.NewProposal(cmd.GetId(), data, required) + proposal := node.NewPreparedProposal(cmd.GetId(), data, required) result.ProposeStartTime = time.Now() fsmFuture, err := proposer.Propose(ctx, proposal) @@ -389,7 +389,7 @@ func (p *Builder) runWithoutPreload( cmd.PredictedIndex = p.TrackerNext() data = AppendProposalPredictedIndex(data, cmd.GetPredictedIndex()) - proposal := node.NewProposal(cmd.GetId(), data, required) + proposal := node.NewPreparedProposal(cmd.GetId(), data, required) proposeStart := time.Now() fsmFuture, err := proposer.Propose(ctx, proposal) From aebd7bbf66e3dde8c2056227436f2c65b1fddaad Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Thu, 22 Oct 2026 19:35:00 +0200 Subject: [PATCH 025/127] fix(consensus): make prepared proposal metadata opaque --- .../admission/capability_policy_test.go | 21 +++------------- internal/infra/capabilities/capabilities.go | 19 +++++++++++--- internal/infra/node/proposal.go | 10 ++++---- internal/infra/node/proposal_test.go | 24 ++++++++++++++++++ internal/infra/plan/capability_policy_test.go | 25 ++++--------------- 5 files changed, 53 insertions(+), 46 deletions(-) create mode 100644 internal/infra/node/proposal_test.go diff --git a/internal/application/admission/capability_policy_test.go b/internal/application/admission/capability_policy_test.go index 1d573a746b..69c2f49366 100644 --- a/internal/application/admission/capability_policy_test.go +++ b/internal/application/admission/capability_policy_test.go @@ -53,23 +53,6 @@ func TestBarrierStampsSemanticCapabilities(t *testing.T) { require.NoError(t, err) } -func TestCapabilityPolicyProposerBlocksUnknownCapabilityAtFinalBoundary(t *testing.T) { - delegate := new(capabilityTestProposer) - proposer := capabilityPolicyProposer{ - delegate: delegate, - readPolicy: func() (*commonpb.ClusterPolicy, error) { - return &commonpb.ClusterPolicy{}, nil - }, - businessWrite: true, - } - data, err := (&raftcmdpb.Proposal{RequiredCapabilities: []uint32{99}}).MarshalVT() - require.NoError(t, err) - _, err = proposer.Propose(context.Background(), node.NewPreparedProposal(1, data, []capabilities.CapabilityID{99})) - require.Error(t, err) - require.ErrorIs(t, err, capabilities.ErrCapabilityUnsupported) - require.False(t, delegate.called) -} - func TestCapabilityPolicyProposerAllowsUnrelatedWrite(t *testing.T) { delegate := new(capabilityTestProposer) proposer := capabilityPolicyProposer{ @@ -81,7 +64,9 @@ func TestCapabilityPolicyProposerAllowsUnrelatedWrite(t *testing.T) { } data, err := (&raftcmdpb.Proposal{}).MarshalVT() require.NoError(t, err) - _, err = proposer.Propose(context.Background(), node.NewPreparedProposal(1, data, nil)) + prepared, err := capabilities.PrepareProposal(&raftcmdpb.Proposal{}) + require.NoError(t, err) + _, err = proposer.Propose(context.Background(), node.NewPreparedProposal(1, data, prepared)) require.NoError(t, err) require.True(t, delegate.called) } diff --git a/internal/infra/capabilities/capabilities.go b/internal/infra/capabilities/capabilities.go index eff5673c2b..c93b6e5c17 100644 --- a/internal/infra/capabilities/capabilities.go +++ b/internal/infra/capabilities/capabilities.go @@ -18,6 +18,19 @@ import ( // The wire representation remains uint32 at protobuf and Raft JSON boundaries. type CapabilityID uint32 +// PreparedProposal is the opaque capability metadata produced while preparing +// a final proposal. Callers can inspect it through RequiredCapabilities, but +// cannot construct the declaration by passing an arbitrary slice to the node +// proposal factory. +type PreparedProposal struct { + required []CapabilityID +} + +// RequiredCapabilities returns a defensive copy of the derived declaration. +func (p PreparedProposal) RequiredCapabilities() []CapabilityID { + return slices.Clone(p.required) +} + // SupportedIDs is the immutable capability set implemented by this binary. // No protocol feature is activated yet; this empty registry keeps the // membership-evidence plumbing ready for the first real capability. @@ -109,17 +122,17 @@ func StampProposal(proposal *raftcmdpb.Proposal) error { // returns the typed result for the caller to pass alongside the serialized // proposal. Keeping the result avoids decoding the same proposal again during // admission. -func PrepareProposal(proposal *raftcmdpb.Proposal) ([]CapabilityID, error) { +func PrepareProposal(proposal *raftcmdpb.Proposal) (PreparedProposal, error) { required, err := RequiredCapabilities(proposal) if err != nil { - return nil, err + return PreparedProposal{}, err } proposal.RequiredCapabilities = make([]uint32, len(required)) for i, id := range required { proposal.RequiredCapabilities[i] = uint32(id) } - return required, nil + return PreparedProposal{required: required}, nil } func (s State) Validate() error { diff --git a/internal/infra/node/proposal.go b/internal/infra/node/proposal.go index d66d7605a8..765b04503b 100644 --- a/internal/infra/node/proposal.go +++ b/internal/infra/node/proposal.go @@ -21,22 +21,22 @@ type Proposal struct { func newProposal(commandID uint64, data []byte, required []capabilities.CapabilityID) *Proposal { return &Proposal{ commandID: commandID, - data: data, + data: append([]byte(nil), data...), requiredCapabilities: append([]capabilities.CapabilityID(nil), required...), Future: futures.New[any](), } } // NewPreparedProposal is the only public proposal factory. Callers must pass -// the capability list returned by capabilities.PrepareProposal for the same +// the opaque result returned by capabilities.PrepareProposal for the same // command before serialization; the raw constructor remains package-private. -func NewPreparedProposal(commandID uint64, data []byte, required []capabilities.CapabilityID) *Proposal { - return newProposal(commandID, data, required) +func NewPreparedProposal(commandID uint64, data []byte, prepared capabilities.PreparedProposal) *Proposal { + return newProposal(commandID, data, prepared.RequiredCapabilities()) } // Data returns the serialized proposal data. func (p *Proposal) Data() []byte { - return p.data + return append([]byte(nil), p.data...) } // RequiredCapabilities returns the capabilities derived by the producer from diff --git a/internal/infra/node/proposal_test.go b/internal/infra/node/proposal_test.go new file mode 100644 index 0000000000..556ebf7f4a --- /dev/null +++ b/internal/infra/node/proposal_test.go @@ -0,0 +1,24 @@ +package node + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/formancehq/ledger/v3/internal/infra/capabilities" + "github.com/formancehq/ledger/v3/internal/proto/raftcmdpb" +) + +func TestNewPreparedProposalCopiesSerializedData(t *testing.T) { + prepared, err := capabilities.PrepareProposal(&raftcmdpb.Proposal{}) + require.NoError(t, err) + + input := []byte{1, 2, 3} + proposal := NewPreparedProposal(1, input, prepared) + input[0] = 9 + require.Equal(t, []byte{1, 2, 3}, proposal.Data()) + + data := proposal.Data() + data[1] = 9 + require.Equal(t, []byte{1, 2, 3}, proposal.Data()) +} diff --git a/internal/infra/plan/capability_policy_test.go b/internal/infra/plan/capability_policy_test.go index e37534a7a4..6537dc678d 100644 --- a/internal/infra/plan/capability_policy_test.go +++ b/internal/infra/plan/capability_policy_test.go @@ -26,36 +26,21 @@ func capabilityPolicyProposal(t *testing.T) *node.Proposal { t.Helper() data, err := (&raftcmdpb.Proposal{ - ExecutionPlan: &raftcmdpb.ExecutionPlan{}, - RequiredCapabilities: []uint32{99}, + ExecutionPlan: &raftcmdpb.ExecutionPlan{}, }).MarshalVT() require.NoError(t, err) + prepared, err := capabilities.PrepareProposal(&raftcmdpb.Proposal{ExecutionPlan: &raftcmdpb.ExecutionPlan{}}) + require.NoError(t, err) - return node.NewPreparedProposal(1, data, []capabilities.CapabilityID{99}) + return node.NewPreparedProposal(1, data, prepared) } -func TestCapabilityPolicyProposerBlocksInactiveCapability(t *testing.T) { +func TestCapabilityPolicyProposerAllowsUnrelatedProposal(t *testing.T) { delegate := new(capabilityPolicyTestProposer) proposer := NewCapabilityPolicyProposer(delegate, func() (*commonpb.ClusterPolicy, error) { return &commonpb.ClusterPolicy{}, nil }, nil, false) - _, err := proposer.Propose(context.Background(), capabilityPolicyProposal(t)) - require.ErrorIs(t, err, capabilities.ErrCapabilityUnsupported) - require.False(t, delegate.called) -} - -func TestCapabilityPolicyProposerAllowsActiveTechnicalCapability(t *testing.T) { - delegate := new(capabilityPolicyTestProposer) - proposer := NewCapabilityPolicyProposer(delegate, func() (*commonpb.ClusterPolicy, error) { - return &commonpb.ClusterPolicy{ - CapabilityActivations: []*commonpb.CapabilityActivation{{ - Id: 99, - Active: true, - }}, - }, nil - }, nil, false) - _, err := proposer.Propose(context.Background(), capabilityPolicyProposal(t)) require.NoError(t, err) require.True(t, delegate.called) From d95d0d1a223dfb2a3feb24c3ce81ade6b3c34d14 Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Thu, 22 Oct 2026 19:39:19 +0200 Subject: [PATCH 026/127] fix(consensus): inject membership policy provider --- internal/bootstrap/module.go | 16 +++++++-- .../membership/confchange_identity_test.go | 4 +-- internal/infra/membership/membership.go | 35 +++++++++---------- 3 files changed, 31 insertions(+), 24 deletions(-) diff --git a/internal/bootstrap/module.go b/internal/bootstrap/module.go index e64473316b..b31eed1410 100644 --- a/internal/bootstrap/module.go +++ b/internal/bootstrap/module.go @@ -235,14 +235,24 @@ func Module() fx.Option { // constructors. raftmembership.NewPeerStore, fx.Annotate(func( - store *raftmembership.PeerStore, + peerStore *raftmembership.PeerStore, defaultTransport *node.DefaultTransport, servicePool *transport.ConnectionPool, cfg node.NodeConfig, + durableStore *dal.Store, logger logging.Logger, ) (*raftmembership.Membership, error) { - return raftmembership.NewMembership(store, defaultTransport, servicePool, cfg.NodeID, cfg.AdvertiseAddr, cfg.ServiceAdvertiseAddr, cfg.InstanceID, logger) - }, fx.ParamTags(``, ``, `name:"service"`, ``, ``)), + policyProvider := func() *commonpb.ClusterPolicy { + policy, err := query.ReadClusterPolicy(durableStore) + if err != nil { + return nil + } + + return policy + } + + return raftmembership.NewMembership(peerStore, defaultTransport, servicePool, cfg.NodeID, cfg.AdvertiseAddr, cfg.ServiceAdvertiseAddr, cfg.InstanceID, logger, policyProvider) + }, fx.ParamTags(``, ``, `name:"service"`, ``, ``, ``)), // Provide events.Proposer from the Raft node (used by event emitter to replicate cursor). // Events go through Builder.Run, which already holds the IndexTracker // mutex around its proposer.Propose call. Wrapping the node in a diff --git a/internal/infra/membership/confchange_identity_test.go b/internal/infra/membership/confchange_identity_test.go index 56e2201b74..ca6af979a1 100644 --- a/internal/infra/membership/confchange_identity_test.go +++ b/internal/infra/membership/confchange_identity_test.go @@ -23,11 +23,11 @@ func TestValidateCapabilityEvidenceRequiresActiveCapabilities(t *testing.T) { func TestWriteConfChangeRejectsCapabilitiesMissingFromCommittedPolicy(t *testing.T) { m := newTestMembership(t) - m.SetCapabilityPolicyProvider(func() *commonpb.ClusterPolicy { + m.capabilityPolicyProvider = func() *commonpb.ClusterPolicy { return &commonpb.ClusterPolicy{ CapabilityActivations: []*commonpb.CapabilityActivation{{Id: 7, Active: true}}, } - }) + } data, err := MarshalConfChangeContext(ConfChangeContext{ RaftAddress: "peer:7777", diff --git a/internal/infra/membership/membership.go b/internal/infra/membership/membership.go index d120b36f37..92577a1f27 100644 --- a/internal/infra/membership/membership.go +++ b/internal/infra/membership/membership.go @@ -101,7 +101,7 @@ type Membership struct { // Failure to read the peer rows is fatal: without them the node would // boot with an empty cache while the WAL ConfState still claims the // cluster has voters. -func NewMembership(store *PeerStore, transport Transport, pool Pool, selfNodeID uint64, selfRaftAddr, selfServiceAddr string, selfInstanceID []byte, logger logging.Logger) (*Membership, error) { +func NewMembership(store *PeerStore, transport Transport, pool Pool, selfNodeID uint64, selfRaftAddr, selfServiceAddr string, selfInstanceID []byte, logger logging.Logger, policyProviders ...func() *commonpb.ClusterPolicy) (*Membership, error) { if err := ValidateInstanceID(selfInstanceID); err != nil { return nil, fmt.Errorf("invalid self identity: %w", err) } @@ -115,16 +115,22 @@ func NewMembership(store *PeerStore, transport Transport, pool Pool, selfNodeID "peerCount": len(addresses), }).Infof("Loaded cluster membership from Pebble") + var policyProvider func() *commonpb.ClusterPolicy + if len(policyProviders) > 0 { + policyProvider = policyProviders[0] + } + return &Membership{ - store: store, - transport: transport, - pool: pool, - selfNodeID: selfNodeID, - selfRaftAddr: selfRaftAddr, - selfServiceAddr: selfServiceAddr, - selfInstanceID: bytes.Clone(selfInstanceID), - logger: logger, - addresses: addresses, + store: store, + transport: transport, + pool: pool, + selfNodeID: selfNodeID, + selfRaftAddr: selfRaftAddr, + selfServiceAddr: selfServiceAddr, + selfInstanceID: bytes.Clone(selfInstanceID), + logger: logger, + capabilityPolicyProvider: policyProvider, + addresses: addresses, }, nil } @@ -261,15 +267,6 @@ func (m *Membership) SetCapabilityGate(required []uint32) error { return nil } -// SetCapabilityPolicyProvider installs the authoritative committed-policy -// view used while applying a ConfChange. It is called only by the FSM apply -// path, where the policy and membership entries are processed in Raft order. -func (m *Membership) SetCapabilityPolicyProvider(provider func() *commonpb.ClusterPolicy) { - m.mu.Lock() - defer m.mu.Unlock() - m.capabilityPolicyProvider = provider -} - // CapabilityGate returns the current committed activation gate. func (m *Membership) CapabilityGate() (required []uint32) { m.mu.RLock() From b97d5a77a2e90a2f7cec6485cbfd52bd12e5aa14 Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Thu, 22 Oct 2026 19:44:34 +0200 Subject: [PATCH 027/127] fix(consensus): preserve capability activations --- .../cluster_policy_reconciler_test.go | 21 +++++++++++++++++++ internal/bootstrap/module.go | 8 +++++++ 2 files changed, 29 insertions(+) diff --git a/internal/bootstrap/cluster_policy_reconciler_test.go b/internal/bootstrap/cluster_policy_reconciler_test.go index 1039046e25..16e0bf4bd7 100644 --- a/internal/bootstrap/cluster_policy_reconciler_test.go +++ b/internal/bootstrap/cluster_policy_reconciler_test.go @@ -195,4 +195,25 @@ func TestReconcileClusterPolicy(t *testing.T) { require.Zero(t, admission.callCount(), "a newer applied revision must not be overwritten") }) + + t.Run("preserves committed capability activations on a revision bump", func(t *testing.T) { + t.Parallel() + + store := newReconcilerTestStore(t) + cfg := validBaseConfig() + batch := store.OpenWriteSession() + require.NoError(t, state.SaveClusterPolicy(batch, &commonpb.ClusterPolicy{ + Revision: cfg.ClusterPolicyRevision - 1, + CapabilityActivations: []*commonpb.CapabilityActivation{{ + Id: 7, Active: true, + }}, + })) + require.NoError(t, batch.Commit()) + + admission := &fakeAdmission{} + reconcileClusterPolicy(context.Background(), admission, store, cfg, func() bool { return true }, logging.Testing()) + + require.Equal(t, 1, admission.callCount()) + require.Equal(t, []*commonpb.CapabilityActivation{{Id: 7, Active: true}}, admission.lastPolicy.GetCapabilityActivations()) + }) } diff --git a/internal/bootstrap/module.go b/internal/bootstrap/module.go index b31eed1410..2ae800cfa7 100644 --- a/internal/bootstrap/module.go +++ b/internal/bootstrap/module.go @@ -1750,6 +1750,14 @@ func reconcileClusterPolicy(ctx context.Context, admission ctrl.Admission, store MetadataMaxEntityBytes: cfg.MetadataMaxEntityBytes, MetadataMaxCommandBytes: cfg.MetadataMaxCommandBytes, } + if applied != nil { + desired.CapabilityActivations = make([]*commonpb.CapabilityActivation, 0, len(applied.GetCapabilityActivations())) + for _, activation := range applied.GetCapabilityActivations() { + if activation != nil { + desired.CapabilityActivations = append(desired.CapabilityActivations, activation.CloneVT()) + } + } + } appliedRev := applied.GetRevision() From 406e5314ea20d081923d97f047ad25bb173cfeb5 Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Thu, 22 Oct 2026 20:01:17 +0200 Subject: [PATCH 028/127] fix(consensus): preserve capabilities across snapshots --- internal/infra/membership/membership.go | 5 ++++- internal/infra/membership/membership_test.go | 2 ++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/internal/infra/membership/membership.go b/internal/infra/membership/membership.go index 92577a1f27..5f7bdc6e99 100644 --- a/internal/infra/membership/membership.go +++ b/internal/infra/membership/membership.go @@ -615,7 +615,10 @@ func (m *Membership) OnSnapshotInstalled() { // Better to fall through and let Rehydrate reload from Pebble // (with the leader's possibly-stale self); the next OnSnapshot // or restart will re-attempt the self-Put. - if err := m.store.Put(m.selfNodeID, m.selfRaftAddr, m.selfServiceAddr, m.selfInstanceID); err != nil { + m.mu.RLock() + selfCapabilities := append([]uint32(nil), m.addresses[m.selfNodeID].Capabilities...) + m.mu.RUnlock() + if err := m.store.PutWithEvidence(m.selfNodeID, m.selfRaftAddr, m.selfServiceAddr, m.selfInstanceID, selfCapabilities); err != nil { m.logger.WithFields(map[string]any{ "error": err, }).Errorf("Refreshing self in Pebble before snapshot reload failed; next checkpoint we serve may carry a stale self address") diff --git a/internal/infra/membership/membership_test.go b/internal/infra/membership/membership_test.go index 98be62ac55..32e8fbc858 100644 --- a/internal/infra/membership/membership_test.go +++ b/internal/infra/membership/membership_test.go @@ -408,6 +408,7 @@ func TestMembership_OnSnapshotInstalled(t *testing.T) { // Pre-swap state: cluster A had peer 7. require.NoError(t, ps.Put(7, "old:1", "old:2", fixedInstanceID(7))) + require.NoError(t, ps.PutWithEvidence(testSelfNodeID, testSelfRaftAddr, testSelfServiceAddr, fixedInstanceID(0x42), []uint32{7})) m, err := NewMembership(ps, noopTransport{}, noopPool{}, testSelfNodeID, testSelfRaftAddr, testSelfServiceAddr, fixedInstanceID(0x42), logging.Testing()) require.NoError(t, err) @@ -430,6 +431,7 @@ func TestMembership_OnSnapshotInstalled(t *testing.T) { require.Equal(t, "new:1", got[1].RaftAddress) require.Equal(t, "new:3", got[3].RaftAddress) require.Equal(t, testSelfRaftAddr, got[testSelfNodeID].RaftAddress, "self must be re-upserted with local truth") + require.Equal(t, []uint32{7}, got[testSelfNodeID].Capabilities, "self capabilities must survive snapshot restore") require.NotContains(t, got, uint64(7)) } From 0e7676b7fbbc1616dd25c7273fda3011efcfe12d Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Thu, 22 Oct 2026 20:09:12 +0200 Subject: [PATCH 029/127] fix(consensus): propagate capabilities on learner admission --- internal/adapter/grpc/server_cluster.go | 2 +- internal/application/membership/membership.go | 8 +- .../membership/membership_node_test.go | 63 +++++++++------ internal/proto/clusterpb/cluster_reader.pb.go | 5 ++ .../proto/clusterpb/cluster_vtproto.pb.go | 78 +++++++++++++++++++ misc/proto/cluster.proto | 1 + 6 files changed, 130 insertions(+), 27 deletions(-) diff --git a/internal/adapter/grpc/server_cluster.go b/internal/adapter/grpc/server_cluster.go index e917c5f1bc..27d810beda 100644 --- a/internal/adapter/grpc/server_cluster.go +++ b/internal/adapter/grpc/server_cluster.go @@ -500,7 +500,7 @@ func (impl *ClusterServiceServerImpl) AddLearner(ctx context.Context, req *clust // Administrative registration is allowed only for a concrete peer // incarnation. The caller must read the target's persisted INSTANCE_ID; // creating an identity-less phantom member is forbidden. - if err := impl.membership.AddLearner(ctx, req.GetNodeId(), req.GetRaftAddress(), req.GetServiceAddress(), req.GetInstanceId()); err != nil { + if err := impl.membership.AddLearnerWithCapabilities(ctx, req.GetNodeId(), req.GetRaftAddress(), req.GetServiceAddress(), req.GetInstanceId(), req.GetCapabilities()); err != nil { return nil, err } diff --git a/internal/application/membership/membership.go b/internal/application/membership/membership.go index 045afbbb18..6725291bd2 100644 --- a/internal/application/membership/membership.go +++ b/internal/application/membership/membership.go @@ -91,7 +91,13 @@ func (s *Service) IsRemoved(nodeID uint64, instanceID []byte) (bool, error) { // Returns node.ErrNodeAlreadyInCluster on idempotent retries; adapters // map that to their transport-specific "already-exists" status. func (s *Service) AddLearner(ctx context.Context, nodeID uint64, raftAddr, serviceAddr string, instanceID []byte) error { - return s.addLearner(ctx, nodeID, raftAddr, serviceAddr, instanceID, nil, false) + return s.AddLearnerWithCapabilities(ctx, nodeID, raftAddr, serviceAddr, instanceID, nil) +} + +// AddLearnerWithCapabilities registers a learner and carries the joining +// binary's capability evidence in the committed ConfChange. +func (s *Service) AddLearnerWithCapabilities(ctx context.Context, nodeID uint64, raftAddr, serviceAddr string, instanceID []byte, capabilityIDs []uint32) error { + return s.addLearner(ctx, nodeID, raftAddr, serviceAddr, instanceID, capabilityIDs, false) } // JoinAsLearner registers a node that is booting with an empty WAL and no diff --git a/internal/application/membership/membership_node_test.go b/internal/application/membership/membership_node_test.go index c401b7d95d..5eafc1d8ae 100644 --- a/internal/application/membership/membership_node_test.go +++ b/internal/application/membership/membership_node_test.go @@ -142,34 +142,34 @@ func TestService_LearnerRegistrationCommitsAndPreservesCallerIntent(t *testing.T } t.Run(name, func(t *testing.T) { t.Parallel() - synctest.Test(t, func(t *testing.T) { - s := newMembershipServiceNode(t) - ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second) - defer cancel() - register := s.AddLearner + s := newMembershipServiceNode(t) + ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second) + defer cancel() + register := func(ctx context.Context, nodeID uint64, raftAddr, serviceAddr string, instanceID []byte, capabilities []uint32) error { if bootJoin { - register = s.JoinAsLearner + return s.JoinAsLearnerWithCapabilities(ctx, nodeID, raftAddr, serviceAddr, instanceID, capabilities) } - require.NoError(t, register(ctx, 2, "peer:7777", "peer:8888", []byte("peer-instance-id"))) - peers, err := s.ListPeers(ctx) - require.NoError(t, err) - require.Contains(t, peers, Peer{ - ID: 2, RaftAddress: "peer:7777", ServiceAddress: "peer:8888", InstanceID: []byte("peer-instance-id"), - }) - - // The existing voter has replicated its own no-op. An administrative - // retry is idempotent, while a fresh-WAL boot against that progress - // must report stale state, even with the same persisted identity. - before := s.infraMembership.PeerAddresses() - err = register(ctx, 1, "replacement:7777", "replacement:8888", []byte("self-instance-id")) - if bootJoin { - require.ErrorIs(t, err, node.ErrNodeStaleProgress) - } else { - require.ErrorIs(t, err, node.ErrNodeAlreadyInCluster) - } - require.ErrorContains(t, err, "adding learner:") - require.Equal(t, before, s.infraMembership.PeerAddresses(), "rejected registration must retain committed addresses") + return s.AddLearnerWithCapabilities(ctx, nodeID, raftAddr, serviceAddr, instanceID, capabilities) + } + require.NoError(t, register(ctx, 2, "peer:7777", "peer:8888", []byte("peer-instance-id"), nil)) + peers, err := s.ListPeers(ctx) + require.NoError(t, err) + require.Contains(t, peers, Peer{ + ID: 2, RaftAddress: "peer:7777", ServiceAddress: "peer:8888", InstanceID: []byte("peer-instance-id"), }) + + // The existing voter has replicated its own no-op. An administrative + // retry is idempotent, while a fresh-WAL boot against that progress + // must report stale state, even with the same persisted identity. + before := s.infraMembership.PeerAddresses() + err = register(ctx, 1, "replacement:7777", "replacement:8888", []byte("self-instance-id"), nil) + if bootJoin { + require.ErrorIs(t, err, node.ErrNodeStaleProgress) + } else { + require.ErrorIs(t, err, node.ErrNodeAlreadyInCluster) + } + require.ErrorContains(t, err, "adding learner:") + require.Equal(t, before, s.infraMembership.PeerAddresses(), "rejected registration must retain committed addresses") }) } } @@ -218,3 +218,16 @@ func TestService_JoinAsLearnerWithCapabilitiesPersistsEvidence(t *testing.T) { InstanceID: []byte("peer-instance-id"), Capabilities: []uint32{7, 9}, }) } + +func TestService_AddLearnerWithCapabilitiesPersistsEvidence(t *testing.T) { + t.Parallel() + + s := newMembershipServiceNode(t) + ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second) + defer cancel() + + require.NoError(t, s.AddLearnerWithCapabilities(ctx, 2, "peer:7777", "peer:8888", []byte("peer-instance-id"), []uint32{7, 9})) + + peer := s.infraMembership.PeerAddresses()[2] + require.Equal(t, []uint32{7, 9}, peer.Capabilities) +} diff --git a/internal/proto/clusterpb/cluster_reader.pb.go b/internal/proto/clusterpb/cluster_reader.pb.go index 4b488c478b..1754b53887 100644 --- a/internal/proto/clusterpb/cluster_reader.pb.go +++ b/internal/proto/clusterpb/cluster_reader.pb.go @@ -1660,6 +1660,7 @@ type AddLearnerRequestReader interface { GetRaftAddress() string GetServiceAddress() string GetInstanceId() []byte + GetCapabilities() []uint32 Mutate() *AddLearnerRequest } @@ -1681,6 +1682,10 @@ func (r *addLearnerRequestReadonly) GetInstanceId() []byte { return bytes.Clone((*AddLearnerRequest)(r).GetInstanceId()) } +func (r *addLearnerRequestReadonly) GetCapabilities() []uint32 { + return slices.Clone((*AddLearnerRequest)(r).GetCapabilities()) +} + func (r *addLearnerRequestReadonly) Mutate() *AddLearnerRequest { return (*AddLearnerRequest)(r).CloneVT() } diff --git a/internal/proto/clusterpb/cluster_vtproto.pb.go b/internal/proto/clusterpb/cluster_vtproto.pb.go index 9d4422760a..7bbbcc9c9f 100644 --- a/internal/proto/clusterpb/cluster_vtproto.pb.go +++ b/internal/proto/clusterpb/cluster_vtproto.pb.go @@ -443,6 +443,11 @@ func (m *AddLearnerRequest) CloneVT() *AddLearnerRequest { copy(tmpBytes, rhs) r.InstanceId = tmpBytes } + if rhs := m.Capabilities; rhs != nil { + tmpContainer := make([]uint32, len(rhs)) + copy(tmpContainer, rhs) + r.Capabilities = tmpContainer + } if len(m.unknownFields) > 0 { r.unknownFields = make([]byte, len(m.unknownFields)) copy(r.unknownFields, m.unknownFields) @@ -1419,6 +1424,15 @@ func (this *AddLearnerRequest) EqualVT(that *AddLearnerRequest) bool { if string(this.InstanceId) != string(that.InstanceId) { return false } + if len(this.Capabilities) != len(that.Capabilities) { + return false + } + for i, vx := range this.Capabilities { + vy := that.Capabilities[i] + if vx != vy { + return false + } + } return string(this.unknownFields) == string(that.unknownFields) } @@ -3114,6 +3128,15 @@ func (m *AddLearnerRequest) MarshalToSizedBufferVT(dAtA []byte) (int, error) { i -= len(m.unknownFields) copy(dAtA[i:], m.unknownFields) } + if len(m.Capabilities) > 0 { + for iNdEx := len(m.Capabilities) - 1; iNdEx >= 0; iNdEx-- { + i -= 4 + binary.LittleEndian.PutUint32(dAtA[i:], uint32(m.Capabilities[iNdEx])) + } + i = protohelpers.EncodeVarint(dAtA, i, uint64(len(m.Capabilities)*4)) + i-- + dAtA[i] = 0x2a + } if len(m.InstanceId) > 0 { i -= len(m.InstanceId) copy(dAtA[i:], m.InstanceId) @@ -4534,6 +4557,9 @@ func (m *AddLearnerRequest) SizeVT() (n int) { if l > 0 { n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) } + if len(m.Capabilities) > 0 { + n += 1 + protohelpers.SizeOfVarint(uint64(len(m.Capabilities)*4)) + len(m.Capabilities)*4 + } n += len(m.unknownFields) return n } @@ -7716,6 +7742,58 @@ func (m *AddLearnerRequest) UnmarshalVT(dAtA []byte) error { m.InstanceId = []byte{} } iNdEx = postIndex + case 5: + if wireType == 5 { + var v uint32 + if (iNdEx + 4) > l { + return io.ErrUnexpectedEOF + } + v = uint32(binary.LittleEndian.Uint32(dAtA[iNdEx:])) + iNdEx += 4 + m.Capabilities = append(m.Capabilities, v) + } else if wireType == 2 { + var packedLen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + packedLen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if packedLen < 0 { + return protohelpers.ErrInvalidLength + } + postIndex := iNdEx + packedLen + if postIndex < 0 { + return protohelpers.ErrInvalidLength + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + var elementCount int + elementCount = packedLen / 4 + if elementCount != 0 && len(m.Capabilities) == 0 { + m.Capabilities = make([]uint32, 0, elementCount) + } + for iNdEx < postIndex { + var v uint32 + if (iNdEx + 4) > l { + return io.ErrUnexpectedEOF + } + v = uint32(binary.LittleEndian.Uint32(dAtA[iNdEx:])) + iNdEx += 4 + m.Capabilities = append(m.Capabilities, v) + } + } else { + return fmt.Errorf("proto: wrong wireType = %d for field Capabilities", wireType) + } default: iNdEx = preIndex skippy, err := protohelpers.Skip(dAtA[iNdEx:]) diff --git a/misc/proto/cluster.proto b/misc/proto/cluster.proto index c651e49505..29a4b8029d 100644 --- a/misc/proto/cluster.proto +++ b/misc/proto/cluster.proto @@ -287,6 +287,7 @@ message AddLearnerRequest { string raft_address = 2; // Raft transport address of the learner string service_address = 3; // Service API address of the learner bytes instance_id = 4; // 16-byte UUID from the learner's persisted INSTANCE_ID marker; required + repeated fixed32 capabilities = 5; // Capabilities supported by the learner binary } message AddLearnerResponse {} From 6c001a64452bfc943d7951bd695b0c88ba68c0b5 Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Thu, 22 Oct 2026 20:16:26 +0200 Subject: [PATCH 030/127] fix(consensus): retry refresh against older peers --- internal/bootstrap/module.go | 20 ++++++++++++- .../module_refresh_membership_test.go | 28 +++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) create mode 100644 internal/bootstrap/module_refresh_membership_test.go diff --git a/internal/bootstrap/module.go b/internal/bootstrap/module.go index 2ae800cfa7..a5f418e926 100644 --- a/internal/bootstrap/module.go +++ b/internal/bootstrap/module.go @@ -1582,7 +1582,7 @@ func tryRefreshMembership(ctx context.Context, cfg Config, tlsCfg TLSConfig, log return nil } - if st, ok := status.FromError(callErr); ok && st.Code() == codes.Unavailable { + if isRetryableMembershipRefreshError(callErr) { continue } @@ -1598,6 +1598,24 @@ func tryRefreshMembership(ctx context.Context, cfg Config, tlsCfg TLSConfig, log } } +func isRetryableMembershipRefreshError(err error) bool { + st, ok := status.FromError(err) + if !ok { + return false + } + + if st.Code() == codes.Unavailable { + return true + } + + // TODO(rc.3): remove this compatibility retry once every supported peer + // implements RefreshMembership. During the rc.1/rc.2 rolling upgrade, an + // older peer returns Unimplemented rather than Unavailable for this newly + // introduced RPC; treating it as transient lets the node keep trying newer + // peers without making the startup hook fail immediately. + return st.Code() == codes.Unimplemented +} + func capabilityIDsAsUint32(ids []capabilities.CapabilityID) []uint32 { result := make([]uint32, len(ids)) for i, id := range ids { diff --git a/internal/bootstrap/module_refresh_membership_test.go b/internal/bootstrap/module_refresh_membership_test.go new file mode 100644 index 0000000000..51e3307ef1 --- /dev/null +++ b/internal/bootstrap/module_refresh_membership_test.go @@ -0,0 +1,28 @@ +package bootstrap + +import ( + "testing" + + "github.com/stretchr/testify/require" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +func TestIsRetryableMembershipRefreshError(t *testing.T) { + t.Parallel() + + for _, tc := range []struct { + name string + code codes.Code + want bool + }{ + {name: "unavailable", code: codes.Unavailable, want: true}, + {name: "old peer missing rpc", code: codes.Unimplemented, want: true}, + {name: "invalid request", code: codes.InvalidArgument, want: false}, + } { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + require.Equal(t, tc.want, isRetryableMembershipRefreshError(status.Error(tc.code, "refresh failed"))) + }) + } +} From 2880013d86fe5ae438a6c92ea059686055ed41b6 Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Thu, 22 Oct 2026 20:19:05 +0200 Subject: [PATCH 031/127] fix(consensus): allow membership before initial policy --- internal/bootstrap/module.go | 9 +--- .../membership/confchange_identity_test.go | 53 ++++++++++++++++++- internal/infra/membership/membership.go | 23 ++++---- 3 files changed, 66 insertions(+), 19 deletions(-) diff --git a/internal/bootstrap/module.go b/internal/bootstrap/module.go index a5f418e926..c6e8cdb5af 100644 --- a/internal/bootstrap/module.go +++ b/internal/bootstrap/module.go @@ -242,13 +242,8 @@ func Module() fx.Option { durableStore *dal.Store, logger logging.Logger, ) (*raftmembership.Membership, error) { - policyProvider := func() *commonpb.ClusterPolicy { - policy, err := query.ReadClusterPolicy(durableStore) - if err != nil { - return nil - } - - return policy + policyProvider := func() (*commonpb.ClusterPolicy, error) { + return query.ReadClusterPolicy(durableStore) } return raftmembership.NewMembership(peerStore, defaultTransport, servicePool, cfg.NodeID, cfg.AdvertiseAddr, cfg.ServiceAdvertiseAddr, cfg.InstanceID, logger, policyProvider) diff --git a/internal/infra/membership/confchange_identity_test.go b/internal/infra/membership/confchange_identity_test.go index ca6af979a1..cce1bcffb4 100644 --- a/internal/infra/membership/confchange_identity_test.go +++ b/internal/infra/membership/confchange_identity_test.go @@ -1,6 +1,7 @@ package membership import ( + "errors" "testing" "github.com/stretchr/testify/require" @@ -10,6 +11,54 @@ import ( "github.com/formancehq/ledger/v3/internal/proto/commonpb" ) +func TestWriteConfChangeAllowsRegistrationBeforeInitialPolicy(t *testing.T) { + m := newTestMembership(t) + m.capabilityPolicyProvider = func() (*commonpb.ClusterPolicy, error) { + return nil, nil + } + + data, err := MarshalConfChangeContext(ConfChangeContext{ + RaftAddress: "peer:7777", + ServiceAddress: "peer:8888", + InstanceID: fixedInstanceID(1), + }) + require.NoError(t, err) + cc := &raftpb.ConfChangeV2{ + Changes: []*raftpb.ConfChangeSingle{{Type: new(raftpb.ConfChangeAddLearnerNode), NodeId: proto.Uint64(9)}}, + Context: data, + } + encoded, err := proto.Marshal(cc) + require.NoError(t, err) + + session := m.store.OpenWriteSession() + require.NoError(t, m.WriteConfChange(&raftpb.Entry{Type: new(raftpb.EntryConfChangeV2), Data: encoded}, session)) + require.NoError(t, session.Commit()) +} + +func TestWriteConfChangeFailsClosedOnPolicyReadError(t *testing.T) { + m := newTestMembership(t) + m.capabilityPolicyProvider = func() (*commonpb.ClusterPolicy, error) { + return nil, errors.New("pebble unavailable") + } + + data, err := MarshalConfChangeContext(ConfChangeContext{ + RaftAddress: "peer:7777", + ServiceAddress: "peer:8888", + InstanceID: fixedInstanceID(1), + }) + require.NoError(t, err) + cc := &raftpb.ConfChangeV2{ + Changes: []*raftpb.ConfChangeSingle{{Type: new(raftpb.ConfChangeAddLearnerNode), NodeId: proto.Uint64(9)}}, + Context: data, + } + encoded, err := proto.Marshal(cc) + require.NoError(t, err) + + session := m.store.OpenWriteSession() + require.ErrorContains(t, m.WriteConfChange(&raftpb.Entry{Type: new(raftpb.EntryConfChangeV2), Data: encoded}, session), "pebble unavailable") + require.NoError(t, session.Commit()) +} + func TestValidateCapabilityEvidenceRequiresActiveCapabilities(t *testing.T) { ctx := &ConfChangeContext{ InstanceID: bytesN(0x42), @@ -23,10 +72,10 @@ func TestValidateCapabilityEvidenceRequiresActiveCapabilities(t *testing.T) { func TestWriteConfChangeRejectsCapabilitiesMissingFromCommittedPolicy(t *testing.T) { m := newTestMembership(t) - m.capabilityPolicyProvider = func() *commonpb.ClusterPolicy { + m.capabilityPolicyProvider = func() (*commonpb.ClusterPolicy, error) { return &commonpb.ClusterPolicy{ CapabilityActivations: []*commonpb.CapabilityActivation{{Id: 7, Active: true}}, - } + }, nil } data, err := MarshalConfChangeContext(ConfChangeContext{ diff --git a/internal/infra/membership/membership.go b/internal/infra/membership/membership.go index 5f7bdc6e99..948c0478e2 100644 --- a/internal/infra/membership/membership.go +++ b/internal/infra/membership/membership.go @@ -75,7 +75,7 @@ type Membership struct { // admission observes one lock-consistent view of active requirements and // member identity evidence. requiredCapabilities []uint32 - capabilityPolicyProvider func() *commonpb.ClusterPolicy + capabilityPolicyProvider func() (*commonpb.ClusterPolicy, error) // started gates transport / service-pool wiring. Until Start fires // (OnStart hook, after the local Raft gRPC server is listening), // Set / Remove / Register / Rehydrate mutate only the cache — the @@ -101,7 +101,7 @@ type Membership struct { // Failure to read the peer rows is fatal: without them the node would // boot with an empty cache while the WAL ConfState still claims the // cluster has voters. -func NewMembership(store *PeerStore, transport Transport, pool Pool, selfNodeID uint64, selfRaftAddr, selfServiceAddr string, selfInstanceID []byte, logger logging.Logger, policyProviders ...func() *commonpb.ClusterPolicy) (*Membership, error) { +func NewMembership(store *PeerStore, transport Transport, pool Pool, selfNodeID uint64, selfRaftAddr, selfServiceAddr string, selfInstanceID []byte, logger logging.Logger, policyProviders ...func() (*commonpb.ClusterPolicy, error)) (*Membership, error) { if err := ValidateInstanceID(selfInstanceID); err != nil { return nil, fmt.Errorf("invalid self identity: %w", err) } @@ -115,7 +115,7 @@ func NewMembership(store *PeerStore, transport Transport, pool Pool, selfNodeID "peerCount": len(addresses), }).Infof("Loaded cluster membership from Pebble") - var policyProvider func() *commonpb.ClusterPolicy + var policyProvider func() (*commonpb.ClusterPolicy, error) if len(policyProviders) > 0 { policyProvider = policyProviders[0] } @@ -668,14 +668,17 @@ func (m *Membership) WriteConfChange(entry *raftpb.Entry, session *dal.WriteSess provider := m.capabilityPolicyProvider m.mu.RUnlock() if provider != nil { - policy := provider() - if policy == nil { - return fmt.Errorf("invalid capability evidence for peer %d: missing committed cluster policy", nodeID) + policy, err := provider() + if err != nil { + return fmt.Errorf("reading committed cluster policy for peer %d: %w", nodeID, err) } - required := make([]uint32, 0, len(policy.GetCapabilityActivations())) - for _, activation := range policy.GetCapabilityActivations() { - if activation != nil && activation.GetActive() { - required = append(required, activation.GetId()) + required := make([]uint32, 0) + if policy != nil { + required = make([]uint32, 0, len(policy.GetCapabilityActivations())) + for _, activation := range policy.GetCapabilityActivations() { + if activation != nil && activation.GetActive() { + required = append(required, activation.GetId()) + } } } slices.Sort(required) From c131aad1c043bd4c088dd3ed82a731b68bb79348 Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Thu, 22 Oct 2026 20:22:05 +0200 Subject: [PATCH 032/127] fix(consensus): reconcile capability membership changes --- docs/ops/upgrades.md | 11 +++--- .../subsystems/consensus/capabilities.md | 35 +++++++++++-------- internal/bootstrap/module.go | 10 +++--- internal/infra/node/member_ids_test.go | 20 +++++++++++ internal/infra/node/node.go | 30 ++++++++++++---- 5 files changed, 76 insertions(+), 30 deletions(-) diff --git a/docs/ops/upgrades.md b/docs/ops/upgrades.md index d65dcc7636..475a9d053e 100644 --- a/docs/ops/upgrades.md +++ b/docs/ops/upgrades.md @@ -330,11 +330,12 @@ For RC.3 to RC.4, use the [operator-required stopped restart](#operator-required Capability barriers are the prerequisite for any future incompatible Raft entry, execution-plan, replay, or snapshot format. Deploy barrier-aware Ledger -servers to every voter and learner first, then arm the replicated policy. An -old leader that predates the barrier is not made safe by upgrading other nodes. -See the [capability contract](../technical/architecture/subsystems/consensus/capabilities.md) -for the `FEATURE_ONLY` and `ALL_WRITES` admission policies, membership evidence, -diagnostics, and downgrade boundary. +servers to every voter and learner first. This release only deploys the +capability-evidence mechanism; it does not provide an operator-facing arming +procedure or activate a new capability automatically. An old leader that +predates the barrier is not made safe by upgrading other nodes. See the +[capability contract](../technical/architecture/subsystems/consensus/capabilities.md) +for membership evidence and the downgrade boundary. Use this for RC updates after the mixed-version validation above passes: diff --git a/docs/technical/architecture/subsystems/consensus/capabilities.md b/docs/technical/architecture/subsystems/consensus/capabilities.md index f0ed1412a6..7f8357b8a6 100644 --- a/docs/technical/architecture/subsystems/consensus/capabilities.md +++ b/docs/technical/architecture/subsystems/consensus/capabilities.md @@ -116,12 +116,16 @@ validate their capability evidence separately. ## Restart refresh A normal restart does not run `JoinAsLearner`: the node restores its existing -WAL `ConfState` and keeps the same `(node_id, instance_id)` incarnation. Before -inbound Raft traffic is enabled, a node whose WAL carries `CLUSTER_JOINED` -sends an internal `RefreshMembership` request to a configured peer. Followers -forward that request to the current leader over the Raft gRPC server. The -leader validates the identity and capability evidence against the active policy, -then proposes a +WAL `ConfState` and keeps the same `(node_id, instance_id)` incarnation. After +the node and its Raft gRPC server are started, a node whose WAL carries +`CLUSTER_JOINED` sends an internal `RefreshMembership` request to a configured +peer. Followers forward that request to the current leader over the Raft gRPC +server. The node can therefore receive inbound Raft traffic before the refresh +commits; the refresh is an evidence update, not a pre-traffic barrier. During +the rc.1/rc.2 rolling upgrade, an older peer that returns `Unimplemented` is +treated as transient and the node retries until a compatible peer is available. +The leader validates the identity and capability evidence against the active +policy, then proposes a `ConfChangeUpdateNode` containing the current endpoints and capabilities. Only the committed change is authoritative. The update is replicated through @@ -129,11 +133,14 @@ Raft and applied to every member's peer store; a local restart-time upsert is not sufficient to activate a capability for admission. A fresh WAL instead uses `JoinAsLearner` and `ConfChangeAddLearnerNode`. -The cluster-policy reconciler activates a capability automatically once every -current voter and learner has refreshed membership with that capability. The -candidate set is the intersection of the local binary registry and all -committed membership evidence; an empty or incomplete membership set never -activates anything. Membership commits trigger reconciliation immediately, and -the periodic retry remains only as a self-healing fallback for leadership or -proposal failures. Once committed, an activation is monotonic and a new member -that does not advertise it is rejected. +The cluster-policy reconciler currently maintains the replicated policy and +preserves its committed capability activations; it does not automatically arm +or activate a newly advertised capability. A future explicit activation path +must first verify the candidate against every current voter and learner. The +candidate set should be the intersection of the local binary registry and all +committed membership evidence; an empty or incomplete membership set must not +activate anything. Membership commits trigger reconciliation immediately, and +the periodic retry remains as a self-healing fallback for leadership or +proposal failures. Once an activation path exists and an activation is +committed, it is monotonic and a new member that does not advertise it is +rejected. diff --git a/internal/bootstrap/module.go b/internal/bootstrap/module.go index c6e8cdb5af..3d7831f257 100644 --- a/internal/bootstrap/module.go +++ b/internal/bootstrap/module.go @@ -901,10 +901,9 @@ func Module() fx.Option { RequestShutdown: shutdownRequester(shutdowner), })) }, - // Wire Observer: handle LeadershipChange and LeaderReady events. - // ConfChange events are no longer dispatched here — Membership - // owns the transport / service-pool wiring and reacts directly - // to peer mutations (EN-1413). + // Wire Observer: handle LeadershipChange, LeaderReady and capability + // membership events. Membership owns transport / service-pool wiring; + // the capability event wakes policy reconciliation immediately. func( n *node.Node, store *dal.Store, @@ -915,6 +914,7 @@ func Module() fx.Option { mirrorManager *mirror.Manager, backupOrchestrator *backupapp.Orchestrator, healthChecker *clusterhealth.HealthChecker, + reconciler *ClusterPolicyReconciler, ) { n.SetObserver(node.NewObserver(func(event any) { switch e := event.(type) { @@ -946,6 +946,8 @@ func Module() fx.Option { // can block on its own commit. handleLeaderReadyEvent(eventsManager, mirrorManager, logger) proposeClusterConfigIfNeeded(e.Context, n, builder, store, cfg, logger) + case node.CapabilityMembershipChangedEvent: + reconciler.Trigger() default: logger.Errorf("Unknown observer event type: %T", event) } diff --git a/internal/infra/node/member_ids_test.go b/internal/infra/node/member_ids_test.go index 8c24e054c7..5fd59b96bb 100644 --- a/internal/infra/node/member_ids_test.go +++ b/internal/infra/node/member_ids_test.go @@ -5,6 +5,8 @@ import ( "github.com/stretchr/testify/require" "go.etcd.io/raft/v3/raftpb" + + "github.com/formancehq/ledger/v3/internal/infra/capabilities" ) func TestNodeMemberIDs(t *testing.T) { @@ -22,3 +24,21 @@ func TestNodeMemberIDs(t *testing.T) { require.Equal(t, []uint64{1, 2, 3, 4, 5}, n.MemberIDs()) } + +func TestCapabilityMemberRolesIncludesJointConfiguration(t *testing.T) { + t.Parallel() + + roles := capabilityMemberRoles(&raftpb.ConfState{ + Voters: []uint64{1}, + Learners: []uint64{4}, + VotersOutgoing: []uint64{2}, + LearnersNext: []uint64{5}, + }) + + require.Equal(t, map[uint64]capabilities.MemberRole{ + 1: capabilities.Voter, + 2: capabilities.Voter, + 4: capabilities.Learner, + 5: capabilities.Learner, + }, roles) +} diff --git a/internal/infra/node/node.go b/internal/infra/node/node.go index ecd347e5cd..f549837d3f 100644 --- a/internal/infra/node/node.go +++ b/internal/infra/node/node.go @@ -2579,13 +2579,7 @@ func (node *Node) CapabilityMembers() []capabilities.Member { return nil } addresses := node.membership.PeerAddresses() - roles := make(map[uint64]capabilities.MemberRole) - for _, id := range confState.GetVoters() { - roles[id] = capabilities.Voter - } - for _, id := range confState.GetLearners() { - roles[id] = capabilities.Learner - } + roles := capabilityMemberRoles(confState) result := make([]capabilities.Member, 0, len(roles)) for id, role := range roles { peer, ok := addresses[id] @@ -2613,6 +2607,28 @@ func (node *Node) CapabilityMembers() []capabilities.Member { return result } +func capabilityMemberRoles(confState *raftpb.ConfState) map[uint64]capabilities.MemberRole { + roles := make(map[uint64]capabilities.MemberRole) + for _, ids := range [][]uint64{ + confState.GetVoters(), + confState.GetVotersOutgoing(), + } { + for _, id := range ids { + roles[id] = capabilities.Voter + } + } + for _, ids := range [][]uint64{ + confState.GetLearners(), + confState.GetLearnersNext(), + } { + for _, id := range ids { + roles[id] = capabilities.Learner + } + } + + return roles +} + // Logger returns the node's logger. func (node *Node) Logger() logging.Logger { return node.logger From 4fd35646eb02cb60dd6ba238e144894817d01bfc Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Thu, 22 Oct 2026 20:24:53 +0200 Subject: [PATCH 033/127] fix(consensus): validate membership against apply policy --- internal/bootstrap/module.go | 11 ++++++++- internal/infra/membership/membership.go | 32 +++++++++++++++++++------ internal/infra/state/machine.go | 9 +++++++ 3 files changed, 44 insertions(+), 8 deletions(-) diff --git a/internal/bootstrap/module.go b/internal/bootstrap/module.go index 3d7831f257..4363bdda64 100644 --- a/internal/bootstrap/module.go +++ b/internal/bootstrap/module.go @@ -13,6 +13,7 @@ import ( "strings" "time" + "go.etcd.io/raft/v3/raftpb" "go.opentelemetry.io/otel/metric" sdkmetric "go.opentelemetry.io/otel/sdk/metric" "go.uber.org/fx" @@ -295,6 +296,7 @@ func Module() fx.Option { registry := state.NewStateRegistry(c, attrs) snapshotter := state.NewCacheSnapshotter(logger, registry, bloomFilters) + var machine *state.Machine m, err := state.NewMachine( logger, registry, @@ -308,11 +310,18 @@ func Module() fx.Option { bloomFilters, "", // Audit key is loaded from replicated state during recovery. cfg.NumscriptCacheSize, - membership.WriteConfChange, + func(entry *raftpb.Entry, session *dal.WriteSession) error { + var policy *commonpb.ClusterPolicy + if machine != nil { + policy = machine.ClusterPolicy() + } + return membership.WriteConfChangeWithPolicy(entry, session, policy) + }, ) if err != nil { return nil, err } + machine = m logger.WithFields(map[string]any{ "duration": time.Since(machineStart).String(), diff --git a/internal/infra/membership/membership.go b/internal/infra/membership/membership.go index 948c0478e2..e31e5374b8 100644 --- a/internal/infra/membership/membership.go +++ b/internal/infra/membership/membership.go @@ -648,6 +648,18 @@ func (m *Membership) OnSnapshotInstalled() { // Promotions repeat the existing peer registration, making replay independent // of the node-local membership cache. func (m *Membership) WriteConfChange(entry *raftpb.Entry, session *dal.WriteSession) error { + return m.writeConfChange(entry, session, nil, false) +} + +// WriteConfChangeWithPolicy applies a ConfChange using the policy state at the +// entry's position in the current FSM batch. This avoids validating a +// membership change against the policy that was durable before an earlier +// SetClusterPolicy order in the same batch. +func (m *Membership) WriteConfChangeWithPolicy(entry *raftpb.Entry, session *dal.WriteSession, policy *commonpb.ClusterPolicy) error { + return m.writeConfChange(entry, session, policy, true) +} + +func (m *Membership) writeConfChange(entry *raftpb.Entry, session *dal.WriteSession, effectivePolicy *commonpb.ClusterPolicy, hasEffectivePolicy bool) error { cc, ok, err := UnmarshalConfChangeV2(entry) if err != nil { return fmt.Errorf("decoding ConfChange entry: %w", err) @@ -664,14 +676,20 @@ func (m *Membership) WriteConfChange(entry *raftpb.Entry, session *dal.WriteSess if err := ValidateCapabilities(ctx.Capabilities); err != nil { return fmt.Errorf("invalid capability evidence for peer %d: %w", nodeID, err) } - m.mu.RLock() - provider := m.capabilityPolicyProvider - m.mu.RUnlock() - if provider != nil { - policy, err := provider() - if err != nil { - return fmt.Errorf("reading committed cluster policy for peer %d: %w", nodeID, err) + policy := effectivePolicy + if !hasEffectivePolicy { + m.mu.RLock() + provider := m.capabilityPolicyProvider + m.mu.RUnlock() + if provider != nil { + var err error + policy, err = provider() + if err != nil { + return fmt.Errorf("reading committed cluster policy for peer %d: %w", nodeID, err) + } } + } + if policy != nil || hasEffectivePolicy { required := make([]uint32, 0) if policy != nil { required = make([]uint32, 0, len(policy.GetCapabilityActivations())) diff --git a/internal/infra/state/machine.go b/internal/infra/state/machine.go index b53e1fdfe8..6efbf29b8f 100644 --- a/internal/infra/state/machine.go +++ b/internal/infra/state/machine.go @@ -316,6 +316,15 @@ func (fsm *Machine) LastAppliedIndex() uint64 { return fsm.State.LastAppliedIndex } +// ClusterPolicy returns the policy state at the current apply position. It is +// intended for callbacks invoked while PrepareEntries holds fsm.mu. +func (fsm *Machine) ClusterPolicy() *commonpb.ClusterPolicy { + if fsm.State.ClusterPolicy == nil { + return nil + } + return fsm.State.ClusterPolicy.CloneVT() +} + // RestoreState atomically replaces the FSM-level state. The intended callers // are Recovery (at boot) and Synchronizer (after install-snapshot), which // build a fresh FSMState from Pebble via LoadFSMStateFromStore and swap it From 40882da5ca31f4910f763cb219715bcfb1bad22b Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Thu, 22 Oct 2026 20:25:16 +0200 Subject: [PATCH 034/127] docs(consensus): clarify proposal capability scope --- .../subsystems/consensus/capabilities.md | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/docs/technical/architecture/subsystems/consensus/capabilities.md b/docs/technical/architecture/subsystems/consensus/capabilities.md index 7f8357b8a6..66d039e29d 100644 --- a/docs/technical/architecture/subsystems/consensus/capabilities.md +++ b/docs/technical/architecture/subsystems/consensus/capabilities.md @@ -52,14 +52,19 @@ The capability has two roles in this case: 2. The order declares the capability that selects the behavior during FSM apply. -The order must carry the semantic marker explicitly: +The serialized proposal currently carries `required_capabilities` at proposal +scope. That is sufficient only when every order in the proposal has the same +semantic contract. A proposal can batch multiple orders, so a future change +that mixes old and new semantics must either add a persisted marker to each +order (and dispatch apply from that marker) or reject mixed-semantic batches. +The conceptual marker for a homogeneous proposal is: ```text -old order: required_capabilities = [] -new order: required_capabilities = [MirrorIngestRejectInvalidAssetV2] +old proposal: required_capabilities = [] +new proposal: required_capabilities = [MirrorIngestRejectInvalidAssetV2] ``` -The FSM must use the order's declaration to select the behavior. It must not +The FSM must use the persisted declaration to select the behavior. It must not infer the behavior from the current policy alone. An old order can be replayed after the capability becomes active, and it must keep its old semantics. From 58c5203138c4e6536d60a66a4148a6cf44a784fe Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Fri, 23 Oct 2026 11:31:10 +0200 Subject: [PATCH 035/127] fix(consensus): gate proposals at Raft boundary --- internal/application/admission/admission.go | 57 ++----------------- .../admission/capability_policy_test.go | 32 ----------- internal/bootstrap/module.go | 26 ++++++++- internal/infra/node/node.go | 28 +++++++++ .../infra/node/proposal_admission_test.go | 38 +++++++++++++ 5 files changed, 95 insertions(+), 86 deletions(-) create mode 100644 internal/infra/node/proposal_admission_test.go diff --git a/internal/application/admission/admission.go b/internal/application/admission/admission.go index 6787a52828..d84eca1e40 100644 --- a/internal/application/admission/admission.go +++ b/internal/application/admission/admission.go @@ -43,41 +43,6 @@ import ( var tracer = otel.Tracer("admission") -// capabilityPolicyProposer is the final admission barrier for capability -// policy. It deliberately wraps the node proposer instead of checking during -// request preparation: plan.Builder.Run may rebuild a proposal under its -// tracker guard, and this wrapper therefore observes the exact bytes that are -// about to be queued for Raft. -type capabilityPolicyProposer struct { - delegate plan.Proposer - readPolicy func() (*commonpb.ClusterPolicy, error) - readMembers func() []capabilities.Member - businessWrite bool -} - -func (p capabilityPolicyProposer) Propose(ctx context.Context, proposal *node.Proposal) (*futures.Future[state.ApplyResult], error) { - policy, err := p.readPolicy() - if err != nil { - return nil, fmt.Errorf("reading cluster capability policy before proposal: %w", err) - } - state := capabilities.FromPolicy(policy) - if err := state.Validate(); err != nil { - return nil, err - } - - required := proposal.RequiredCapabilities() - var members []capabilities.Member - if p.readMembers != nil { - members = p.readMembers() - } - decision := capabilities.Evaluate(state, required, p.businessWrite, members) - if !decision.Allowed { - return nil, fmt.Errorf("%w: %s", capabilities.ErrCapabilityUnsupported, decision.Reason) - } - - return p.delegate.Propose(ctx, proposal) -} - //go:generate mockgen -write_source_comment=false -write_package_comment=false -source admission.go -destination proposer_generated_test.go -typed -package admission . Proposer type Proposer interface { Propose(context.Context, *node.Proposal) (*futures.Future[state.ApplyResult], error) @@ -99,8 +64,7 @@ type Admission struct { waitLeaderReady func(context.Context) error // auditProjectionState is node-local admission state. It may reject a // checkpoint proposal before Raft, but never affects deterministic apply. - auditProjectionState func() (disabled, rebuilding bool) - readCapabilityMembers func() []capabilities.Member + auditProjectionState func() (disabled, rebuilding bool) // clusterPolicyCommitted latches once the replicated cluster policy is // observed committed (revision > 0). The steady-state write path then skips @@ -172,10 +136,6 @@ func WithAuditProjectionState(state func() (disabled, rebuilding bool)) func(*Ad } } -func WithCapabilityMembership(read func() []capabilities.Member) func(*Admission) { - return func(a *Admission) { a.readCapabilityMembers = read } -} - func NewAdmission( store *dal.Store, logger logging.Logger, @@ -627,7 +587,7 @@ func (a *Admission) admit(ctx context.Context, prepare func(context.Context, *co return nil, err } - fsmFuture, cleanup, err := a.proposeAdmission(ctx, prepared.command, prepared.build, prepared.command.GetBatch() != nil) + fsmFuture, cleanup, err := a.proposeAdmission(ctx, prepared.command, prepared.build) if err != nil { return nil, err } @@ -812,7 +772,7 @@ func (a *Admission) buildPreload(ctx context.Context, cmd *raftcmdpb.Proposal, n return build, nil } -func (a *Admission) proposeAdmission(ctx context.Context, cmd *raftcmdpb.Proposal, build *plan.BuildResult, businessWrite bool) (*futures.Future[state.ApplyResult], func(), error) { +func (a *Admission) proposeAdmission(ctx context.Context, cmd *raftcmdpb.Proposal, build *plan.BuildResult) (*futures.Future[state.ApplyResult], func(), error) { // Step 5: Marshal + acquire proposal guard + set PredictedIndex // + propose, all via the shared preload runner. The runner also // patches PredictedIndex onto the pre-marshaled buffer (or @@ -826,15 +786,6 @@ func (a *Admission) proposeAdmission(ctx context.Context, cmd *raftcmdpb.Proposa // rebuild, keeping bits and plans in sync. ctx, proposeSpan := tracer.Start(ctx, "admission.propose") - // Keep the policy check inside the canonical runner's proposer callback. This - // is the last point before the proposal enters the node queue, including the - // runner's rebuild path, so a policy update cannot be bypassed by a stale - // preflight check. The policy is read from the replicated store rather than - // from process-local configuration. - policyProposer := capabilityPolicyProposer{delegate: a.proposer, readPolicy: func() (*commonpb.ClusterPolicy, error) { - return query.ReadClusterPolicy(a.store) - }, readMembers: a.readCapabilityMembers, businessWrite: businessWrite} - runResult, err := a.builder.Run( ctx, cmd, build, func(c *raftcmdpb.Proposal) ([]byte, error) { @@ -843,7 +794,7 @@ func (a *Admission) proposeAdmission(ctx context.Context, cmd *raftcmdpb.Proposa // admission metrics. return a.marshalCommand(ctx, c) }, - policyProposer, + a.proposer, ) if err != nil { proposeSpan.End() diff --git a/internal/application/admission/capability_policy_test.go b/internal/application/admission/capability_policy_test.go index 69c2f49366..96a993502b 100644 --- a/internal/application/admission/capability_policy_test.go +++ b/internal/application/admission/capability_policy_test.go @@ -6,26 +6,12 @@ import ( "github.com/stretchr/testify/require" - "github.com/formancehq/ledger/v3/internal/infra/capabilities" "github.com/formancehq/ledger/v3/internal/infra/health" "github.com/formancehq/ledger/v3/internal/infra/node" - "github.com/formancehq/ledger/v3/internal/infra/plan" "github.com/formancehq/ledger/v3/internal/infra/state" "github.com/formancehq/ledger/v3/internal/pkg/futures" - "github.com/formancehq/ledger/v3/internal/proto/commonpb" - "github.com/formancehq/ledger/v3/internal/proto/raftcmdpb" ) -type capabilityTestProposer struct{ called bool } - -func (p *capabilityTestProposer) Propose(context.Context, *node.Proposal) (*futures.Future[state.ApplyResult], error) { - p.called = true - - return futures.New[state.ApplyResult](), nil -} - -var _ plan.Proposer = (*capabilityTestProposer)(nil) - type barrierTestProposer struct{} func (barrierTestProposer) InitialIndex() uint64 { return 1 } @@ -52,21 +38,3 @@ func TestBarrierStampsSemanticCapabilities(t *testing.T) { _, err := a.Barrier(context.Background()) require.NoError(t, err) } - -func TestCapabilityPolicyProposerAllowsUnrelatedWrite(t *testing.T) { - delegate := new(capabilityTestProposer) - proposer := capabilityPolicyProposer{ - delegate: delegate, - readPolicy: func() (*commonpb.ClusterPolicy, error) { - return &commonpb.ClusterPolicy{}, nil - }, - businessWrite: true, - } - data, err := (&raftcmdpb.Proposal{}).MarshalVT() - require.NoError(t, err) - prepared, err := capabilities.PrepareProposal(&raftcmdpb.Proposal{}) - require.NoError(t, err) - _, err = proposer.Propose(context.Background(), node.NewPreparedProposal(1, data, prepared)) - require.NoError(t, err) - require.True(t, delegate.called) -} diff --git a/internal/bootstrap/module.go b/internal/bootstrap/module.go index 4363bdda64..0309874e62 100644 --- a/internal/bootstrap/module.go +++ b/internal/bootstrap/module.go @@ -343,6 +343,7 @@ func Module() fx.Option { Recovery *state.Recovery Synchronizer *state.Synchronizer Membership *raftmembership.Membership + Store *dal.Store LocalResponses node.LocalResponses }, ) (nodeProvideResult, error) { @@ -361,6 +362,28 @@ func Module() fx.Option { "snapshotTerm": snapshot.GetMetadata().GetTerm(), }).Infof("WAL fresh start detection") + var raftNode *node.Node + proposalAdmission := func(_ context.Context, proposal *node.Proposal) error { + policy, err := query.ReadClusterPolicy(params.Store) + if err != nil { + return fmt.Errorf("reading cluster capability policy before Raft proposal: %w", err) + } + if raftNode == nil { + return errors.New("raft proposal gate is not initialized") + } + + state := capabilities.FromPolicy(policy) + if err := state.Validate(); err != nil { + return err + } + decision := capabilities.Evaluate(state, proposal.RequiredCapabilities(), false, raftNode.CapabilityMembers()) + if !decision.Allowed { + return fmt.Errorf("%w: %s", capabilities.ErrCapabilityUnsupported, decision.Reason) + } + + return nil + } + n, err := node.NewNode( params.NodeConfig, params.Transport, @@ -373,10 +396,12 @@ func Module() fx.Option { params.Synchronizer, params.Membership, params.LocalResponses, + node.WithProposalAdmission(proposalAdmission), ) if err != nil { return nodeProvideResult{}, err } + raftNode = n return nodeProvideResult{Node: n, FreshStart: freshStart}, nil }, @@ -586,7 +611,6 @@ func Module() fx.Option { } opts = append(opts, admission.WithAuditProjectionState(rs.AuditProjectionState)) - opts = append(opts, admission.WithCapabilityMembership(node.CapabilityMembers)) return admission.NewAdmission( store, diff --git a/internal/infra/node/node.go b/internal/infra/node/node.go index f549837d3f..cbcb657f02 100644 --- a/internal/infra/node/node.go +++ b/internal/infra/node/node.go @@ -408,6 +408,10 @@ type Node struct { drainOutstanding uint64 // guarded by proposalAdmissionMu drainReleaseSeen uint64 // guarded by proposalAdmissionMu drainGeneration uint64 // guarded by proposalAdmissionMu + // proposalAdmission is the authoritative policy gate immediately before a + // proposal enters the Raft queue. It is injected at construction so every + // proposal producer shares the same serialized-boundary check. + proposalAdmission ProposalAdmission // membership owns the Raft peer-address state (Pebble + in-memory // cache) and the OnSnapshotInstalled / WriteConfChange callbacks // wired into Applier and Machine. EN-1413. @@ -478,6 +482,18 @@ type Node struct { readIndexDurationHistogram metric.Float64Histogram } +// ProposalAdmission is the authoritative admission check applied immediately +// before a proposal is queued for Raft. +type ProposalAdmission func(context.Context, *Proposal) error + +// NodeOption configures a Node dependency at construction time. +type NodeOption func(*Node) + +// WithProposalAdmission installs the serialized-boundary proposal gate. +func WithProposalAdmission(admission ProposalAdmission) NodeOption { + return func(node *Node) { node.proposalAdmission = admission } +} + // NewNode creates a new wrapper around a RawNode. func NewNode( cfg NodeConfig, @@ -491,6 +507,7 @@ func NewNode( synchronizer *state.Synchronizer, membership *membership.Membership, localResponses LocalResponses, + opts ...NodeOption, ) (*Node, error) { if err := cfg.validateTickIntervals(); err != nil { return nil, err @@ -726,6 +743,9 @@ func NewNode( indexTracker: NewIndexTracker(initialIndex(wal)), observer: NewNoOpObserver(), } + for _, opt := range opts { + opt(node) + } // Start ready: no leadership transition is in progress yet. node.leaderReady.Store(newCompletedLeaderReadyGeneration()) @@ -2474,6 +2494,14 @@ func (node *Node) handleProposal(p *Proposal) { err = node.rawNode.Propose(p.data) } } + if node.proposalAdmission != nil { + if admissionErr := node.proposalAdmission(context.Background(), p); admissionErr != nil { + err = admissionErr + } + } + if err == nil { + err = node.rawNode.Propose(p.data) + } if err != nil { // The IndexTracker was optimistically incremented in Propose(). // Since Raft rejected this proposal, no log entry was created— diff --git a/internal/infra/node/proposal_admission_test.go b/internal/infra/node/proposal_admission_test.go new file mode 100644 index 0000000000..1da82f45ac --- /dev/null +++ b/internal/infra/node/proposal_admission_test.go @@ -0,0 +1,38 @@ +package node + +import ( + "context" + "errors" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/formancehq/ledger/v3/internal/infra/state" + "github.com/formancehq/ledger/v3/internal/pkg/futures" +) + +func TestHandleProposalAppliesAdmissionAtRaftBoundary(t *testing.T) { + setup := newTestApplierSetup(t) + rejected := errors.New("capability policy changed before Raft proposal") + + n := &Node{ + applier: setup.applier, + indexTracker: NewIndexTracker(10), + proposalAdmission: func(context.Context, *Proposal) error { + return rejected + }, + } + + proposal := newProposal(42, []byte("proposal"), nil) + fsmFuture := futures.New[state.ApplyResult]() + setup.applier.StoreFuture(proposal.commandID, 1, fsmFuture) + n.indexTracker.Increment(1) + + n.handleProposal(proposal) + + _, err := proposal.Wait(context.Background()) + require.ErrorIs(t, err, rejected) + _, err = fsmFuture.Wait(context.Background()) + require.ErrorIs(t, err, rejected) + require.Equal(t, uint64(10), n.indexTracker.Next()) +} From c3739ca26d197438c03455ee972b6cfc54145225 Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Fri, 23 Oct 2026 11:40:17 +0200 Subject: [PATCH 036/127] fix(consensus): republish capability gate after recovery --- internal/bootstrap/module.go | 35 +++++++++++++------ .../state/cluster_policy_recovery_test.go | 23 ++++++++++++ internal/infra/state/recovery.go | 34 +++++++++++++++--- 3 files changed, 78 insertions(+), 14 deletions(-) diff --git a/internal/bootstrap/module.go b/internal/bootstrap/module.go index 0309874e62..3fab510036 100644 --- a/internal/bootstrap/module.go +++ b/internal/bootstrap/module.go @@ -83,6 +83,26 @@ import ( // It is used to decide whether a joining node needs to register as a learner. type walFreshStart bool +func activeCapabilityIDs(policy *commonpb.ClusterPolicy) []uint32 { + if policy == nil { + return nil + } + + required := make([]uint32, 0, len(policy.GetCapabilityActivations())) + for _, activation := range policy.GetCapabilityActivations() { + if activation != nil && activation.GetActive() { + required = append(required, activation.GetId()) + } + } + slices.Sort(required) + + return required +} + +func publishCapabilityGate(membership *raftmembership.Membership, policy *commonpb.ClusterPolicy) error { + return membership.SetCapabilityGate(activeCapabilityIDs(policy)) +} + // nodeProvideResult groups the outputs of the Node provider so we can also // expose the WAL-fresh-start indicator through the fx dependency graph. type nodeProvideResult struct { @@ -216,8 +236,10 @@ func Module() fx.Option { // — applier, node, background workers — observes the FSM at its // recovered state. NewMachine deliberately skips this step so it does // not perform any Pebble read itself. - func(machine *state.Machine, store *dal.Store) (*state.Recovery, error) { - recovery := state.NewRecovery(machine, store) + func(machine *state.Machine, store *dal.Store, membership *raftmembership.Membership) (*state.Recovery, error) { + recovery := state.NewRecovery(machine, store, state.WithStateRecovered(func(recovered *state.FSMState) error { + return publishCapabilityGate(membership, recovered.ClusterPolicy) + })) if err := recovery.RecoverState(); err != nil { return nil, fmt.Errorf("recovering FSM state at boot: %w", err) } @@ -687,14 +709,7 @@ func Module() fx.Option { return } if policy, err := query.ReadClusterPolicy(store); err == nil { - required := make([]uint32, 0, len(policy.GetCapabilityActivations())) - for _, activation := range policy.GetCapabilityActivations() { - if activation != nil && activation.GetActive() { - required = append(required, activation.GetId()) - } - } - slices.Sort(required) - if err := raftNode.SetCapabilityGate(required); err != nil { + if err := raftNode.SetCapabilityGate(activeCapabilityIDs(policy)); err != nil { logger.WithFields(map[string]any{"error": err}).Errorf("Invalid capability gate in committed cluster policy") return diff --git a/internal/infra/state/cluster_policy_recovery_test.go b/internal/infra/state/cluster_policy_recovery_test.go index f85dd11499..b712c553a9 100644 --- a/internal/infra/state/cluster_policy_recovery_test.go +++ b/internal/infra/state/cluster_policy_recovery_test.go @@ -32,6 +32,29 @@ func TestRecoverState_ClusterPolicyRoundtrip(t *testing.T) { "the persisted policy must be recovered verbatim into FSMState") } +func TestRecoverStatePublishesRecoveredState(t *testing.T) { + t.Parallel() + + machine, store, _ := newTestMachineWithThreshold(t, 1000) + policy := &commonpb.ClusterPolicy{ + Revision: 4, + CapabilityActivations: []*commonpb.CapabilityActivation{{Id: 17, Active: true}}, + } + batch := store.OpenWriteSession() + require.NoError(t, SaveClusterPolicy(batch, policy)) + require.NoError(t, batch.Commit()) + + var recovered *FSMState + recovery := NewRecovery(machine, store, WithStateRecovered(func(state *FSMState) error { + recovered = state + + return nil + })) + require.NoError(t, recovery.RecoverState()) + require.NotNil(t, recovered) + require.True(t, policy.EqualVT(recovered.ClusterPolicy)) +} + // NewFSMState seeds a non-nil revision-0 default so the apply path and the // readiness gate can read the policy before any is committed. func TestNewFSMState_ClusterPolicyDefault(t *testing.T) { diff --git a/internal/infra/state/recovery.go b/internal/infra/state/recovery.go index 6796098a9a..8a3d46e8a4 100644 --- a/internal/infra/state/recovery.go +++ b/internal/infra/state/recovery.go @@ -18,14 +18,34 @@ import ( // it uses, and a future contributor reading machine.go won't find any read // primitive to pull from. type Recovery struct { - apply *Machine - reader dal.RecoveryReader + apply *Machine + reader dal.RecoveryReader + onStateRecovered func(*FSMState) error +} + +// RecoveryOption customizes the post-recovery hooks without coupling the +// state package to any of its runtime consumers. +type RecoveryOption func(*Recovery) + +// WithStateRecovered registers a hook invoked after the FSM and its +// in-memory trackers have been rebuilt. Runtime consumers use this to +// republish derived state (for example, the membership capability gate) after +// boot or snapshot recovery. +func WithStateRecovered(hook func(*FSMState) error) RecoveryOption { + return func(recovery *Recovery) { + recovery.onStateRecovered = hook + } } // NewRecovery wires a Recovery onto an existing Machine. The reader is held // only on the Recovery, never on the Machine. -func NewRecovery(apply *Machine, reader dal.RecoveryReader) *Recovery { - return &Recovery{apply: apply, reader: reader} +func NewRecovery(apply *Machine, reader dal.RecoveryReader, opts ...RecoveryOption) *Recovery { + recovery := &Recovery{apply: apply, reader: reader} + for _, opt := range opts { + opt(recovery) + } + + return recovery } // RecoverState rebuilds the Machine from the Pebble data store, in three @@ -181,6 +201,12 @@ func (r *Recovery) RecoverState() error { "reversionLedgers": len(reversions), }).Infof("Recovered FSM state from store") + if r.onStateRecovered != nil { + if err := r.onStateRecovered(newState); err != nil { + return fmt.Errorf("publishing derived state after recovery: %w", err) + } + } + return nil } From 8e8ed1cabdbd2235c53d752123f6a28d9c1a3ebd Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Fri, 23 Oct 2026 11:58:02 +0200 Subject: [PATCH 037/127] fix(consensus): bind proposal metadata to serialization --- internal/application/admission/admission.go | 49 ++++---------- internal/application/mirror/worker.go | 22 ++----- .../bootstrap/node_hook_commit_drain_test.go | 6 +- internal/infra/node/proposal.go | 21 ++++-- internal/infra/node/proposal_test.go | 16 ++--- internal/infra/plan/capability_policy_test.go | 10 ++- internal/infra/plan/runner.go | 66 +++++++------------ internal/infra/plan/runner_test.go | 14 ++-- internal/infra/plan/submit_technical.go | 2 +- 9 files changed, 77 insertions(+), 129 deletions(-) diff --git a/internal/application/admission/admission.go b/internal/application/admission/admission.go index d84eca1e40..b6a700000d 100644 --- a/internal/application/admission/admission.go +++ b/internal/application/admission/admission.go @@ -25,14 +25,12 @@ import ( "github.com/formancehq/ledger/v3/internal/domain/indexes" "github.com/formancehq/ledger/v3/internal/domain/processing/numscript" "github.com/formancehq/ledger/v3/internal/infra/attributes" - "github.com/formancehq/ledger/v3/internal/infra/capabilities" "github.com/formancehq/ledger/v3/internal/infra/health" "github.com/formancehq/ledger/v3/internal/infra/node" "github.com/formancehq/ledger/v3/internal/infra/plan" "github.com/formancehq/ledger/v3/internal/infra/state" "github.com/formancehq/ledger/v3/internal/pkg/commands" "github.com/formancehq/ledger/v3/internal/pkg/futures" - "github.com/formancehq/ledger/v3/internal/pkg/vtmarshal" "github.com/formancehq/ledger/v3/internal/proto/commonpb" "github.com/formancehq/ledger/v3/internal/proto/raftcmdpb" "github.com/formancehq/ledger/v3/internal/proto/servicepb" @@ -781,18 +779,21 @@ func (a *Admission) proposeAdmission(ctx context.Context, cmd *raftcmdpb.Proposa // Per-order coverage bits depend on the final AttributeCoverage slice // (positions in cmd.ExecutionPlan.Attributes), and AcquireProposalGuard may // swap cmd.ExecutionPlan for a rebuilt ExecutionPlan on a generation shift. - // Compute the bits inside marshalFn so every (re-)marshal sees the - // current Preload — the runner calls marshalFn again after the + // Compute the bits before final proposal construction so every rebuild sees the + // current Preload — the runner calls finalizeFn again after the // rebuild, keeping bits and plans in sync. ctx, proposeSpan := tracer.Start(ctx, "admission.propose") runResult, err := a.builder.Run( ctx, cmd, build, - func(c *raftcmdpb.Proposal) ([]byte, error) { + func(c *raftcmdpb.Proposal) error { // Coverage and productions are already assigned by the - // runner before this callback runs. We just marshal + emit - // admission metrics. - return a.marshalCommand(ctx, c) + // runner before this callback runs. The node proposal factory + // performs the canonical serialization; this callback only + // records the final command size. + a.commandSizeHistogram.Record(ctx, int64(c.SizeVT())) + + return nil }, a.proposer, ) @@ -997,21 +998,14 @@ func (a *Admission) Barrier(ctx context.Context) (uint64, error) { } cmd := commands.NewCommand() // no orders = no-op - // Barrier bypasses plan.Builder.Run, so it must still stamp the semantic - // capability declaration before serialization. The no-op command created by - // commands.NewCommand carries an empty ExecutionPlan, which is itself a - // protocol dependency for replaying the envelope. - required, err := capabilities.PrepareProposal(cmd) - if err != nil { - return 0, fmt.Errorf("stamping barrier capabilities: %w", err) - } - proposalData, err := a.marshalCommand(ctx, cmd) + // The proposal factory stamps the semantic capability declaration and + // serializes this final command atomically. + a.commandSizeHistogram.Record(ctx, int64(cmd.SizeVT())) + proposal, err := node.NewProposal(cmd.GetId(), cmd) if err != nil { return 0, fmt.Errorf("marshaling barrier command: %w", err) } - proposal := node.NewPreparedProposal(cmd.GetId(), proposalData, required) - // Lock the tracker to serialize the Increment with guarded proposals, // preventing preload boundary mismatches in the FSM. a.builder.LockTracker() @@ -1044,23 +1038,6 @@ func (a *Admission) Barrier(ctx context.Context) (uint64, error) { return result.AppliedIndex, nil } -// marshalCommand marshals a proposal command into a newly allocated byte slice -// using a pooled buffer. The returned slice is safe for Raft retention. -func (a *Admission) marshalCommand(ctx context.Context, cmd *raftcmdpb.Proposal) ([]byte, error) { - _, marshalSpan := tracer.Start(ctx, "admission.marshal") - defer marshalSpan.End() - - proposalData, err := vtmarshal.MarshalCopy(cmd) - if err != nil { - return nil, fmt.Errorf("marshaling command: %w", err) - } - - a.commandSizeHistogram.Record(ctx, int64(len(proposalData))) - marshalSpan.SetAttributes(attribute.Int("command.size_bytes", len(proposalData))) - - return proposalData, nil -} - // verifiedBatch is the trusted content of an ApplyRequest after signature // verification: the ordered requests, the batch idempotency key, and the // signing envelope (nil if unsigned), propagated onto the Proposal for audit. diff --git a/internal/application/mirror/worker.go b/internal/application/mirror/worker.go index 9480ae279f..01c1565030 100644 --- a/internal/application/mirror/worker.go +++ b/internal/application/mirror/worker.go @@ -18,7 +18,6 @@ import ( "github.com/formancehq/ledger/v3/internal/infra/plan" "github.com/formancehq/ledger/v3/internal/pkg/commands" "github.com/formancehq/ledger/v3/internal/pkg/signal" - "github.com/formancehq/ledger/v3/internal/pkg/vtmarshal" "github.com/formancehq/ledger/v3/internal/pkg/worker" "github.com/formancehq/ledger/v3/internal/proto/commonpb" "github.com/formancehq/ledger/v3/internal/proto/raftcmdpb" @@ -555,18 +554,13 @@ func (w *Worker) processBatch(ctx context.Context) (bool, error) { // Run preload + propose via the shared runner. Mirror is a // single-shot caller (no concurrent admissions sharing loaders), // so we release loaders immediately after the runner returns. - marshalFn := func(c *raftcmdpb.Proposal) ([]byte, error) { - data, err := marshalMirrorCommand(c) - if err != nil { - return nil, err - } - - w.commandSize.Record(ctx, int64(len(data)), attrs) + finalizeFn := func(c *raftcmdpb.Proposal) error { + w.commandSize.Record(ctx, int64(c.SizeVT()), attrs) - return data, nil + return nil } - runResult, err := w.builder.Run(ctx, cmd, build, marshalFn, w.proposer) + runResult, err := w.builder.Run(ctx, cmd, build, finalizeFn, w.proposer) if err != nil { return false, err } @@ -780,7 +774,7 @@ func (w *Worker) proposeMirrorSync(ctx context.Context, update *raftcmdpb.Mirror return false } - runResult, err := w.builder.Run(ctx, cmd, build, marshalMirrorCommand, w.proposer) + runResult, err := w.builder.Run(ctx, cmd, build, func(*raftcmdpb.Proposal) error { return nil }, w.proposer) if err != nil { logFailure("could not be proposed", err) @@ -814,12 +808,6 @@ func (w *Worker) proposeMirrorSync(ctx context.Context, update *raftcmdpb.Mirror return true } -// marshalMirrorCommand marshals a proposal command into a newly allocated byte -// slice using a pooled buffer. The returned slice is safe for Raft retention. -func marshalMirrorCommand(cmd *raftcmdpb.Proposal) ([]byte, error) { - return vtmarshal.MarshalCopy(cmd) -} - // extractMirrorNeeds builds plan.Coverage from a mirror proposal's orders. // Returns the proposal-wide aggregate Coverage alongside a parallel slice with // one Coverage per order, used to compute Order.coverage_bits after diff --git a/internal/bootstrap/node_hook_commit_drain_test.go b/internal/bootstrap/node_hook_commit_drain_test.go index c97a07632d..f21f89d47f 100644 --- a/internal/bootstrap/node_hook_commit_drain_test.go +++ b/internal/bootstrap/node_hook_commit_drain_test.go @@ -17,7 +17,6 @@ import ( "github.com/formancehq/ledger/v3/internal/domain" "github.com/formancehq/ledger/v3/internal/infra/attributes" "github.com/formancehq/ledger/v3/internal/infra/cache" - "github.com/formancehq/ledger/v3/internal/infra/capabilities" "github.com/formancehq/ledger/v3/internal/infra/membership" "github.com/formancehq/ledger/v3/internal/infra/node" "github.com/formancehq/ledger/v3/internal/infra/state" @@ -100,11 +99,8 @@ func TestNodeHookStopCancellationRetainsRealCommitDrain(t *testing.T) { command.ExecutionPlan = &raftcmdpb.ExecutionPlan{Attributes: []*raftcmdpb.AttributeCoverage{{ Id: &raftcmdpb.AttributeID{Id: ledgerKey[:]}, AttrCode: uint32(dal.SubAttrLedger), }}} - required, err := capabilities.PrepareProposal(command) + proposal, err := node.NewProposal(command.GetId(), command) require.NoError(t, err) - data, err := command.MarshalVT() - require.NoError(t, err) - proposal := node.NewPreparedProposal(command.GetId(), data, required) future, err := n.Propose(ctx, proposal) require.NoError(t, err) select { diff --git a/internal/infra/node/proposal.go b/internal/infra/node/proposal.go index 765b04503b..f9409a9bef 100644 --- a/internal/infra/node/proposal.go +++ b/internal/infra/node/proposal.go @@ -3,6 +3,7 @@ package node import ( "github.com/formancehq/ledger/v3/internal/infra/capabilities" "github.com/formancehq/ledger/v3/internal/pkg/futures" + "github.com/formancehq/ledger/v3/internal/proto/raftcmdpb" ) type Proposal struct { @@ -27,11 +28,21 @@ func newProposal(commandID uint64, data []byte, required []capabilities.Capabili } } -// NewPreparedProposal is the only public proposal factory. Callers must pass -// the opaque result returned by capabilities.PrepareProposal for the same -// command before serialization; the raw constructor remains package-private. -func NewPreparedProposal(commandID uint64, data []byte, prepared capabilities.PreparedProposal) *Proposal { - return newProposal(commandID, data, prepared.RequiredCapabilities()) +// NewProposal is the only public proposal factory. It derives the capability +// declaration and serializes the same final command, making it impossible for +// the typed admission metadata to diverge from the wire payload. +func NewProposal(commandID uint64, command *raftcmdpb.Proposal) (*Proposal, error) { + prepared, err := capabilities.PrepareProposal(command) + if err != nil { + return nil, err + } + + data, err := command.MarshalVT() + if err != nil { + return nil, err + } + + return newProposal(commandID, data, prepared.RequiredCapabilities()), nil } // Data returns the serialized proposal data. diff --git a/internal/infra/node/proposal_test.go b/internal/infra/node/proposal_test.go index 556ebf7f4a..c0f4d762be 100644 --- a/internal/infra/node/proposal_test.go +++ b/internal/infra/node/proposal_test.go @@ -5,20 +5,18 @@ import ( "github.com/stretchr/testify/require" - "github.com/formancehq/ledger/v3/internal/infra/capabilities" "github.com/formancehq/ledger/v3/internal/proto/raftcmdpb" ) -func TestNewPreparedProposalCopiesSerializedData(t *testing.T) { - prepared, err := capabilities.PrepareProposal(&raftcmdpb.Proposal{}) +func TestNewProposalBindsCapabilitiesToSerializedCommand(t *testing.T) { + command := &raftcmdpb.Proposal{Id: 1} + proposal, err := NewProposal(1, command) require.NoError(t, err) - input := []byte{1, 2, 3} - proposal := NewPreparedProposal(1, input, prepared) - input[0] = 9 - require.Equal(t, []byte{1, 2, 3}, proposal.Data()) + command.RequiredCapabilities = []uint32{99} + require.NotEqual(t, command.GetRequiredCapabilities(), proposal.RequiredCapabilities()) data := proposal.Data() - data[1] = 9 - require.Equal(t, []byte{1, 2, 3}, proposal.Data()) + data[0] ^= 0xff + require.NotEqual(t, data, proposal.Data()) } diff --git a/internal/infra/plan/capability_policy_test.go b/internal/infra/plan/capability_policy_test.go index 6537dc678d..808088141f 100644 --- a/internal/infra/plan/capability_policy_test.go +++ b/internal/infra/plan/capability_policy_test.go @@ -6,7 +6,6 @@ import ( "github.com/stretchr/testify/require" - "github.com/formancehq/ledger/v3/internal/infra/capabilities" "github.com/formancehq/ledger/v3/internal/infra/node" "github.com/formancehq/ledger/v3/internal/infra/state" "github.com/formancehq/ledger/v3/internal/pkg/futures" @@ -25,14 +24,13 @@ func (p *capabilityPolicyTestProposer) Propose(context.Context, *node.Proposal) func capabilityPolicyProposal(t *testing.T) *node.Proposal { t.Helper() - data, err := (&raftcmdpb.Proposal{ + proposal := &raftcmdpb.Proposal{ ExecutionPlan: &raftcmdpb.ExecutionPlan{}, - }).MarshalVT() - require.NoError(t, err) - prepared, err := capabilities.PrepareProposal(&raftcmdpb.Proposal{ExecutionPlan: &raftcmdpb.ExecutionPlan{}}) + } + result, err := node.NewProposal(1, proposal) require.NoError(t, err) - return node.NewPreparedProposal(1, data, prepared) + return result } func TestCapabilityPolicyProposerAllowsUnrelatedProposal(t *testing.T) { diff --git a/internal/infra/plan/runner.go b/internal/infra/plan/runner.go index 1afb33a078..340f884e22 100644 --- a/internal/infra/plan/runner.go +++ b/internal/infra/plan/runner.go @@ -6,7 +6,6 @@ import ( "fmt" "time" - "github.com/formancehq/ledger/v3/internal/infra/capabilities" "github.com/formancehq/ledger/v3/internal/infra/node" "github.com/formancehq/ledger/v3/internal/infra/state" "github.com/formancehq/ledger/v3/internal/pkg/commands" @@ -98,22 +97,22 @@ type RunResult struct { // The caller is responsible for: // - Calling Build with the slice of WriteOperations the proposal // carries (emitting whatever metrics it likes around that call). -// - Marshaling the command via the marshalFn callback (proto marshal -// plus caller-specific instrumentation like commandSize.Record). +// - Finalizing the command via the finalizeFn callback (caller-specific +// instrumentation like commandSize.Record). // - Releasing the loaders via guard.ReleaseLoaders() at the // appropriate time. // // Run assigns coverage_bits (and production_bits when a productions // assigner was attached via WithProductions) on each operation right -// before every marshalFn invocation — both on the happy path and on the -// rare rebuild path under the proposal guard. The caller's marshalFn +// before every finalizeFn invocation — both on the happy path and on the +// rare rebuild path under the proposal guard. The caller's finalizeFn // must not touch those fields. // // On the happy path Run: // 1. attaches build.ExecutionPlan to cmd, // 2. iterates build.operations to assign coverage bits over plans, // 3. invokes the productions assigner if any, -// 4. calls marshalFn to serialize cmd, +// 4. finalizes cmd before proposal construction, // 5. acquires the proposal guard (IndexTracker mutex), // 6. patches cmd.PredictedIndex from the tracker's next index, // 7. re-runs steps 2-4 + marshal if the next-index generation shifted, @@ -128,7 +127,7 @@ func (p *Builder) Run( ctx context.Context, cmd *raftcmdpb.Proposal, build *BuildResult, - marshalFn func(*raftcmdpb.Proposal) ([]byte, error), + finalizeFn func(*raftcmdpb.Proposal) error, proposer Proposer, ) (*RunResult, error) { // Operational updates write directly to the shared apply batch. Only @@ -154,7 +153,7 @@ func (p *Builder) Run( } // Guarantee a unique command ID. The applier keys FSM futures by - // `cmd.Id` (`node.NewPreparedProposal(cmd.GetId(), data, required)` below); two in-flight + // `cmd.Id` (used by the node proposal factory); two in-flight // proposals sharing Id=0 would overwrite each other in // `Applier.StoreFuture`, hanging one waiter and routing the apply // result to the wrong caller. Several technical callers build @@ -181,24 +180,17 @@ func (p *Builder) Run( // cache reset between Build and apply would spuriously reject the // proposal as stale even though it never consulted the cache. if build.aggregate.AttributeKeysCount() == 0 { - return p.runWithoutPreload(ctx, cmd, build, marshalFn, proposer) + return p.runWithoutPreload(ctx, cmd, build, finalizeFn, proposer) } cmd.ExecutionPlan = build.ExecutionPlan - required, err := capabilities.PrepareProposal(cmd) - if err != nil { - build.ReleaseLoaders() - - return nil, fmt.Errorf("%w: %w", ErrMarshalProposal, err) - } if err := build.applyBits(cmd, build.ExecutionPlan.GetAttributes()); err != nil { build.ReleaseLoaders() return nil, fmt.Errorf("%w: %w", ErrIncompleteCoverage, err) } - data, err := marshalFn(cmd) - if err != nil { + if err := finalizeFn(cmd); err != nil { build.ReleaseLoaders() return nil, fmt.Errorf("%w: %w", ErrMarshalProposal, err) @@ -232,32 +224,25 @@ func (p *Builder) Run( // possibly values. result.Rebuilt = true cmd.ExecutionPlan = updatedPreloads - required, err = capabilities.PrepareProposal(cmd) - if err != nil { - guard.ReleaseAll() - - return nil, fmt.Errorf("%w (under guard): %w", ErrMarshalProposal, err) - } if err := build.applyBits(cmd, updatedPreloads.GetAttributes()); err != nil { guard.ReleaseAll() return nil, fmt.Errorf("%w: %w", ErrIncompleteCoverage, err) } - data, err = marshalFn(cmd) - if err != nil { + if err := finalizeFn(cmd); err != nil { guard.ReleaseAll() return nil, fmt.Errorf("%w (under guard): %w", ErrMarshalProposal, err) } - } else { - // Common path: patch the PredictedIndex onto the pre-marshaled - // buffer rather than re-marshaling the whole proposal (which - // can be megabytes for large batches). - data = AppendProposalPredictedIndex(data, cmd.GetPredictedIndex()) } - proposal := node.NewPreparedProposal(cmd.GetId(), data, required) + proposal, err := node.NewProposal(cmd.GetId(), cmd) + if err != nil { + guard.ReleaseAll() + + return nil, fmt.Errorf("%w: %w", ErrMarshalProposal, err) + } result.ProposeStartTime = time.Now() fsmFuture, err := proposer.Propose(ctx, proposal) @@ -334,7 +319,7 @@ func (p *Builder) runWithoutPreload( ctx context.Context, cmd *raftcmdpb.Proposal, build *BuildResult, - marshalFn func(*raftcmdpb.Proposal) ([]byte, error), + finalizeFn func(*raftcmdpb.Proposal) error, proposer Proposer, ) (*RunResult, error) { // The empty-needs path reads nothing from the cache, so neither @@ -360,21 +345,13 @@ func (p *Builder) runWithoutPreload( } else { cmd.ExecutionPlan = nil } - required, err := capabilities.PrepareProposal(cmd) - if err != nil { - build.ReleaseLoaders() - - return nil, fmt.Errorf("%w: %w", ErrMarshalProposal, err) - } - if err := build.applyBits(cmd, nil); err != nil { build.ReleaseLoaders() return nil, fmt.Errorf("%w: %w", ErrIncompleteCoverage, err) } - data, err := marshalFn(cmd) - if err != nil { + if err := finalizeFn(cmd); err != nil { build.ReleaseLoaders() return nil, fmt.Errorf("%w: %w", ErrMarshalProposal, err) @@ -387,9 +364,12 @@ func (p *Builder) runWithoutPreload( p.LockTracker() cmd.PredictedIndex = p.TrackerNext() - data = AppendProposalPredictedIndex(data, cmd.GetPredictedIndex()) + proposal, err := node.NewProposal(cmd.GetId(), cmd) + if err != nil { + guard.ReleaseLoaders() - proposal := node.NewPreparedProposal(cmd.GetId(), data, required) + return nil, fmt.Errorf("%w: %w", ErrMarshalProposal, err) + } proposeStart := time.Now() fsmFuture, err := proposer.Propose(ctx, proposal) diff --git a/internal/infra/plan/runner_test.go b/internal/infra/plan/runner_test.go index 00025889a2..d3e15e306a 100644 --- a/internal/infra/plan/runner_test.go +++ b/internal/infra/plan/runner_test.go @@ -50,18 +50,18 @@ func TestRunWithoutPreload_ClearsPreSetExecutionPlan(t *testing.T) { } marshalled := false - marshalFn := func(p *raftcmdpb.Proposal) ([]byte, error) { + finalizeFn := func(p *raftcmdpb.Proposal) error { marshalled = true require.Nil(t, p.GetExecutionPlan(), "ExecutionPlan must be cleared before marshal") - return []byte{0}, nil + return nil } proposer := stubFailingProposer{err: errors.New("stub")} - _, err := builder.Run(context.Background(), cmd, build, marshalFn, proposer) + _, err := builder.Run(context.Background(), cmd, build, finalizeFn, proposer) require.Error(t, err, "stub proposer should bubble its error") - require.True(t, marshalled, "marshalFn must have run") + require.True(t, marshalled, "finalizeFn must have run") require.Nil(t, cmd.GetExecutionPlan(), "cmd.ExecutionPlan must be nil after Run on the no-preload fast path") } @@ -103,7 +103,7 @@ func TestRun_IdempotencyOnlyNeedsTakesFastPath(t *testing.T) { token: &preload.CleanupToken{}, } - marshalFn := func(p *raftcmdpb.Proposal) ([]byte, error) { + finalizeFn := func(p *raftcmdpb.Proposal) error { ep := p.GetExecutionPlan() require.NotNil(t, ep, "ExecutionPlan must survive to carry IdempotencyKeys") require.Empty(t, ep.GetAttributes(), "Attributes must be dropped on the fast path") @@ -111,12 +111,12 @@ func TestRun_IdempotencyOnlyNeedsTakesFastPath(t *testing.T) { require.Len(t, ep.GetIdempotencyKeys(), 1, "IdempotencyKeys must survive") require.Equal(t, "idem-only", ep.GetIdempotencyKeys()[0].GetKey()) - return []byte{0}, nil + return nil } proposer := stubFailingProposer{err: errors.New("stub")} - _, err := builder.Run(context.Background(), cmd, build, marshalFn, proposer) + _, err := builder.Run(context.Background(), cmd, build, finalizeFn, proposer) require.Error(t, err, "stub proposer should bubble its error") ep := cmd.GetExecutionPlan() diff --git a/internal/infra/plan/submit_technical.go b/internal/infra/plan/submit_technical.go index c1e214b311..ba50fee3a7 100644 --- a/internal/infra/plan/submit_technical.go +++ b/internal/infra/plan/submit_technical.go @@ -96,7 +96,7 @@ func submitTechnicalOnce(ctx context.Context, builder *Builder, proposer Propose result, err := builder.Run( ctx, cmd, build, - func(c *raftcmdpb.Proposal) ([]byte, error) { return c.MarshalVT() }, + func(*raftcmdpb.Proposal) error { return nil }, proposer, ) if err != nil { From 68190ed45c9f1b2bdba6d528730f84027171dfc6 Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Fri, 23 Oct 2026 12:02:58 +0200 Subject: [PATCH 038/127] test(consensus): cover capability policy rejection paths --- internal/infra/plan/capability_policy_test.go | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/internal/infra/plan/capability_policy_test.go b/internal/infra/plan/capability_policy_test.go index 808088141f..c55eefdf76 100644 --- a/internal/infra/plan/capability_policy_test.go +++ b/internal/infra/plan/capability_policy_test.go @@ -2,6 +2,7 @@ package plan import ( "context" + "errors" "testing" "github.com/stretchr/testify/require" @@ -43,3 +44,20 @@ func TestCapabilityPolicyProposerAllowsUnrelatedProposal(t *testing.T) { require.NoError(t, err) require.True(t, delegate.called) } + +func TestCapabilityPolicyProposerRejectsPolicyReadFailureForBothProposalModes(t *testing.T) { + t.Parallel() + + for _, businessWrite := range []bool{false, true} { + t.Run(map[bool]string{false: "technical", true: "business"}[businessWrite], func(t *testing.T) { + delegate := new(capabilityPolicyTestProposer) + proposer := NewCapabilityPolicyProposer(delegate, func() (*commonpb.ClusterPolicy, error) { + return nil, errors.New("policy unavailable") + }, nil, businessWrite) + + _, err := proposer.Propose(context.Background(), capabilityPolicyProposal(t)) + require.ErrorContains(t, err, "policy unavailable") + require.False(t, delegate.called) + }) + } +} From 316486c1b19f9d58f0cb05672a61356e932f48da Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Fri, 23 Oct 2026 15:35:16 +0200 Subject: [PATCH 039/127] test(consensus): cover inactive capability rejection --- internal/infra/plan/capability_policy_test.go | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/internal/infra/plan/capability_policy_test.go b/internal/infra/plan/capability_policy_test.go index c55eefdf76..7ab975c806 100644 --- a/internal/infra/plan/capability_policy_test.go +++ b/internal/infra/plan/capability_policy_test.go @@ -3,10 +3,13 @@ package plan import ( "context" "errors" + "reflect" "testing" + "unsafe" "github.com/stretchr/testify/require" + "github.com/formancehq/ledger/v3/internal/infra/capabilities" "github.com/formancehq/ledger/v3/internal/infra/node" "github.com/formancehq/ledger/v3/internal/infra/state" "github.com/formancehq/ledger/v3/internal/pkg/futures" @@ -34,6 +37,19 @@ func capabilityPolicyProposal(t *testing.T) *node.Proposal { return result } +// capabilityPolicyProposalWithRequired is a package-local seam for exercising +// the policy wrapper until the capability registry contains a real feature. +// Production proposals can only get this metadata through node.NewProposal. +func capabilityPolicyProposalWithRequired(t *testing.T, required ...capabilities.CapabilityID) *node.Proposal { + t.Helper() + + proposal := capabilityPolicyProposal(t) + field := reflect.ValueOf(proposal).Elem().FieldByName("requiredCapabilities") + reflect.NewAt(field.Type(), unsafe.Pointer(field.UnsafeAddr())).Elem().Set(reflect.ValueOf(required)) + + return proposal +} + func TestCapabilityPolicyProposerAllowsUnrelatedProposal(t *testing.T) { delegate := new(capabilityPolicyTestProposer) proposer := NewCapabilityPolicyProposer(delegate, func() (*commonpb.ClusterPolicy, error) { @@ -61,3 +77,20 @@ func TestCapabilityPolicyProposerRejectsPolicyReadFailureForBothProposalModes(t }) } } + +func TestCapabilityPolicyProposerRejectsInactiveCapabilityForBothProposalModes(t *testing.T) { + t.Parallel() + + for _, businessWrite := range []bool{false, true} { + t.Run(map[bool]string{false: "technical", true: "business"}[businessWrite], func(t *testing.T) { + delegate := new(capabilityPolicyTestProposer) + proposer := NewCapabilityPolicyProposer(delegate, func() (*commonpb.ClusterPolicy, error) { + return &commonpb.ClusterPolicy{}, nil + }, nil, businessWrite) + + _, err := proposer.Propose(context.Background(), capabilityPolicyProposalWithRequired(t, 99)) + require.ErrorIs(t, err, capabilities.ErrCapabilityUnsupported) + require.False(t, delegate.called) + }) + } +} From 8194393d68c44d2d07738a24fbf171917645ba6a Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Fri, 23 Oct 2026 15:47:45 +0200 Subject: [PATCH 040/127] fix(consensus): gate proposals before index reservation --- internal/infra/node/node.go | 23 +++++++++++-------- .../infra/node/proposal_admission_test.go | 17 ++------------ 2 files changed, 16 insertions(+), 24 deletions(-) diff --git a/internal/infra/node/node.go b/internal/infra/node/node.go index cbcb657f02..68b076992e 100644 --- a/internal/infra/node/node.go +++ b/internal/infra/node/node.go @@ -2415,6 +2415,17 @@ func (node *Node) Propose(ctx context.Context, proposal *Proposal) (*futures.Fut } } + // Evaluate the authoritative policy gate while proposalAdmissionMu and the + // IndexTracker guard are held by the caller. A rejected proposal therefore + // never consumes a predicted Raft index and can never leave a hole for the + // proposals already queued behind it. Policy-changing proposals use the + // same canonical runner, so this is also their serialization point. + if node.proposalAdmission != nil { + if err := node.proposalAdmission(ctx, proposal); err != nil { + return nil, err + } + } + // Create a separate future for Machine results. // The proposal's embedded Future is for Raft consensus (resolved by rawNode.Propose). // The fsmFuture is for Machine processing (resolved when entry is applied). @@ -2455,10 +2466,9 @@ func (node *Node) Propose(ctx context.Context, proposal *Proposal) (*futures.Fut } } -// rejectQueuedProposal resolves a proposal that was admitted before a terminal -// command failure but can no longer be handed to rawNode. The admission caller -// has released the tracker guard before this runs, so Decrement takes the -// regular synchronized rollback path. +// rejectQueuedProposal resolves a proposal drained during terminal shutdown. +// Normal capability rejection happens before index reservation in Propose and +// never reaches the queue. func (node *Node) rejectQueuedProposal(proposal *Proposal, err error) { node.indexTracker.Decrement(1) node.applier.ResolveDroppedFuture(proposal.commandID, err) @@ -2494,11 +2504,6 @@ func (node *Node) handleProposal(p *Proposal) { err = node.rawNode.Propose(p.data) } } - if node.proposalAdmission != nil { - if admissionErr := node.proposalAdmission(context.Background(), p); admissionErr != nil { - err = admissionErr - } - } if err == nil { err = node.rawNode.Propose(p.data) } diff --git a/internal/infra/node/proposal_admission_test.go b/internal/infra/node/proposal_admission_test.go index 1da82f45ac..db82de066f 100644 --- a/internal/infra/node/proposal_admission_test.go +++ b/internal/infra/node/proposal_admission_test.go @@ -6,17 +6,12 @@ import ( "testing" "github.com/stretchr/testify/require" - - "github.com/formancehq/ledger/v3/internal/infra/state" - "github.com/formancehq/ledger/v3/internal/pkg/futures" ) -func TestHandleProposalAppliesAdmissionAtRaftBoundary(t *testing.T) { - setup := newTestApplierSetup(t) +func TestProposeAppliesAdmissionBeforeIndexReservation(t *testing.T) { rejected := errors.New("capability policy changed before Raft proposal") n := &Node{ - applier: setup.applier, indexTracker: NewIndexTracker(10), proposalAdmission: func(context.Context, *Proposal) error { return rejected @@ -24,15 +19,7 @@ func TestHandleProposalAppliesAdmissionAtRaftBoundary(t *testing.T) { } proposal := newProposal(42, []byte("proposal"), nil) - fsmFuture := futures.New[state.ApplyResult]() - setup.applier.StoreFuture(proposal.commandID, 1, fsmFuture) - n.indexTracker.Increment(1) - - n.handleProposal(proposal) - - _, err := proposal.Wait(context.Background()) - require.ErrorIs(t, err, rejected) - _, err = fsmFuture.Wait(context.Background()) + _, err := n.Propose(context.Background(), proposal) require.ErrorIs(t, err, rejected) require.Equal(t, uint64(10), n.indexTracker.Next()) } From 22087922234173fc05eba6dd339ad63c4382bd00 Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Fri, 23 Oct 2026 15:48:16 +0200 Subject: [PATCH 041/127] docs(consensus): document proposal gate boundary --- .../subsystems/consensus/capabilities.md | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/docs/technical/architecture/subsystems/consensus/capabilities.md b/docs/technical/architecture/subsystems/consensus/capabilities.md index 66d039e29d..ac100bd89f 100644 --- a/docs/technical/architecture/subsystems/consensus/capabilities.md +++ b/docs/technical/architecture/subsystems/consensus/capabilities.md @@ -107,16 +107,18 @@ Deploy barrier-aware binaries to every eligible member before arming an incompatible feature. An old leader that does not understand the barrier cannot be made safe by upgrading only the other nodes. Restart, snapshot install, restore, and downgrade code must preserve the policy. Membership changes racing activation are -serialized at the Raft proposal boundary and must be revalidated immediately -before `rawNode.Propose`, because `Node.Propose` queues work. Removing a member -remains allowed; adding, promoting, or refreshing one without all active -capabilities is rejected. +serialized at the proposal boundary. `Node.Propose` evaluates the authoritative +policy while holding the proposal and index-tracker guards, before reserving a +predicted index or queueing work; this prevents a rejected proposal from +leaving a hole for queued successors. Removing a member remains allowed; +adding, promoting, or refreshing one without all active capabilities is +rejected. The admission path and the shared plan runner apply the policy check to business and technical producers (including mirror, events, maintenance, and -backup flows). Direct `Node.Propose` callers remain low-level APIs and must -stamp their proposal before use; membership `ConfChange` operations carry and -validate their capability evidence separately. +backup flows). `Node.Propose` is the final common boundary for every producer; +callers must still stamp their proposal before use. Membership `ConfChange` +operations carry and validate their capability evidence separately. ## Restart refresh From aa82969c488cf3f8d6dea4385407416e8fcfac61 Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Fri, 23 Oct 2026 15:56:26 +0200 Subject: [PATCH 042/127] perf(consensus): serialize proposals outside tracker lock --- internal/infra/node/proposal.go | 24 ++++++++++++ internal/infra/node/proposal_test.go | 12 ++++++ internal/infra/plan/runner.go | 55 ++++++++++++++++++---------- 3 files changed, 72 insertions(+), 19 deletions(-) diff --git a/internal/infra/node/proposal.go b/internal/infra/node/proposal.go index f9409a9bef..00966c390b 100644 --- a/internal/infra/node/proposal.go +++ b/internal/infra/node/proposal.go @@ -1,11 +1,23 @@ package node import ( + "google.golang.org/protobuf/encoding/protowire" + "github.com/formancehq/ledger/v3/internal/infra/capabilities" "github.com/formancehq/ledger/v3/internal/pkg/futures" + "github.com/formancehq/ledger/v3/internal/pkg/protowireutil" "github.com/formancehq/ledger/v3/internal/proto/raftcmdpb" ) +var predictedIndexField = func() protowire.Number { + fd := (&raftcmdpb.Proposal{}).ProtoReflect().Descriptor().Fields().ByName("predicted_index") + if fd == nil { + panic("invariant: Proposal has no predicted_index field") + } + + return fd.Number() +}() + type Proposal struct { *futures.Future[any] @@ -50,6 +62,18 @@ func (p *Proposal) Data() []byte { return append([]byte(nil), p.data...) } +// SetPredictedIndex patches the predicted Raft index onto the already +// serialized proposal. NewProposal deliberately serializes with a zero index; +// callers can therefore perform the expensive payload serialization before +// acquiring the shared proposal lock and hold it only for this bounded append. +func (p *Proposal) SetPredictedIndex(index uint64) { + if index == 0 { + return + } + + p.data = protowireutil.AppendFixed64(p.data, predictedIndexField, index) +} + // RequiredCapabilities returns the capabilities derived by the producer from // the final command before it was serialized. Admission reuses this typed // value instead of decoding the serialized proposal a second time. diff --git a/internal/infra/node/proposal_test.go b/internal/infra/node/proposal_test.go index c0f4d762be..23f98a071d 100644 --- a/internal/infra/node/proposal_test.go +++ b/internal/infra/node/proposal_test.go @@ -20,3 +20,15 @@ func TestNewProposalBindsCapabilitiesToSerializedCommand(t *testing.T) { data[0] ^= 0xff require.NotEqual(t, data, proposal.Data()) } + +func TestProposalSetPredictedIndexPatchesSerializedPayload(t *testing.T) { + command := &raftcmdpb.Proposal{Id: 1} + proposal, err := NewProposal(1, command) + require.NoError(t, err) + + proposal.SetPredictedIndex(42) + + decoded := new(raftcmdpb.Proposal) + require.NoError(t, decoded.UnmarshalVT(proposal.Data())) + require.Equal(t, uint64(42), decoded.GetPredictedIndex()) +} diff --git a/internal/infra/plan/runner.go b/internal/infra/plan/runner.go index 340f884e22..629506b103 100644 --- a/internal/infra/plan/runner.go +++ b/internal/infra/plan/runner.go @@ -113,11 +113,13 @@ type RunResult struct { // 2. iterates build.operations to assign coverage bits over plans, // 3. invokes the productions assigner if any, // 4. finalizes cmd before proposal construction, -// 5. acquires the proposal guard (IndexTracker mutex), -// 6. patches cmd.PredictedIndex from the tracker's next index, -// 7. re-runs steps 2-4 + marshal if the next-index generation shifted, -// 8. submits the proposal via proposer.Propose, -// 9. releases the proposal-lock side of the guard. +// 5. marshals the proposal before acquiring the proposal guard, +// 6. acquires the proposal guard (IndexTracker mutex), +// 7. patches PredictedIndex from the tracker's next index, +// 8. re-runs steps 2-4 + marshal if the next-index generation shifted, +// 9. submits the proposal via proposer.Propose, +// +// 10. releases the proposal-lock side of the guard. // // PredictedIndex is always set. It is a no-cost backstop: the FSM // rejects with ErrStaleProposal if the actual Raft index doesn't @@ -196,6 +198,17 @@ func (p *Builder) Run( return nil, fmt.Errorf("%w: %w", ErrMarshalProposal, err) } + // Serialize the full payload before taking the shared proposal lock. The + // predicted index is zero at this point and is patched below once the + // tracker guard is held. + cmd.PredictedIndex = 0 + proposal, err := node.NewProposal(cmd.GetId(), cmd) + if err != nil { + build.ReleaseLoaders() + + return nil, fmt.Errorf("%w: %w", ErrMarshalProposal, err) + } + guardStart := time.Now() updatedPreloads, guard, err := p.AcquireProposalGuard(build) @@ -212,8 +225,6 @@ func (p *Builder) Run( return nil, fmt.Errorf("%w: %w", ErrAcquireProposalGuard, err) } - cmd.PredictedIndex = p.TrackerNext() - result := &RunResult{Guard: guard} if updatedPreloads != nil { @@ -235,15 +246,17 @@ func (p *Builder) Run( return nil, fmt.Errorf("%w (under guard): %w", ErrMarshalProposal, err) } - } - proposal, err := node.NewProposal(cmd.GetId(), cmd) - if err != nil { - guard.ReleaseAll() + proposal, err = node.NewProposal(cmd.GetId(), cmd) + if err != nil { + guard.ReleaseAll() - return nil, fmt.Errorf("%w: %w", ErrMarshalProposal, err) + return nil, fmt.Errorf("%w (under guard): %w", ErrMarshalProposal, err) + } } + proposal.SetPredictedIndex(p.TrackerNext()) + result.ProposeStartTime = time.Now() fsmFuture, err := proposer.Propose(ctx, proposal) result.ProposeDuration = time.Since(result.ProposeStartTime) @@ -357,19 +370,23 @@ func (p *Builder) runWithoutPreload( return nil, fmt.Errorf("%w: %w", ErrMarshalProposal, err) } + // Marshal before taking the tracker lock; only the bounded predicted-index + // append below belongs in the critical section. + cmd.PredictedIndex = 0 + proposal, err := node.NewProposal(cmd.GetId(), cmd) + if err != nil { + build.ReleaseLoaders() + + return nil, fmt.Errorf("%w: %w", ErrMarshalProposal, err) + } + guard := &ProposalGuard{p: p, token: build.token} lockStart := time.Now() p.LockTracker() - cmd.PredictedIndex = p.TrackerNext() - proposal, err := node.NewProposal(cmd.GetId(), cmd) - if err != nil { - guard.ReleaseLoaders() - - return nil, fmt.Errorf("%w: %w", ErrMarshalProposal, err) - } + proposal.SetPredictedIndex(p.TrackerNext()) proposeStart := time.Now() fsmFuture, err := proposer.Propose(ctx, proposal) From 079ef1af2b7bb04944d53dd9a4fa869fff263669 Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Fri, 23 Oct 2026 15:57:56 +0200 Subject: [PATCH 043/127] docs(upgrades): declare add-learner CLI break --- docs/ops/upgrades.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/docs/ops/upgrades.md b/docs/ops/upgrades.md index 475a9d053e..fd641072e9 100644 --- a/docs/ops/upgrades.md +++ b/docs/ops/upgrades.md @@ -70,6 +70,20 @@ through each adjacent RC in order; do not jump to the newest RC. If the source cannot be identified or the current Operator already rejects the pinning step, stop and plan a separately validated migration. Do not delete the StatefulSet or PVCs to bypass the source check. +### Maintainer-declared CLI break + +The `ledgerctl cluster add-learner` command is intentionally removed from the +capability-evidence release. Existing scripts or runbooks invoking that command +must be updated; the command is not kept as a compatibility alias and no +protocol-version bump is required because the service gRPC contract is +unchanged. + +To add a new node, configure its peer seed and start the Ledger binary. The +bootstrap sequence calls `JoinAsLearner` and advertises the capabilities +compiled into that binary. A node restarting with an existing WAL uses +`RefreshMembership` to publish updated endpoints and capability evidence. +The administrative `AddLearner` RPC remains an internal recovery API, but it is +not a supported replacement for the removed CLI workflow. Before tagging a new release, verify the upgrade with state created by the supported source releases. Cover affected Pebble records, Raft log and snapshot From e400de69656a4f8494ed60529b8fe22b671443b6 Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Fri, 23 Oct 2026 16:13:15 +0200 Subject: [PATCH 044/127] docs(preload): document predicted index patching --- docs/technical/architecture/subsystems/fsm/preload.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/technical/architecture/subsystems/fsm/preload.md b/docs/technical/architecture/subsystems/fsm/preload.md index fd47547c3c..428a87a464 100644 --- a/docs/technical/architecture/subsystems/fsm/preload.md +++ b/docs/technical/architecture/subsystems/fsm/preload.md @@ -203,7 +203,7 @@ The bitset lives on `OrderTechnical` (`misc/proto/raft_cmd.proto:64`) and `Techn Between the moment a producer resolves `(key=K, value=V)` and the moment the FSM applies the proposal at Raft index `I`, another proposal may mutate `K`. The preloaded `V` would then be stale at apply. -`AppendProposalPredictedIndex(data []byte, index uint64) []byte` (`internal/infra/plan/predicted_index.go`) stamps the expected index onto the already-marshalled proposal by appending the raw wire encoding of `Proposal.predicted_index` (a `fixed64`, under the field number read from the descriptor at init rather than a hardcoded literal); it is a no-op when the index is 0. Appending raw bytes avoids re-marshalling a possibly-megabyte `Proposal` while holding the proposal lock. That is the common path only: when the guard finds the boundary shifted it must re-marshal the rebuilt proposal anyway, and the fresh marshal serializes `PredictedIndex` inline, so the append is skipped (`internal/infra/plan/runner.go` — the index is assigned to `cmd` before the branch, then either re-marshalled or appended, never both). +`Proposal.SetPredictedIndex(index)` (`internal/infra/node/proposal.go`) stamps the expected index onto the already-marshalled proposal by appending the raw wire encoding of `Proposal.predicted_index` (a `fixed64`, under the field number read from the descriptor at init rather than a hardcoded literal); it is a no-op when the index is 0. Appending raw bytes avoids re-marshalling a possibly-megabyte `Proposal` while holding the proposal lock. The full proposal is first marshalled with `PredictedIndex = 0` outside the lock on both the common and no-preload paths. After the guard is acquired, `SetPredictedIndex` appends the bounded field. If the guard detects a boundary shift, the rebuilt command is re-marshalled under the guard and the same `SetPredictedIndex` patch is then applied to the rebuilt bytes. At apply, the FSM compares the predicted index against the actual one. A mismatch means the observed cache state may no longer hold, so the proposal is rejected with `ErrStaleProposal` and the producer rebuilds and retries. @@ -252,12 +252,12 @@ sequenceDiagram P->>P: marshal — outside the critical section P->>B: AcquireProposalGuard: lock IndexTracker, re-check Gen(nextIndex) B-->>P: ProposalGuard - P->>P: cmd.PredictedIndex = TrackerNext() (index known under the lock) + P->>P: Proposal.SetPredictedIndex(TrackerNext()) (bounded patch under the lock) alt boundary shifted between Build and guard (rare) - B-->>P: rebuilt ExecutionPlan → re-apply bits, re-marshal under the guard - Note over P: the re-marshal serializes PredictedIndex inline — no append + B-->>P: rebuilt ExecutionPlan → re-apply bits, re-marshal with index zero + P->>P: Proposal.SetPredictedIndex(TrackerNext()) else common path - P->>P: AppendProposalPredictedIndex onto the pre-marshalled buffer + P->>P: Proposal.SetPredictedIndex(TrackerNext()) onto the pre-marshalled buffer end P->>P: Propose under the guard Note over P,FSM: Raft commit (no Pebble reads on the hot path) From fd7ec7e0038932251716cde4c55d36d6b20672ac Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Mon, 26 Oct 2026 12:27:43 +0100 Subject: [PATCH 045/127] fix(capabilities): converge policy activation from membership evidence --- internal/bootstrap/module.go | 87 +++++++++++++++++++++++---------- internal/infra/state/machine.go | 18 ++++--- 2 files changed, 70 insertions(+), 35 deletions(-) diff --git a/internal/bootstrap/module.go b/internal/bootstrap/module.go index 3fab510036..d0f3fbd443 100644 --- a/internal/bootstrap/module.go +++ b/internal/bootstrap/module.go @@ -339,6 +339,11 @@ func Module() fx.Option { } return membership.WriteConfChangeWithPolicy(entry, session, policy) }, + func(policy *commonpb.ClusterPolicy) { + if err := publishCapabilityGate(membership, policy); err != nil { + logger.WithFields(map[string]any{"error": err}).Errorf("Publishing committed capability policy failed") + } + }, ) if err != nil { return nil, err @@ -720,7 +725,7 @@ func Module() fx.Option { return } - reconcileClusterPolicy(ctx, admission, store, cfg, raftNode.IsLeader, logger) + reconcileClusterPolicy(ctx, admission, store, cfg, raftNode.IsLeader, logger, raftNode.CapabilityMembers) }) }, ), @@ -1390,7 +1395,16 @@ func membershipRefreshHook(cfg Config, logger logging.Logger) fx.Hook { "serviceAddress": cfg.ServiceAdvertiseAddr(), }).Infof("Restart mode: refreshing membership capabilities") - return tryRefreshMembership(ctx, cfg, cfg.TLSConfig, logger) + if err := tryRefreshMembership(ctx, cfg, cfg.TLSConfig, logger); err != nil { + // TODO(rc.3): replace this compatibility fallback with explicit + // startup error handling once every supported peer implements + // RefreshMembership. The leader-side reconciler below keeps + // polling membership evidence and will arm capabilities only + // after all current members converge. + logger.WithFields(map[string]any{"error": err}).Errorf("Membership capability refresh failed; continuing with conservative evidence") + } + + return nil }, } } @@ -1609,38 +1623,35 @@ func tryRefreshMembership(ctx context.Context, cfg Config, tlsCfg TLSConfig, log ctx = metadata.AppendToOutgoingContext(ctx, node.MetadataKeyClusterID, cfg.ClusterID) } - backoff := 500 * time.Millisecond - for { - for _, peer := range cfg.RaftConfig.Peers { - conn, dialErr := ggrpc.NewClient(peer.Address, dialOpts...) - if dialErr != nil { - logger.WithFields(map[string]any{"peer": peer.ID, "address": peer.Address, "error": dialErr}).Infof("Failed to dial peer for membership refresh, will retry") - - continue - } + var lastErr error + for _, peer := range cfg.RaftConfig.Peers { + conn, dialErr := ggrpc.NewClient(peer.Address, dialOpts...) + if dialErr != nil { + lastErr = dialErr + logger.WithFields(map[string]any{"peer": peer.ID, "address": peer.Address, "error": dialErr}).Infof("Failed to dial peer for membership refresh") - _, callErr := clusterbootstrappb.NewClusterBootstrapServiceClient(conn).RefreshMembership(ctx, req) - _ = conn.Close() - if callErr == nil { - logger.WithFields(map[string]any{"peer": peer.ID}).Infof("Successfully refreshed membership capabilities") + continue + } - return nil - } + _, callErr := clusterbootstrappb.NewClusterBootstrapServiceClient(conn).RefreshMembership(ctx, req) + _ = conn.Close() + if callErr == nil { + logger.WithFields(map[string]any{"peer": peer.ID}).Infof("Successfully refreshed membership capabilities") - if isRetryableMembershipRefreshError(callErr) { - continue - } + return nil + } + lastErr = callErr + if !isRetryableMembershipRefreshError(callErr) { return fmt.Errorf("failed to refresh membership via peer %d (%s): %w", peer.ID, peer.Address, callErr) } + } - select { - case <-ctx.Done(): - return ctx.Err() - case <-time.After(backoff): - } - backoff = min(backoff*2, 5*time.Second) + if lastErr == nil { + return errors.New("no configured peer available for membership refresh") } + + return fmt.Errorf("membership refresh unavailable: %w", lastErr) } func isRetryableMembershipRefreshError(err error) bool { @@ -1791,7 +1802,7 @@ func proposeClusterConfigIfNeeded(parent context.Context, n plan.Proposer, build // through AdmitClusterPolicy as an internal audited SetClusterPolicy order so // the checker can re-derive it; the write-readiness gate exempts this // non-business proposal so it is never blocked by its own cluster-policy gate. -func reconcileClusterPolicy(ctx context.Context, admission ctrl.Admission, store *dal.Store, cfg Config, isLeader func() bool, logger logging.Logger) { +func reconcileClusterPolicy(ctx context.Context, admission ctrl.Admission, store *dal.Store, cfg Config, isLeader func() bool, logger logging.Logger, memberReaders ...func() []capabilities.Member) { if !isLeader() { return } @@ -1821,6 +1832,28 @@ func reconcileClusterPolicy(ctx context.Context, admission ctrl.Admission, store } } } + if len(memberReaders) > 0 { + ready, readyErr := capabilities.Ready(capabilities.SupportedIDs(), memberReaders[0]()) + if readyErr != nil { + logger.WithFields(map[string]any{"error": readyErr}).Errorf("Capability evidence is invalid; delaying policy reconciliation") + + return + } + active := make(map[uint32]bool, len(desired.GetCapabilityActivations())) + for _, activation := range desired.GetCapabilityActivations() { + if activation != nil { + active[activation.GetId()] = activation.GetActive() + } + } + for _, id := range ready { + if _, exists := active[uint32(id)]; !exists { + desired.CapabilityActivations = append(desired.CapabilityActivations, &commonpb.CapabilityActivation{Id: uint32(id), Active: true}) + } + } + if applied != nil && !applied.EqualVT(desired) && desired.GetRevision() <= applied.GetRevision() { + desired.Revision = applied.GetRevision() + 1 + } + } appliedRev := applied.GetRevision() diff --git a/internal/infra/state/machine.go b/internal/infra/state/machine.go index 6efbf29b8f..50261af097 100644 --- a/internal/infra/state/machine.go +++ b/internal/infra/state/machine.go @@ -173,7 +173,15 @@ type Machine struct { // confChangeHandler is invoked from PrepareEntries for every // EntryConfChange* in the in-flight batch (see the field comment on // Machine). Must be non-nil. -func NewMachine(logger logging.Logger, registry *StateRegistry, cacheSnapshotter *CacheSnapshotter, fileArtifacts dal.QueryCheckpoints, sentinel dal.SentinelFactory, meterProvider metric.MeterProvider, ks *keystore.KeyStore, sharedState *SharedState, notifier Notifier, bloomFilters *bloom.FilterSet, auditKey string, numscriptCacheSize int, confChangeHandler func(entry *raftpb.Entry, session *dal.WriteSession) error) (*Machine, error) { +func NewMachine(logger logging.Logger, registry *StateRegistry, cacheSnapshotter *CacheSnapshotter, fileArtifacts dal.QueryCheckpoints, sentinel dal.SentinelFactory, meterProvider metric.MeterProvider, ks *keystore.KeyStore, sharedState *SharedState, notifier Notifier, bloomFilters *bloom.FilterSet, auditKey string, numscriptCacheSize int, confChangeHandler func(entry *raftpb.Entry, session *dal.WriteSession) error, capabilityPolicyCommitHandlers ...func(*commonpb.ClusterPolicy)) (*Machine, error) { + var capabilityPolicyCommitHandler func(*commonpb.ClusterPolicy) + if len(capabilityPolicyCommitHandlers) > 1 { + return nil, errors.New("at most one capability policy commit handler is allowed") + } + if len(capabilityPolicyCommitHandlers) == 1 { + capabilityPolicyCommitHandler = capabilityPolicyCommitHandlers[0] + } + sentinelMode := sentinel.IsEnabled() // raft.* metrics describe the consensus engine and follow the // upstream etcd-raft naming convention; numscript.* metrics are @@ -248,6 +256,7 @@ func NewMachine(logger logging.Logger, registry *StateRegistry, cacheSnapshotter bloomRebuildCh: make(chan string, 1), auditHashBuf: make([]byte, 0, 4096), confChangeHandler: confChangeHandler, + capabilityPolicyCommitHandler: capabilityPolicyCommitHandler, } fsm.appliedCond = sync.NewCond(&fsm.appliedMu) fsm.cacheSnapshotter = cacheSnapshotter @@ -2101,13 +2110,6 @@ type PreparedBatch struct { entryCount int } -// SetCapabilityPolicyCommitHandler installs the post-commit hook used to -// publish the authoritative membership admission gate. The callback runs only -// after the corresponding FSM batch has committed. -func (fsm *Machine) SetCapabilityPolicyCommitHandler(handler func(*commonpb.ClusterPolicy)) { - fsm.capabilityPolicyCommitHandler = handler -} - // Close cancels the uncommitted batch. Must be called if CommitPreparedBatch // will not be called (e.g. on error in the caller). func (pb *PreparedBatch) Close() { From e38b475fec6c16cf8dd4b0a7b1e4eb764ef74485 Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Mon, 26 Oct 2026 13:09:52 +0100 Subject: [PATCH 046/127] fix: close capability activation races --- .../subsystems/consensus/capabilities.md | 30 ++++--- internal/bootstrap/module.go | 83 ++++++++++++------- .../processing/processor_cluster_policy.go | 10 +++ .../membership/confchange_identity_test.go | 16 ++++ internal/infra/membership/membership.go | 22 +++++ internal/infra/plan/runner.go | 1 - 6 files changed, 113 insertions(+), 49 deletions(-) diff --git a/docs/technical/architecture/subsystems/consensus/capabilities.md b/docs/technical/architecture/subsystems/consensus/capabilities.md index ac100bd89f..ed7e6191f8 100644 --- a/docs/technical/architecture/subsystems/consensus/capabilities.md +++ b/docs/technical/architecture/subsystems/consensus/capabilities.md @@ -114,11 +114,11 @@ leaving a hole for queued successors. Removing a member remains allowed; adding, promoting, or refreshing one without all active capabilities is rejected. -The admission path and the shared plan runner apply the policy check to -business and technical producers (including mirror, events, maintenance, and -backup flows). `Node.Propose` is the final common boundary for every producer; -callers must still stamp their proposal before use. Membership `ConfChange` -operations carry and validate their capability evidence separately. +`Node.Propose` is the single final policy boundary for every producer, +including mirror, events, maintenance, and backup flows; the shared plan +runner does not duplicate this check. Callers must still stamp their proposal +before use. Membership `ConfChange` operations carry and validate their +capability evidence separately. ## Restart refresh @@ -130,7 +130,8 @@ peer. Followers forward that request to the current leader over the Raft gRPC server. The node can therefore receive inbound Raft traffic before the refresh commits; the refresh is an evidence update, not a pre-traffic barrier. During the rc.1/rc.2 rolling upgrade, an older peer that returns `Unimplemented` is -treated as transient and the node retries until a compatible peer is available. +treated as transient and the node keeps retrying in the background until a +compatible peer is available. The leader validates the identity and capability evidence against the active policy, then proposes a `ConfChangeUpdateNode` containing the current endpoints and capabilities. @@ -140,14 +141,11 @@ Raft and applied to every member's peer store; a local restart-time upsert is not sufficient to activate a capability for admission. A fresh WAL instead uses `JoinAsLearner` and `ConfChangeAddLearnerNode`. -The cluster-policy reconciler currently maintains the replicated policy and -preserves its committed capability activations; it does not automatically arm -or activate a newly advertised capability. A future explicit activation path -must first verify the candidate against every current voter and learner. The -candidate set should be the intersection of the local binary registry and all -committed membership evidence; an empty or incomplete membership set must not -activate anything. Membership commits trigger reconciliation immediately, and -the periodic retry remains as a self-healing fallback for leadership or -proposal failures. Once an activation path exists and an activation is -committed, it is monotonic and a new member that does not advertise it is +The cluster-policy reconciler maintains the replicated policy and automatically +arms newly advertised capabilities only when the candidate is present in the +intersection of the local binary registry and every current voter and learner. +An empty or incomplete membership set activates nothing. Membership commits +trigger reconciliation immediately, and the periodic retry remains as a +self-healing fallback for leadership or proposal failures. Once an activation +is committed, it is monotonic and a new member that does not advertise it is rejected. diff --git a/internal/bootstrap/module.go b/internal/bootstrap/module.go index d0f3fbd443..bf3cfc1e3d 100644 --- a/internal/bootstrap/module.go +++ b/internal/bootstrap/module.go @@ -1395,14 +1395,16 @@ func membershipRefreshHook(cfg Config, logger logging.Logger) fx.Hook { "serviceAddress": cfg.ServiceAdvertiseAddr(), }).Infof("Restart mode: refreshing membership capabilities") - if err := tryRefreshMembership(ctx, cfg, cfg.TLSConfig, logger); err != nil { - // TODO(rc.3): replace this compatibility fallback with explicit - // startup error handling once every supported peer implements - // RefreshMembership. The leader-side reconciler below keeps - // polling membership evidence and will arm capabilities only - // after all current members converge. - logger.WithFields(map[string]any{"error": err}).Errorf("Membership capability refresh failed; continuing with conservative evidence") - } + go func() { + if err := tryRefreshMembership(ctx, cfg, cfg.TLSConfig, logger); err != nil { + // TODO(rc.3): replace this compatibility fallback with explicit + // startup error handling once every supported peer implements + // RefreshMembership. The leader-side reconciler below keeps + // polling membership evidence and will arm capabilities only + // after all current members converge. + logger.WithFields(map[string]any{"error": err}).Errorf("Membership capability refresh failed; continuing with conservative evidence") + } + }() return nil }, @@ -1623,35 +1625,42 @@ func tryRefreshMembership(ctx context.Context, cfg Config, tlsCfg TLSConfig, log ctx = metadata.AppendToOutgoingContext(ctx, node.MetadataKeyClusterID, cfg.ClusterID) } - var lastErr error - for _, peer := range cfg.RaftConfig.Peers { - conn, dialErr := ggrpc.NewClient(peer.Address, dialOpts...) - if dialErr != nil { - lastErr = dialErr - logger.WithFields(map[string]any{"peer": peer.ID, "address": peer.Address, "error": dialErr}).Infof("Failed to dial peer for membership refresh") + backoff := 500 * time.Millisecond + for { + var lastErr error + for _, peer := range cfg.RaftConfig.Peers { + conn, dialErr := ggrpc.NewClient(peer.Address, dialOpts...) + if dialErr != nil { + lastErr = dialErr + logger.WithFields(map[string]any{"peer": peer.ID, "address": peer.Address, "error": dialErr}).Infof("Failed to dial peer for membership refresh") - continue - } + continue + } - _, callErr := clusterbootstrappb.NewClusterBootstrapServiceClient(conn).RefreshMembership(ctx, req) - _ = conn.Close() - if callErr == nil { - logger.WithFields(map[string]any{"peer": peer.ID}).Infof("Successfully refreshed membership capabilities") + _, callErr := clusterbootstrappb.NewClusterBootstrapServiceClient(conn).RefreshMembership(ctx, req) + _ = conn.Close() + if callErr == nil { + logger.WithFields(map[string]any{"peer": peer.ID}).Infof("Successfully refreshed membership capabilities") - return nil - } + return nil + } - lastErr = callErr - if !isRetryableMembershipRefreshError(callErr) { - return fmt.Errorf("failed to refresh membership via peer %d (%s): %w", peer.ID, peer.Address, callErr) + lastErr = callErr + if !isRetryableMembershipRefreshError(callErr) { + return fmt.Errorf("failed to refresh membership via peer %d (%s): %w", peer.ID, peer.Address, callErr) + } } - } - if lastErr == nil { - return errors.New("no configured peer available for membership refresh") + if lastErr == nil { + lastErr = errors.New("no configured peer available for membership refresh") + } + select { + case <-ctx.Done(): + return ctx.Err() + case <-time.After(backoff): + } + backoff = min(backoff*2, 5*time.Second) } - - return fmt.Errorf("membership refresh unavailable: %w", lastErr) } func isRetryableMembershipRefreshError(err error) bool { @@ -1846,9 +1855,19 @@ func reconcileClusterPolicy(ctx context.Context, admission ctrl.Admission, store } } for _, id := range ready { - if _, exists := active[uint32(id)]; !exists { - desired.CapabilityActivations = append(desired.CapabilityActivations, &commonpb.CapabilityActivation{Id: uint32(id), Active: true}) + if enabled, exists := active[uint32(id)]; exists { + if !enabled { + for _, activation := range desired.CapabilityActivations { + if activation != nil && activation.GetId() == uint32(id) { + activation.Active = true + break + } + } + } + + continue } + desired.CapabilityActivations = append(desired.CapabilityActivations, &commonpb.CapabilityActivation{Id: uint32(id), Active: true}) } if applied != nil && !applied.EqualVT(desired) && desired.GetRevision() <= applied.GetRevision() { desired.Revision = applied.GetRevision() + 1 diff --git a/internal/domain/processing/processor_cluster_policy.go b/internal/domain/processing/processor_cluster_policy.go index 915446a743..cbaeeb8220 100644 --- a/internal/domain/processing/processor_cluster_policy.go +++ b/internal/domain/processing/processor_cluster_policy.go @@ -42,6 +42,16 @@ func applyClusterPolicy(newPolicy *commonpb.ClusterPolicy, scope Scope) (*common if err := validateClusterPolicyMetadataLimits(newPolicy); err != nil { return nil, err } + seenCapabilities := make(map[uint32]struct{}, len(newPolicy.GetCapabilityActivations())) + for _, activation := range newPolicy.GetCapabilityActivations() { + if activation == nil || activation.GetId() == 0 { + return nil, &domain.ErrClusterPolicyInvalid{Detail: "capability activation id must be non-zero"} + } + if _, exists := seenCapabilities[activation.GetId()]; exists { + return nil, &domain.ErrClusterPolicyInvalid{Detail: fmt.Sprintf("duplicate capability activation %d", activation.GetId())} + } + seenCapabilities[activation.GetId()] = struct{}{} + } current := scope.GetClusterPolicy() appliedRev := current.GetRevision() diff --git a/internal/infra/membership/confchange_identity_test.go b/internal/infra/membership/confchange_identity_test.go index cce1bcffb4..5551348688 100644 --- a/internal/infra/membership/confchange_identity_test.go +++ b/internal/infra/membership/confchange_identity_test.go @@ -70,6 +70,22 @@ func TestValidateCapabilityEvidenceRequiresActiveCapabilities(t *testing.T) { require.NoError(t, ValidateCapabilityEvidence(&ConfChangeContext{InstanceID: bytesN(0x42)}, nil)) } +func TestValidateTransitionEvidenceReadsCommittedPolicyBeforeCachedGate(t *testing.T) { + m := newTestMembership(t) + m.requiredCapabilities = nil // simulate the post-commit cache update window + m.capabilityPolicyProvider = func() (*commonpb.ClusterPolicy, error) { + return &commonpb.ClusterPolicy{ + CapabilityActivations: []*commonpb.CapabilityActivation{{Id: 7, Active: true}}, + }, nil + } + + err := m.ValidateTransitionEvidence(&ConfChangeContext{ + InstanceID: fixedInstanceID(1), + Capabilities: nil, + }) + require.ErrorContains(t, err, "capability 7 is required") +} + func TestWriteConfChangeRejectsCapabilitiesMissingFromCommittedPolicy(t *testing.T) { m := newTestMembership(t) m.capabilityPolicyProvider = func() (*commonpb.ClusterPolicy, error) { diff --git a/internal/infra/membership/membership.go b/internal/infra/membership/membership.go index e31e5374b8..9d22bda6d4 100644 --- a/internal/infra/membership/membership.go +++ b/internal/infra/membership/membership.go @@ -281,8 +281,30 @@ func (m *Membership) CapabilityGate() (required []uint32) { func (m *Membership) ValidateTransitionEvidence(ctx *ConfChangeContext) error { m.mu.RLock() required := append([]uint32(nil), m.requiredCapabilities...) + provider := m.capabilityPolicyProvider m.mu.RUnlock() + // Read the durable policy at the admission boundary as well as the + // published cache. The cache is updated immediately after the Pebble + // commit; consulting the provider closes the small window where a newly + // activated capability is durable but the in-memory gate has not caught up + // yet. In that window, a proposal must be checked against the stricter, + // committed policy rather than admitted optimistically. + if provider != nil { + policy, err := provider() + if err != nil { + return fmt.Errorf("reading committed cluster policy: %w", err) + } + if policy != nil { + required = required[:0] + for _, activation := range policy.GetCapabilityActivations() { + if activation != nil && activation.GetActive() { + required = append(required, activation.GetId()) + } + } + } + } + return ValidateCapabilityEvidence(ctx, required) } diff --git a/internal/infra/plan/runner.go b/internal/infra/plan/runner.go index 629506b103..73620a1723 100644 --- a/internal/infra/plan/runner.go +++ b/internal/infra/plan/runner.go @@ -13,7 +13,6 @@ import ( "github.com/formancehq/ledger/v3/internal/pkg/proposals" "github.com/formancehq/ledger/v3/internal/proto/commonpb" "github.com/formancehq/ledger/v3/internal/proto/raftcmdpb" - "github.com/formancehq/ledger/v3/internal/query" ) // Sentinel errors returned by Run to let callers distinguish which From 9803b300348988401dd2fd7bd3cd15bd12789f98 Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Mon, 26 Oct 2026 13:27:30 +0100 Subject: [PATCH 047/127] fix: keep membership refresh alive after startup --- internal/bootstrap/module.go | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/internal/bootstrap/module.go b/internal/bootstrap/module.go index bf3cfc1e3d..e9a04a6115 100644 --- a/internal/bootstrap/module.go +++ b/internal/bootstrap/module.go @@ -1387,16 +1387,20 @@ func joinPreflightHook(cfg Config, logger logging.Logger) fx.Hook { } func membershipRefreshHook(cfg Config, logger logging.Logger) fx.Hook { + var cancel context.CancelFunc + return fx.Hook{ - OnStart: func(ctx context.Context) error { + OnStart: func(context.Context) error { logger.WithFields(map[string]any{ "nodeID": cfg.RaftConfig.NodeID, "raftAddress": cfg.RaftConfig.AdvertiseAddr, "serviceAddress": cfg.ServiceAdvertiseAddr(), }).Infof("Restart mode: refreshing membership capabilities") + refreshCtx, refreshCancel := context.WithCancel(context.Background()) + cancel = refreshCancel go func() { - if err := tryRefreshMembership(ctx, cfg, cfg.TLSConfig, logger); err != nil { + if err := tryRefreshMembership(refreshCtx, cfg, cfg.TLSConfig, logger); err != nil { // TODO(rc.3): replace this compatibility fallback with explicit // startup error handling once every supported peer implements // RefreshMembership. The leader-side reconciler below keeps @@ -1406,6 +1410,13 @@ func membershipRefreshHook(cfg Config, logger logging.Logger) fx.Hook { } }() + return nil + }, + OnStop: func(context.Context) error { + if cancel != nil { + cancel() + } + return nil }, } From 7699030272bb6c642aed48ba7fbe5f034b768806 Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Mon, 26 Oct 2026 13:34:39 +0100 Subject: [PATCH 048/127] test(consensus): remove dead proposal gate and cover refresh retry --- .../module_refresh_membership_test.go | 89 +++++++++++++++++ internal/infra/plan/capability_policy.go | 63 ------------ internal/infra/plan/capability_policy_test.go | 96 ------------------- 3 files changed, 89 insertions(+), 159 deletions(-) delete mode 100644 internal/infra/plan/capability_policy.go delete mode 100644 internal/infra/plan/capability_policy_test.go diff --git a/internal/bootstrap/module_refresh_membership_test.go b/internal/bootstrap/module_refresh_membership_test.go index 51e3307ef1..8da93336cf 100644 --- a/internal/bootstrap/module_refresh_membership_test.go +++ b/internal/bootstrap/module_refresh_membership_test.go @@ -1,11 +1,21 @@ package bootstrap import ( + "context" + "net" + "sync/atomic" "testing" + "time" "github.com/stretchr/testify/require" + "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" + + logging "github.com/formancehq/go-libs/v5/pkg/observe/log" + + "github.com/formancehq/ledger/v3/internal/infra/node" + "github.com/formancehq/ledger/v3/internal/proto/clusterbootstrappb" ) func TestIsRetryableMembershipRefreshError(t *testing.T) { @@ -26,3 +36,82 @@ func TestIsRetryableMembershipRefreshError(t *testing.T) { }) } } + +type retryingRefreshServer struct { + clusterbootstrappb.UnimplementedClusterBootstrapServiceServer + attempts atomic.Int32 + firstErr error +} + +func (s *retryingRefreshServer) RefreshMembership(context.Context, *clusterbootstrappb.RefreshMembershipRequest) (*clusterbootstrappb.RefreshMembershipResponse, error) { + if s.attempts.Add(1) == 1 { + return nil, s.firstErr + } + + return &clusterbootstrappb.RefreshMembershipResponse{}, nil +} + +func serveRefreshMembership(t *testing.T, server clusterbootstrappb.ClusterBootstrapServiceServer) string { + t.Helper() + + lis, err := net.Listen("tcp", "127.0.0.1:0") + require.NoError(t, err) + srv := grpc.NewServer() + clusterbootstrappb.RegisterClusterBootstrapServiceServer(srv, server) + go func() { _ = srv.Serve(lis) }() + t.Cleanup(srv.Stop) + + return lis.Addr().String() +} + +func refreshTestConfig(t *testing.T, peerAddress string) Config { + t.Helper() + + return Config{ + ClusterID: "test-cluster", + TLSConfig: TLSConfig{Mode: TLSModeDisabled}, + RaftConfig: node.NodeConfig{ + NodeID: 2, + WalDir: t.TempDir(), + AdvertiseAddr: "127.0.0.1:7777", + Peers: []node.Peer{{ID: 1, Address: peerAddress}}, + }, + } +} + +func TestTryRefreshMembershipRetriesUnimplementedUntilPeerSupportsRPC(t *testing.T) { + t.Parallel() + + peer := &retryingRefreshServer{firstErr: status.Error(codes.Unimplemented, "old peer")} + cfg := refreshTestConfig(t, serveRefreshMembership(t, peer)) + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + t.Cleanup(cancel) + require.NoError(t, tryRefreshMembership(ctx, cfg, cfg.TLSConfig, logging.Testing())) + require.Equal(t, int32(2), peer.attempts.Load()) +} + +func TestTryRefreshMembershipStopsRetryingWhenContextIsCanceled(t *testing.T) { + t.Parallel() + + peer := &retryingRefreshServer{firstErr: status.Error(codes.Unavailable, "leader unavailable")} + cfg := refreshTestConfig(t, serveRefreshMembership(t, peer)) + + ctx, cancel := context.WithCancel(context.Background()) + started := make(chan struct{}) + go func() { + for { + if peer.attempts.Load() > 0 { + close(started) + return + } + time.Sleep(time.Millisecond) + } + }() + + result := make(chan error, 1) + go func() { result <- tryRefreshMembership(ctx, cfg, cfg.TLSConfig, logging.Testing()) }() + <-started + cancel() + require.ErrorIs(t, <-result, context.Canceled) +} diff --git a/internal/infra/plan/capability_policy.go b/internal/infra/plan/capability_policy.go deleted file mode 100644 index ed2b38340d..0000000000 --- a/internal/infra/plan/capability_policy.go +++ /dev/null @@ -1,63 +0,0 @@ -package plan - -import ( - "context" - "fmt" - - "github.com/formancehq/ledger/v3/internal/infra/capabilities" - "github.com/formancehq/ledger/v3/internal/infra/node" - "github.com/formancehq/ledger/v3/internal/infra/state" - "github.com/formancehq/ledger/v3/internal/pkg/futures" - "github.com/formancehq/ledger/v3/internal/proto/commonpb" -) - -// CapabilityPolicyProposer rechecks the committed capability policy against -// the capabilities derived by the producer from the final proposal immediately -// before handing it to the Raft proposer. -type CapabilityPolicyProposer struct { - delegate Proposer - readPolicy func() (*commonpb.ClusterPolicy, error) - readMembers func() []capabilities.Member - businessWrite bool -} - -// NewCapabilityPolicyProposer wraps a proposer with the replicated policy -// admission check. The callback must read the committed policy projection, not -// process-local configuration. -func NewCapabilityPolicyProposer( - delegate Proposer, - readPolicy func() (*commonpb.ClusterPolicy, error), - readMembers func() []capabilities.Member, - businessWrite bool, -) Proposer { - return CapabilityPolicyProposer{ - delegate: delegate, - readPolicy: readPolicy, - readMembers: readMembers, - businessWrite: businessWrite, - } -} - -func (p CapabilityPolicyProposer) Propose(ctx context.Context, proposal *node.Proposal) (*futures.Future[state.ApplyResult], error) { - policy, err := p.readPolicy() - if err != nil { - return nil, fmt.Errorf("reading cluster capability policy before proposal: %w", err) - } - - state := capabilities.FromPolicy(policy) - if err := state.Validate(); err != nil { - return nil, err - } - - required := proposal.RequiredCapabilities() - var members []capabilities.Member - if p.readMembers != nil { - members = p.readMembers() - } - decision := capabilities.Evaluate(state, required, p.businessWrite, members) - if !decision.Allowed { - return nil, fmt.Errorf("%w: %s", capabilities.ErrCapabilityUnsupported, decision.Reason) - } - - return p.delegate.Propose(ctx, proposal) -} diff --git a/internal/infra/plan/capability_policy_test.go b/internal/infra/plan/capability_policy_test.go deleted file mode 100644 index 7ab975c806..0000000000 --- a/internal/infra/plan/capability_policy_test.go +++ /dev/null @@ -1,96 +0,0 @@ -package plan - -import ( - "context" - "errors" - "reflect" - "testing" - "unsafe" - - "github.com/stretchr/testify/require" - - "github.com/formancehq/ledger/v3/internal/infra/capabilities" - "github.com/formancehq/ledger/v3/internal/infra/node" - "github.com/formancehq/ledger/v3/internal/infra/state" - "github.com/formancehq/ledger/v3/internal/pkg/futures" - "github.com/formancehq/ledger/v3/internal/proto/commonpb" - "github.com/formancehq/ledger/v3/internal/proto/raftcmdpb" -) - -type capabilityPolicyTestProposer struct{ called bool } - -func (p *capabilityPolicyTestProposer) Propose(context.Context, *node.Proposal) (*futures.Future[state.ApplyResult], error) { - p.called = true - - return futures.New[state.ApplyResult](), nil -} - -func capabilityPolicyProposal(t *testing.T) *node.Proposal { - t.Helper() - - proposal := &raftcmdpb.Proposal{ - ExecutionPlan: &raftcmdpb.ExecutionPlan{}, - } - result, err := node.NewProposal(1, proposal) - require.NoError(t, err) - - return result -} - -// capabilityPolicyProposalWithRequired is a package-local seam for exercising -// the policy wrapper until the capability registry contains a real feature. -// Production proposals can only get this metadata through node.NewProposal. -func capabilityPolicyProposalWithRequired(t *testing.T, required ...capabilities.CapabilityID) *node.Proposal { - t.Helper() - - proposal := capabilityPolicyProposal(t) - field := reflect.ValueOf(proposal).Elem().FieldByName("requiredCapabilities") - reflect.NewAt(field.Type(), unsafe.Pointer(field.UnsafeAddr())).Elem().Set(reflect.ValueOf(required)) - - return proposal -} - -func TestCapabilityPolicyProposerAllowsUnrelatedProposal(t *testing.T) { - delegate := new(capabilityPolicyTestProposer) - proposer := NewCapabilityPolicyProposer(delegate, func() (*commonpb.ClusterPolicy, error) { - return &commonpb.ClusterPolicy{}, nil - }, nil, false) - - _, err := proposer.Propose(context.Background(), capabilityPolicyProposal(t)) - require.NoError(t, err) - require.True(t, delegate.called) -} - -func TestCapabilityPolicyProposerRejectsPolicyReadFailureForBothProposalModes(t *testing.T) { - t.Parallel() - - for _, businessWrite := range []bool{false, true} { - t.Run(map[bool]string{false: "technical", true: "business"}[businessWrite], func(t *testing.T) { - delegate := new(capabilityPolicyTestProposer) - proposer := NewCapabilityPolicyProposer(delegate, func() (*commonpb.ClusterPolicy, error) { - return nil, errors.New("policy unavailable") - }, nil, businessWrite) - - _, err := proposer.Propose(context.Background(), capabilityPolicyProposal(t)) - require.ErrorContains(t, err, "policy unavailable") - require.False(t, delegate.called) - }) - } -} - -func TestCapabilityPolicyProposerRejectsInactiveCapabilityForBothProposalModes(t *testing.T) { - t.Parallel() - - for _, businessWrite := range []bool{false, true} { - t.Run(map[bool]string{false: "technical", true: "business"}[businessWrite], func(t *testing.T) { - delegate := new(capabilityPolicyTestProposer) - proposer := NewCapabilityPolicyProposer(delegate, func() (*commonpb.ClusterPolicy, error) { - return &commonpb.ClusterPolicy{}, nil - }, nil, businessWrite) - - _, err := proposer.Propose(context.Background(), capabilityPolicyProposalWithRequired(t, 99)) - require.ErrorIs(t, err, capabilities.ErrCapabilityUnsupported) - require.False(t, delegate.called) - }) - } -} From 7e61a6c03509632973672157d879a0cb14587d34 Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Mon, 26 Oct 2026 13:35:13 +0100 Subject: [PATCH 049/127] fix(capabilities): gate policy activation on member evidence --- internal/bootstrap/module.go | 18 ++++++++++++ internal/infra/node/proposal.go | 26 +++++++++++++++++- internal/infra/node/proposal_test.go | 41 ++++++++++++++++++++++++++++ 3 files changed, 84 insertions(+), 1 deletion(-) diff --git a/internal/bootstrap/module.go b/internal/bootstrap/module.go index e9a04a6115..1cff616f65 100644 --- a/internal/bootstrap/module.go +++ b/internal/bootstrap/module.go @@ -398,6 +398,24 @@ func Module() fx.Option { if raftNode == nil { return errors.New("raft proposal gate is not initialized") } + // A policy activation is itself a consensus-sensitive write. Do + // not rely solely on the periodic reconciler: callers that submit a + // SetClusterPolicy command must carry the same all-member evidence + // before the command can enter Raft. NewProposal derives this + // typed candidate from the final command, so this check does not + // decode the serialized proposal or trust caller-supplied metadata. + activePolicyCapabilities := proposal.ActivePolicyCapabilities() + if len(activePolicyCapabilities) > 0 { + ready, readyErr := capabilities.Ready(capabilities.SupportedIDs(), raftNode.CapabilityMembers()) + if readyErr != nil { + return fmt.Errorf("validating capability activation evidence: %w", readyErr) + } + for _, required := range activePolicyCapabilities { + if !slices.Contains(ready, required) { + return fmt.Errorf("%w: capability %d is not acknowledged by every current member", capabilities.ErrCapabilityUnsupported, required) + } + } + } state := capabilities.FromPolicy(policy) if err := state.Validate(); err != nil { diff --git a/internal/infra/node/proposal.go b/internal/infra/node/proposal.go index 00966c390b..0b67fb5e20 100644 --- a/internal/infra/node/proposal.go +++ b/internal/infra/node/proposal.go @@ -29,6 +29,11 @@ type Proposal struct { // expectedTerm fences a drain preflight against a later leadership term. expectedTerm uint64 requiredCapabilities []capabilities.CapabilityID + // activePolicyCapabilities is populated by the proposal factory for + // SetClusterPolicy orders. Keeping the typed candidate alongside the + // serialized command lets the leader validate activation evidence before + // handing the bytes to Raft without decoding the command a second time. + activePolicyCapabilities []capabilities.CapabilityID } func newProposal(commandID uint64, data []byte, required []capabilities.CapabilityID) *Proposal { @@ -54,7 +59,19 @@ func NewProposal(commandID uint64, command *raftcmdpb.Proposal) (*Proposal, erro return nil, err } - return newProposal(commandID, data, prepared.RequiredCapabilities()), nil + proposal := newProposal(commandID, data, prepared.RequiredCapabilities()) + for _, order := range command.GetOrders() { + policy := order.GetSystemScoped().GetSetClusterPolicy().GetPolicy() + if policy == nil { + continue + } + for _, activation := range policy.GetCapabilityActivations() { + if activation != nil && activation.GetActive() { + proposal.activePolicyCapabilities = append(proposal.activePolicyCapabilities, capabilities.CapabilityID(activation.GetId())) + } + } + } + return proposal, nil } // Data returns the serialized proposal data. @@ -80,3 +97,10 @@ func (p *Proposal) SetPredictedIndex(index uint64) { func (p *Proposal) RequiredCapabilities() []capabilities.CapabilityID { return append([]capabilities.CapabilityID(nil), p.requiredCapabilities...) } + +// ActivePolicyCapabilities returns the active capabilities requested by a +// SetClusterPolicy order, if any. The values are derived by NewProposal from +// the typed command and are not read from the serialized payload. +func (p *Proposal) ActivePolicyCapabilities() []capabilities.CapabilityID { + return append([]capabilities.CapabilityID(nil), p.activePolicyCapabilities...) +} diff --git a/internal/infra/node/proposal_test.go b/internal/infra/node/proposal_test.go index 23f98a071d..df16d83d20 100644 --- a/internal/infra/node/proposal_test.go +++ b/internal/infra/node/proposal_test.go @@ -5,6 +5,8 @@ import ( "github.com/stretchr/testify/require" + "github.com/formancehq/ledger/v3/internal/infra/capabilities" + "github.com/formancehq/ledger/v3/internal/proto/commonpb" "github.com/formancehq/ledger/v3/internal/proto/raftcmdpb" ) @@ -32,3 +34,42 @@ func TestProposalSetPredictedIndexPatchesSerializedPayload(t *testing.T) { require.NoError(t, decoded.UnmarshalVT(proposal.Data())) require.Equal(t, uint64(42), decoded.GetPredictedIndex()) } + +func TestNewProposalReservesPredictedIndexSuffix(t *testing.T) { + command := &raftcmdpb.Proposal{Id: 1} + proposal, err := NewProposal(1, command) + require.NoError(t, err) + + before := proposal.Data() + reserved := cap(proposal.data) + require.GreaterOrEqual(t, reserved-len(proposal.data), predictedIndexEncodedSize) + + proposal.SetPredictedIndex(42) + after := proposal.Data() + require.Equal(t, before, after[:len(before)]) + require.Equal(t, reserved, cap(proposal.data), "predicted-index patch must not grow the backing buffer") +} + +func TestNewProposalDerivesActivePolicyCapabilities(t *testing.T) { + command := &raftcmdpb.Proposal{ + Id: 1, + Orders: []*raftcmdpb.Order{{ + Type: &raftcmdpb.Order_SystemScoped{SystemScoped: &raftcmdpb.SystemScopedOrder{ + Payload: &raftcmdpb.SystemScopedOrder_SetClusterPolicy{SetClusterPolicy: &raftcmdpb.SetClusterPolicyOrder{ + Policy: &commonpb.ClusterPolicy{CapabilityActivations: []*commonpb.CapabilityActivation{ + {Id: 7, Active: true}, + {Id: 8, Active: false}, + }}, + }}, + }}, + }}, + } + + proposal, err := NewProposal(1, command) + require.NoError(t, err) + require.Equal(t, []capabilities.CapabilityID{7}, proposal.ActivePolicyCapabilities()) + + active := proposal.ActivePolicyCapabilities() + active[0] = 99 + require.Equal(t, []capabilities.CapabilityID{7}, proposal.ActivePolicyCapabilities(), "candidate metadata must be defensive") +} From 5884f250d29deccaa62c4fd6449181d29ed4bed6 Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Mon, 26 Oct 2026 13:37:48 +0100 Subject: [PATCH 050/127] fix: require capability dependencies in constructors --- .../application/check/checker_fsm_purge_test.go | 1 + .../check/checker_fsm_tombstone_test.go | 1 + .../membership/membership_node_test.go | 5 +++-- .../application/mirror/worker_diagnostic_test.go | 2 +- .../application/mirror/worker_rotation_test.go | 2 +- .../controller_routed_checkpoint_fence_test.go | 4 ++-- .../bootstrap/node_hook_commit_drain_test.go | 4 ++-- internal/bootstrap/service_address_test.go | 2 +- internal/infra/backup/audit_key_restore_test.go | 2 +- .../backup/numscript_restore_parity_test.go | 2 +- .../infra/backup/signing_restore_parity_test.go | 1 + .../membership/committed_peer_refresh_test.go | 1 + internal/infra/membership/membership.go | 15 +++++++++------ internal/infra/membership/membership_test.go | 16 +++++++++------- .../membership/remove_node_blacklist_test.go | 12 ++++++------ .../infra/node/applier_async_storage_test.go | 1 + internal/infra/node/applier_test.go | 2 +- .../node/delete_ledger_sentinel_recovery_test.go | 2 +- internal/infra/node/membership_helpers_test.go | 1 + .../node/startup_membership_removal_test.go | 2 +- internal/infra/state/audit_replay.go | 2 +- .../state/checkpoint_pipeline_regression_test.go | 1 + internal/infra/state/machine.go | 15 ++++++++------- internal/infra/state/machine_test.go | 10 +++++----- .../state/sentinel_tracer_isolation_test.go | 1 + internal/infra/state/signing_cascade_test.go | 1 + 26 files changed, 62 insertions(+), 46 deletions(-) diff --git a/internal/application/check/checker_fsm_purge_test.go b/internal/application/check/checker_fsm_purge_test.go index 09adb835c4..7893dbd5cf 100644 --- a/internal/application/check/checker_fsm_purge_test.go +++ b/internal/application/check/checker_fsm_purge_test.go @@ -70,6 +70,7 @@ func newFSMPurgeMachine(t *testing.T) (*state.Machine, *dal.Store, *attributes.A fsmPurgeAuditKey, 0, func(*raftpb.Entry, *dal.WriteSession) error { return nil }, + state.NoopCapabilityPolicyCommitHandler, ) require.NoError(t, err) diff --git a/internal/application/check/checker_fsm_tombstone_test.go b/internal/application/check/checker_fsm_tombstone_test.go index 583587ddd1..ff2ca0dc2f 100644 --- a/internal/application/check/checker_fsm_tombstone_test.go +++ b/internal/application/check/checker_fsm_tombstone_test.go @@ -67,6 +67,7 @@ func newTombstoneFSMMachine(t *testing.T) (*state.Machine, *dal.Store, *attribut tombstoneFSMAuditKey, 0, func(*raftpb.Entry, *dal.WriteSession) error { return nil }, + state.NoopCapabilityPolicyCommitHandler, ) require.NoError(t, err) diff --git a/internal/application/membership/membership_node_test.go b/internal/application/membership/membership_node_test.go index 5eafc1d8ae..bc92a6c4cb 100644 --- a/internal/application/membership/membership_node_test.go +++ b/internal/application/membership/membership_node_test.go @@ -72,7 +72,7 @@ func newMembershipServiceNodeWithPool(t *testing.T) (*Service, *transport.Connec } }) m, err := inframembership.NewMembership(inframembership.NewPeerStore(store), raftTransport, pool, - 1, "self:7777", "self:8888", []byte("self-instance-id"), logger) + 1, "self:7777", "self:8888", []byte("self-instance-id"), logger, inframembership.NoopClusterPolicyProvider) require.NoError(t, err) c, err := cache.New(1000, nil) @@ -80,7 +80,8 @@ func newMembershipServiceNodeWithPool(t *testing.T) (*Service, *transport.Connec registry := state.NewStateRegistry(c, attributes.New()) fsm, err := state.NewMachine(logger, registry, state.NewCacheSnapshotter(logger, registry, nil), store, dal.NewSentinelFactory(store, false), meters, nil, state.NewSharedState(), - signal.NewNotifications(), nil, "membership-service-test", 0, m.WriteConfChange) + signal.NewNotifications(), nil, "membership-service-test", 0, m.WriteConfChange, + state.NoopCapabilityPolicyCommitHandler) require.NoError(t, err) recovery := state.NewRecovery(fsm, store) require.NoError(t, recovery.RecoverState()) diff --git a/internal/application/mirror/worker_diagnostic_test.go b/internal/application/mirror/worker_diagnostic_test.go index a4babb5a39..f642e26b90 100644 --- a/internal/application/mirror/worker_diagnostic_test.go +++ b/internal/application/mirror/worker_diagnostic_test.go @@ -80,7 +80,7 @@ func TestWorker_MalformedURLDoesNotDisclosePassword(t *testing.T) { registry := state.NewStateRegistry(c, attrs) keys := keystore.NewKeyStore() shared := state.NewSharedState() - machine, err := state.NewMachine(logger, registry, state.NewCacheSnapshotter(logger, registry, nil), store, dal.NewSentinelFactory(store, false), meters, keys, shared, signal.NewNotifications(), nil, "test-cluster", 0, func(*raftpb.Entry, *dal.WriteSession) error { return nil }) + machine, err := state.NewMachine(logger, registry, state.NewCacheSnapshotter(logger, registry, nil), store, dal.NewSentinelFactory(store, false), meters, keys, shared, signal.NewNotifications(), nil, "test-cluster", 0, func(*raftpb.Entry, *dal.WriteSession) error { return nil }, state.NoopCapabilityPolicyCommitHandler) require.NoError(t, err) writeMirrorMetadataPolicy(t, store, mirrorMetadataPolicy()) require.NoError(t, state.NewRecovery(machine, store).RecoverState()) diff --git a/internal/application/mirror/worker_rotation_test.go b/internal/application/mirror/worker_rotation_test.go index db88ccfbb2..653357d19e 100644 --- a/internal/application/mirror/worker_rotation_test.go +++ b/internal/application/mirror/worker_rotation_test.go @@ -82,7 +82,7 @@ func TestWorker_RotationPreloadsReadOnlyLedger(t *testing.T) { require.NoError(t, err) registry := state.NewStateRegistry(c, attrs) notifier := signal.NewNotifications() - machine, err := state.NewMachine(logger, registry, state.NewCacheSnapshotter(logger, registry, nil), store, dal.NewSentinelFactory(store, false), meters, keystore.NewKeyStore(), state.NewSharedState(), notifier, nil, "test-cluster", 0, func(*raftpb.Entry, *dal.WriteSession) error { return nil }) + machine, err := state.NewMachine(logger, registry, state.NewCacheSnapshotter(logger, registry, nil), store, dal.NewSentinelFactory(store, false), meters, keystore.NewKeyStore(), state.NewSharedState(), notifier, nil, "test-cluster", 0, func(*raftpb.Entry, *dal.WriteSession) error { return nil }, state.NoopCapabilityPolicyCommitHandler) require.NoError(t, err) writeMirrorMetadataPolicy(t, store, mirrorMetadataPolicy()) require.NoError(t, state.NewRecovery(machine, store).RecoverState()) diff --git a/internal/bootstrap/controller_routed_checkpoint_fence_test.go b/internal/bootstrap/controller_routed_checkpoint_fence_test.go index 34bb0d2e43..f6a91653b3 100644 --- a/internal/bootstrap/controller_routed_checkpoint_fence_test.go +++ b/internal/bootstrap/controller_routed_checkpoint_fence_test.go @@ -274,7 +274,7 @@ func startCheckpointFenceNode(t *testing.T, ctx context.Context, store *dal.Stor require.NoError(t, err) registry := state.NewStateRegistry(cacheStore, attributes.New()) shared := state.NewSharedState() - machine, err := state.NewMachine(logger, registry, state.NewCacheSnapshotter(logger, registry, nil), store, dal.NewSentinelFactory(store, false), meters, nil, shared, signal.NewNotifications(), nil, "checkpoint-fence", 0, func(*raftpb.Entry, *dal.WriteSession) error { return nil }) + machine, err := state.NewMachine(logger, registry, state.NewCacheSnapshotter(logger, registry, nil), store, dal.NewSentinelFactory(store, false), meters, nil, shared, signal.NewNotifications(), nil, "checkpoint-fence", 0, func(*raftpb.Entry, *dal.WriteSession) error { return nil }, state.NoopCapabilityPolicyCommitHandler) require.NoError(t, err) t.Cleanup(machine.Close) recovery := state.NewRecovery(machine, store) @@ -296,7 +296,7 @@ func startCheckpointFenceNode(t *testing.T, ctx context.Context, store *dal.Stor serviceAddr = raftAddr } instanceID := []byte(fmt.Sprintf("%016d", id)) - member, err := membership.NewMembership(membership.NewPeerStore(store), raftTransport, pool, id, raftAddr, serviceAddr, instanceID, logger) + member, err := membership.NewMembership(membership.NewPeerStore(store), raftTransport, pool, id, raftAddr, serviceAddr, instanceID, logger, membership.NoopClusterPolicyProvider) require.NoError(t, err) responses := node.NewLocalResponses() applier, err := node.NewApplier(machine, recovery, synchronizer, sp, store, w, logger, meter, 1000, 1000, nil, member, responses) diff --git a/internal/bootstrap/node_hook_commit_drain_test.go b/internal/bootstrap/node_hook_commit_drain_test.go index f21f89d47f..50183f6a31 100644 --- a/internal/bootstrap/node_hook_commit_drain_test.go +++ b/internal/bootstrap/node_hook_commit_drain_test.go @@ -185,7 +185,7 @@ func newHookCommitDrainNode(t *testing.T, notifier state.Notifier) (*observedDra registry := state.NewStateRegistry(cacheStore, attributes.New()) machine, err := state.NewMachine(logger, registry, state.NewCacheSnapshotter(logger, registry, nil), store, dal.NewSentinelFactory(store, false), meters, nil, state.NewSharedState(), notifier, nil, "hook-drain", 0, - func(*raftpb.Entry, *dal.WriteSession) error { return nil }) + func(*raftpb.Entry, *dal.WriteSession) error { return nil }, state.NoopCapabilityPolicyCommitHandler) require.NoError(t, err) t.Cleanup(machine.Close) recovery := state.NewRecovery(machine, store) @@ -205,7 +205,7 @@ func newHookCommitDrainNode(t *testing.T, notifier state.Notifier) (*observedDra }) instanceID := []byte("0000000000000001") member, err := membership.NewMembership(membership.NewPeerStore(store), raftTransport, servicePool, - 1, "node-1:7000", "node-1:8000", instanceID, logger) + 1, "node-1:7000", "node-1:8000", instanceID, logger, membership.NoopClusterPolicyProvider) require.NoError(t, err) responses := node.NewLocalResponses() applier, err := node.NewApplier(machine, recovery, synchronizer, sp, store, w, logger, meter, 1000, 1000, nil, member, responses) diff --git a/internal/bootstrap/service_address_test.go b/internal/bootstrap/service_address_test.go index 0979151a86..5355e7a643 100644 --- a/internal/bootstrap/service_address_test.go +++ b/internal/bootstrap/service_address_test.go @@ -69,7 +69,7 @@ func TestServiceAdvertiseAddr_IPv6RemoteRPC(t *testing.T) { t.Cleanup(func() { require.NoError(t, pool.Close()) }) peerStore := membership.NewPeerStore(newTestStore(t)) remote, err := membership.NewMembership(peerStore, serviceAddressRaftTransport{}, pool, - 1, "", "", []byte("self-instance-id"), logging.Testing()) + 1, "", "", []byte("self-instance-id"), logging.Testing(), membership.NoopClusterPolicyProvider) require.NoError(t, err) require.NoError(t, remote.Register(published.NodeID, published.AdvertiseAddr, published.ServiceAdvertiseAddr, published.InstanceID)) diff --git a/internal/infra/backup/audit_key_restore_test.go b/internal/infra/backup/audit_key_restore_test.go index 061e8a7a8f..611f47a64d 100644 --- a/internal/infra/backup/audit_key_restore_test.go +++ b/internal/infra/backup/audit_key_restore_test.go @@ -64,7 +64,7 @@ func recoveredAuditKeyMachine(t *testing.T, store *dal.Store) *state.Machine { registry := state.NewStateRegistry(c, attributes.New()) m, err := state.NewMachine(testLogger(), registry, state.NewCacheSnapshotter(testLogger(), registry, nil), store, dal.NewSentinelFactory(store, false), meter, keystore.NewKeyStore(), state.NewSharedState(), - signingParityNotifier{}, nil, "", 0, func(*raftpb.Entry, *dal.WriteSession) error { return nil }) + signingParityNotifier{}, nil, "", 0, func(*raftpb.Entry, *dal.WriteSession) error { return nil }, state.NoopCapabilityPolicyCommitHandler) require.NoError(t, err) require.NoError(t, state.NewRecovery(m, store).RecoverState()) diff --git a/internal/infra/backup/numscript_restore_parity_test.go b/internal/infra/backup/numscript_restore_parity_test.go index 3f33fef639..84be1ad591 100644 --- a/internal/infra/backup/numscript_restore_parity_test.go +++ b/internal/infra/backup/numscript_restore_parity_test.go @@ -92,7 +92,7 @@ func newScriptedNode(t *testing.T, store *dal.Store) *scriptedNode { machine, err := state.NewMachine(logger, registry, state.NewCacheSnapshotter(logger, registry, nil), store, dal.NewSentinelFactory(store, false), meters, keyStore, sharedState, signingParityNotifier{}, nil, - "", 0, func(*raftpb.Entry, *dal.WriteSession) error { return nil }) + "", 0, func(*raftpb.Entry, *dal.WriteSession) error { return nil }, state.NoopCapabilityPolicyCommitHandler) require.NoError(t, err) require.NoError(t, state.NewRecovery(machine, store).RecoverState()) diff --git a/internal/infra/backup/signing_restore_parity_test.go b/internal/infra/backup/signing_restore_parity_test.go index 6c8e2400f8..c9cabd3a29 100644 --- a/internal/infra/backup/signing_restore_parity_test.go +++ b/internal/infra/backup/signing_restore_parity_test.go @@ -71,6 +71,7 @@ func newSigningParityMachine(t *testing.T) (*state.Machine, *dal.Store, *attribu signingParityAuditKey, 0, func(*raftpb.Entry, *dal.WriteSession) error { return nil }, + state.NoopCapabilityPolicyCommitHandler, ) require.NoError(t, err) policy := &commonpb.ClusterPolicy{ diff --git a/internal/infra/membership/committed_peer_refresh_test.go b/internal/infra/membership/committed_peer_refresh_test.go index 846cfd7067..9b963becff 100644 --- a/internal/infra/membership/committed_peer_refresh_test.go +++ b/internal/infra/membership/committed_peer_refresh_test.go @@ -61,6 +61,7 @@ func TestCommittedPeerRefreshUpdatesRaftTransport(t *testing.T) { "self:8000", []byte("self-instance-id"), logger, + membership.NoopClusterPolicyProvider, ) require.NoError(t, err) m.Start() diff --git a/internal/infra/membership/membership.go b/internal/infra/membership/membership.go index 9d22bda6d4..6cf454c89f 100644 --- a/internal/infra/membership/membership.go +++ b/internal/infra/membership/membership.go @@ -101,7 +101,15 @@ type Membership struct { // Failure to read the peer rows is fatal: without them the node would // boot with an empty cache while the WAL ConfState still claims the // cluster has voters. -func NewMembership(store *PeerStore, transport Transport, pool Pool, selfNodeID uint64, selfRaftAddr, selfServiceAddr string, selfInstanceID []byte, logger logging.Logger, policyProviders ...func() (*commonpb.ClusterPolicy, error)) (*Membership, error) { +// NoopClusterPolicyProvider is an explicit dependency for isolated membership +// users that do not have a durable ClusterPolicy projection (for example unit +// tests). Production bootstrap injects the Pebble-backed provider. +func NoopClusterPolicyProvider() (*commonpb.ClusterPolicy, error) { return nil, nil } + +func NewMembership(store *PeerStore, transport Transport, pool Pool, selfNodeID uint64, selfRaftAddr, selfServiceAddr string, selfInstanceID []byte, logger logging.Logger, policyProvider func() (*commonpb.ClusterPolicy, error)) (*Membership, error) { + if policyProvider == nil { + return nil, errors.New("cluster policy provider is required") + } if err := ValidateInstanceID(selfInstanceID); err != nil { return nil, fmt.Errorf("invalid self identity: %w", err) } @@ -115,11 +123,6 @@ func NewMembership(store *PeerStore, transport Transport, pool Pool, selfNodeID "peerCount": len(addresses), }).Infof("Loaded cluster membership from Pebble") - var policyProvider func() (*commonpb.ClusterPolicy, error) - if len(policyProviders) > 0 { - policyProvider = policyProviders[0] - } - return &Membership{ store: store, transport: transport, diff --git a/internal/infra/membership/membership_test.go b/internal/infra/membership/membership_test.go index 32e8fbc858..2b63f32372 100644 --- a/internal/infra/membership/membership_test.go +++ b/internal/infra/membership/membership_test.go @@ -122,7 +122,7 @@ const ( func newTestMembership(t *testing.T) *Membership { t.Helper() - m, err := NewMembership(newTestPeerStore(t), noopTransport{}, noopPool{}, testSelfNodeID, testSelfRaftAddr, testSelfServiceAddr, fixedInstanceID(0x42), logging.Testing()) + m, err := NewMembership(newTestPeerStore(t), noopTransport{}, noopPool{}, testSelfNodeID, testSelfRaftAddr, testSelfServiceAddr, fixedInstanceID(0x42), logging.Testing(), NoopClusterPolicyProvider) require.NoError(t, err) m.Start() @@ -188,6 +188,7 @@ func TestNewMembershipRejectsInvalidSelfInstanceID(t *testing.T) { testSelfServiceAddr, nil, logging.Testing(), + NoopClusterPolicyProvider, ) require.ErrorContains(t, err, "invalid self identity") } @@ -410,7 +411,7 @@ func TestMembership_OnSnapshotInstalled(t *testing.T) { require.NoError(t, ps.Put(7, "old:1", "old:2", fixedInstanceID(7))) require.NoError(t, ps.PutWithEvidence(testSelfNodeID, testSelfRaftAddr, testSelfServiceAddr, fixedInstanceID(0x42), []uint32{7})) - m, err := NewMembership(ps, noopTransport{}, noopPool{}, testSelfNodeID, testSelfRaftAddr, testSelfServiceAddr, fixedInstanceID(0x42), logging.Testing()) + m, err := NewMembership(ps, noopTransport{}, noopPool{}, testSelfNodeID, testSelfRaftAddr, testSelfServiceAddr, fixedInstanceID(0x42), logging.Testing(), NoopClusterPolicyProvider) require.NoError(t, err) m.Start() require.Equal(t, "old:1", m.PeerAddresses()[7].RaftAddress) @@ -453,7 +454,7 @@ func TestMembership_OnSnapshotInstalledPanicsOnInvalidPeerIdentity(t *testing.T) t.Parallel() ps := newTestPeerStore(t) - m, err := NewMembership(ps, noopTransport{}, noopPool{}, testSelfNodeID, testSelfRaftAddr, testSelfServiceAddr, fixedInstanceID(0x42), logging.Testing()) + m, err := NewMembership(ps, noopTransport{}, noopPool{}, testSelfNodeID, testSelfRaftAddr, testSelfServiceAddr, fixedInstanceID(0x42), logging.Testing(), NoopClusterPolicyProvider) require.NoError(t, err) // Simulate a leader checkpoint that replaced Pebble with an invalid peer @@ -493,7 +494,7 @@ func TestMembership_RehydrateAfterReplay(t *testing.T) { raftTransport := &recordingTransport{peers: map[uint64]string{}} servicePool := &recordingPool{peers: map[uint64]string{}} - m, err := NewMembership(ps, raftTransport, servicePool, testSelfNodeID, testSelfRaftAddr, testSelfServiceAddr, fixedInstanceID(0x42), logging.Testing()) + m, err := NewMembership(ps, raftTransport, servicePool, testSelfNodeID, testSelfRaftAddr, testSelfServiceAddr, fixedInstanceID(0x42), logging.Testing(), NoopClusterPolicyProvider) require.NoError(t, err) m.Start() require.Equal(t, "before:1", m.PeerAddresses()[7].RaftAddress) @@ -545,7 +546,7 @@ func TestMembership_StartGate(t *testing.T) { require.NoError(t, ps.Put(1, "pod-0:7777", "pod-0:8888", fixedInstanceID(1))) - m, err := NewMembership(ps, transport, pool, testSelfNodeID, testSelfRaftAddr, testSelfServiceAddr, fixedInstanceID(0x42), logging.Testing()) + m, err := NewMembership(ps, transport, pool, testSelfNodeID, testSelfRaftAddr, testSelfServiceAddr, fixedInstanceID(0x42), logging.Testing(), NoopClusterPolicyProvider) require.NoError(t, err) require.Equal(t, 0, transport.adds, "no wire before Start (cache-only construction)") @@ -591,6 +592,7 @@ func TestMembership_SetRewiresChangedAddresses(t *testing.T) { testSelfServiceAddr, fixedInstanceID(0x42), logging.Testing(), + NoopClusterPolicyProvider, ) require.NoError(t, err) m.Start() @@ -622,7 +624,7 @@ func TestMembership_SetKeepsRaftPeerOnServiceAddressChange(t *testing.T) { transport := &countingTransport{} pool := &countingPool{} m, err := NewMembership(newTestPeerStore(t), transport, pool, - testSelfNodeID, testSelfRaftAddr, testSelfServiceAddr, fixedInstanceID(0x42), logging.Testing()) + testSelfNodeID, testSelfRaftAddr, testSelfServiceAddr, fixedInstanceID(0x42), logging.Testing(), NoopClusterPolicyProvider) require.NoError(t, err) m.Start() @@ -694,7 +696,7 @@ func TestMembership_ReconcileAgainstConfState(t *testing.T) { require.NoError(t, ps.Put(3, "learner:1", "learner:2", fixedInstanceID(3))) require.NoError(t, ps.Put(99, "stale:1", "stale:2", fixedInstanceID(99))) - m, err := NewMembership(ps, noopTransport{}, noopPool{}, 7, "self:1", "self:2", fixedInstanceID(7), logging.Testing()) + m, err := NewMembership(ps, noopTransport{}, noopPool{}, 7, "self:1", "self:2", fixedInstanceID(7), logging.Testing(), NoopClusterPolicyProvider) require.NoError(t, err) m.Start() diff --git a/internal/infra/membership/remove_node_blacklist_test.go b/internal/infra/membership/remove_node_blacklist_test.go index 8ec6056cc0..f37f14edb5 100644 --- a/internal/infra/membership/remove_node_blacklist_test.go +++ b/internal/infra/membership/remove_node_blacklist_test.go @@ -54,7 +54,7 @@ func TestWriteConfChange_RemoveNodeBlacklistsPeer(t *testing.T) { require.NoError(t, ps.Put(3, "pod-2:7777", "pod-2:8888", instanceID)) - m, err := NewMembership(ps, noopTransport{}, noopPool{}, testSelfNodeID, testSelfRaftAddr, testSelfServiceAddr, fixedInstanceID(0x42), logging.Testing()) + m, err := NewMembership(ps, noopTransport{}, noopPool{}, testSelfNodeID, testSelfRaftAddr, testSelfServiceAddr, fixedInstanceID(0x42), logging.Testing(), NoopClusterPolicyProvider) require.NoError(t, err) ccCtx, err := MarshalConfChangeContext(ConfChangeContext{InstanceID: instanceID}) @@ -95,7 +95,7 @@ func TestWriteConfChange_RemoveNodeWithoutIdentityFailsLoudly(t *testing.T) { instanceID := fixedInstanceID(0xAA) require.NoError(t, ps.Put(3, "pod-2:7777", "pod-2:8888", instanceID)) - m, err := NewMembership(ps, noopTransport{}, noopPool{}, testSelfNodeID, testSelfRaftAddr, testSelfServiceAddr, fixedInstanceID(0x42), logging.Testing()) + m, err := NewMembership(ps, noopTransport{}, noopPool{}, testSelfNodeID, testSelfRaftAddr, testSelfServiceAddr, fixedInstanceID(0x42), logging.Testing(), NoopClusterPolicyProvider) require.NoError(t, err) ccCtx, err := MarshalConfChangeContext(ConfChangeContext{ProposalID: "remove-3"}) require.NoError(t, err) @@ -133,7 +133,7 @@ func TestWriteConfChange_AddLearnerWithoutIdentityFailsLoudly(t *testing.T) { t.Parallel() ps, store := storesForBlacklistTest(t) - m, err := NewMembership(ps, noopTransport{}, noopPool{}, testSelfNodeID, testSelfRaftAddr, testSelfServiceAddr, fixedInstanceID(0x42), logging.Testing()) + m, err := NewMembership(ps, noopTransport{}, noopPool{}, testSelfNodeID, testSelfRaftAddr, testSelfServiceAddr, fixedInstanceID(0x42), logging.Testing(), NoopClusterPolicyProvider) require.NoError(t, err) cc := &raftpb.ConfChangeV2{ @@ -166,7 +166,7 @@ func TestUnregisterAndBlacklist_AtomicBatch(t *testing.T) { require.NoError(t, ps.Put(7, "pod-6:7777", "pod-6:8888", instanceID)) - m, err := NewMembership(ps, noopTransport{}, noopPool{}, testSelfNodeID, testSelfRaftAddr, testSelfServiceAddr, fixedInstanceID(0x42), logging.Testing()) + m, err := NewMembership(ps, noopTransport{}, noopPool{}, testSelfNodeID, testSelfRaftAddr, testSelfServiceAddr, fixedInstanceID(0x42), logging.Testing(), NoopClusterPolicyProvider) require.NoError(t, err) require.NoError(t, m.UnregisterAndBlacklist(7, instanceID, 42)) @@ -187,7 +187,7 @@ func TestUnregisterAndBlacklist_RejectsWrongInstanceIDLen(t *testing.T) { ps, _ := storesForBlacklistTest(t) - m, err := NewMembership(ps, noopTransport{}, noopPool{}, testSelfNodeID, testSelfRaftAddr, testSelfServiceAddr, fixedInstanceID(0x42), logging.Testing()) + m, err := NewMembership(ps, noopTransport{}, noopPool{}, testSelfNodeID, testSelfRaftAddr, testSelfServiceAddr, fixedInstanceID(0x42), logging.Testing(), NoopClusterPolicyProvider) require.NoError(t, err) require.Error(t, m.UnregisterAndBlacklist(7, []byte("short"), 0)) @@ -202,7 +202,7 @@ func TestIsRemoved_MatchesOnExactTuple(t *testing.T) { ps, _ := storesForBlacklistTest(t) blacklisted := fixedInstanceID(0xCC) - m, err := NewMembership(ps, noopTransport{}, noopPool{}, testSelfNodeID, testSelfRaftAddr, testSelfServiceAddr, fixedInstanceID(0x42), logging.Testing()) + m, err := NewMembership(ps, noopTransport{}, noopPool{}, testSelfNodeID, testSelfRaftAddr, testSelfServiceAddr, fixedInstanceID(0x42), logging.Testing(), NoopClusterPolicyProvider) require.NoError(t, err) require.NoError(t, m.UnregisterAndBlacklist(9, blacklisted, 100)) diff --git a/internal/infra/node/applier_async_storage_test.go b/internal/infra/node/applier_async_storage_test.go index 8cb5fbf273..9a263cb7c9 100644 --- a/internal/infra/node/applier_async_storage_test.go +++ b/internal/infra/node/applier_async_storage_test.go @@ -165,6 +165,7 @@ func TestApplierNewApplierRejectsNilSink(t *testing.T) { logger, nodeRegistry, nodeSnapshotter, pebbleStore, dal.NewSentinelFactory(pebbleStore, false), meterProvider, nil, state.NewSharedState(), newNoopNotifier(t), nil, "test-cluster", 0, func(*raftpb.Entry, *dal.WriteSession) error { return nil }, + state.NoopCapabilityPolicyCommitHandler, ) require.NoError(t, err) diff --git a/internal/infra/node/applier_test.go b/internal/infra/node/applier_test.go index d7427c9869..a41dcf3660 100644 --- a/internal/infra/node/applier_test.go +++ b/internal/infra/node/applier_test.go @@ -167,7 +167,7 @@ func newTestApplierSetupWithHandlers(t *testing.T, sink LocalResponses, notifier logger, nodeRegistry, nodeSnapshotter, pebbleStore, dal.NewSentinelFactory(pebbleStore, false), meterProvider, nil, sharedState, notifier, nil, "test-cluster", 0, handler, - ) + state.NoopCapabilityPolicyCommitHandler) require.NoError(t, err) recovery := state.NewRecovery(fsm, pebbleStore) diff --git a/internal/infra/node/delete_ledger_sentinel_recovery_test.go b/internal/infra/node/delete_ledger_sentinel_recovery_test.go index 6b6e919aaf..9911318a3a 100644 --- a/internal/infra/node/delete_ledger_sentinel_recovery_test.go +++ b/internal/infra/node/delete_ledger_sentinel_recovery_test.go @@ -173,7 +173,7 @@ func newSentinelApplier(t *testing.T, dir string, sentinelMode bool) (*testAppli registry := state.NewStateRegistry(c, attributes.New()) fsm, err := state.NewMachine(logger, registry, state.NewCacheSnapshotter(logger, registry, nil), s, dal.NewSentinelFactory(s, sentinelMode), provider, nil, state.NewSharedState(), newNoopNotifier(t), nil, - "", 0, func(*raftpb.Entry, *dal.WriteSession) error { return nil }) + "", 0, func(*raftpb.Entry, *dal.WriteSession) error { return nil }, state.NoopCapabilityPolicyCommitHandler) require.NoError(t, err) recovery := state.NewRecovery(fsm, s) require.NoError(t, recovery.RecoverState()) diff --git a/internal/infra/node/membership_helpers_test.go b/internal/infra/node/membership_helpers_test.go index e38aa6f99a..c350cbec96 100644 --- a/internal/infra/node/membership_helpers_test.go +++ b/internal/infra/node/membership_helpers_test.go @@ -47,6 +47,7 @@ func newTestMembership(t *testing.T) *membership.Membership { 42, "self:7777", "self:8888", []byte("self-instance-id"), logging.Testing(), + membership.NoopClusterPolicyProvider, ) require.NoError(t, err) m.Start() diff --git a/internal/infra/node/startup_membership_removal_test.go b/internal/infra/node/startup_membership_removal_test.go index ea7cb1ceed..8f42aedbe9 100644 --- a/internal/infra/node/startup_membership_removal_test.go +++ b/internal/infra/node/startup_membership_removal_test.go @@ -26,7 +26,7 @@ func TestNewNodeAcceptsReplayedRemovalBeforeConfStatePersistence(t *testing.T) { }) var err error m, err = membership.NewMembership(membership.NewPeerStore(setup.store), noopMemTransport{}, noopMemPool{}, - 1, "self:7777", "self:8888", []byte("self-instance-id"), logging.Testing()) + 1, "self:7777", "self:8888", []byte("self-instance-id"), logging.Testing(), membership.NoopClusterPolicyProvider) require.NoError(t, err) identity := []byte("peer-instance-id") require.NoError(t, m.Register(2, "peer:7777", "peer:8888", identity)) diff --git a/internal/infra/state/audit_replay.go b/internal/infra/state/audit_replay.go index 55b07f45ff..955c6fa69e 100644 --- a/internal/infra/state/audit_replay.go +++ b/internal/infra/state/audit_replay.go @@ -96,7 +96,7 @@ func NewAuditReplayer(logger logging.Logger, auditKey string) (*AuditReplayer, e nil, auditKey, 0, - func(_ *raftpb.Entry, _ *dal.WriteSession) error { return nil }, + func(_ *raftpb.Entry, _ *dal.WriteSession) error { return nil }, NoopCapabilityPolicyCommitHandler, ) if err != nil { _ = store.Close() diff --git a/internal/infra/state/checkpoint_pipeline_regression_test.go b/internal/infra/state/checkpoint_pipeline_regression_test.go index 15c2e3b5e2..824d08b73c 100644 --- a/internal/infra/state/checkpoint_pipeline_regression_test.go +++ b/internal/infra/state/checkpoint_pipeline_regression_test.go @@ -58,6 +58,7 @@ func TestPipelinedApplyWithCheckpointDoesNotDiverge(t *testing.T) { "test-cluster", 0, noopConfChangeHandler, + NoopCapabilityPolicyCommitHandler, ) require.NoError(t, err) require.NoError(t, NewRecovery(machine, dataStore).RecoverState()) diff --git a/internal/infra/state/machine.go b/internal/infra/state/machine.go index 50261af097..76dde17a16 100644 --- a/internal/infra/state/machine.go +++ b/internal/infra/state/machine.go @@ -163,6 +163,11 @@ type Machine struct { capabilityPolicyCommitHandler func(*commonpb.ClusterPolicy) } +// NoopCapabilityPolicyCommitHandler is an explicit dependency for callers +// that do not maintain an in-memory membership gate (for example replay and +// isolated FSM tests). Production bootstrap injects the membership publisher. +func NoopCapabilityPolicyCommitHandler(*commonpb.ClusterPolicy) {} + // NewMachine constructs the hot-path FSM. It composes pre-built sub-objects // (Registry, CacheSnapshotter) so the constructor reads no Pebble and keeps // the surface focused on hot-path concerns. The bootstrap is responsible for @@ -173,13 +178,9 @@ type Machine struct { // confChangeHandler is invoked from PrepareEntries for every // EntryConfChange* in the in-flight batch (see the field comment on // Machine). Must be non-nil. -func NewMachine(logger logging.Logger, registry *StateRegistry, cacheSnapshotter *CacheSnapshotter, fileArtifacts dal.QueryCheckpoints, sentinel dal.SentinelFactory, meterProvider metric.MeterProvider, ks *keystore.KeyStore, sharedState *SharedState, notifier Notifier, bloomFilters *bloom.FilterSet, auditKey string, numscriptCacheSize int, confChangeHandler func(entry *raftpb.Entry, session *dal.WriteSession) error, capabilityPolicyCommitHandlers ...func(*commonpb.ClusterPolicy)) (*Machine, error) { - var capabilityPolicyCommitHandler func(*commonpb.ClusterPolicy) - if len(capabilityPolicyCommitHandlers) > 1 { - return nil, errors.New("at most one capability policy commit handler is allowed") - } - if len(capabilityPolicyCommitHandlers) == 1 { - capabilityPolicyCommitHandler = capabilityPolicyCommitHandlers[0] +func NewMachine(logger logging.Logger, registry *StateRegistry, cacheSnapshotter *CacheSnapshotter, fileArtifacts dal.QueryCheckpoints, sentinel dal.SentinelFactory, meterProvider metric.MeterProvider, ks *keystore.KeyStore, sharedState *SharedState, notifier Notifier, bloomFilters *bloom.FilterSet, auditKey string, numscriptCacheSize int, confChangeHandler func(entry *raftpb.Entry, session *dal.WriteSession) error, capabilityPolicyCommitHandler func(*commonpb.ClusterPolicy)) (*Machine, error) { + if capabilityPolicyCommitHandler == nil { + return nil, errors.New("capability policy commit handler is required") } sentinelMode := sentinel.IsEnabled() diff --git a/internal/infra/state/machine_test.go b/internal/infra/state/machine_test.go index 9e0f24cdf1..f14cdda368 100644 --- a/internal/infra/state/machine_test.go +++ b/internal/infra/state/machine_test.go @@ -49,7 +49,7 @@ func newTestMachineWithThreshold(t *testing.T, generationThreshold uint64) (*Mac registry := NewStateRegistry(c, attrs) snapshotter := NewCacheSnapshotter(logger, registry, nil) - machine, err := NewMachine(logger, registry, snapshotter, dataStore, dal.NewSentinelFactory(dataStore, false), meterProvider, keystore.NewKeyStore(), NewSharedState(), newNoopNotifier(t), nil, "test-cluster", 0, noopConfChangeHandler) + machine, err := NewMachine(logger, registry, snapshotter, dataStore, dal.NewSentinelFactory(dataStore, false), meterProvider, keystore.NewKeyStore(), NewSharedState(), newNoopNotifier(t), nil, "test-cluster", 0, noopConfChangeHandler, NoopCapabilityPolicyCommitHandler) require.NoError(t, err) // NewMachine no longer performs the initial recoverState — the caller is @@ -113,7 +113,7 @@ func recoverMachineOnStore(t *testing.T, dataStore *dal.Store) *Machine { registry := NewStateRegistry(c, attributes.New()) snapshotter := NewCacheSnapshotter(logger, registry, nil) - m, err := NewMachine(logger, registry, snapshotter, dataStore, dal.NewSentinelFactory(dataStore, false), meterProvider, keystore.NewKeyStore(), NewSharedState(), newNoopNotifier(t), nil, "test-cluster", 0, noopConfChangeHandler) + m, err := NewMachine(logger, registry, snapshotter, dataStore, dal.NewSentinelFactory(dataStore, false), meterProvider, keystore.NewKeyStore(), NewSharedState(), newNoopNotifier(t), nil, "test-cluster", 0, noopConfChangeHandler, NoopCapabilityPolicyCommitHandler) require.NoError(t, err) require.NoError(t, NewRecovery(m, dataStore).RecoverState()) @@ -922,7 +922,7 @@ func TestNextLedgerIDRecovery(t *testing.T) { // Create first machine and 3 ledgers. reg1 := NewStateRegistry(c, attrs) snap1 := NewCacheSnapshotter(logger, reg1, nil) - machine1, err := NewMachine(logger, reg1, snap1, dataStore, dal.NewSentinelFactory(dataStore, false), meterProvider, keystore.NewKeyStore(), NewSharedState(), newNoopNotifier(t), nil, "test-cluster", 0, noopConfChangeHandler) + machine1, err := NewMachine(logger, reg1, snap1, dataStore, dal.NewSentinelFactory(dataStore, false), meterProvider, keystore.NewKeyStore(), NewSharedState(), newNoopNotifier(t), nil, "test-cluster", 0, noopConfChangeHandler, NoopCapabilityPolicyCommitHandler) require.NoError(t, err) require.NoError(t, NewRecovery(machine1, dataStore).RecoverState()) @@ -958,7 +958,7 @@ func TestNextLedgerIDRecovery(t *testing.T) { reg2 := NewStateRegistry(c2, attrs2) snap2 := NewCacheSnapshotter(logger, reg2, nil) - machine2, err := NewMachine(logger, reg2, snap2, dataStore, dal.NewSentinelFactory(dataStore, false), meterProvider, keystore.NewKeyStore(), NewSharedState(), newNoopNotifier(t), nil, "test-cluster", 0, noopConfChangeHandler) + machine2, err := NewMachine(logger, reg2, snap2, dataStore, dal.NewSentinelFactory(dataStore, false), meterProvider, keystore.NewKeyStore(), NewSharedState(), newNoopNotifier(t), nil, "test-cluster", 0, noopConfChangeHandler, NoopCapabilityPolicyCommitHandler) require.NoError(t, err) require.NoError(t, NewRecovery(machine2, dataStore).RecoverState()) @@ -1018,7 +1018,7 @@ func TestPrepareEntriesTraceLogPipeliningLag(t *testing.T) { registry := NewStateRegistry(c, attributes.New()) snapshotter := NewCacheSnapshotter(logger, registry, nil) - machine, err := NewMachine(logger, registry, snapshotter, dataStore, dal.NewSentinelFactory(dataStore, false), meterProvider, keystore.NewKeyStore(), NewSharedState(), newNoopNotifier(t), nil, "test-cluster", 0, noopConfChangeHandler) + machine, err := NewMachine(logger, registry, snapshotter, dataStore, dal.NewSentinelFactory(dataStore, false), meterProvider, keystore.NewKeyStore(), NewSharedState(), newNoopNotifier(t), nil, "test-cluster", 0, noopConfChangeHandler, NoopCapabilityPolicyCommitHandler) require.NoError(t, err) require.NoError(t, NewRecovery(machine, dataStore).RecoverState()) diff --git a/internal/infra/state/sentinel_tracer_isolation_test.go b/internal/infra/state/sentinel_tracer_isolation_test.go index 9f33f7b2df..fee8c57e15 100644 --- a/internal/infra/state/sentinel_tracer_isolation_test.go +++ b/internal/infra/state/sentinel_tracer_isolation_test.go @@ -49,6 +49,7 @@ func TestSentinelTracerIsolatedAcrossBatches(t *testing.T) { "test-cluster", 0, noopConfChangeHandler, + NoopCapabilityPolicyCommitHandler, ) require.NoError(t, err) require.NoError(t, NewRecovery(machine, dataStore).RecoverState()) diff --git a/internal/infra/state/signing_cascade_test.go b/internal/infra/state/signing_cascade_test.go index 2456e048a2..3ffe18ea92 100644 --- a/internal/infra/state/signing_cascade_test.go +++ b/internal/infra/state/signing_cascade_test.go @@ -257,6 +257,7 @@ func (r *signingBatchRunner) restart() *keystore.KeyStore { logger, registry, NewCacheSnapshotter(logger, registry, nil), r.store, dal.NewSentinelFactory(r.store, false), meterProvider, recovered, NewSharedState(), newNoopNotifier(r.t), nil, "test-cluster", 0, noopConfChangeHandler, + NoopCapabilityPolicyCommitHandler, ) require.NoError(r.t, err) require.NoError(r.t, NewRecovery(machine, r.store).RecoverState()) From 485b51315787e61e072a59a5bb00465822a6f2b7 Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Mon, 26 Oct 2026 13:41:47 +0100 Subject: [PATCH 051/127] cleanup: remove dead capability builder hook --- internal/bootstrap/module.go | 3 +-- internal/infra/plan/builder.go | 11 ----------- 2 files changed, 1 insertion(+), 13 deletions(-) diff --git a/internal/bootstrap/module.go b/internal/bootstrap/module.go index 1cff616f65..a3bfbaeb6e 100644 --- a/internal/bootstrap/module.go +++ b/internal/bootstrap/module.go @@ -289,8 +289,7 @@ func Module() fx.Option { }, newCommittedBloomFilterSet, func(cfg Config, n *node.Node, c *cache.Cache, attrs *attributes.Attributes, store *dal.Store, bloomFilters *bloom.FilterSet, logger logging.Logger) *plan.Builder { - return plan.NewBuilder(n.IndexTracker(), c, attrs, store, bloomFilters, logger, cfg.MaxExecutionPlanSize). - WithCapabilityMembers(n.CapabilityMembers) + return plan.NewBuilder(n.IndexTracker(), c, attrs, store, bloomFilters, logger, cfg.MaxExecutionPlanSize) }, fx.Annotate(func( cfg Config, diff --git a/internal/infra/plan/builder.go b/internal/infra/plan/builder.go index 53e796a461..6f65c2c963 100644 --- a/internal/infra/plan/builder.go +++ b/internal/infra/plan/builder.go @@ -12,7 +12,6 @@ import ( "github.com/formancehq/ledger/v3/internal/infra/attributes" "github.com/formancehq/ledger/v3/internal/infra/bloom" "github.com/formancehq/ledger/v3/internal/infra/cache" - "github.com/formancehq/ledger/v3/internal/infra/capabilities" "github.com/formancehq/ledger/v3/internal/infra/node" "github.com/formancehq/ledger/v3/internal/infra/preload" "github.com/formancehq/ledger/v3/internal/infra/state" @@ -52,7 +51,6 @@ type Builder struct { // front rather than paying for proportionally-large coverage slices // on every NewScope downstream. 0 disables the cap. maxPlanSize int - readMembers func() []capabilities.Member } // BuildResult carries the result of an optimistic Build call. @@ -138,15 +136,6 @@ func NewBuilder(tracker *node.IndexTracker, c *cache.Cache, attrs *attributes.At return b } -// WithCapabilityMembers supplies the committed membership evidence used by -// the final capability admission barrier. It remains optional for lightweight -// builders used by tests and non-Raft tooling. -func (b *Builder) WithCapabilityMembers(read func() []capabilities.Member) *Builder { - b.readMembers = read - - return b -} - // Loaders returns the shared preload.Loaders instance, allowing callers to // release tokens from a BuildResult on error paths. func (p *Builder) Loaders() *preload.Loaders { From 70e3005389dcab9d2b0257d5cec3b5d3b44b940a Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Mon, 26 Oct 2026 13:49:49 +0100 Subject: [PATCH 052/127] test: use deterministic refresh cancellation wait --- .../bootstrap/module_refresh_membership_test.go | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/internal/bootstrap/module_refresh_membership_test.go b/internal/bootstrap/module_refresh_membership_test.go index 8da93336cf..6c65b3ff15 100644 --- a/internal/bootstrap/module_refresh_membership_test.go +++ b/internal/bootstrap/module_refresh_membership_test.go @@ -98,20 +98,9 @@ func TestTryRefreshMembershipStopsRetryingWhenContextIsCanceled(t *testing.T) { cfg := refreshTestConfig(t, serveRefreshMembership(t, peer)) ctx, cancel := context.WithCancel(context.Background()) - started := make(chan struct{}) - go func() { - for { - if peer.attempts.Load() > 0 { - close(started) - return - } - time.Sleep(time.Millisecond) - } - }() - result := make(chan error, 1) go func() { result <- tryRefreshMembership(ctx, cfg, cfg.TLSConfig, logging.Testing()) }() - <-started + require.Eventually(t, func() bool { return peer.attempts.Load() > 0 }, time.Second, time.Millisecond) cancel() require.ErrorIs(t, <-result, context.Canceled) } From e592f1ca921f623c06817e1d4b7b5a7b7304e90c Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Mon, 26 Oct 2026 13:57:21 +0100 Subject: [PATCH 053/127] fix: remove dead membership refresh error state --- internal/bootstrap/module.go | 7 ------- 1 file changed, 7 deletions(-) diff --git a/internal/bootstrap/module.go b/internal/bootstrap/module.go index a3bfbaeb6e..0e05fb2fb0 100644 --- a/internal/bootstrap/module.go +++ b/internal/bootstrap/module.go @@ -1655,11 +1655,9 @@ func tryRefreshMembership(ctx context.Context, cfg Config, tlsCfg TLSConfig, log backoff := 500 * time.Millisecond for { - var lastErr error for _, peer := range cfg.RaftConfig.Peers { conn, dialErr := ggrpc.NewClient(peer.Address, dialOpts...) if dialErr != nil { - lastErr = dialErr logger.WithFields(map[string]any{"peer": peer.ID, "address": peer.Address, "error": dialErr}).Infof("Failed to dial peer for membership refresh") continue @@ -1673,15 +1671,10 @@ func tryRefreshMembership(ctx context.Context, cfg Config, tlsCfg TLSConfig, log return nil } - lastErr = callErr if !isRetryableMembershipRefreshError(callErr) { return fmt.Errorf("failed to refresh membership via peer %d (%s): %w", peer.ID, peer.Address, callErr) } } - - if lastErr == nil { - lastErr = errors.New("no configured peer available for membership refresh") - } select { case <-ctx.Done(): return ctx.Err() From f44541ec77831a44bf94e6fa273567cac19d06c4 Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Mon, 26 Oct 2026 14:05:20 +0100 Subject: [PATCH 054/127] chore: keep generated formatting clean --- internal/bootstrap/module.go | 2 +- internal/bootstrap/module_refresh_membership_test.go | 1 + internal/infra/node/proposal.go | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/internal/bootstrap/module.go b/internal/bootstrap/module.go index 0e05fb2fb0..4ab70e8323 100644 --- a/internal/bootstrap/module.go +++ b/internal/bootstrap/module.go @@ -1878,7 +1878,7 @@ func reconcileClusterPolicy(ctx context.Context, admission ctrl.Admission, store for _, id := range ready { if enabled, exists := active[uint32(id)]; exists { if !enabled { - for _, activation := range desired.CapabilityActivations { + for _, activation := range desired.GetCapabilityActivations() { if activation != nil && activation.GetId() == uint32(id) { activation.Active = true break diff --git a/internal/bootstrap/module_refresh_membership_test.go b/internal/bootstrap/module_refresh_membership_test.go index 6c65b3ff15..5033a0b24c 100644 --- a/internal/bootstrap/module_refresh_membership_test.go +++ b/internal/bootstrap/module_refresh_membership_test.go @@ -39,6 +39,7 @@ func TestIsRetryableMembershipRefreshError(t *testing.T) { type retryingRefreshServer struct { clusterbootstrappb.UnimplementedClusterBootstrapServiceServer + attempts atomic.Int32 firstErr error } diff --git a/internal/infra/node/proposal.go b/internal/infra/node/proposal.go index 0b67fb5e20..51b5827eaf 100644 --- a/internal/infra/node/proposal.go +++ b/internal/infra/node/proposal.go @@ -71,6 +71,7 @@ func NewProposal(commandID uint64, command *raftcmdpb.Proposal) (*Proposal, erro } } } + return proposal, nil } From ad883dc472f3976f04ef16596b3e7f46d0984a0e Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Mon, 26 Oct 2026 14:12:33 +0100 Subject: [PATCH 055/127] fix: preserve refresh cancellation error --- internal/bootstrap/module.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/internal/bootstrap/module.go b/internal/bootstrap/module.go index 4ab70e8323..d9cf679d91 100644 --- a/internal/bootstrap/module.go +++ b/internal/bootstrap/module.go @@ -1671,6 +1671,10 @@ func tryRefreshMembership(ctx context.Context, cfg Config, tlsCfg TLSConfig, log return nil } + if ctx.Err() != nil { + return ctx.Err() + } + if !isRetryableMembershipRefreshError(callErr) { return fmt.Errorf("failed to refresh membership via peer %d (%s): %w", peer.ID, peer.Address, callErr) } From 5d7105a8859fe9feea9f5a97c6a8a5fc16695c9b Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Mon, 26 Oct 2026 14:17:21 +0100 Subject: [PATCH 056/127] chore: keep generated policy formatting clean --- internal/bootstrap/module.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/bootstrap/module.go b/internal/bootstrap/module.go index d9cf679d91..fd0bddb138 100644 --- a/internal/bootstrap/module.go +++ b/internal/bootstrap/module.go @@ -1885,6 +1885,7 @@ func reconcileClusterPolicy(ctx context.Context, admission ctrl.Admission, store for _, activation := range desired.GetCapabilityActivations() { if activation != nil && activation.GetId() == uint32(id) { activation.Active = true + break } } From aad3980afa53078c24520d9c409fea7590b6eaaa Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Mon, 26 Oct 2026 14:44:59 +0100 Subject: [PATCH 057/127] fix: enforce membership refresh startup errors --- .../bootstrap/capability_admission_test.go | 37 +++++ internal/bootstrap/module.go | 127 ++++++++++++------ .../module_refresh_membership_test.go | 13 ++ 3 files changed, 134 insertions(+), 43 deletions(-) create mode 100644 internal/bootstrap/capability_admission_test.go diff --git a/internal/bootstrap/capability_admission_test.go b/internal/bootstrap/capability_admission_test.go new file mode 100644 index 0000000000..574107261a --- /dev/null +++ b/internal/bootstrap/capability_admission_test.go @@ -0,0 +1,37 @@ +package bootstrap + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/formancehq/ledger/v3/internal/infra/capabilities" +) + +func TestValidateCapabilityActivationEvidenceRejectsIncompleteMembership(t *testing.T) { + t.Parallel() + + err := validateCapabilityActivationEvidence( + []capabilities.CapabilityID{7}, + []capabilities.CapabilityID{7}, + []capabilities.Member{{NodeID: 1, Role: capabilities.Voter}}, + ) + + require.ErrorIs(t, err, capabilities.ErrCapabilityUnsupported) +} + +func TestValidateCapabilityActivationEvidenceAcceptsNoActivation(t *testing.T) { + t.Parallel() + + require.NoError(t, validateCapabilityActivationEvidence(nil, nil, nil)) +} + +func TestValidateCapabilityActivationEvidenceAcceptsConvergedMembership(t *testing.T) { + t.Parallel() + + require.NoError(t, validateCapabilityActivationEvidence( + []capabilities.CapabilityID{7}, + []capabilities.CapabilityID{7}, + []capabilities.Member{{NodeID: 1, Role: capabilities.Voter, Capabilities: []capabilities.CapabilityID{7}}}, + )) +} diff --git a/internal/bootstrap/module.go b/internal/bootstrap/module.go index fd0bddb138..cb0045b391 100644 --- a/internal/bootstrap/module.go +++ b/internal/bootstrap/module.go @@ -404,16 +404,8 @@ func Module() fx.Option { // typed candidate from the final command, so this check does not // decode the serialized proposal or trust caller-supplied metadata. activePolicyCapabilities := proposal.ActivePolicyCapabilities() - if len(activePolicyCapabilities) > 0 { - ready, readyErr := capabilities.Ready(capabilities.SupportedIDs(), raftNode.CapabilityMembers()) - if readyErr != nil { - return fmt.Errorf("validating capability activation evidence: %w", readyErr) - } - for _, required := range activePolicyCapabilities { - if !slices.Contains(ready, required) { - return fmt.Errorf("%w: capability %d is not acknowledged by every current member", capabilities.ErrCapabilityUnsupported, required) - } - } + if err := validateCapabilityActivationEvidence(capabilities.SupportedIDs(), activePolicyCapabilities, raftNode.CapabilityMembers()); err != nil { + return err } state := capabilities.FromPolicy(policy) @@ -1207,6 +1199,24 @@ func Module() fx.Option { ) } +func validateCapabilityActivationEvidence(supported, active []capabilities.CapabilityID, members []capabilities.Member) error { + if len(active) == 0 { + return nil + } + + ready, err := capabilities.Ready(supported, members) + if err != nil { + return fmt.Errorf("validating capability activation evidence: %w", err) + } + for _, required := range active { + if !slices.Contains(ready, required) { + return fmt.Errorf("%w: capability %d is not acknowledged by every current member", capabilities.ErrCapabilityUnsupported, required) + } + } + + return nil +} + // JoinAuthError is returned when a joining node is rejected by the target // cluster's inter-node RaftServer with codes.Unauthenticated. This is always // a cluster-secret misconfiguration (EN-1080): the join flow carries no user @@ -1407,22 +1417,34 @@ func membershipRefreshHook(cfg Config, logger logging.Logger) fx.Hook { var cancel context.CancelFunc return fx.Hook{ - OnStart: func(context.Context) error { + OnStart: func(ctx context.Context) error { logger.WithFields(map[string]any{ "nodeID": cfg.RaftConfig.NodeID, "raftAddress": cfg.RaftConfig.AdvertiseAddr, "serviceAddress": cfg.ServiceAdvertiseAddr(), }).Infof("Restart mode: refreshing membership capabilities") + // Perform one synchronous attempt before the node starts serving. A + // permanent rejection (for example a removed incarnation or invalid + // identity) must abort startup rather than letting stale WAL membership + // evidence escape into the cluster. Only the rolling-upgrade cases that + // are explicitly classified as transient are handed to the background + // retry loop. + err := tryRefreshMembershipOnce(ctx, cfg, cfg.TLSConfig, logger) + if err == nil { + return nil + } + if !errors.Is(err, errMembershipRefreshTransient) { + return err + } + refreshCtx, refreshCancel := context.WithCancel(context.Background()) cancel = refreshCancel go func() { if err := tryRefreshMembership(refreshCtx, cfg, cfg.TLSConfig, logger); err != nil { - // TODO(rc.3): replace this compatibility fallback with explicit - // startup error handling once every supported peer implements - // RefreshMembership. The leader-side reconciler below keeps - // polling membership evidence and will arm capabilities only - // after all current members converge. + // TODO(rc.3): remove this compatibility fallback once every + // supported peer implements RefreshMembership. The leader-side + // reconciler keeps retrying transient evidence until convergence. logger.WithFields(map[string]any{"error": err}).Errorf("Membership capability refresh failed; continuing with conservative evidence") } }() @@ -1627,6 +1649,25 @@ func tryAddLearner(ctx context.Context, cfg Config, tlsCfg TLSConfig, logger log // tryAddLearner: a node with a populated WAL already owns Raft progress and // must never be admitted through the empty-WAL learner path. func tryRefreshMembership(ctx context.Context, cfg Config, tlsCfg TLSConfig, logger logging.Logger) error { + backoff := 500 * time.Millisecond + for { + err := tryRefreshMembershipOnce(ctx, cfg, tlsCfg, logger) + if err == nil || !errors.Is(err, errMembershipRefreshTransient) { + return err + } + + select { + case <-ctx.Done(): + return ctx.Err() + case <-time.After(backoff): + } + backoff = min(backoff*2, 5*time.Second) + } +} + +var errMembershipRefreshTransient = errors.New("membership refresh temporarily unavailable") + +func tryRefreshMembershipOnce(ctx context.Context, cfg Config, tlsCfg TLSConfig, logger logging.Logger) error { instanceID, err := wal.EnsureInstanceID(cfg.RaftConfig.WalDir) if err != nil { return fmt.Errorf("ensuring instance id for membership refresh: %w", err) @@ -1653,39 +1694,39 @@ func tryRefreshMembership(ctx context.Context, cfg Config, tlsCfg TLSConfig, log ctx = metadata.AppendToOutgoingContext(ctx, node.MetadataKeyClusterID, cfg.ClusterID) } - backoff := 500 * time.Millisecond - for { - for _, peer := range cfg.RaftConfig.Peers { - conn, dialErr := ggrpc.NewClient(peer.Address, dialOpts...) - if dialErr != nil { - logger.WithFields(map[string]any{"peer": peer.ID, "address": peer.Address, "error": dialErr}).Infof("Failed to dial peer for membership refresh") - - continue - } - - _, callErr := clusterbootstrappb.NewClusterBootstrapServiceClient(conn).RefreshMembership(ctx, req) - _ = conn.Close() - if callErr == nil { - logger.WithFields(map[string]any{"peer": peer.ID}).Infof("Successfully refreshed membership capabilities") - - return nil - } + var lastTransient error + for _, peer := range cfg.RaftConfig.Peers { + conn, dialErr := ggrpc.NewClient(peer.Address, dialOpts...) + if dialErr != nil { + logger.WithFields(map[string]any{"peer": peer.ID, "address": peer.Address, "error": dialErr}).Infof("Failed to dial peer for membership refresh") + lastTransient = dialErr + continue + } - if ctx.Err() != nil { - return ctx.Err() - } + _, callErr := clusterbootstrappb.NewClusterBootstrapServiceClient(conn).RefreshMembership(ctx, req) + _ = conn.Close() + if callErr == nil { + logger.WithFields(map[string]any{"peer": peer.ID}).Infof("Successfully refreshed membership capabilities") - if !isRetryableMembershipRefreshError(callErr) { - return fmt.Errorf("failed to refresh membership via peer %d (%s): %w", peer.ID, peer.Address, callErr) - } + return nil } - select { - case <-ctx.Done(): + + if ctx.Err() != nil { return ctx.Err() - case <-time.After(backoff): } - backoff = min(backoff*2, 5*time.Second) + + if !isRetryableMembershipRefreshError(callErr) { + return fmt.Errorf("failed to refresh membership via peer %d (%s): %w", peer.ID, peer.Address, callErr) + } + lastTransient = callErr + } + if err := ctx.Err(); err != nil { + return err + } + if lastTransient != nil { + return fmt.Errorf("%w: %v", errMembershipRefreshTransient, lastTransient) } + return errMembershipRefreshTransient } func isRetryableMembershipRefreshError(err error) bool { diff --git a/internal/bootstrap/module_refresh_membership_test.go b/internal/bootstrap/module_refresh_membership_test.go index 5033a0b24c..bbfc7a56bb 100644 --- a/internal/bootstrap/module_refresh_membership_test.go +++ b/internal/bootstrap/module_refresh_membership_test.go @@ -105,3 +105,16 @@ func TestTryRefreshMembershipStopsRetryingWhenContextIsCanceled(t *testing.T) { cancel() require.ErrorIs(t, <-result, context.Canceled) } + +func TestTryRefreshMembershipPropagatesPermanentRejection(t *testing.T) { + t.Parallel() + + peer := &retryingRefreshServer{firstErr: status.Error(codes.FailedPrecondition, "removed incarnation")} + cfg := refreshTestConfig(t, serveRefreshMembership(t, peer)) + + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + t.Cleanup(cancel) + err := tryRefreshMembership(ctx, cfg, cfg.TLSConfig, logging.Testing()) + require.ErrorContains(t, err, "removed incarnation") + require.Equal(t, int32(1), peer.attempts.Load(), "permanent rejection must not be retried") +} From 41cc2bda44f67280a412b26b5208b7cd4cccd9d1 Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Mon, 26 Oct 2026 14:59:10 +0100 Subject: [PATCH 058/127] chore: record generated membership refresh formatting --- internal/bootstrap/module.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/internal/bootstrap/module.go b/internal/bootstrap/module.go index cb0045b391..008d3e331e 100644 --- a/internal/bootstrap/module.go +++ b/internal/bootstrap/module.go @@ -1700,6 +1700,7 @@ func tryRefreshMembershipOnce(ctx context.Context, cfg Config, tlsCfg TLSConfig, if dialErr != nil { logger.WithFields(map[string]any{"peer": peer.ID, "address": peer.Address, "error": dialErr}).Infof("Failed to dial peer for membership refresh") lastTransient = dialErr + continue } @@ -1724,7 +1725,7 @@ func tryRefreshMembershipOnce(ctx context.Context, cfg Config, tlsCfg TLSConfig, return err } if lastTransient != nil { - return fmt.Errorf("%w: %v", errMembershipRefreshTransient, lastTransient) + return fmt.Errorf("%w: %w", errMembershipRefreshTransient, lastTransient) } return errMembershipRefreshTransient } From 68bce13005b43cc4677b20bba036530ff938bb8b Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Mon, 26 Oct 2026 15:03:54 +0100 Subject: [PATCH 059/127] Revert "refactor(cli): remove manual learner registration command" This reverts commit ded7b765df54ec8bf6a051da5c8ee2448eb2392a. --- cmd/ledgerctl/cluster/add_learner.go | 95 +++++++++++++++++++++++ cmd/ledgerctl/cluster/add_learner_test.go | 51 ++++++++++++ cmd/ledgerctl/cluster/command.go | 2 + docs/ops/cli.md | 70 +++++++++++++++++ docs/ops/cluster-operations.md | 2 +- 5 files changed, 219 insertions(+), 1 deletion(-) create mode 100644 cmd/ledgerctl/cluster/add_learner.go create mode 100644 cmd/ledgerctl/cluster/add_learner_test.go diff --git a/cmd/ledgerctl/cluster/add_learner.go b/cmd/ledgerctl/cluster/add_learner.go new file mode 100644 index 0000000000..95230766cc --- /dev/null +++ b/cmd/ledgerctl/cluster/add_learner.go @@ -0,0 +1,95 @@ +package cluster + +import ( + "encoding/hex" + "errors" + "fmt" + "strconv" + + "github.com/pterm/pterm" + "github.com/spf13/cobra" + + "github.com/formancehq/ledger/v3/cmd/ledgerctl/cmdutil" + "github.com/formancehq/ledger/v3/internal/infra/membership" + "github.com/formancehq/ledger/v3/internal/proto/clusterpb" +) + +// NewAddLearnerCommand creates the cluster add-learner command. +func NewAddLearnerCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "add-learner ", + Short: "Add a non-voting learner node to the cluster", + Long: "Add a learner (non-voting) node to the Raft cluster. instance-id is the 32-character hexadecimal encoding of the target's persisted INSTANCE_ID file. The request is forwarded to the leader.", + Args: cobra.ExactArgs(4), + ValidArgsFunction: cobra.NoFileCompletions, + RunE: runAddLearner, + } + + cmd.Flags().Duration("timeout", cmdutil.DefaultTimeout, "Request timeout") + cmd.Flags().StringSlice("capability", nil, "Capability ID supported by the learner binary (repeatable)") + + return cmd +} + +func runAddLearner(cmd *cobra.Command, args []string) error { + nodeID, err := strconv.ParseUint(args[0], 10, 64) + if err != nil { + return fmt.Errorf("invalid node ID %q: %w", args[0], err) + } + + if nodeID == 0 { + return errors.New("node ID must be non-zero") + } + + raftAddress := args[1] + serviceAddress := args[2] + instanceID, err := hex.DecodeString(args[3]) + if err != nil { + return fmt.Errorf("invalid instance ID %q: expected hexadecimal: %w", args[3], err) + } + if err := membership.ValidateInstanceID(instanceID); err != nil { + return fmt.Errorf("invalid instance ID %q: %w", args[3], err) + } + + client, conn, err := cmdutil.GetClusterClient(cmd) + if err != nil { + return err + } + + defer func() { _ = conn.Close() }() + + ctx, cancel := cmdutil.GetContext(cmd) + defer cancel() + + capabilityValues, err := cmd.Flags().GetStringSlice("capability") + if err != nil { + return fmt.Errorf("invalid capability: %w", err) + } + capabilities := make([]uint32, len(capabilityValues)) + for i, value := range capabilityValues { + parsed, parseErr := strconv.ParseUint(value, 10, 32) + if parseErr != nil { + return fmt.Errorf("invalid capability %q: %w", value, parseErr) + } + capabilities[i] = uint32(parsed) + } + + _, err = client.AddLearner(ctx, &clusterpb.AddLearnerRequest{ + NodeId: nodeID, + RaftAddress: raftAddress, + ServiceAddress: serviceAddress, + InstanceId: instanceID, + Capabilities: capabilities, + }) + if err != nil { + return cmdutil.FormatGRPCError("failed to add learner", err) + } + + pterm.Success.Printfln("Learner node %s added to cluster (raft=%s, service=%s)", + pterm.Green(strconv.FormatUint(nodeID, 10)), + pterm.Cyan(raftAddress), + pterm.Cyan(serviceAddress), + ) + + return nil +} diff --git a/cmd/ledgerctl/cluster/add_learner_test.go b/cmd/ledgerctl/cluster/add_learner_test.go new file mode 100644 index 0000000000..688d843a76 --- /dev/null +++ b/cmd/ledgerctl/cluster/add_learner_test.go @@ -0,0 +1,51 @@ +package cluster + +import ( + "io" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestAddLearnerCommandRequiresInstanceID(t *testing.T) { + t.Parallel() + + cmd := NewAddLearnerCommand() + cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) + cmd.SilenceUsage = true + cmd.SilenceErrors = true + cmd.SetArgs([]string{"1", "node-1:7777", "node-1:8888"}) + + require.ErrorContains(t, cmd.Execute(), "accepts 4 arg") +} + +func TestAddLearnerCommandRejectsInvalidInstanceID(t *testing.T) { + t.Parallel() + + t.Run("not hexadecimal", func(t *testing.T) { + t.Parallel() + + cmd := NewAddLearnerCommand() + cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) + cmd.SilenceUsage = true + cmd.SilenceErrors = true + cmd.SetArgs([]string{"1", "node-1:7777", "node-1:8888", "not-hex"}) + + require.ErrorContains(t, cmd.Execute(), "expected hexadecimal") + }) + + t.Run("wrong decoded length", func(t *testing.T) { + t.Parallel() + + cmd := NewAddLearnerCommand() + cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) + cmd.SilenceUsage = true + cmd.SilenceErrors = true + cmd.SetArgs([]string{"1", "node-1:7777", "node-1:8888", "0123"}) + + require.ErrorContains(t, cmd.Execute(), "instance_id must be 16 bytes") + }) +} diff --git a/cmd/ledgerctl/cluster/command.go b/cmd/ledgerctl/cluster/command.go index 6fa06d7bd7..9dba5045b6 100644 --- a/cmd/ledgerctl/cluster/command.go +++ b/cmd/ledgerctl/cluster/command.go @@ -15,6 +15,8 @@ func NewCommand() *cobra.Command { cmd.AddCommand(NewWatchCommand()) cmd.AddCommand(NewDiskUsageCommand()) cmd.AddCommand(NewTransferLeaderCommand()) + cmd.AddCommand(NewAddLearnerCommand()) + cmd.AddCommand(NewPromoteLearnerCommand()) cmd.AddCommand(NewRemoveNodeCommand()) cmd.AddCommand(NewMaintenanceCommand()) cmd.AddCommand(NewBeginDrainCommand()) diff --git a/docs/ops/cli.md b/docs/ops/cli.md index ccfd84f9ab..df74376a24 100644 --- a/docs/ops/cli.md +++ b/docs/ops/cli.md @@ -2937,6 +2937,76 @@ ledgerctl cluster transfer-leader 2 ledgerctl cluster transfer-leader 3 --timeout 5s ``` +#### cluster add-learner + +Add a non-voting (learner) node to the Raft cluster. The request is forwarded to the current leader. + +```bash +ledgerctl cluster add-learner [flags] +``` + +**Flags:** + +| Flag | Default | Description | +|------|---------|-------------| +| `--timeout` | `10s` | Request timeout | + +**Behavior:** +- The request is forwarded to the current leader if sent to a follower +- `instance-id` is the 32-character hexadecimal encoding of the target node's 16-byte persisted `INSTANCE_ID` file +- An existing member with replicated progress rejects a different identity with `FailedPrecondition`; retrying the same active identity returns `AlreadyExists`. With no replicated progress, a different identity refreshes the registration. +- For stale progress, retire the old instance, remove its membership, then retry. If quorum is unavailable, use the leader-only `cluster remove-node --force` procedure in [Cluster operations](./cluster-operations.md), ensuring the retired instance cannot rejoin. +- The leader proposes a ConfChange to add the node as a learner (non-voting member) +- Once committed, all nodes add the learner to their transport and service pool +- The learner receives log entries and snapshots but cannot vote or become leader + +**Example:** + +```bash +# Read the INSTANCE_ID from the target node's configured --wal-dir (default: ./wal). +# When running under the Formance Operator, the mount path is /data/raft by default. +WAL_DIR=${WAL_DIR:-./wal} +INSTANCE_ID_HEX=$(od -An -tx1 "$WAL_DIR/INSTANCE_ID" | tr -d ' \n') + +# Add node 4 as a learner +ledgerctl cluster add-learner 4 node-4:7777 node-4:8888 "$INSTANCE_ID_HEX" + +# Add node 5 as a learner - read its own WAL directory for its unique INSTANCE_ID. +# Reusing node-4 identity here would register node-5 under the wrong incarnation. +NODE5_WAL_DIR=${NODE5_WAL_DIR:-./wal} +NODE5_INSTANCE_ID_HEX=$(od -An -tx1 "$NODE5_WAL_DIR/INSTANCE_ID" | tr -d ' \n') +ledgerctl cluster add-learner 5 node-5:7777 node-5:8888 "$NODE5_INSTANCE_ID_HEX" --timeout 30s +``` + +#### cluster promote-learner + +Promote a learner (non-voting) node to a full voter in the Raft cluster. The request is forwarded to the leader. + +```bash +ledgerctl cluster promote-learner [flags] +``` + +**Flags:** + +| Flag | Default | Description | +|------|---------|-------------| +| `--timeout` | `30s` | Request timeout | + +**Behavior:** +- The request is forwarded to the current leader if sent to a follower +- The leader proposes a ConfChange to promote the learner to a voter +- Once committed, the node participates in elections and can become leader + +**Example:** + +```bash +# Promote learner node 4 to voter +ledgerctl cluster promote-learner 4 + +# Promote with custom timeout +ledgerctl cluster promote-learner 5 --timeout 60s +``` + #### cluster remove-node Remove a node (voter or learner) from the Raft cluster. The request is forwarded to the leader. Cannot remove the leader itself; transfer leadership first. diff --git a/docs/ops/cluster-operations.md b/docs/ops/cluster-operations.md index 108c41f00a..bf73d97768 100644 --- a/docs/ops/cluster-operations.md +++ b/docs/ops/cluster-operations.md @@ -722,6 +722,6 @@ After the reset, the preloader falls back to Pebble reads (0xF1 zone) for all ca - [Data Flows](../technical/architecture/data-flows.md) - Spool mechanics, entry processing - [Spool](../technical/architecture/subsystems/storage/spool.md) - Spool technical implementation - [Deployment](./deployment.md) - Helm chart configuration, CLI flags -- [CLI Reference](./cli.md) - cluster commands +- [CLI Reference](./cli.md) - `cluster add-learner`, `cluster promote-learner` commands - [Memory Management](./memory.md) - Cache rotation threshold memory impact - [Availability and Disaster Recovery](./availability-and-recovery.md) - Failure-domain placement and quorum-loss recovery From f4f929f8c06fc97c6b61eb1208dab95adf211db0 Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Mon, 26 Oct 2026 16:31:20 +0100 Subject: [PATCH 060/127] test: update membership fixtures for policy provider --- internal/infra/node/historical_removal_lifecycle_test.go | 2 +- internal/infra/node/historical_removal_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/infra/node/historical_removal_lifecycle_test.go b/internal/infra/node/historical_removal_lifecycle_test.go index 1c581818d6..baff1d3af3 100644 --- a/internal/infra/node/historical_removal_lifecycle_test.go +++ b/internal/infra/node/historical_removal_lifecycle_test.go @@ -25,7 +25,7 @@ func newHistoricalRemovalLifecycleSetup(t *testing.T) (*Node, *testApplierSetup) }) var err error m, err = membership.NewMembership(membership.NewPeerStore(setup.store), noopMemTransport{}, noopMemPool{}, - 1, "self:7777", "self:8888", []byte("self-instance-id"), logging.Testing()) + 1, "self:7777", "self:8888", []byte("self-instance-id"), logging.Testing(), membership.NoopClusterPolicyProvider) require.NoError(t, err) n := &Node{fsm: setup.fsm, membership: m, logger: logging.Testing(), runDone: make(chan struct{})} t.Cleanup(func() { close(n.runDone) }) diff --git a/internal/infra/node/historical_removal_test.go b/internal/infra/node/historical_removal_test.go index 395b5d3af7..adda7ad0e5 100644 --- a/internal/infra/node/historical_removal_test.go +++ b/internal/infra/node/historical_removal_test.go @@ -37,7 +37,7 @@ func TestHistoricalRemovalFollowerReplay(t *testing.T) { }) var err error m, err = membership.NewMembership(membership.NewPeerStore(setup.store), noopMemTransport{}, noopMemPool{}, - 3, "self:7777", "self:8888", []byte("self-instance-id"), logging.Testing()) + 3, "self:7777", "self:8888", []byte("self-instance-id"), logging.Testing(), membership.NoopClusterPolicyProvider) require.NoError(t, err) oldIdentity := []byte("old-instance-id-") newIdentity := []byte("new-instance-id-") From 7992edb9d10a751365ec5bb62e02589f0504a271 Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Mon, 26 Oct 2026 16:33:25 +0100 Subject: [PATCH 061/127] Reapply "refactor(cli): remove manual learner registration command" This reverts commit 43ee22fe1b65114bf736823e0adea7aae75c1da2. --- cmd/ledgerctl/cluster/add_learner.go | 95 ----------------------- cmd/ledgerctl/cluster/add_learner_test.go | 51 ------------ cmd/ledgerctl/cluster/command.go | 1 - docs/ops/cli.md | 41 ---------- docs/ops/cluster-operations.md | 2 +- 5 files changed, 1 insertion(+), 189 deletions(-) delete mode 100644 cmd/ledgerctl/cluster/add_learner.go delete mode 100644 cmd/ledgerctl/cluster/add_learner_test.go diff --git a/cmd/ledgerctl/cluster/add_learner.go b/cmd/ledgerctl/cluster/add_learner.go deleted file mode 100644 index 95230766cc..0000000000 --- a/cmd/ledgerctl/cluster/add_learner.go +++ /dev/null @@ -1,95 +0,0 @@ -package cluster - -import ( - "encoding/hex" - "errors" - "fmt" - "strconv" - - "github.com/pterm/pterm" - "github.com/spf13/cobra" - - "github.com/formancehq/ledger/v3/cmd/ledgerctl/cmdutil" - "github.com/formancehq/ledger/v3/internal/infra/membership" - "github.com/formancehq/ledger/v3/internal/proto/clusterpb" -) - -// NewAddLearnerCommand creates the cluster add-learner command. -func NewAddLearnerCommand() *cobra.Command { - cmd := &cobra.Command{ - Use: "add-learner ", - Short: "Add a non-voting learner node to the cluster", - Long: "Add a learner (non-voting) node to the Raft cluster. instance-id is the 32-character hexadecimal encoding of the target's persisted INSTANCE_ID file. The request is forwarded to the leader.", - Args: cobra.ExactArgs(4), - ValidArgsFunction: cobra.NoFileCompletions, - RunE: runAddLearner, - } - - cmd.Flags().Duration("timeout", cmdutil.DefaultTimeout, "Request timeout") - cmd.Flags().StringSlice("capability", nil, "Capability ID supported by the learner binary (repeatable)") - - return cmd -} - -func runAddLearner(cmd *cobra.Command, args []string) error { - nodeID, err := strconv.ParseUint(args[0], 10, 64) - if err != nil { - return fmt.Errorf("invalid node ID %q: %w", args[0], err) - } - - if nodeID == 0 { - return errors.New("node ID must be non-zero") - } - - raftAddress := args[1] - serviceAddress := args[2] - instanceID, err := hex.DecodeString(args[3]) - if err != nil { - return fmt.Errorf("invalid instance ID %q: expected hexadecimal: %w", args[3], err) - } - if err := membership.ValidateInstanceID(instanceID); err != nil { - return fmt.Errorf("invalid instance ID %q: %w", args[3], err) - } - - client, conn, err := cmdutil.GetClusterClient(cmd) - if err != nil { - return err - } - - defer func() { _ = conn.Close() }() - - ctx, cancel := cmdutil.GetContext(cmd) - defer cancel() - - capabilityValues, err := cmd.Flags().GetStringSlice("capability") - if err != nil { - return fmt.Errorf("invalid capability: %w", err) - } - capabilities := make([]uint32, len(capabilityValues)) - for i, value := range capabilityValues { - parsed, parseErr := strconv.ParseUint(value, 10, 32) - if parseErr != nil { - return fmt.Errorf("invalid capability %q: %w", value, parseErr) - } - capabilities[i] = uint32(parsed) - } - - _, err = client.AddLearner(ctx, &clusterpb.AddLearnerRequest{ - NodeId: nodeID, - RaftAddress: raftAddress, - ServiceAddress: serviceAddress, - InstanceId: instanceID, - Capabilities: capabilities, - }) - if err != nil { - return cmdutil.FormatGRPCError("failed to add learner", err) - } - - pterm.Success.Printfln("Learner node %s added to cluster (raft=%s, service=%s)", - pterm.Green(strconv.FormatUint(nodeID, 10)), - pterm.Cyan(raftAddress), - pterm.Cyan(serviceAddress), - ) - - return nil -} diff --git a/cmd/ledgerctl/cluster/add_learner_test.go b/cmd/ledgerctl/cluster/add_learner_test.go deleted file mode 100644 index 688d843a76..0000000000 --- a/cmd/ledgerctl/cluster/add_learner_test.go +++ /dev/null @@ -1,51 +0,0 @@ -package cluster - -import ( - "io" - "testing" - - "github.com/stretchr/testify/require" -) - -func TestAddLearnerCommandRequiresInstanceID(t *testing.T) { - t.Parallel() - - cmd := NewAddLearnerCommand() - cmd.SetOut(io.Discard) - cmd.SetErr(io.Discard) - cmd.SilenceUsage = true - cmd.SilenceErrors = true - cmd.SetArgs([]string{"1", "node-1:7777", "node-1:8888"}) - - require.ErrorContains(t, cmd.Execute(), "accepts 4 arg") -} - -func TestAddLearnerCommandRejectsInvalidInstanceID(t *testing.T) { - t.Parallel() - - t.Run("not hexadecimal", func(t *testing.T) { - t.Parallel() - - cmd := NewAddLearnerCommand() - cmd.SetOut(io.Discard) - cmd.SetErr(io.Discard) - cmd.SilenceUsage = true - cmd.SilenceErrors = true - cmd.SetArgs([]string{"1", "node-1:7777", "node-1:8888", "not-hex"}) - - require.ErrorContains(t, cmd.Execute(), "expected hexadecimal") - }) - - t.Run("wrong decoded length", func(t *testing.T) { - t.Parallel() - - cmd := NewAddLearnerCommand() - cmd.SetOut(io.Discard) - cmd.SetErr(io.Discard) - cmd.SilenceUsage = true - cmd.SilenceErrors = true - cmd.SetArgs([]string{"1", "node-1:7777", "node-1:8888", "0123"}) - - require.ErrorContains(t, cmd.Execute(), "instance_id must be 16 bytes") - }) -} diff --git a/cmd/ledgerctl/cluster/command.go b/cmd/ledgerctl/cluster/command.go index 9dba5045b6..d50df528a8 100644 --- a/cmd/ledgerctl/cluster/command.go +++ b/cmd/ledgerctl/cluster/command.go @@ -15,7 +15,6 @@ func NewCommand() *cobra.Command { cmd.AddCommand(NewWatchCommand()) cmd.AddCommand(NewDiskUsageCommand()) cmd.AddCommand(NewTransferLeaderCommand()) - cmd.AddCommand(NewAddLearnerCommand()) cmd.AddCommand(NewPromoteLearnerCommand()) cmd.AddCommand(NewRemoveNodeCommand()) cmd.AddCommand(NewMaintenanceCommand()) diff --git a/docs/ops/cli.md b/docs/ops/cli.md index df74376a24..10354309d1 100644 --- a/docs/ops/cli.md +++ b/docs/ops/cli.md @@ -2937,47 +2937,6 @@ ledgerctl cluster transfer-leader 2 ledgerctl cluster transfer-leader 3 --timeout 5s ``` -#### cluster add-learner - -Add a non-voting (learner) node to the Raft cluster. The request is forwarded to the current leader. - -```bash -ledgerctl cluster add-learner [flags] -``` - -**Flags:** - -| Flag | Default | Description | -|------|---------|-------------| -| `--timeout` | `10s` | Request timeout | - -**Behavior:** -- The request is forwarded to the current leader if sent to a follower -- `instance-id` is the 32-character hexadecimal encoding of the target node's 16-byte persisted `INSTANCE_ID` file -- An existing member with replicated progress rejects a different identity with `FailedPrecondition`; retrying the same active identity returns `AlreadyExists`. With no replicated progress, a different identity refreshes the registration. -- For stale progress, retire the old instance, remove its membership, then retry. If quorum is unavailable, use the leader-only `cluster remove-node --force` procedure in [Cluster operations](./cluster-operations.md), ensuring the retired instance cannot rejoin. -- The leader proposes a ConfChange to add the node as a learner (non-voting member) -- Once committed, all nodes add the learner to their transport and service pool -- The learner receives log entries and snapshots but cannot vote or become leader - -**Example:** - -```bash -# Read the INSTANCE_ID from the target node's configured --wal-dir (default: ./wal). -# When running under the Formance Operator, the mount path is /data/raft by default. -WAL_DIR=${WAL_DIR:-./wal} -INSTANCE_ID_HEX=$(od -An -tx1 "$WAL_DIR/INSTANCE_ID" | tr -d ' \n') - -# Add node 4 as a learner -ledgerctl cluster add-learner 4 node-4:7777 node-4:8888 "$INSTANCE_ID_HEX" - -# Add node 5 as a learner - read its own WAL directory for its unique INSTANCE_ID. -# Reusing node-4 identity here would register node-5 under the wrong incarnation. -NODE5_WAL_DIR=${NODE5_WAL_DIR:-./wal} -NODE5_INSTANCE_ID_HEX=$(od -An -tx1 "$NODE5_WAL_DIR/INSTANCE_ID" | tr -d ' \n') -ledgerctl cluster add-learner 5 node-5:7777 node-5:8888 "$NODE5_INSTANCE_ID_HEX" --timeout 30s -``` - #### cluster promote-learner Promote a learner (non-voting) node to a full voter in the Raft cluster. The request is forwarded to the leader. diff --git a/docs/ops/cluster-operations.md b/docs/ops/cluster-operations.md index bf73d97768..f83bf84062 100644 --- a/docs/ops/cluster-operations.md +++ b/docs/ops/cluster-operations.md @@ -722,6 +722,6 @@ After the reset, the preloader falls back to Pebble reads (0xF1 zone) for all ca - [Data Flows](../technical/architecture/data-flows.md) - Spool mechanics, entry processing - [Spool](../technical/architecture/subsystems/storage/spool.md) - Spool technical implementation - [Deployment](./deployment.md) - Helm chart configuration, CLI flags -- [CLI Reference](./cli.md) - `cluster add-learner`, `cluster promote-learner` commands +- [CLI Reference](./cli.md) - `cluster promote-learner` and other cluster commands - [Memory Management](./memory.md) - Cache rotation threshold memory impact - [Availability and Disaster Recovery](./availability-and-recovery.md) - Failure-domain placement and quorum-loss recovery From 2085c518b94385ef84e47a77f83223cce0185dc1 Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Mon, 26 Oct 2026 16:48:18 +0100 Subject: [PATCH 062/127] refactor(membership): unify learner admission API --- internal/adapter/grpc/server_cluster.go | 2 +- internal/application/membership/membership.go | 12 ++++-------- .../application/membership/membership_node_test.go | 2 +- internal/application/membership/membership_test.go | 4 ++-- internal/infra/node/add_learner_classify_test.go | 2 +- internal/infra/node/learner_admission_test.go | 2 +- internal/infra/node/membership_invariant_test.go | 2 +- internal/infra/node/membership_leader_ready_test.go | 2 +- internal/infra/node/node.go | 12 ++++-------- 9 files changed, 16 insertions(+), 24 deletions(-) diff --git a/internal/adapter/grpc/server_cluster.go b/internal/adapter/grpc/server_cluster.go index 27d810beda..76a3e58a5b 100644 --- a/internal/adapter/grpc/server_cluster.go +++ b/internal/adapter/grpc/server_cluster.go @@ -500,7 +500,7 @@ func (impl *ClusterServiceServerImpl) AddLearner(ctx context.Context, req *clust // Administrative registration is allowed only for a concrete peer // incarnation. The caller must read the target's persisted INSTANCE_ID; // creating an identity-less phantom member is forbidden. - if err := impl.membership.AddLearnerWithCapabilities(ctx, req.GetNodeId(), req.GetRaftAddress(), req.GetServiceAddress(), req.GetInstanceId(), req.GetCapabilities()); err != nil { + if err := impl.membership.AddLearner(ctx, req.GetNodeId(), req.GetRaftAddress(), req.GetServiceAddress(), req.GetInstanceId(), req.GetCapabilities()); err != nil { return nil, err } diff --git a/internal/application/membership/membership.go b/internal/application/membership/membership.go index 6725291bd2..32c13f4bf5 100644 --- a/internal/application/membership/membership.go +++ b/internal/application/membership/membership.go @@ -90,13 +90,9 @@ func (s *Service) IsRemoved(nodeID uint64, instanceID []byte) (bool, error) { // // Returns node.ErrNodeAlreadyInCluster on idempotent retries; adapters // map that to their transport-specific "already-exists" status. -func (s *Service) AddLearner(ctx context.Context, nodeID uint64, raftAddr, serviceAddr string, instanceID []byte) error { - return s.AddLearnerWithCapabilities(ctx, nodeID, raftAddr, serviceAddr, instanceID, nil) -} - -// AddLearnerWithCapabilities registers a learner and carries the joining -// binary's capability evidence in the committed ConfChange. -func (s *Service) AddLearnerWithCapabilities(ctx context.Context, nodeID uint64, raftAddr, serviceAddr string, instanceID []byte, capabilityIDs []uint32) error { +// AddLearner registers a learner and carries the joining binary's capability +// evidence in the committed ConfChange. +func (s *Service) AddLearner(ctx context.Context, nodeID uint64, raftAddr, serviceAddr string, instanceID []byte, capabilityIDs []uint32) error { return s.addLearner(ctx, nodeID, raftAddr, serviceAddr, instanceID, capabilityIDs, false) } @@ -149,7 +145,7 @@ func (s *Service) addLearner(ctx context.Context, nodeID uint64, raftAddr, servi if bootJoin { err = s.node.JoinAsLearner(ctx, nodeID, raftAddr, serviceAddr, instanceID, capabilityIDs) } else { - err = s.node.AddLearnerWithCapabilities(ctx, nodeID, raftAddr, serviceAddr, instanceID, capabilityIDs) + err = s.node.AddLearner(ctx, nodeID, raftAddr, serviceAddr, instanceID, capabilityIDs) } if err != nil { return fmt.Errorf("adding learner: %w", err) diff --git a/internal/application/membership/membership_node_test.go b/internal/application/membership/membership_node_test.go index bc92a6c4cb..655a2c2b40 100644 --- a/internal/application/membership/membership_node_test.go +++ b/internal/application/membership/membership_node_test.go @@ -182,7 +182,7 @@ func TestService_ListPeersReturnsCompleteIndependentSnapshot(t *testing.T) { s := newMembershipServiceNode(t) ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second) defer cancel() - require.NoError(t, s.AddLearner(ctx, 2, "peer:7777", "peer:8888", []byte("peer-instance-id"))) + require.NoError(t, s.AddLearner(ctx, 2, "peer:7777", "peer:8888", []byte("peer-instance-id"), nil)) // A cached row alone must not leak into the configured member response. require.NoError(t, s.infraMembership.Set(3, "extra:7777", "extra:8888", []byte("extra-instanceid"))) want := []Peer{ diff --git a/internal/application/membership/membership_test.go b/internal/application/membership/membership_test.go index 4cbba884a2..6b6c96da72 100644 --- a/internal/application/membership/membership_test.go +++ b/internal/application/membership/membership_test.go @@ -41,7 +41,7 @@ func TestService_AddLearner_ValidatesRequest(t *testing.T) { for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { t.Parallel() - err := s.AddLearner(context.Background(), tc.nodeID, tc.raftAddr, tc.serviceAddr, tc.instanceID) + err := s.AddLearner(context.Background(), tc.nodeID, tc.raftAddr, tc.serviceAddr, tc.instanceID, nil) require.Error(t, err) assert.Contains(t, err.Error(), tc.wantSubstr) }) @@ -58,7 +58,7 @@ func TestService_AddLearner_RejectionPreservesServiceRouting(t *testing.T) { ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second) defer cancel() - err := s.AddLearner(ctx, 1, "self:7777", "attacker:8080", []byte("self-instance-id")) + err := s.AddLearner(ctx, 1, "self:7777", "attacker:8080", []byte("self-instance-id"), nil) require.ErrorIs(t, err, node.ErrNodeAlreadyInCluster) require.Equal(t, committedAddr, servicePool.GetPeerAddress(1)) }) diff --git a/internal/infra/node/add_learner_classify_test.go b/internal/infra/node/add_learner_classify_test.go index 80f5ce504a..4095a82ca6 100644 --- a/internal/infra/node/add_learner_classify_test.go +++ b/internal/infra/node/add_learner_classify_test.go @@ -11,7 +11,7 @@ func TestAddLearnerPathsRejectInvalidInstanceIDBeforeRaftAccess(t *testing.T) { t.Parallel() n := &Node{} - require.ErrorContains(t, n.AddLearner(context.Background(), 1, "r:1", "s:1", nil), "instance_id must be 16 bytes") + require.ErrorContains(t, n.AddLearner(context.Background(), 1, "r:1", "s:1", nil, nil), "instance_id must be 16 bytes") require.ErrorContains(t, n.JoinAsLearner(context.Background(), 1, "r:1", "s:1", []byte("short"), nil), "instance_id must be 16 bytes") } diff --git a/internal/infra/node/learner_admission_test.go b/internal/infra/node/learner_admission_test.go index 5b956cf8a8..de73e2a598 100644 --- a/internal/infra/node/learner_admission_test.go +++ b/internal/infra/node/learner_admission_test.go @@ -88,7 +88,7 @@ func TestAddLearnerZeroProgressRefreshProposesCompleteReplacement(t *testing.T) defer cancel() results := make(chan error, 1) go func() { - results <- n.AddLearner(ctx, 2, "new:7777", "new:8888", []byte("next-instance-id")) + results <- n.AddLearner(ctx, 2, "new:7777", "new:8888", []byte("next-instance-id"), nil) }() cmd := <-n.clusterCommandCh require.NoError(t, cmd.fn()) diff --git a/internal/infra/node/membership_invariant_test.go b/internal/infra/node/membership_invariant_test.go index e950c01b59..bd670a72d5 100644 --- a/internal/infra/node/membership_invariant_test.go +++ b/internal/infra/node/membership_invariant_test.go @@ -16,7 +16,7 @@ func TestLeaderMembershipOperationsRejectMissingRowBeforeMutation(t *testing.T) for name, invoke := range map[string]func(context.Context, *Node) error{ "add learner": func(ctx context.Context, n *Node) error { - return n.AddLearner(ctx, 2, "peer:7777", "peer:8888", []byte("next-instance-id")) + return n.AddLearner(ctx, 2, "peer:7777", "peer:8888", []byte("next-instance-id"), nil) }, "remove node": func(ctx context.Context, n *Node) error { return n.RemoveNode(ctx, 2) diff --git a/internal/infra/node/membership_leader_ready_test.go b/internal/infra/node/membership_leader_ready_test.go index ff27d6d94a..6126dadd21 100644 --- a/internal/infra/node/membership_leader_ready_test.go +++ b/internal/infra/node/membership_leader_ready_test.go @@ -48,7 +48,7 @@ func TestAddLearnerWaitsForLeaderReady(t *testing.T) { results := make(chan error, 1) go func() { - results <- n.AddLearner(t.Context(), 2, "new:7777", "new:8888", []byte("next-instance-id")) + results <- n.AddLearner(t.Context(), 2, "new:7777", "new:8888", []byte("next-instance-id"), nil) }() // The command channel is unbuffered: an admission that reached it would diff --git a/internal/infra/node/node.go b/internal/infra/node/node.go index 68b076992e..f412f38917 100644 --- a/internal/infra/node/node.go +++ b/internal/infra/node/node.go @@ -3229,14 +3229,10 @@ func classifyExistingLearner(match uint64, existingInstanceID, incomingInstanceI // the explicit call path selects the admission semantics. // // Must be called on the leader. -func (node *Node) AddLearner(ctx context.Context, nodeID uint64, raftAddr, serviceAddr string, instanceID []byte) error { - return node.AddLearnerWithCapabilities(ctx, nodeID, raftAddr, serviceAddr, instanceID, nil) -} - -// AddLearnerWithCapabilities proposes a learner registration carrying the -// caller's capability proof. The proof is persisted with the peer identity and -// is required by the active capability policy. -func (node *Node) AddLearnerWithCapabilities(ctx context.Context, nodeID uint64, raftAddr, serviceAddr string, instanceID []byte, capabilityIDs []uint32) error { +// AddLearner proposes a learner registration carrying the caller's capability +// proof. The proof is persisted with the peer identity and is required by the +// active capability policy. +func (node *Node) AddLearner(ctx context.Context, nodeID uint64, raftAddr, serviceAddr string, instanceID []byte, capabilityIDs []uint32) error { return node.addLearnerWithCapabilities(ctx, nodeID, raftAddr, serviceAddr, instanceID, capabilityIDs, false) } From 3f585ae42aedfe46e96b7cb075b47bd0ee12c0e4 Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Tue, 27 Oct 2026 13:50:11 +0100 Subject: [PATCH 063/127] fix(membership): bound startup refresh attempt --- internal/bootstrap/module.go | 34 +++++++++++++++++-- .../module_refresh_membership_test.go | 25 ++++++++++++++ 2 files changed, 57 insertions(+), 2 deletions(-) diff --git a/internal/bootstrap/module.go b/internal/bootstrap/module.go index 008d3e331e..d0f8ec747b 100644 --- a/internal/bootstrap/module.go +++ b/internal/bootstrap/module.go @@ -1424,13 +1424,13 @@ func membershipRefreshHook(cfg Config, logger logging.Logger) fx.Hook { "serviceAddress": cfg.ServiceAdvertiseAddr(), }).Infof("Restart mode: refreshing membership capabilities") - // Perform one synchronous attempt before the node starts serving. A + // Perform one bounded synchronous attempt before the node starts serving. A // permanent rejection (for example a removed incarnation or invalid // identity) must abort startup rather than letting stale WAL membership // evidence escape into the cluster. Only the rolling-upgrade cases that // are explicitly classified as transient are handed to the background // retry loop. - err := tryRefreshMembershipOnce(ctx, cfg, cfg.TLSConfig, logger) + err := tryRefreshMembershipForStartup(ctx, cfg, cfg.TLSConfig, logger) if err == nil { return nil } @@ -1461,6 +1461,36 @@ func membershipRefreshHook(cfg Config, logger logging.Logger) fx.Hook { } } +const membershipRefreshStartupTimeout = 5 * time.Second + +// tryRefreshMembershipForStartup gives the synchronous startup attempt a +// bounded context. A leader can accept a RefreshMembership RPC while a Raft +// proposal is waiting for a quorum; that proposal must not hold the lifecycle +// start hook until the process-wide startup deadline. Once this attempt times +// out, the caller can continue with conservative evidence and the lifecycle- +// owned retry loop can converge when quorum returns. +func tryRefreshMembershipForStartup(ctx context.Context, cfg Config, tlsCfg TLSConfig, logger logging.Logger) error { + return tryRefreshMembershipForStartupWithTimeout(ctx, cfg, tlsCfg, logger, membershipRefreshStartupTimeout) +} + +func tryRefreshMembershipForStartupWithTimeout(ctx context.Context, cfg Config, tlsCfg TLSConfig, logger logging.Logger, timeout time.Duration) error { + attemptCtx, cancel := context.WithTimeout(ctx, timeout) + defer cancel() + + err := tryRefreshMembershipOnce(attemptCtx, cfg, tlsCfg, logger) + if err == nil { + return nil + } + + // Preserve cancellation of the lifecycle itself as a hard error. Only the + // timeout introduced for this bounded attempt is transient. + if ctx.Err() == nil && errors.Is(attemptCtx.Err(), context.DeadlineExceeded) { + return fmt.Errorf("%w: synchronous refresh attempt exceeded %s: %v", errMembershipRefreshTransient, timeout, err) + } + + return err +} + // tryAddLearner attempts to register this node as a learner on an existing // cluster, using the inter-node ClusterBootstrapService exposed on the // RaftServer. It retries on transient errors (Unavailable, no leader) diff --git a/internal/bootstrap/module_refresh_membership_test.go b/internal/bootstrap/module_refresh_membership_test.go index bbfc7a56bb..c42269b11e 100644 --- a/internal/bootstrap/module_refresh_membership_test.go +++ b/internal/bootstrap/module_refresh_membership_test.go @@ -44,6 +44,15 @@ type retryingRefreshServer struct { firstErr error } +type blockingRefreshServer struct { + clusterbootstrappb.UnimplementedClusterBootstrapServiceServer +} + +func (blockingRefreshServer) RefreshMembership(ctx context.Context, _ *clusterbootstrappb.RefreshMembershipRequest) (*clusterbootstrappb.RefreshMembershipResponse, error) { + <-ctx.Done() + return nil, ctx.Err() +} + func (s *retryingRefreshServer) RefreshMembership(context.Context, *clusterbootstrappb.RefreshMembershipRequest) (*clusterbootstrappb.RefreshMembershipResponse, error) { if s.attempts.Add(1) == 1 { return nil, s.firstErr @@ -118,3 +127,19 @@ func TestTryRefreshMembershipPropagatesPermanentRejection(t *testing.T) { require.ErrorContains(t, err, "removed incarnation") require.Equal(t, int32(1), peer.attempts.Load(), "permanent rejection must not be retried") } + +func TestTryRefreshMembershipForStartupTreatsBoundedAttemptAsTransient(t *testing.T) { + t.Parallel() + + cfg := refreshTestConfig(t, serveRefreshMembership(t, blockingRefreshServer{})) + err := tryRefreshMembershipForStartupWithTimeout( + context.Background(), + cfg, + cfg.TLSConfig, + logging.Testing(), + 10*time.Millisecond, + ) + + require.ErrorIs(t, err, errMembershipRefreshTransient) + require.ErrorIs(t, err, context.DeadlineExceeded) +} From 289eff1d0bdc71ad39f5534d93ce171479c442ae Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Tue, 27 Oct 2026 13:51:01 +0100 Subject: [PATCH 064/127] docs(membership): clarify refresh retry handoff --- internal/bootstrap/module.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/internal/bootstrap/module.go b/internal/bootstrap/module.go index d0f8ec747b..9e064ec1fe 100644 --- a/internal/bootstrap/module.go +++ b/internal/bootstrap/module.go @@ -1427,9 +1427,10 @@ func membershipRefreshHook(cfg Config, logger logging.Logger) fx.Hook { // Perform one bounded synchronous attempt before the node starts serving. A // permanent rejection (for example a removed incarnation or invalid // identity) must abort startup rather than letting stale WAL membership - // evidence escape into the cluster. Only the rolling-upgrade cases that - // are explicitly classified as transient are handed to the background - // retry loop. + // evidence escape into the cluster. Transient connectivity failures + // (including Unavailable) and rolling-upgrade compatibility responses + // are handed to the background retry loop; permanent rejections remain + // startup failures. err := tryRefreshMembershipForStartup(ctx, cfg, cfg.TLSConfig, logger) if err == nil { return nil From 510f78b48992aa27a0bc5e81a0e3aa291685ad58 Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Tue, 27 Oct 2026 13:59:31 +0100 Subject: [PATCH 065/127] test(capabilities): document proposal gate cost evidence --- .../subsystems/consensus/capabilities.md | 28 +++++++++++ internal/bootstrap/module.go | 10 ++-- .../capabilities/capabilities_bench_test.go | 48 +++++++++++++++++++ .../infra/capabilities/capabilities_test.go | 40 ++++++++++++++++ 4 files changed, 123 insertions(+), 3 deletions(-) create mode 100644 internal/infra/capabilities/capabilities_bench_test.go diff --git a/docs/technical/architecture/subsystems/consensus/capabilities.md b/docs/technical/architecture/subsystems/consensus/capabilities.md index ed7e6191f8..23cf575480 100644 --- a/docs/technical/architecture/subsystems/consensus/capabilities.md +++ b/docs/technical/architecture/subsystems/consensus/capabilities.md @@ -120,6 +120,34 @@ runner does not duplicate this check. Callers must still stamp their proposal before use. Membership `ConfChange` operations carry and validate their capability evidence separately. +## Admission cost budget and evidence + +The proposal gate has a deliberately bounded cost shape. For a proposal that +declares `r` capabilities and a current membership of `m` voters and learners, +the pure decision is `O(r + m*r)` and allocates only its normalized input and +blocker report. The membership snapshot is copied once per gate evaluation; +the gate must not perform network calls, wait for Raft, or hold a storage +transaction while evaluating the result. Policy decoding and the snapshot +copy remain serialized by the proposal admission boundary, so they are part +of the write-path budget rather than background work. + +The deterministic behavior matrix is covered by +`internal/infra/capabilities/capabilities_test.go`: empty declarations, +inactive policy, active policy with converged voters and learners, missing +evidence, and unreachable members. The representative cost shape is measured +by: + +```text +go test ./internal/infra/capabilities -run '^$' -bench BenchmarkEvaluate -benchmem -count=5 +``` + +The benchmark uses three and seven members with zero, one, and four required +capabilities. Any future capability registry or policy-provider change must +rerun it and include the result in the change review. A regression must be +explained against the `m*r` bound; if the bound no longer fits the admission +budget, move decoding/snapshot preparation outside the serialized section and +pass an immutable admission snapshot instead of widening the lock. + ## Restart refresh A normal restart does not run `JoinAsLearner`: the node restores its existing diff --git a/internal/bootstrap/module.go b/internal/bootstrap/module.go index 9e064ec1fe..2f978a5233 100644 --- a/internal/bootstrap/module.go +++ b/internal/bootstrap/module.go @@ -404,7 +404,11 @@ func Module() fx.Option { // typed candidate from the final command, so this check does not // decode the serialized proposal or trust caller-supplied metadata. activePolicyCapabilities := proposal.ActivePolicyCapabilities() - if err := validateCapabilityActivationEvidence(capabilities.SupportedIDs(), activePolicyCapabilities, raftNode.CapabilityMembers()); err != nil { + // Take one immutable membership snapshot for both policy checks. The + // gate runs under the serialized proposal/index boundary; taking two + // snapshots here needlessly doubles lock, map-copy and allocation cost. + members := raftNode.CapabilityMembers() + if err := validateCapabilityActivationEvidence(capabilities.SupportedIDs(), activePolicyCapabilities, members); err != nil { return err } @@ -412,7 +416,7 @@ func Module() fx.Option { if err := state.Validate(); err != nil { return err } - decision := capabilities.Evaluate(state, proposal.RequiredCapabilities(), false, raftNode.CapabilityMembers()) + decision := capabilities.Evaluate(state, proposal.RequiredCapabilities(), false, members) if !decision.Allowed { return fmt.Errorf("%w: %s", capabilities.ErrCapabilityUnsupported, decision.Reason) } @@ -1486,7 +1490,7 @@ func tryRefreshMembershipForStartupWithTimeout(ctx context.Context, cfg Config, // Preserve cancellation of the lifecycle itself as a hard error. Only the // timeout introduced for this bounded attempt is transient. if ctx.Err() == nil && errors.Is(attemptCtx.Err(), context.DeadlineExceeded) { - return fmt.Errorf("%w: synchronous refresh attempt exceeded %s: %v", errMembershipRefreshTransient, timeout, err) + return fmt.Errorf("%w: synchronous refresh attempt exceeded %s: %w", errMembershipRefreshTransient, timeout, err) } return err diff --git a/internal/infra/capabilities/capabilities_bench_test.go b/internal/infra/capabilities/capabilities_bench_test.go new file mode 100644 index 0000000000..32bb8621b9 --- /dev/null +++ b/internal/infra/capabilities/capabilities_bench_test.go @@ -0,0 +1,48 @@ +package capabilities + +import ( + "strconv" + "testing" +) + +// BenchmarkEvaluate records the deterministic cost shape of the proposal +// gate. The admission path supplies a lock-consistent membership snapshot and +// calls this pure function while holding its proposal/index serialization +// guards. Keep the matrix representative of a small production cluster and a +// larger deployment so changes to the O(members*required) work are visible. +func BenchmarkEvaluate(b *testing.B) { + for _, size := range []int{3, 7} { + for _, requiredCount := range []int{0, 1, 4} { + b.Run(memberCaseName(size, requiredCount), func(b *testing.B) { + state := State{Activations: make(map[CapabilityID]bool, requiredCount)} + required := make([]CapabilityID, requiredCount) + for i := range required { + id := CapabilityID(i + 1) + required[i] = id + state.Activations[id] = true + } + members := make([]Member, size) + for i := range members { + members[i] = Member{ + NodeID: uint64(i + 1), + Role: Voter, + Reachable: true, + Capabilities: append([]CapabilityID(nil), required...), + } + } + + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + if !Evaluate(state, required, false, members).Allowed { + b.Fatal("representative capability state was rejected") + } + } + }) + } + } +} + +func memberCaseName(memberCount, requiredCount int) string { + return "members=" + strconv.Itoa(memberCount) + "/required=" + strconv.Itoa(requiredCount) +} diff --git a/internal/infra/capabilities/capabilities_test.go b/internal/infra/capabilities/capabilities_test.go index 36bfe00a83..2788ef6a4b 100644 --- a/internal/infra/capabilities/capabilities_test.go +++ b/internal/infra/capabilities/capabilities_test.go @@ -29,6 +29,46 @@ func TestEvaluateAllowsUnrelatedWriteWhenNoFeatureIsDeclared(t *testing.T) { require.True(t, decision.Allowed) } +func TestEvaluateRejectsInactiveCapabilityBeforeInspectingMembers(t *testing.T) { + decision := Evaluate(capabilityState(), []CapabilityID{7}, true, []Member{{ + NodeID: 1, + Role: Voter, + Reachable: true, + Capabilities: []CapabilityID{7}, + }}) + + require.False(t, decision.Allowed) + require.Contains(t, decision.Reason, "unsupported") + require.Empty(t, decision.Blockers[0].NodeID, "policy blockers do not identify a member") +} + +func TestEvaluateRejectsUnreachableMemberEvenWhenCapabilityIsAdvertised(t *testing.T) { + state := capabilityState() + state.Activations[7] = true + decision := Evaluate(state, []CapabilityID{7}, true, []Member{{ + NodeID: 1, + Role: Voter, + Reachable: false, + Capabilities: []CapabilityID{7}, + }}) + + require.False(t, decision.Allowed) + require.Len(t, decision.Blockers, 1) + require.Equal(t, uint64(1), decision.Blockers[0].NodeID) +} + +func TestEvaluateAllowsActiveCapabilityWhenAllMembersAdvertiseAndReachIt(t *testing.T) { + state := capabilityState() + state.Activations[7] = true + decision := Evaluate(state, []CapabilityID{7}, true, []Member{ + {NodeID: 1, Role: Voter, Reachable: true, Capabilities: []CapabilityID{7}}, + {NodeID: 2, Role: Learner, Reachable: true, Capabilities: []CapabilityID{7}}, + }) + + require.True(t, decision.Allowed) + require.Empty(t, decision.Blockers) +} + func TestStampProposalClearsFutureDeclarations(t *testing.T) { proposal := &raftcmdpb.Proposal{RequiredCapabilities: []uint32{99}} require.NoError(t, StampProposal(proposal)) From f24b10473bbf66b85b6d4f1fbf3537714f62e5cc Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Tue, 27 Oct 2026 14:03:50 +0100 Subject: [PATCH 066/127] fix(bootstrap): preserve policy revision admission guards --- .../bootstrap/capability_admission_test.go | 27 ++++++ .../cluster_policy_reconciler_test.go | 41 +++++++++ internal/bootstrap/module.go | 88 ++++++++++++++----- 3 files changed, 133 insertions(+), 23 deletions(-) diff --git a/internal/bootstrap/capability_admission_test.go b/internal/bootstrap/capability_admission_test.go index 574107261a..f27eee817e 100644 --- a/internal/bootstrap/capability_admission_test.go +++ b/internal/bootstrap/capability_admission_test.go @@ -6,6 +6,9 @@ import ( "github.com/stretchr/testify/require" "github.com/formancehq/ledger/v3/internal/infra/capabilities" + "github.com/formancehq/ledger/v3/internal/infra/node" + "github.com/formancehq/ledger/v3/internal/proto/commonpb" + "github.com/formancehq/ledger/v3/internal/proto/raftcmdpb" ) func TestValidateCapabilityActivationEvidenceRejectsIncompleteMembership(t *testing.T) { @@ -35,3 +38,27 @@ func TestValidateCapabilityActivationEvidenceAcceptsConvergedMembership(t *testi []capabilities.Member{{NodeID: 1, Role: capabilities.Voter, Capabilities: []capabilities.CapabilityID{7}}}, )) } + +func TestProposalCapabilityAdmissionRejectsActivationBeforeRaft(t *testing.T) { + t.Parallel() + + proposal, err := node.NewProposal(1, &raftcmdpb.Proposal{ + Id: 1, + Orders: []*raftcmdpb.Order{{ + Type: &raftcmdpb.Order_SystemScoped{SystemScoped: &raftcmdpb.SystemScopedOrder{ + Payload: &raftcmdpb.SystemScopedOrder_SetClusterPolicy{SetClusterPolicy: &raftcmdpb.SetClusterPolicyOrder{ + Policy: &commonpb.ClusterPolicy{CapabilityActivations: []*commonpb.CapabilityActivation{{Id: 7, Active: true}}}, + }}, + }}, + }}, + }) + require.NoError(t, err) + + err = validateProposalCapabilityAdmission( + &commonpb.ClusterPolicy{}, + proposal, + []capabilities.Member{{NodeID: 1, Role: capabilities.Voter}}, + ) + + require.ErrorIs(t, err, capabilities.ErrCapabilityUnsupported) +} diff --git a/internal/bootstrap/cluster_policy_reconciler_test.go b/internal/bootstrap/cluster_policy_reconciler_test.go index 16e0bf4bd7..402d52ee18 100644 --- a/internal/bootstrap/cluster_policy_reconciler_test.go +++ b/internal/bootstrap/cluster_policy_reconciler_test.go @@ -14,6 +14,7 @@ import ( logging "github.com/formancehq/go-libs/v5/pkg/observe/log" "github.com/formancehq/ledger/v3/internal/domain" + "github.com/formancehq/ledger/v3/internal/infra/capabilities" "github.com/formancehq/ledger/v3/internal/infra/state" "github.com/formancehq/ledger/v3/internal/proto/commonpb" "github.com/formancehq/ledger/v3/internal/proto/servicepb" @@ -196,6 +197,46 @@ func TestReconcileClusterPolicy(t *testing.T) { require.Zero(t, admission.callCount(), "a newer applied revision must not be overwritten") }) + t.Run("does not manufacture a revision when a member reader is present", func(t *testing.T) { + t.Parallel() + + store := newReconcilerTestStore(t) + cfg := validBaseConfig() + batch := store.OpenWriteSession() + require.NoError(t, state.SaveClusterPolicy(batch, &commonpb.ClusterPolicy{ + Revision: cfg.ClusterPolicyRevision + 1, + QueryCheckpointLimit: cfg.QueryCheckpointLimit, + })) + require.NoError(t, batch.Commit()) + + admission := &fakeAdmission{} + reconcileClusterPolicy(context.Background(), admission, store, cfg, func() bool { return true }, logging.Testing(), func() []capabilities.Member { + return []capabilities.Member{{NodeID: 1, Role: capabilities.Voter}} + }) + + require.Zero(t, admission.callCount(), "member evidence must not bypass the stale revision guard") + }) + + t.Run("does not replace a same-revision configuration conflict with a capability bump", func(t *testing.T) { + t.Parallel() + + store := newReconcilerTestStore(t) + cfg := validBaseConfig() + batch := store.OpenWriteSession() + require.NoError(t, state.SaveClusterPolicy(batch, &commonpb.ClusterPolicy{ + Revision: cfg.ClusterPolicyRevision, + QueryCheckpointLimit: cfg.QueryCheckpointLimit + 1, + })) + require.NoError(t, batch.Commit()) + + admission := &fakeAdmission{} + reconcileClusterPolicy(context.Background(), admission, store, cfg, func() bool { return true }, logging.Testing(), func() []capabilities.Member { + return []capabilities.Member{{NodeID: 1, Role: capabilities.Voter}} + }) + + require.Zero(t, admission.callCount(), "same-revision configuration conflicts must remain rejected") + }) + t.Run("preserves committed capability activations on a revision bump", func(t *testing.T) { t.Parallel() diff --git a/internal/bootstrap/module.go b/internal/bootstrap/module.go index 2f978a5233..a16609cfdb 100644 --- a/internal/bootstrap/module.go +++ b/internal/bootstrap/module.go @@ -397,31 +397,11 @@ func Module() fx.Option { if raftNode == nil { return errors.New("raft proposal gate is not initialized") } - // A policy activation is itself a consensus-sensitive write. Do - // not rely solely on the periodic reconciler: callers that submit a - // SetClusterPolicy command must carry the same all-member evidence - // before the command can enter Raft. NewProposal derives this - // typed candidate from the final command, so this check does not - // decode the serialized proposal or trust caller-supplied metadata. - activePolicyCapabilities := proposal.ActivePolicyCapabilities() // Take one immutable membership snapshot for both policy checks. The // gate runs under the serialized proposal/index boundary; taking two // snapshots here needlessly doubles lock, map-copy and allocation cost. members := raftNode.CapabilityMembers() - if err := validateCapabilityActivationEvidence(capabilities.SupportedIDs(), activePolicyCapabilities, members); err != nil { - return err - } - - state := capabilities.FromPolicy(policy) - if err := state.Validate(); err != nil { - return err - } - decision := capabilities.Evaluate(state, proposal.RequiredCapabilities(), false, members) - if !decision.Allowed { - return fmt.Errorf("%w: %s", capabilities.ErrCapabilityUnsupported, decision.Reason) - } - - return nil + return validateProposalCapabilityAdmission(policy, proposal, members) } n, err := node.NewNode( @@ -1221,6 +1201,35 @@ func validateCapabilityActivationEvidence(supported, active []capabilities.Capab return nil } +// validateProposalCapabilityAdmission is the production proposal gate shared +// by every Node.Propose caller. The proposal carries typed declarations derived +// from the final command, while policy and membership are the committed +// snapshots captured at the serialized admission boundary. +func validateProposalCapabilityAdmission(policy *commonpb.ClusterPolicy, proposal *node.Proposal, members []capabilities.Member) error { + if proposal == nil { + return errors.New("raft proposal gate received a nil proposal") + } + + if err := validateCapabilityActivationEvidence( + capabilities.SupportedIDs(), + proposal.ActivePolicyCapabilities(), + members, + ); err != nil { + return err + } + + state := capabilities.FromPolicy(policy) + if err := state.Validate(); err != nil { + return err + } + decision := capabilities.Evaluate(state, proposal.RequiredCapabilities(), false, members) + if !decision.Allowed { + return fmt.Errorf("%w: %s", capabilities.ErrCapabilityUnsupported, decision.Reason) + } + + return nil +} + // JoinAuthError is returned when a joining node is rejected by the target // cluster's inter-node RaftServer with codes.Unauthenticated. This is always // a cluster-secret misconfiguration (EN-1080): the join flow carries no user @@ -1943,6 +1952,22 @@ func reconcileClusterPolicy(ctx context.Context, admission ctrl.Admission, store } } } + + // The operator-controlled revision is an independent monotonic guard. A + // capability-only reconciliation may advance a policy revision, but it must + // never make a stale leader's ordinary configuration look newer than the + // revision it was configured to own. Check this before deriving capability + // activations; otherwise a stale leader can manufacture appliedRev+1. + appliedRev := applied.GetRevision() + if applied != nil && appliedRev > cfg.ClusterPolicyRevision { + return + } + if applied != nil && appliedRev == cfg.ClusterPolicyRevision && !sameClusterPolicyConfiguration(applied, desired) { + logger.Errorf("cluster policy revision %d conflicts with the configured policy; refusing to overwrite it", appliedRev) + + return + } + if len(memberReaders) > 0 { ready, readyErr := capabilities.Ready(capabilities.SupportedIDs(), memberReaders[0]()) if readyErr != nil { @@ -1977,8 +2002,6 @@ func reconcileClusterPolicy(ctx context.Context, admission ctrl.Admission, store } } - appliedRev := applied.GetRevision() - switch { case appliedRev > desired.GetRevision(): // A newer policy is already applied; the FSM would reject this as stale. @@ -2006,6 +2029,25 @@ func reconcileClusterPolicy(ctx context.Context, admission ctrl.Admission, store } } +// sameClusterPolicyConfiguration compares the operator-controlled fields while +// deliberately ignoring capability activations. Capabilities are reconciled +// from membership evidence and may legitimately require a revision bump; all +// other fields must remain protected by the configured revision. +func sameClusterPolicyConfiguration(a, b *commonpb.ClusterPolicy) bool { + if a == nil || b == nil { + return a == b + } + + left := a.CloneVT() + right := b.CloneVT() + left.Revision = 0 + right.Revision = 0 + left.CapabilityActivations = nil + right.CapabilityActivations = nil + + return left.EqualVT(right) +} + // buildResponseSigner returns the Ed25519 response signer for the gRPC // service plane. // From ba88ec97f1e145c605b495d2de5095f369519121 Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Tue, 27 Oct 2026 14:33:17 +0100 Subject: [PATCH 067/127] fix(bootstrap): allow capability-only policy revisions --- .../cluster_policy_reconciler_test.go | 46 +++++++++++++++++-- internal/bootstrap/module.go | 28 +++++++++-- 2 files changed, 65 insertions(+), 9 deletions(-) diff --git a/internal/bootstrap/cluster_policy_reconciler_test.go b/internal/bootstrap/cluster_policy_reconciler_test.go index 402d52ee18..c02a441038 100644 --- a/internal/bootstrap/cluster_policy_reconciler_test.go +++ b/internal/bootstrap/cluster_policy_reconciler_test.go @@ -186,8 +186,14 @@ func TestReconcileClusterPolicy(t *testing.T) { batch := store.OpenWriteSession() require.NoError(t, state.SaveClusterPolicy(batch, &commonpb.ClusterPolicy{ - Revision: cfg.ClusterPolicyRevision + 1, - QueryCheckpointLimit: cfg.QueryCheckpointLimit, + Revision: cfg.ClusterPolicyRevision + 1, + IdempotencyTtlMicros: uint64(cfg.IdempotencyTTL.Microseconds()), + QueryCheckpointLimit: cfg.QueryCheckpointLimit, + MetadataMaxEntriesPerEntity: cfg.MetadataMaxEntriesPerEntity, + MetadataMaxKeyBytes: cfg.MetadataMaxKeyBytes, + MetadataMaxValueBytes: cfg.MetadataMaxValueBytes, + MetadataMaxEntityBytes: cfg.MetadataMaxEntityBytes, + MetadataMaxCommandBytes: cfg.MetadataMaxCommandBytes, })) require.NoError(t, batch.Commit()) @@ -204,17 +210,49 @@ func TestReconcileClusterPolicy(t *testing.T) { cfg := validBaseConfig() batch := store.OpenWriteSession() require.NoError(t, state.SaveClusterPolicy(batch, &commonpb.ClusterPolicy{ - Revision: cfg.ClusterPolicyRevision + 1, - QueryCheckpointLimit: cfg.QueryCheckpointLimit, + Revision: cfg.ClusterPolicyRevision + 1, + IdempotencyTtlMicros: uint64(cfg.IdempotencyTTL.Microseconds()), + QueryCheckpointLimit: cfg.QueryCheckpointLimit, + MetadataMaxEntriesPerEntity: cfg.MetadataMaxEntriesPerEntity, + MetadataMaxKeyBytes: cfg.MetadataMaxKeyBytes, + MetadataMaxValueBytes: cfg.MetadataMaxValueBytes, + MetadataMaxEntityBytes: cfg.MetadataMaxEntityBytes, + MetadataMaxCommandBytes: cfg.MetadataMaxCommandBytes, })) require.NoError(t, batch.Commit()) admission := &fakeAdmission{} + var memberReads atomic.Int32 reconcileClusterPolicy(context.Background(), admission, store, cfg, func() bool { return true }, logging.Testing(), func() []capabilities.Member { + memberReads.Add(1) return []capabilities.Member{{NodeID: 1, Role: capabilities.Voter}} }) require.Zero(t, admission.callCount(), "member evidence must not bypass the stale revision guard") + require.EqualValues(t, 1, memberReads.Load(), "matching operator configuration must still allow capability reconciliation") + }) + + t.Run("rejects a newer revision with a configuration conflict", func(t *testing.T) { + t.Parallel() + + store := newReconcilerTestStore(t) + cfg := validBaseConfig() + batch := store.OpenWriteSession() + require.NoError(t, state.SaveClusterPolicy(batch, &commonpb.ClusterPolicy{ + Revision: cfg.ClusterPolicyRevision + 1, + QueryCheckpointLimit: cfg.QueryCheckpointLimit + 1, + })) + require.NoError(t, batch.Commit()) + + admission := &fakeAdmission{} + var memberReads atomic.Int32 + reconcileClusterPolicy(context.Background(), admission, store, cfg, func() bool { return true }, logging.Testing(), func() []capabilities.Member { + memberReads.Add(1) + return []capabilities.Member{{NodeID: 1, Role: capabilities.Voter}} + }) + + require.Zero(t, admission.callCount(), "a newer conflicting configuration must not be re-proposed") + require.Zero(t, memberReads.Load(), "configuration conflicts must stop before capability reconciliation") }) t.Run("does not replace a same-revision configuration conflict with a capability bump", func(t *testing.T) { diff --git a/internal/bootstrap/module.go b/internal/bootstrap/module.go index a16609cfdb..6fe0569258 100644 --- a/internal/bootstrap/module.go +++ b/internal/bootstrap/module.go @@ -1953,13 +1953,15 @@ func reconcileClusterPolicy(ctx context.Context, admission ctrl.Admission, store } } - // The operator-controlled revision is an independent monotonic guard. A + // The operator-controlled revision is an independent monotonic guard. A // capability-only reconciliation may advance a policy revision, but it must // never make a stale leader's ordinary configuration look newer than the - // revision it was configured to own. Check this before deriving capability - // activations; otherwise a stale leader can manufacture appliedRev+1. + // revision it was configured to own. A higher applied revision is therefore + // acceptable only when the operator-controlled fields still match; this lets + // later capability-only reconciliations continue without requiring another + // flag bump. appliedRev := applied.GetRevision() - if applied != nil && appliedRev > cfg.ClusterPolicyRevision { + if applied != nil && appliedRev > cfg.ClusterPolicyRevision && !sameClusterPolicyConfiguration(applied, desired) { return } if applied != nil && appliedRev == cfg.ClusterPolicyRevision && !sameClusterPolicyConfiguration(applied, desired) { @@ -1997,7 +1999,7 @@ func reconcileClusterPolicy(ctx context.Context, admission ctrl.Admission, store } desired.CapabilityActivations = append(desired.CapabilityActivations, &commonpb.CapabilityActivation{Id: uint32(id), Active: true}) } - if applied != nil && !applied.EqualVT(desired) && desired.GetRevision() <= applied.GetRevision() { + if applied != nil && !sameClusterPolicyPayload(applied, desired) && desired.GetRevision() <= applied.GetRevision() { desired.Revision = applied.GetRevision() + 1 } } @@ -2048,6 +2050,22 @@ func sameClusterPolicyConfiguration(a, b *commonpb.ClusterPolicy) bool { return left.EqualVT(right) } +// sameClusterPolicyPayload compares the complete policy while deliberately +// ignoring only the revision. A revision-only difference is not a reason to +// manufacture another revision during capability reconciliation. +func sameClusterPolicyPayload(a, b *commonpb.ClusterPolicy) bool { + if a == nil || b == nil { + return a == b + } + + left := a.CloneVT() + right := b.CloneVT() + left.Revision = 0 + right.Revision = 0 + + return left.EqualVT(right) +} + // buildResponseSigner returns the Ed25519 response signer for the gRPC // service plane. // From f5810d3737f54085a73cca0f2b3c5a5e30ed8909 Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Tue, 27 Oct 2026 16:11:16 +0100 Subject: [PATCH 068/127] refactor(membership): unify learner join API --- internal/adapter/grpc/server_bootstrap.go | 2 +- internal/application/membership/membership.go | 15 +++++---------- .../membership/membership_node_test.go | 6 +++--- .../application/membership/membership_test.go | 2 +- 4 files changed, 10 insertions(+), 15 deletions(-) diff --git a/internal/adapter/grpc/server_bootstrap.go b/internal/adapter/grpc/server_bootstrap.go index b3085e9246..ed01c0a964 100644 --- a/internal/adapter/grpc/server_bootstrap.go +++ b/internal/adapter/grpc/server_bootstrap.go @@ -224,7 +224,7 @@ func (impl *ClusterBootstrapServiceServerImpl) JoinAsLearner(ctx context.Context req.GetNodeId(), req.GetInstanceId()) } - if err := impl.membership.JoinAsLearnerWithCapabilities(ctx, req.GetNodeId(), req.GetRaftAddress(), req.GetServiceAddress(), req.GetInstanceId(), req.GetCapabilities()); err != nil { + if err := impl.membership.JoinAsLearner(ctx, req.GetNodeId(), req.GetRaftAddress(), req.GetServiceAddress(), req.GetInstanceId(), req.GetCapabilities()); err != nil { // EN-1436: a JoinAsLearner call reaches the leader only when the caller // has no CLUSTER_JOINED marker on its WAL (see bootstrap/module.go // tryAddLearner). If the leader's Progress already carries this diff --git a/internal/application/membership/membership.go b/internal/application/membership/membership.go index 32c13f4bf5..005ba94923 100644 --- a/internal/application/membership/membership.go +++ b/internal/application/membership/membership.go @@ -97,16 +97,11 @@ func (s *Service) AddLearner(ctx context.Context, nodeID uint64, raftAddr, servi } // JoinAsLearner registers a node that is booting with an empty WAL and no -// CLUSTER_JOINED marker. Keeping this entry point distinct from the admin -// AddLearner operation preserves EN-1436's stale-progress fail-fast without -// inferring caller intent from a missing instanceID sentinel. -func (s *Service) JoinAsLearner(ctx context.Context, nodeID uint64, raftAddr, serviceAddr string, instanceID []byte) error { - return s.JoinAsLearnerWithCapabilities(ctx, nodeID, raftAddr, serviceAddr, instanceID, nil) -} - -// JoinAsLearnerWithCapabilities is the bootstrap path carrying the joining -// binary's capability evidence through the committed ConfChange. -func (s *Service) JoinAsLearnerWithCapabilities(ctx context.Context, nodeID uint64, raftAddr, serviceAddr string, instanceID []byte, capabilityIDs []uint32) error { +// CLUSTER_JOINED marker, carrying the joining binary's capability evidence +// through the committed ConfChange. Keeping this entry point distinct from the +// admin AddLearner operation preserves EN-1436's stale-progress fail-fast +// without inferring caller intent from a missing instanceID sentinel. +func (s *Service) JoinAsLearner(ctx context.Context, nodeID uint64, raftAddr, serviceAddr string, instanceID []byte, capabilityIDs []uint32) error { return s.addLearner(ctx, nodeID, raftAddr, serviceAddr, instanceID, capabilityIDs, true) } diff --git a/internal/application/membership/membership_node_test.go b/internal/application/membership/membership_node_test.go index 655a2c2b40..40e0cf953e 100644 --- a/internal/application/membership/membership_node_test.go +++ b/internal/application/membership/membership_node_test.go @@ -148,7 +148,7 @@ func TestService_LearnerRegistrationCommitsAndPreservesCallerIntent(t *testing.T defer cancel() register := func(ctx context.Context, nodeID uint64, raftAddr, serviceAddr string, instanceID []byte, capabilities []uint32) error { if bootJoin { - return s.JoinAsLearnerWithCapabilities(ctx, nodeID, raftAddr, serviceAddr, instanceID, capabilities) + return s.JoinAsLearner(ctx, nodeID, raftAddr, serviceAddr, instanceID, capabilities) } return s.AddLearnerWithCapabilities(ctx, nodeID, raftAddr, serviceAddr, instanceID, capabilities) } @@ -201,14 +201,14 @@ func TestService_ListPeersReturnsCompleteIndependentSnapshot(t *testing.T) { }) } -func TestService_JoinAsLearnerWithCapabilitiesPersistsEvidence(t *testing.T) { +func TestService_JoinAsLearnerPersistsEvidence(t *testing.T) { t.Parallel() s := newMembershipServiceNode(t) ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second) defer cancel() - require.NoError(t, s.JoinAsLearnerWithCapabilities(ctx, 2, "peer:7777", "peer:8888", []byte("peer-instance-id"), []uint32{7, 9})) + require.NoError(t, s.JoinAsLearner(ctx, 2, "peer:7777", "peer:8888", []byte("peer-instance-id"), []uint32{7, 9})) peer := s.infraMembership.PeerAddresses()[2] require.Equal(t, []uint32{7, 9}, peer.Capabilities) diff --git a/internal/application/membership/membership_test.go b/internal/application/membership/membership_test.go index 6b6c96da72..f180f0d1ab 100644 --- a/internal/application/membership/membership_test.go +++ b/internal/application/membership/membership_test.go @@ -67,7 +67,7 @@ func TestService_AddLearner_RejectionPreservesServiceRouting(t *testing.T) { func TestService_JoinAsLearner_RejectsMissingInstanceID(t *testing.T) { t.Parallel() - err := (&Service{}).JoinAsLearner(context.Background(), 1, "r:1", "s:1", nil) + err := (&Service{}).JoinAsLearner(context.Background(), 1, "r:1", "s:1", nil, nil) require.ErrorContains(t, err, "instance_id") } From c2869081da7b712c68ce7fb3ad5973dcc908305d Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Tue, 27 Oct 2026 16:28:09 +0100 Subject: [PATCH 069/127] refactor(bootstrap): share membership grpc dialer --- internal/bootstrap/module.go | 38 +++++++++++++++++++++++------------- 1 file changed, 24 insertions(+), 14 deletions(-) diff --git a/internal/bootstrap/module.go b/internal/bootstrap/module.go index 6fe0569258..873441b109 100644 --- a/internal/bootstrap/module.go +++ b/internal/bootstrap/module.go @@ -1426,6 +1426,26 @@ func joinPreflightHook(cfg Config, logger logging.Logger) fx.Hook { } } +// newMembershipPeerDialer builds the common outbound gRPC dialer used by the +// learner bootstrap and membership-refresh paths. Keeping credential and +// option construction in one place prevents those two startup paths from +// drifting while still allowing each call site to control connection lifetime. +func newMembershipPeerDialer(cfg Config, tlsCfg TLSConfig) (func(string) (*ggrpc.ClientConn, error), error) { + creds, _, err := ClientTransportCredentials(tlsCfg) + if err != nil { + return nil, err + } + + dialOpts := []ggrpc.DialOption{ggrpc.WithTransportCredentials(creds)} + if cfg.ClusterSecret != "" { + dialOpts = append(dialOpts, transport.BearerTokenDialOption(cfg.ClusterSecret)) + } + + return func(address string) (*ggrpc.ClientConn, error) { + return ggrpc.NewClient(address, dialOpts...) + }, nil +} + func membershipRefreshHook(cfg Config, logger logging.Logger) fx.Hook { var cancel context.CancelFunc @@ -1535,16 +1555,11 @@ func tryAddLearner(ctx context.Context, cfg Config, tlsCfg TLSConfig, logger log Capabilities: capabilityIDsAsUint32(capabilities.SupportedIDs()), } - creds, _, err := ClientTransportCredentials(tlsCfg) + dialPeer, err := newMembershipPeerDialer(cfg, tlsCfg) if err != nil { return fmt.Errorf("loading TLS credentials for learner registration: %w", err) } - dialOpts := []ggrpc.DialOption{ggrpc.WithTransportCredentials(creds)} - if cfg.ClusterSecret != "" { - dialOpts = append(dialOpts, transport.BearerTokenDialOption(cfg.ClusterSecret)) - } - backoff := 500 * time.Millisecond maxBackoff := 5 * time.Second @@ -1555,7 +1570,7 @@ func tryAddLearner(ctx context.Context, cfg Config, tlsCfg TLSConfig, logger log for { // Try each peer in order until one succeeds. for _, peer := range peers { - conn, err := ggrpc.NewClient(peer.Address, dialOpts...) + conn, err := dialPeer(peer.Address) if err != nil { logger.WithFields(map[string]any{ "peer": peer.ID, @@ -1725,22 +1740,17 @@ func tryRefreshMembershipOnce(ctx context.Context, cfg Config, tlsCfg TLSConfig, Capabilities: capabilityIDsAsUint32(capabilities.SupportedIDs()), } - creds, _, err := ClientTransportCredentials(tlsCfg) + dialPeer, err := newMembershipPeerDialer(cfg, tlsCfg) if err != nil { return fmt.Errorf("loading TLS credentials for membership refresh: %w", err) } - - dialOpts := []ggrpc.DialOption{ggrpc.WithTransportCredentials(creds)} - if cfg.ClusterSecret != "" { - dialOpts = append(dialOpts, transport.BearerTokenDialOption(cfg.ClusterSecret)) - } if cfg.ClusterID != "" { ctx = metadata.AppendToOutgoingContext(ctx, node.MetadataKeyClusterID, cfg.ClusterID) } var lastTransient error for _, peer := range cfg.RaftConfig.Peers { - conn, dialErr := ggrpc.NewClient(peer.Address, dialOpts...) + conn, dialErr := dialPeer(peer.Address) if dialErr != nil { logger.WithFields(map[string]any{"peer": peer.ID, "address": peer.Address, "error": dialErr}).Infof("Failed to dial peer for membership refresh") lastTransient = dialErr From 4a83dbe5618abeba83622e9c16510d65370818ac Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Tue, 27 Oct 2026 16:36:32 +0100 Subject: [PATCH 070/127] refactor(bootstrap): extract periodic reconciler --- .../bootstrap/cluster_policy_reconciler.go | 74 ------------------- .../cluster_policy_reconciler_test.go | 50 ------------- internal/bootstrap/module.go | 20 +++-- internal/pkg/reconciler/reconciler.go | 68 +++++++++++++++++ internal/pkg/reconciler/reconciler_test.go | 55 ++++++++++++++ 5 files changed, 136 insertions(+), 131 deletions(-) delete mode 100644 internal/bootstrap/cluster_policy_reconciler.go create mode 100644 internal/pkg/reconciler/reconciler.go create mode 100644 internal/pkg/reconciler/reconciler_test.go diff --git a/internal/bootstrap/cluster_policy_reconciler.go b/internal/bootstrap/cluster_policy_reconciler.go deleted file mode 100644 index e5440e9bb1..0000000000 --- a/internal/bootstrap/cluster_policy_reconciler.go +++ /dev/null @@ -1,74 +0,0 @@ -package bootstrap - -import ( - "context" - "time" - - "github.com/formancehq/ledger/v3/internal/pkg/worker" -) - -// clusterPolicyReconcileInterval is how often the leader re-checks the applied -// cluster policy against the desired one. Short so a fresh leader commits the -// policy — and thereby opens write readiness — promptly. -const clusterPolicyReconcileInterval = 1 * time.Second - -// ClusterPolicyReconciler periodically drives the replicated cluster policy -// toward the desired revision while this node is leader. It is periodic rather -// than one-shot on leadership acquisition so a transient proposal failure -// (propose timeout, a momentary write gate, leadership churn) self-heals on the -// next tick instead of leaving the node stuck NOT_SERVING until another -// leadership event — write readiness depends on the policy being committed -// (EN-1827). Reconciliation is idempotent: once the applied revision reaches the -// desired one, every tick is a no-op. -type ClusterPolicyReconciler struct { - reconcile func(context.Context) - interval time.Duration - trigger chan struct{} - w worker.Worker -} - -// NewClusterPolicyReconciler builds a reconciler that invokes reconcile on start -// and on every tick. reconcile receives a context cancelled on Stop and must -// gate on leadership itself. -func NewClusterPolicyReconciler(reconcile func(context.Context)) *ClusterPolicyReconciler { - return &ClusterPolicyReconciler{ - reconcile: reconcile, - interval: clusterPolicyReconcileInterval, - trigger: make(chan struct{}, 1), - w: worker.New(), - } -} - -// Trigger asks the running reconciler to converge immediately. The signal is -// coalesced because several ConfChanges may be committed in one Ready batch. -func (r *ClusterPolicyReconciler) Trigger() { - select { - case r.trigger <- struct{}{}: - default: - } -} - -// Start runs an immediate reconciliation, then one per interval, until Stop. -func (r *ClusterPolicyReconciler) Start() { - r.w.Run(func(stop <-chan struct{}) { - ctx := worker.ContextFromStop(stop) - r.reconcile(ctx) - ticker := time.NewTicker(r.interval) - defer ticker.Stop() - for { - select { - case <-stop: - return - case <-ticker.C: - r.reconcile(ctx) - case <-r.trigger: - r.reconcile(ctx) - } - } - }) -} - -// Stop signals the background goroutine to stop and waits for it to finish. -func (r *ClusterPolicyReconciler) Stop() { - r.w.Stop() -} diff --git a/internal/bootstrap/cluster_policy_reconciler_test.go b/internal/bootstrap/cluster_policy_reconciler_test.go index c02a441038..fa0071e58e 100644 --- a/internal/bootstrap/cluster_policy_reconciler_test.go +++ b/internal/bootstrap/cluster_policy_reconciler_test.go @@ -5,8 +5,6 @@ import ( "sync" "sync/atomic" "testing" - "testing/synctest" - "time" "github.com/stretchr/testify/require" "go.opentelemetry.io/otel/metric/noop" @@ -21,54 +19,6 @@ import ( "github.com/formancehq/ledger/v3/internal/storage/dal" ) -// The reconciler keeps ticking until stopped, so a transient failure retries. -func TestClusterPolicyReconciler_TicksUntilStopped(t *testing.T) { - t.Parallel() - - synctest.Test(t, func(t *testing.T) { - var count atomic.Int32 - - r := NewClusterPolicyReconciler(func(context.Context) { count.Add(1) }) - r.interval = 5 * time.Millisecond - r.Start() - - // On the fake clock, two intervals yield exactly the immediate run plus - // one per tick. - time.Sleep(2 * r.interval) - synctest.Wait() - require.Equal(t, int32(3), count.Load(), "reconcile must run immediately, then on every tick") - - r.Stop() - - // Stop is synchronous (waits for the goroutine), so the count is now final. - settled := count.Load() - time.Sleep(20 * r.interval) - synctest.Wait() - require.Equal(t, settled, count.Load(), "no reconcile must run after Stop") - }) -} - -func TestClusterPolicyReconciler_TriggerIsCoalescedAndRaceSafe(t *testing.T) { - t.Parallel() - - var count atomic.Int32 - r := NewClusterPolicyReconciler(func(context.Context) { count.Add(1) }) - r.interval = time.Hour - r.Start() - require.Eventually(t, func() bool { return count.Load() >= 1 }, time.Second, time.Millisecond) - - var wg sync.WaitGroup - for range 100 { - wg.Go(func() { - r.Trigger() - }) - } - wg.Wait() - require.Eventually(t, func() bool { return count.Load() >= 2 }, time.Second, time.Millisecond, - "a membership event must wake reconciliation without waiting for the ticker") - r.Stop() -} - type fakeAdmission struct { mu sync.Mutex calls int diff --git a/internal/bootstrap/module.go b/internal/bootstrap/module.go index 873441b109..3cf0c291d2 100644 --- a/internal/bootstrap/module.go +++ b/internal/bootstrap/module.go @@ -61,6 +61,7 @@ import ( "github.com/formancehq/ledger/v3/internal/infra/transport" "github.com/formancehq/ledger/v3/internal/pkg/commands" "github.com/formancehq/ledger/v3/internal/pkg/network" + "github.com/formancehq/ledger/v3/internal/pkg/reconciler" "github.com/formancehq/ledger/v3/internal/pkg/signal" "github.com/formancehq/ledger/v3/internal/pkg/version" "github.com/formancehq/ledger/v3/internal/pkg/worker" @@ -701,8 +702,8 @@ func Module() fx.Option { return clusterhealth.NewGRPCHealthUpdater(n, hs, clusterPolicyReady, rs.ReadProjectionHealthy) }, - func(admission ctrl.Admission, store *dal.Store, builder *plan.Builder, cfg Config, raftNode *node.Node, logger logging.Logger) *ClusterPolicyReconciler { - return NewClusterPolicyReconciler(func(ctx context.Context) { + func(admission ctrl.Admission, store *dal.Store, builder *plan.Builder, cfg Config, raftNode *node.Node, logger logging.Logger) *reconciler.Reconciler { + return reconciler.New(func(ctx context.Context) { if !raftNode.IsLeader() { return } @@ -719,7 +720,7 @@ func Module() fx.Option { return } reconcileClusterPolicy(ctx, admission, store, cfg, raftNode.IsLeader, logger, raftNode.CapabilityMembers) - }) + }, clusterPolicyReconcileInterval) }, ), fx.Decorate(func( @@ -960,7 +961,7 @@ func Module() fx.Option { mirrorManager *mirror.Manager, backupOrchestrator *backupapp.Orchestrator, healthChecker *clusterhealth.HealthChecker, - reconciler *ClusterPolicyReconciler, + reconciler *reconciler.Reconciler, ) { n.SetObserver(node.NewObserver(func(event any) { switch e := event.(type) { @@ -1078,7 +1079,7 @@ func Module() fx.Option { func(lc fx.Lifecycle, manager *mirror.Manager) { lc.Append(worker.FxHook(manager)) }, - func(lc fx.Lifecycle, reconciler *ClusterPolicyReconciler) { + func(lc fx.Lifecycle, reconciler *reconciler.Reconciler) { lc.Append(worker.FxHook(reconciler)) }, func(lc fx.Lifecycle, cfg Config, logger logging.Logger, raftNode *node.Node, store *dal.Store, machine *state.Machine, builder *plan.Builder) { @@ -1518,7 +1519,7 @@ func tryRefreshMembershipForStartupWithTimeout(ctx context.Context, cfg Config, // Preserve cancellation of the lifecycle itself as a hard error. Only the // timeout introduced for this bounded attempt is transient. - if ctx.Err() == nil && errors.Is(attemptCtx.Err(), context.DeadlineExceeded) { + if ctx.Err() == nil && (errors.Is(attemptCtx.Err(), context.DeadlineExceeded) || errors.Is(err, context.DeadlineExceeded)) { return fmt.Errorf("%w: synchronous refresh attempt exceeded %s: %w", errMembershipRefreshTransient, timeout, err) } @@ -1925,9 +1926,14 @@ func proposeClusterConfigIfNeeded(parent context.Context, n plan.Proposer, build } } +// clusterPolicyReconcileInterval is how often the leader re-checks the applied +// cluster policy against the desired one. Short so a fresh leader commits the +// policy — and thereby opens write readiness — promptly. +const clusterPolicyReconcileInterval = time.Second + // reconcileClusterPolicy drives the replicated cluster policy toward the desired // revision. It gates on leadership itself and is safe to call repeatedly: the -// ClusterPolicyReconciler invokes it on a ticker so a transient proposal failure +// generic reconciler invokes it on a ticker so a transient proposal failure // self-heals on the next tick. Unlike the cluster config, the policy flows // through AdmitClusterPolicy as an internal audited SetClusterPolicy order so // the checker can re-derive it; the write-readiness gate exempts this diff --git a/internal/pkg/reconciler/reconciler.go b/internal/pkg/reconciler/reconciler.go new file mode 100644 index 0000000000..d634947734 --- /dev/null +++ b/internal/pkg/reconciler/reconciler.go @@ -0,0 +1,68 @@ +// Package reconciler provides a small periodic reconciliation loop with an +// explicit trigger for events that should be handled before the next tick. +package reconciler + +import ( + "context" + "time" + + "github.com/formancehq/ledger/v3/internal/pkg/worker" +) + +// Reconciler invokes reconcile immediately when started, periodically at +// interval, and whenever Trigger is called. Trigger notifications are +// coalesced so bursts of events cannot grow an unbounded queue. +type Reconciler struct { + reconcile func(context.Context) + interval time.Duration + trigger chan struct{} + w worker.Worker +} + +// New creates a periodic reconciler. interval must be positive. +func New(reconcile func(context.Context), interval time.Duration) *Reconciler { + if interval <= 0 { + panic("reconciler interval must be positive") + } + + return &Reconciler{ + reconcile: reconcile, + interval: interval, + trigger: make(chan struct{}, 1), + w: worker.New(), + } +} + +// Trigger asks the running reconciler to converge immediately. The signal is +// coalesced because several events may be committed in one batch. +func (r *Reconciler) Trigger() { + select { + case r.trigger <- struct{}{}: + default: + } +} + +// Start runs an immediate reconciliation, then one per interval, until Stop. +func (r *Reconciler) Start() { + r.w.Run(func(stop <-chan struct{}) { + ctx := worker.ContextFromStop(stop) + r.reconcile(ctx) + ticker := time.NewTicker(r.interval) + defer ticker.Stop() + for { + select { + case <-stop: + return + case <-ticker.C: + r.reconcile(ctx) + case <-r.trigger: + r.reconcile(ctx) + } + } + }) +} + +// Stop signals the background goroutine to stop and waits for it to finish. +func (r *Reconciler) Stop() { + r.w.Stop() +} diff --git a/internal/pkg/reconciler/reconciler_test.go b/internal/pkg/reconciler/reconciler_test.go new file mode 100644 index 0000000000..8d61360c78 --- /dev/null +++ b/internal/pkg/reconciler/reconciler_test.go @@ -0,0 +1,55 @@ +package reconciler + +import ( + "context" + "sync" + "sync/atomic" + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +func TestReconcilerTicksUntilStopped(t *testing.T) { + t.Parallel() + + var count atomic.Int32 + r := New(func(context.Context) { count.Add(1) }, 5*time.Millisecond) + r.Start() + + require.Eventually(t, func() bool { return count.Load() >= 3 }, 2*time.Second, 5*time.Millisecond, + "reconcile must run repeatedly on the ticker") + + r.Stop() + settled := count.Load() + require.Never(t, func() bool { return count.Load() > settled }, 100*time.Millisecond, 10*time.Millisecond, + "no reconcile must run after Stop") +} + +func TestReconcilerTriggerIsCoalescedAndRaceSafe(t *testing.T) { + t.Parallel() + + var count atomic.Int32 + r := New(func(context.Context) { count.Add(1) }, time.Hour) + r.Start() + require.Eventually(t, func() bool { return count.Load() >= 1 }, time.Second, time.Millisecond) + + var wg sync.WaitGroup + for range 100 { + wg.Go(func() { + r.Trigger() + }) + } + wg.Wait() + require.Eventually(t, func() bool { return count.Load() >= 2 }, time.Second, time.Millisecond, + "an event must wake reconciliation without waiting for the ticker") + r.Stop() +} + +func TestNewRejectsNonPositiveInterval(t *testing.T) { + t.Parallel() + + require.Panics(t, func() { + New(func(context.Context) {}, 0) + }) +} From 4d649bc7690c2f95374643e8cadd6836c3ca3187 Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Tue, 27 Oct 2026 16:43:48 +0100 Subject: [PATCH 071/127] refactor(bootstrap): name policy reconciler provider --- internal/bootstrap/module.go | 45 ++++++++++++++++++++---------------- 1 file changed, 25 insertions(+), 20 deletions(-) diff --git a/internal/bootstrap/module.go b/internal/bootstrap/module.go index 3cf0c291d2..9815490dcc 100644 --- a/internal/bootstrap/module.go +++ b/internal/bootstrap/module.go @@ -125,6 +125,30 @@ func provideWAL(cfg node.NodeConfig, logger logging.Logger, meterProvider metric return wal.New(cfg.WalDir, logger, meterProvider.Meter("wal")) } +// newClusterPolicyReconciler wires the leader-only policy maintenance into the +// generic periodic reconciler. Keeping this assembly in a named provider keeps +// the Fx graph readable while leaving the policy logic in dedicated helpers. +func newClusterPolicyReconciler(admission ctrl.Admission, store *dal.Store, builder *plan.Builder, cfg Config, raftNode *node.Node, logger logging.Logger) *reconciler.Reconciler { + return reconciler.New(func(ctx context.Context) { + if !raftNode.IsLeader() { + return + } + if policy, err := query.ReadClusterPolicy(store); err == nil { + if err := raftNode.SetCapabilityGate(activeCapabilityIDs(policy)); err != nil { + logger.WithFields(map[string]any{"error": err}).Errorf("Invalid capability gate in committed cluster policy") + + return + } + } + if err := proposeAuditKeyIfNeeded(ctx, raftNode, builder, store); err != nil { + logger.WithFields(map[string]any{"error": err}).Errorf("Audit key initialization failed; will retry") + + return + } + reconcileClusterPolicy(ctx, admission, store, cfg, raftNode.IsLeader, logger, raftNode.CapabilityMembers) + }, clusterPolicyReconcileInterval) +} + func Module() fx.Option { return fx.Options( transport.Module(), @@ -702,26 +726,7 @@ func Module() fx.Option { return clusterhealth.NewGRPCHealthUpdater(n, hs, clusterPolicyReady, rs.ReadProjectionHealthy) }, - func(admission ctrl.Admission, store *dal.Store, builder *plan.Builder, cfg Config, raftNode *node.Node, logger logging.Logger) *reconciler.Reconciler { - return reconciler.New(func(ctx context.Context) { - if !raftNode.IsLeader() { - return - } - if policy, err := query.ReadClusterPolicy(store); err == nil { - if err := raftNode.SetCapabilityGate(activeCapabilityIDs(policy)); err != nil { - logger.WithFields(map[string]any{"error": err}).Errorf("Invalid capability gate in committed cluster policy") - - return - } - } - if err := proposeAuditKeyIfNeeded(ctx, raftNode, builder, store); err != nil { - logger.WithFields(map[string]any{"error": err}).Errorf("Audit key initialization failed; will retry") - - return - } - reconcileClusterPolicy(ctx, admission, store, cfg, raftNode.IsLeader, logger, raftNode.CapabilityMembers) - }, clusterPolicyReconcileInterval) - }, + newClusterPolicyReconciler, ), fx.Decorate(func( params struct { From 3890bd4f285b72bc107e6f24dd0c2f7cc5b9faaa Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Tue, 27 Oct 2026 16:54:44 +0100 Subject: [PATCH 072/127] refactor(capabilities): centralize policy projections --- .../bootstrap/capability_admission_test.go | 12 ++--- internal/bootstrap/module.go | 54 ++----------------- internal/infra/capabilities/capabilities.go | 50 +++++++++++++++++ .../infra/capabilities/capabilities_test.go | 16 ++++++ internal/infra/node/node.go | 13 +---- 5 files changed, 79 insertions(+), 66 deletions(-) diff --git a/internal/bootstrap/capability_admission_test.go b/internal/bootstrap/capability_admission_test.go index f27eee817e..cecf20341c 100644 --- a/internal/bootstrap/capability_admission_test.go +++ b/internal/bootstrap/capability_admission_test.go @@ -11,10 +11,10 @@ import ( "github.com/formancehq/ledger/v3/internal/proto/raftcmdpb" ) -func TestValidateCapabilityActivationEvidenceRejectsIncompleteMembership(t *testing.T) { +func TestValidateActiveRejectsIncompleteMembership(t *testing.T) { t.Parallel() - err := validateCapabilityActivationEvidence( + err := capabilities.ValidateActive( []capabilities.CapabilityID{7}, []capabilities.CapabilityID{7}, []capabilities.Member{{NodeID: 1, Role: capabilities.Voter}}, @@ -23,16 +23,16 @@ func TestValidateCapabilityActivationEvidenceRejectsIncompleteMembership(t *test require.ErrorIs(t, err, capabilities.ErrCapabilityUnsupported) } -func TestValidateCapabilityActivationEvidenceAcceptsNoActivation(t *testing.T) { +func TestValidateActiveAcceptsNoActivation(t *testing.T) { t.Parallel() - require.NoError(t, validateCapabilityActivationEvidence(nil, nil, nil)) + require.NoError(t, capabilities.ValidateActive(nil, nil, nil)) } -func TestValidateCapabilityActivationEvidenceAcceptsConvergedMembership(t *testing.T) { +func TestValidateActiveAcceptsConvergedMembership(t *testing.T) { t.Parallel() - require.NoError(t, validateCapabilityActivationEvidence( + require.NoError(t, capabilities.ValidateActive( []capabilities.CapabilityID{7}, []capabilities.CapabilityID{7}, []capabilities.Member{{NodeID: 1, Role: capabilities.Voter, Capabilities: []capabilities.CapabilityID{7}}}, diff --git a/internal/bootstrap/module.go b/internal/bootstrap/module.go index 9815490dcc..fc1021ad82 100644 --- a/internal/bootstrap/module.go +++ b/internal/bootstrap/module.go @@ -8,7 +8,6 @@ import ( "net" "net/http" "path/filepath" - "slices" "strconv" "strings" "time" @@ -84,24 +83,8 @@ import ( // It is used to decide whether a joining node needs to register as a learner. type walFreshStart bool -func activeCapabilityIDs(policy *commonpb.ClusterPolicy) []uint32 { - if policy == nil { - return nil - } - - required := make([]uint32, 0, len(policy.GetCapabilityActivations())) - for _, activation := range policy.GetCapabilityActivations() { - if activation != nil && activation.GetActive() { - required = append(required, activation.GetId()) - } - } - slices.Sort(required) - - return required -} - func publishCapabilityGate(membership *raftmembership.Membership, policy *commonpb.ClusterPolicy) error { - return membership.SetCapabilityGate(activeCapabilityIDs(policy)) + return membership.SetCapabilityGate(capabilities.ActiveIDs(policy)) } // nodeProvideResult groups the outputs of the Node provider so we can also @@ -134,7 +117,7 @@ func newClusterPolicyReconciler(admission ctrl.Admission, store *dal.Store, buil return } if policy, err := query.ReadClusterPolicy(store); err == nil { - if err := raftNode.SetCapabilityGate(activeCapabilityIDs(policy)); err != nil { + if err := raftNode.SetCapabilityGate(capabilities.ActiveIDs(policy)); err != nil { logger.WithFields(map[string]any{"error": err}).Errorf("Invalid capability gate in committed cluster policy") return @@ -1189,24 +1172,6 @@ func Module() fx.Option { ) } -func validateCapabilityActivationEvidence(supported, active []capabilities.CapabilityID, members []capabilities.Member) error { - if len(active) == 0 { - return nil - } - - ready, err := capabilities.Ready(supported, members) - if err != nil { - return fmt.Errorf("validating capability activation evidence: %w", err) - } - for _, required := range active { - if !slices.Contains(ready, required) { - return fmt.Errorf("%w: capability %d is not acknowledged by every current member", capabilities.ErrCapabilityUnsupported, required) - } - } - - return nil -} - // validateProposalCapabilityAdmission is the production proposal gate shared // by every Node.Propose caller. The proposal carries typed declarations derived // from the final command, while policy and membership are the committed @@ -1216,7 +1181,7 @@ func validateProposalCapabilityAdmission(policy *commonpb.ClusterPolicy, proposa return errors.New("raft proposal gate received a nil proposal") } - if err := validateCapabilityActivationEvidence( + if err := capabilities.ValidateActive( capabilities.SupportedIDs(), proposal.ActivePolicyCapabilities(), members, @@ -1558,7 +1523,7 @@ func tryAddLearner(ctx context.Context, cfg Config, tlsCfg TLSConfig, logger log RaftAddress: cfg.RaftConfig.AdvertiseAddr, ServiceAddress: cfg.ServiceAdvertiseAddr(), InstanceId: instanceID, - Capabilities: capabilityIDsAsUint32(capabilities.SupportedIDs()), + Capabilities: capabilities.IDsAsUint32(capabilities.SupportedIDs()), } dialPeer, err := newMembershipPeerDialer(cfg, tlsCfg) @@ -1743,7 +1708,7 @@ func tryRefreshMembershipOnce(ctx context.Context, cfg Config, tlsCfg TLSConfig, RaftAddress: cfg.RaftConfig.AdvertiseAddr, ServiceAddress: cfg.ServiceAdvertiseAddr(), InstanceId: instanceID, - Capabilities: capabilityIDsAsUint32(capabilities.SupportedIDs()), + Capabilities: capabilities.IDsAsUint32(capabilities.SupportedIDs()), } dialPeer, err := newMembershipPeerDialer(cfg, tlsCfg) @@ -1808,15 +1773,6 @@ func isRetryableMembershipRefreshError(err error) bool { return st.Code() == codes.Unimplemented } -func capabilityIDsAsUint32(ids []capabilities.CapabilityID) []uint32 { - result := make([]uint32, len(ids)) - for i, id := range ids { - result[i] = uint32(id) - } - - return result -} - // isStaleRaftProgress reports whether a FailedPrecondition status carries the // EN-1436 STALE_RAFT_PROGRESS ErrorInfo reason, distinguishing a stale-raft- // progress join rejection (remediation: `remove-node --force`) from the diff --git a/internal/infra/capabilities/capabilities.go b/internal/infra/capabilities/capabilities.go index c93b6e5c17..f495725c0a 100644 --- a/internal/infra/capabilities/capabilities.go +++ b/internal/infra/capabilities/capabilities.go @@ -153,6 +153,36 @@ func FromPolicy(policy *commonpb.ClusterPolicy) State { return s } +// ActiveIDs returns the active capability IDs from a committed policy in +// deterministic order. Nil activations and inactive entries are ignored so +// callers can pass the result directly to membership admission gates. +func ActiveIDs(policy *commonpb.ClusterPolicy) []uint32 { + if policy == nil { + return nil + } + + active := make([]uint32, 0, len(policy.GetCapabilityActivations())) + for _, activation := range policy.GetCapabilityActivations() { + if activation != nil && activation.GetActive() { + active = append(active, activation.GetId()) + } + } + slices.Sort(active) + + return active +} + +// IDsAsUint32 converts the typed capability identifiers to their protobuf wire +// representation at an API boundary. +func IDsAsUint32(ids []CapabilityID) []uint32 { + result := make([]uint32, len(ids)) + for i, id := range ids { + result[i] = uint32(id) + } + + return result +} + func Normalize(ids []CapabilityID) ([]CapabilityID, error) { result := slices.Clone(ids) slices.Sort(result) @@ -198,6 +228,26 @@ func Ready(supported []CapabilityID, members []Member) ([]CapabilityID, error) { return ready, nil } +// ValidateActive verifies that every active capability is acknowledged by +// every current member and is implemented by this binary. +func ValidateActive(supported, active []CapabilityID, members []Member) error { + if len(active) == 0 { + return nil + } + + ready, err := Ready(supported, members) + if err != nil { + return fmt.Errorf("validating capability activation evidence: %w", err) + } + for _, required := range active { + if !slices.Contains(ready, required) { + return fmt.Errorf("%w: capability %d is not acknowledged by every current member", ErrCapabilityUnsupported, required) + } + } + + return nil +} + // Evaluate is pure. Reachability is reported separately from support: a // temporarily unreachable voter is still a blocker for activation, while a // learner is included because it must replay every committed entry. diff --git a/internal/infra/capabilities/capabilities_test.go b/internal/infra/capabilities/capabilities_test.go index 2788ef6a4b..bc5ff2bba6 100644 --- a/internal/infra/capabilities/capabilities_test.go +++ b/internal/infra/capabilities/capabilities_test.go @@ -83,6 +83,22 @@ func TestFromPolicyRoundTrip(t *testing.T) { require.NoError(t, state.Validate()) } +func TestActiveIDsReturnsSortedActiveIDs(t *testing.T) { + t.Parallel() + + policy := &commonpb.ClusterPolicy{ + CapabilityActivations: []*commonpb.CapabilityActivation{ + {Id: 9, Active: true}, + nil, + {Id: 3, Active: false}, + {Id: 4, Active: true}, + }, + } + + require.Equal(t, []uint32{4, 9}, ActiveIDs(policy)) + require.Nil(t, ActiveIDs(nil)) +} + func TestReadyReturnsIntersectionAcrossMembers(t *testing.T) { members := []Member{ {NodeID: 1, Role: Voter, Capabilities: []CapabilityID{1, 2}}, diff --git a/internal/infra/node/node.go b/internal/infra/node/node.go index f412f38917..d35d10edd7 100644 --- a/internal/infra/node/node.go +++ b/internal/infra/node/node.go @@ -98,15 +98,6 @@ var ( ErrNodeSyncing = errors.New("node is syncing") ) -func capabilityIDsAsUint32(ids []capabilities.CapabilityID) []uint32 { - result := make([]uint32, len(ids)) - for i, id := range ids { - result[i] = uint32(id) - } - - return result -} - // StaleRaftProgressReason is the machine-readable ErrorInfo reason attached to // the FailedPrecondition status a leader returns when a JoinAsLearner call hits // ErrNodeStaleProgress (EN-1436). The joining node matches on it to tell the @@ -716,7 +707,7 @@ func NewNode( // writes self for Bootstrap/Restore, but the restart branch never // did — that's the gap the previous bootstrap hook used to cover // before transport wiring moved into Membership. - if err := membership.RegisterWithEvidence(cfg.NodeID, cfg.AdvertiseAddr, cfg.ServiceAdvertiseAddr, cfg.InstanceID, capabilityIDsAsUint32(capabilities.SupportedIDs())); err != nil { + if err := membership.RegisterWithEvidence(cfg.NodeID, cfg.AdvertiseAddr, cfg.ServiceAdvertiseAddr, cfg.InstanceID, capabilities.IDsAsUint32(capabilities.SupportedIDs())); err != nil { return nil, fmt.Errorf("refreshing self in peer store: %w", err) } @@ -4071,7 +4062,7 @@ func initialJoinVoters(peers []Peer, selfID uint64) []uint64 { // through WriteConfChange). func registerInitialPeers(m *membership.Membership, cfg NodeConfig, includeSelf bool) error { if includeSelf { - if err := m.RegisterWithEvidence(cfg.NodeID, cfg.AdvertiseAddr, cfg.ServiceAdvertiseAddr, cfg.InstanceID, capabilityIDsAsUint32(capabilities.SupportedIDs())); err != nil { + if err := m.RegisterWithEvidence(cfg.NodeID, cfg.AdvertiseAddr, cfg.ServiceAdvertiseAddr, cfg.InstanceID, capabilities.IDsAsUint32(capabilities.SupportedIDs())); err != nil { return fmt.Errorf("persisting self in peer store: %w", err) } } From 0f6e4be5ed6c2781266d4c2e9775799885a6606c Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Tue, 27 Oct 2026 16:56:46 +0100 Subject: [PATCH 073/127] refactor(membership): unify registration entrypoint --- internal/bootstrap/service_address_test.go | 2 +- .../membership/committed_peer_refresh_test.go | 2 +- .../infra/membership/identity_rejection_test.go | 2 +- internal/infra/membership/membership.go | 16 +++++----------- .../infra/node/force_remove_durability_test.go | 1 + internal/infra/node/force_remove_run_test.go | 2 +- .../node/historical_removal_lifecycle_test.go | 4 ++-- internal/infra/node/historical_removal_test.go | 2 +- internal/infra/node/node.go | 6 +++--- internal/infra/node/promotion_identity_test.go | 2 +- .../node/startup_membership_removal_test.go | 2 +- 11 files changed, 18 insertions(+), 23 deletions(-) diff --git a/internal/bootstrap/service_address_test.go b/internal/bootstrap/service_address_test.go index 5355e7a643..b989bd30be 100644 --- a/internal/bootstrap/service_address_test.go +++ b/internal/bootstrap/service_address_test.go @@ -72,7 +72,7 @@ func TestServiceAdvertiseAddr_IPv6RemoteRPC(t *testing.T) { 1, "", "", []byte("self-instance-id"), logging.Testing(), membership.NoopClusterPolicyProvider) require.NoError(t, err) require.NoError(t, remote.Register(published.NodeID, published.AdvertiseAddr, - published.ServiceAdvertiseAddr, published.InstanceID)) + published.ServiceAdvertiseAddr, published.InstanceID, nil)) remote.Start() conn := pool.GetConnection(published.NodeID) require.NotNil(t, conn) diff --git a/internal/infra/membership/committed_peer_refresh_test.go b/internal/infra/membership/committed_peer_refresh_test.go index 9b963becff..b885ee7a78 100644 --- a/internal/infra/membership/committed_peer_refresh_test.go +++ b/internal/infra/membership/committed_peer_refresh_test.go @@ -67,7 +67,7 @@ func TestCommittedPeerRefreshUpdatesRaftTransport(t *testing.T) { m.Start() oldInstanceID := []byte("old-instance-id-") - require.NoError(t, m.Register(2, "old:7000", "old:8000", oldInstanceID)) + require.NoError(t, m.Register(2, "old:7000", "old:8000", oldInstanceID, nil)) require.Equal(t, "old:7000", raftPool.GetPeerAddress(2)) newInstanceID := []byte("new-instance-id-") diff --git a/internal/infra/membership/identity_rejection_test.go b/internal/infra/membership/identity_rejection_test.go index 6a2726adcf..8b12879184 100644 --- a/internal/infra/membership/identity_rejection_test.go +++ b/internal/infra/membership/identity_rejection_test.go @@ -16,7 +16,7 @@ func TestWriteConfChangeRejectsRemovalWithoutContext(t *testing.T) { m := newTestMembership(t) identity := fixedInstanceID(9) - require.NoError(t, m.Register(9, "peer:7777", "peer:8888", identity)) + require.NoError(t, m.Register(9, "peer:7777", "peer:8888", identity, nil)) original := m.PeerAddresses() data, err := proto.Marshal(&raftpb.ConfChangeV2{Changes: []*raftpb.ConfChangeSingle{{ Type: new(raftpb.ConfChangeRemoveNode), NodeId: new(uint64(9)), diff --git a/internal/infra/membership/membership.go b/internal/infra/membership/membership.go index 6cf454c89f..987c3943a5 100644 --- a/internal/infra/membership/membership.go +++ b/internal/infra/membership/membership.go @@ -389,17 +389,11 @@ func (m *Membership) Remove(nodeID uint64) { // Register writes a peer through Pebble (own session) AND the cache, in // lockstep. Used by lifecycle paths that bypass the FSM: bootstrap -// initial-peer persistence and ForceRemoveNode (dual: Unregister). -// instanceID is always the peer's 16-byte WAL/PVC identity. Identity-less -// membership rows are rejected rather than persisted as a partial member. -func (m *Membership) Register(nodeID uint64, raftAddr, serviceAddr string, instanceID []byte) error { - return m.RegisterWithEvidence(nodeID, raftAddr, serviceAddr, instanceID, nil) -} - -// RegisterWithEvidence is the durable/cache-atomic lifecycle variant of -// Register. It is used for self refresh and bootstrap rows that already carry -// a capability proof. -func (m *Membership) RegisterWithEvidence(nodeID uint64, raftAddr, serviceAddr string, instanceID []byte, capabilityIDs []uint32) error { +// initial-peer persistence, self refresh, and ForceRemoveNode's dual +// Unregister operation. instanceID is always the peer's 16-byte WAL/PVC +// identity. Identity-less membership rows are rejected rather than persisted +// as a partial member. capabilityIDs carries the peer's capability evidence. +func (m *Membership) Register(nodeID uint64, raftAddr, serviceAddr string, instanceID []byte, capabilityIDs []uint32) error { // A restart refreshes the local addresses. Preserve evidence for the same // concrete instance when the caller has no new proof; a changed instance // must never inherit the previous pod's capabilities. diff --git a/internal/infra/node/force_remove_durability_test.go b/internal/infra/node/force_remove_durability_test.go index dcc5df907a..582a192447 100644 --- a/internal/infra/node/force_remove_durability_test.go +++ b/internal/infra/node/force_remove_durability_test.go @@ -216,6 +216,7 @@ func newForceRemoveHarness( fmt.Sprintf("node-%d:7000", id), fmt.Sprintf("node-%d:8000", id), fmt.Appendf(nil, "%016d", id), + nil, )) } diff --git a/internal/infra/node/force_remove_run_test.go b/internal/infra/node/force_remove_run_test.go index 2a5bc88d6a..4ec34cfca7 100644 --- a/internal/infra/node/force_remove_run_test.go +++ b/internal/infra/node/force_remove_run_test.go @@ -34,7 +34,7 @@ func TestForceRemoveNodeDurabilityFailureStopsRun(t *testing.T) { started: make(chan struct{}), } m := newTestMembership(t) - require.NoError(t, m.Register(2, "node-2:7000", "node-2:8000", []byte("0000000000000002"))) + require.NoError(t, m.Register(2, "node-2:7000", "node-2:8000", []byte("0000000000000002"), nil)) n, err := NewNode( NodeConfig{ NodeID: 1, diff --git a/internal/infra/node/historical_removal_lifecycle_test.go b/internal/infra/node/historical_removal_lifecycle_test.go index baff1d3af3..797d457d44 100644 --- a/internal/infra/node/historical_removal_lifecycle_test.go +++ b/internal/infra/node/historical_removal_lifecycle_test.go @@ -119,7 +119,7 @@ func TestHistoricalRemovalLifecycleDurableCleanupCannotClearLaterEvent(t *testin n, setup := newHistoricalRemovalLifecycleSetup(t) oldIdentity := []byte("old-instance-id-") newIdentity := []byte("new-instance-id-") - require.NoError(t, n.membership.Register(2, "peer:7777", "peer:8888", oldIdentity)) + require.NoError(t, n.membership.Register(2, "peer:7777", "peer:8888", oldIdentity, nil)) oldPending, err := n.trackCommittedRemoval(2, oldIdentity, 1) require.NoError(t, err) newPending, err := n.trackCommittedRemoval(2, newIdentity, 3) @@ -179,7 +179,7 @@ func TestNewNodeReplaysHistoricalRemovalCycles(t *testing.T) { fixture, setup := newHistoricalRemovalLifecycleSetup(t) oldIdentity := []byte("old-instance-id-") newIdentity := []byte("new-instance-id-") - require.NoError(t, fixture.membership.Register(2, "peer:7777", "peer:8888", oldIdentity)) + require.NoError(t, fixture.membership.Register(2, "peer:7777", "peer:8888", oldIdentity, nil)) // Retain the pre-crash ConfState: all committed changes exist only in the // WAL, so NewNode must recover their durable membership effects by replay. require.NoError(t, setup.wal.UpdateSnapshotConfState(&raftpb.ConfState{Voters: []uint64{1}, Learners: []uint64{2}})) diff --git a/internal/infra/node/historical_removal_test.go b/internal/infra/node/historical_removal_test.go index adda7ad0e5..001dbec650 100644 --- a/internal/infra/node/historical_removal_test.go +++ b/internal/infra/node/historical_removal_test.go @@ -41,7 +41,7 @@ func TestHistoricalRemovalFollowerReplay(t *testing.T) { require.NoError(t, err) oldIdentity := []byte("old-instance-id-") newIdentity := []byte("new-instance-id-") - require.NoError(t, m.Register(2, "old:7777", "old:8888", oldIdentity)) + require.NoError(t, m.Register(2, "old:7777", "old:8888", oldIdentity, nil)) confState := &raftpb.ConfState{Voters: []uint64{1, 3}, Learners: []uint64{2}} require.NoError(t, setup.wal.UpdateSnapshotConfState(confState)) diff --git a/internal/infra/node/node.go b/internal/infra/node/node.go index d35d10edd7..2fa4b74e04 100644 --- a/internal/infra/node/node.go +++ b/internal/infra/node/node.go @@ -707,7 +707,7 @@ func NewNode( // writes self for Bootstrap/Restore, but the restart branch never // did — that's the gap the previous bootstrap hook used to cover // before transport wiring moved into Membership. - if err := membership.RegisterWithEvidence(cfg.NodeID, cfg.AdvertiseAddr, cfg.ServiceAdvertiseAddr, cfg.InstanceID, capabilities.IDsAsUint32(capabilities.SupportedIDs())); err != nil { + if err := membership.Register(cfg.NodeID, cfg.AdvertiseAddr, cfg.ServiceAdvertiseAddr, cfg.InstanceID, capabilities.IDsAsUint32(capabilities.SupportedIDs())); err != nil { return nil, fmt.Errorf("refreshing self in peer store: %w", err) } @@ -4062,7 +4062,7 @@ func initialJoinVoters(peers []Peer, selfID uint64) []uint64 { // through WriteConfChange). func registerInitialPeers(m *membership.Membership, cfg NodeConfig, includeSelf bool) error { if includeSelf { - if err := m.RegisterWithEvidence(cfg.NodeID, cfg.AdvertiseAddr, cfg.ServiceAdvertiseAddr, cfg.InstanceID, capabilities.IDsAsUint32(capabilities.SupportedIDs())); err != nil { + if err := m.Register(cfg.NodeID, cfg.AdvertiseAddr, cfg.ServiceAdvertiseAddr, cfg.InstanceID, capabilities.IDsAsUint32(capabilities.SupportedIDs())); err != nil { return fmt.Errorf("persisting self in peer store: %w", err) } } @@ -4070,7 +4070,7 @@ func registerInitialPeers(m *membership.Membership, cfg NodeConfig, includeSelf // Peer discovery carries the durable identity of every configured member; // never create an address-only membership row. for _, p := range cfg.Peers { - if err := m.RegisterWithEvidence(p.ID, p.Address, p.ServiceAddress, p.InstanceID, p.Capabilities); err != nil { + if err := m.Register(p.ID, p.Address, p.ServiceAddress, p.InstanceID, p.Capabilities); err != nil { return fmt.Errorf("persisting initial peer %d: %w", p.ID, err) } } diff --git a/internal/infra/node/promotion_identity_test.go b/internal/infra/node/promotion_identity_test.go index 72f322285f..2d8d87cff9 100644 --- a/internal/infra/node/promotion_identity_test.go +++ b/internal/infra/node/promotion_identity_test.go @@ -108,7 +108,7 @@ func TestCheckAndPromoteLearnersMissingRowStopsRun(t *testing.T) { Voters: []uint64{1}, Learners: []uint64{2}, })) m := newTestMembership(t) - require.NoError(t, m.Register(2, "node-2:7000", "node-2:8000", []byte("0000000000000002"))) + require.NoError(t, m.Register(2, "node-2:7000", "node-2:8000", []byte("0000000000000002"), nil)) n, err := NewNode( NodeConfig{ NodeID: 1, diff --git a/internal/infra/node/startup_membership_removal_test.go b/internal/infra/node/startup_membership_removal_test.go index 8f42aedbe9..6deee68e53 100644 --- a/internal/infra/node/startup_membership_removal_test.go +++ b/internal/infra/node/startup_membership_removal_test.go @@ -29,7 +29,7 @@ func TestNewNodeAcceptsReplayedRemovalBeforeConfStatePersistence(t *testing.T) { 1, "self:7777", "self:8888", []byte("self-instance-id"), logging.Testing(), membership.NoopClusterPolicyProvider) require.NoError(t, err) identity := []byte("peer-instance-id") - require.NoError(t, m.Register(2, "peer:7777", "peer:8888", identity)) + require.NoError(t, m.Register(2, "peer:7777", "peer:8888", identity, nil)) require.NoError(t, setup.wal.UpdateSnapshotConfState(&raftpb.ConfState{Voters: []uint64{1, 2}})) context, err := membership.MarshalConfChangeContext(membership.ConfChangeContext{InstanceID: identity}) require.NoError(t, err) From db7cdddfa3c2da28aad996152317ec53466d8a13 Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Tue, 27 Oct 2026 17:40:39 +0100 Subject: [PATCH 074/127] refactor(capabilities): remove redundant proposal stamper --- internal/infra/capabilities/capabilities.go | 10 ---------- internal/infra/capabilities/capabilities_test.go | 6 ++++-- 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/internal/infra/capabilities/capabilities.go b/internal/infra/capabilities/capabilities.go index f495725c0a..1ec313e262 100644 --- a/internal/infra/capabilities/capabilities.go +++ b/internal/infra/capabilities/capabilities.go @@ -108,16 +108,6 @@ func RequiredCapabilities(proposal *raftcmdpb.Proposal) ([]CapabilityID, error) return nil, nil } -// StampProposal replaces required_capabilities with the deterministic result -// of RequiredCapabilities. Producers call this after rebuilding the final -// execution plan and immediately before marshaling; repeated calls are safe on -// retry and rebuild paths. -func StampProposal(proposal *raftcmdpb.Proposal) error { - _, err := PrepareProposal(proposal) - - return err -} - // PrepareProposal derives and stamps the semantic capability declaration and // returns the typed result for the caller to pass alongside the serialized // proposal. Keeping the result avoids decoding the same proposal again during diff --git a/internal/infra/capabilities/capabilities_test.go b/internal/infra/capabilities/capabilities_test.go index bc5ff2bba6..d1373d6ab3 100644 --- a/internal/infra/capabilities/capabilities_test.go +++ b/internal/infra/capabilities/capabilities_test.go @@ -69,9 +69,11 @@ func TestEvaluateAllowsActiveCapabilityWhenAllMembersAdvertiseAndReachIt(t *test require.Empty(t, decision.Blockers) } -func TestStampProposalClearsFutureDeclarations(t *testing.T) { +func TestPrepareProposalClearsFutureDeclarations(t *testing.T) { proposal := &raftcmdpb.Proposal{RequiredCapabilities: []uint32{99}} - require.NoError(t, StampProposal(proposal)) + prepared, err := PrepareProposal(proposal) + require.NoError(t, err) + require.Empty(t, prepared.RequiredCapabilities()) require.Empty(t, proposal.GetRequiredCapabilities()) } From ebfe298c3ef4cd9dd683958850da8d029fb887e7 Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Fri, 30 Oct 2026 14:36:18 +0100 Subject: [PATCH 075/127] fix: reconcile proposal API after rebase --- internal/infra/capabilities/capabilities.go | 35 +++++++++++++++++++ internal/infra/membership/membership_test.go | 2 +- internal/infra/node/drain.go | 6 ++-- internal/infra/node/drain_test.go | 36 ++++++++++---------- 4 files changed, 57 insertions(+), 22 deletions(-) diff --git a/internal/infra/capabilities/capabilities.go b/internal/infra/capabilities/capabilities.go index 1ec313e262..7c27c7c0ac 100644 --- a/internal/infra/capabilities/capabilities.go +++ b/internal/infra/capabilities/capabilities.go @@ -125,6 +125,41 @@ func PrepareProposal(proposal *raftcmdpb.Proposal) (PreparedProposal, error) { return PreparedProposal{required: required}, nil } +// ValidateFinalProposal verifies that the declaration carried by the +// serialized proposal matches both its semantic payload and the metadata +// captured by the proposal factory. It is a defensive check at the Raft +// boundary; normal admission performs the same gate before reserving an index. +func ValidateFinalProposal(data []byte, declared []CapabilityID) error { + proposal := new(raftcmdpb.Proposal) + if err := proposal.UnmarshalVT(data); err != nil { + return fmt.Errorf("decode final proposal: %w", err) + } + expected, err := RequiredCapabilities(proposal) + if err != nil { + return err + } + wire := make([]CapabilityID, len(proposal.GetRequiredCapabilities())) + for i, id := range proposal.GetRequiredCapabilities() { + wire[i] = CapabilityID(id) + } + wire, err = Normalize(wire) + if err != nil { + return err + } + declared, err = Normalize(declared) + if err != nil { + return err + } + if !slices.Equal(wire, expected) { + return fmt.Errorf("%w: wire declaration does not match semantic payload", ErrMissingCapability) + } + if !slices.Equal(wire, declared) { + return fmt.Errorf("%w: declaration does not match final payload", ErrMissingCapability) + } + + return nil +} + func (s State) Validate() error { return nil } diff --git a/internal/infra/membership/membership_test.go b/internal/infra/membership/membership_test.go index 2b63f32372..cc5e7823a6 100644 --- a/internal/infra/membership/membership_test.go +++ b/internal/infra/membership/membership_test.go @@ -444,7 +444,7 @@ func TestMembership_SnapshotDonorCandidates(t *testing.T) { require.NoError(t, ps.Put(2, "peer-2:7000", "peer-2:8000", fixedInstanceID(2))) require.NoError(t, ps.Put(testSelfNodeID, testSelfRaftAddr, testSelfServiceAddr, fixedInstanceID(0x42))) - m, err := NewMembership(ps, noopTransport{}, noopPool{}, testSelfNodeID, testSelfRaftAddr, testSelfServiceAddr, fixedInstanceID(0x42), logging.Testing()) + m, err := NewMembership(ps, noopTransport{}, noopPool{}, testSelfNodeID, testSelfRaftAddr, testSelfServiceAddr, fixedInstanceID(0x42), logging.Testing(), NoopClusterPolicyProvider) require.NoError(t, err) require.Equal(t, []uint64{2, 9}, m.SnapshotDonorCandidates()) diff --git a/internal/infra/node/drain.go b/internal/infra/node/drain.go index ec0758f771..1987af554e 100644 --- a/internal/infra/node/drain.go +++ b/internal/infra/node/drain.go @@ -53,7 +53,7 @@ func (node *Node) waitForPriorMembership(ctx context.Context) error { if err != nil { return fmt.Errorf("marshaling membership barrier: %w", err) } - proposal := NewProposal(cmd.GetId(), data) + proposal := newProposal(cmd.GetId(), data, nil) proposal.membershipBarrier = true node.indexTracker.Lock() future, err := node.Propose(ctx, proposal) @@ -118,7 +118,7 @@ func (node *Node) BeginDrain(ctx context.Context, epoch string, members []uint64 if err != nil { return nil, fmt.Errorf("marshaling drain marker: %w", err) } - proposal := NewProposal(cmd.GetId(), data) + proposal := newProposal(cmd.GetId(), data, nil) proposal.drainMarker = true proposal.expectedTerm = expectedTerm @@ -182,7 +182,7 @@ func (node *Node) EndDrain(ctx context.Context, epoch string, expectedTerm uint6 if err != nil { return nil, fmt.Errorf("marshaling drain release: %w", err) } - proposal := NewProposal(cmd.GetId(), data) + proposal := newProposal(cmd.GetId(), data, nil) proposal.releaseMarker = true proposal.expectedTerm = expectedTerm node.indexTracker.Lock() diff --git a/internal/infra/node/drain_test.go b/internal/infra/node/drain_test.go index 4066cdc96b..585a9f3fd4 100644 --- a/internal/infra/node/drain_test.go +++ b/internal/infra/node/drain_test.go @@ -84,7 +84,7 @@ func TestProposeRejectsAfterDrainEnqueue(t *testing.T) { t.Parallel() n, _ := newTestMaintenanceNode(t) n.drainOutstanding = 1 - _, err := n.Propose(t.Context(), NewProposal(1, []byte("business"))) + _, err := n.Propose(t.Context(), newProposal(1, []byte("business"), nil)) require.ErrorIs(t, err, domain.ErrMaintenanceMode) } @@ -130,7 +130,7 @@ func TestDrainProposalRejectsPreflightFromPriorLeadershipTerm(t *testing.T) { require.Contains(t, rn.Status().Progress, uint64(2)) require.False(t, rn.HasReady()) - marker := NewProposal(1, []byte("stale drain marker for member 1")) + marker := newProposal(1, []byte("stale drain marker for member 1"), nil) marker.drainMarker = true marker.expectedTerm = preflightTerm n.indexTracker.Lock() @@ -146,7 +146,7 @@ func TestDrainProposalRejectsPreflightFromPriorLeadershipTerm(t *testing.T) { require.False(t, n.drainActiveOrPending()) require.False(t, rn.HasReady(), "stale marker must not enter the Raft log") - release := NewProposal(2, []byte("stale release marker")) + release := newProposal(2, []byte("stale release marker"), nil) release.releaseMarker = true release.expectedTerm = preflightTerm n.indexTracker.Lock() @@ -167,7 +167,7 @@ func TestMembershipFenceOutlivesTimedOutDrainCaller(t *testing.T) { n, setup := newTestMaintenanceNode(t) n.proposeCh = make(chan *Proposal, 1) n.indexTracker = NewIndexTracker(1) - marker := NewProposal(1, []byte("drain marker")) + marker := newProposal(1, []byte("drain marker"), nil) marker.drainMarker = true n.indexTracker.Lock() future, err := n.Propose(t.Context(), marker) @@ -206,7 +206,7 @@ func TestReleaseMarkerCanCrossClosedAdmission(t *testing.T) { n.proposeCh = make(chan *Proposal, 1) n.indexTracker = NewIndexTracker(1) n.drainOutstanding = 1 - release := NewProposal(1, []byte("release")) + release := newProposal(1, []byte("release"), nil) release.releaseMarker = true n.indexTracker.Lock() _, err := n.Propose(t.Context(), release) @@ -219,20 +219,20 @@ func TestDroppedDrainMarkerReopensAdmission(t *testing.T) { n, _ := newTestMaintenanceNode(t) n.proposeCh = make(chan *Proposal, 3) n.indexTracker = NewIndexTracker(1) - first := NewProposal(1, []byte("first marker")) + first := newProposal(1, []byte("first marker"), nil) first.drainMarker = true n.indexTracker.Lock() _, err := n.Propose(t.Context(), first) n.indexTracker.Unlock() require.NoError(t, err) - second := NewProposal(2, []byte("second marker")) + second := newProposal(2, []byte("second marker"), nil) second.drainMarker = true n.indexTracker.Lock() secondFuture, err := n.Propose(t.Context(), second) n.indexTracker.Unlock() require.NoError(t, err) n.applier.ResolveDroppedFuture(first.commandID, ErrNotLeader) - _, err = n.Propose(t.Context(), NewProposal(1, []byte("still blocked"))) + _, err = n.Propose(t.Context(), newProposal(1, []byte("still blocked"), nil)) require.ErrorIs(t, err, domain.ErrMaintenanceMode) // The original RPC may have timed out. The applier still owns the // callback, so a later-term truncation releases the remaining slot. @@ -244,7 +244,7 @@ func TestDroppedDrainMarkerReopensAdmission(t *testing.T) { _, err = secondFuture.Wait(t.Context()) require.ErrorIs(t, err, ErrLeadershipLost) n.indexTracker.Lock() - _, err = n.Propose(t.Context(), NewProposal(3, []byte("admitted after definitive drops"))) + _, err = n.Propose(t.Context(), newProposal(3, []byte("admitted after definitive drops"), nil)) n.indexTracker.Unlock() require.NoError(t, err) } @@ -254,7 +254,7 @@ func TestRejectedQueuedDrainMarkerReopensAdmission(t *testing.T) { n, _ := newTestMaintenanceNode(t) n.proposeCh = make(chan *Proposal, 2) n.indexTracker = NewIndexTracker(1) - marker := NewProposal(1, []byte("drain marker")) + marker := newProposal(1, []byte("drain marker"), nil) marker.drainMarker = true n.indexTracker.Lock() _, err := n.Propose(t.Context(), marker) @@ -263,7 +263,7 @@ func TestRejectedQueuedDrainMarkerReopensAdmission(t *testing.T) { require.Same(t, marker, <-n.proposeCh) n.rejectQueuedProposal(marker, ErrNotLeader) n.indexTracker.Lock() - _, err = n.Propose(t.Context(), NewProposal(2, []byte("business after drop"))) + _, err = n.Propose(t.Context(), newProposal(2, []byte("business after drop"), nil)) n.indexTracker.Unlock() require.NoError(t, err) } @@ -273,18 +273,18 @@ func TestDrainEnqueueOrdersInFlightProposals(t *testing.T) { n, _ := newTestMaintenanceNode(t) n.proposeCh = make(chan *Proposal, 3) n.indexTracker = NewIndexTracker(1) - pre := NewProposal(1, []byte("already admitted")) + pre := newProposal(1, []byte("already admitted"), nil) n.indexTracker.Lock() _, err := n.Propose(t.Context(), pre) n.indexTracker.Unlock() require.NoError(t, err) - marker := NewProposal(2, []byte("drain marker")) + marker := newProposal(2, []byte("drain marker"), nil) marker.drainMarker = true n.indexTracker.Lock() _, err = n.Propose(t.Context(), marker) n.indexTracker.Unlock() require.NoError(t, err) - _, err = n.Propose(t.Context(), NewProposal(3, []byte("later write"))) + _, err = n.Propose(t.Context(), newProposal(3, []byte("later write"), nil)) require.ErrorIs(t, err, domain.ErrMaintenanceMode) require.Same(t, pre, <-n.proposeCh) require.Same(t, marker, <-n.proposeCh) @@ -296,25 +296,25 @@ func TestOldDrainDropCannotOpenNewEpoch(t *testing.T) { n, setup := newTestMaintenanceNode(t) n.proposeCh = make(chan *Proposal, 3) n.indexTracker = NewIndexTracker(1) - old := NewProposal(1, []byte("old marker")) + old := newProposal(1, []byte("old marker"), nil) old.drainMarker = true n.indexTracker.Lock() _, err := n.Propose(t.Context(), old) n.indexTracker.Unlock() require.NoError(t, err) setup.sharedState.SetDrainReleased(7) - newMarker := NewProposal(2, []byte("new marker")) + newMarker := newProposal(2, []byte("new marker"), nil) newMarker.drainMarker = true n.indexTracker.Lock() _, err = n.Propose(t.Context(), newMarker) n.indexTracker.Unlock() require.NoError(t, err) n.applier.ResolveDroppedFuture(old.commandID, ErrNotLeader) - _, err = n.Propose(t.Context(), NewProposal(3, []byte("must stay closed"))) + _, err = n.Propose(t.Context(), newProposal(3, []byte("must stay closed"), nil)) require.ErrorIs(t, err, domain.ErrMaintenanceMode) n.applier.ResolveDroppedFuture(newMarker.commandID, ErrNotLeader) n.indexTracker.Lock() - _, err = n.Propose(t.Context(), NewProposal(4, []byte("new marker dropped"))) + _, err = n.Propose(t.Context(), newProposal(4, []byte("new marker dropped"), nil)) n.indexTracker.Unlock() require.NoError(t, err) } From ad420e5ae056941372b976beaa49b2100ebb7f6e Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Fri, 30 Oct 2026 14:37:04 +0100 Subject: [PATCH 076/127] fix: preserve lightweight proposal test setup --- .../propose_cluster_config_defaults_test.go | 2 +- internal/infra/node/node.go | 16 +++++++++++----- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/internal/bootstrap/propose_cluster_config_defaults_test.go b/internal/bootstrap/propose_cluster_config_defaults_test.go index f73b1566d2..2df393dd0f 100644 --- a/internal/bootstrap/propose_cluster_config_defaults_test.go +++ b/internal/bootstrap/propose_cluster_config_defaults_test.go @@ -82,7 +82,7 @@ func TestProposeClusterConfig_DefaultThreshold(t *testing.T) { registry := state.NewStateRegistry(c, attrs) machine, err := state.NewMachine(logger, registry, state.NewCacheSnapshotter(logger, registry, nil), store, dal.NewSentinelFactory(store, false), meters, nil, state.NewSharedState(), signal.NewNotifications(), nil, - "threshold-test", 0, func(*raftpb.Entry, *dal.WriteSession) error { return nil }) + "threshold-test", 0, func(*raftpb.Entry, *dal.WriteSession) error { return nil }, state.NoopCapabilityPolicyCommitHandler) require.NoError(t, err) t.Cleanup(machine.Close) result, err := machine.ApplyEntries(t.Context(), store, &raftpb.Entry{ diff --git a/internal/infra/node/node.go b/internal/infra/node/node.go index 2fa4b74e04..e9ac18b798 100644 --- a/internal/infra/node/node.go +++ b/internal/infra/node/node.go @@ -2395,13 +2395,19 @@ func (node *Node) Propose(ctx context.Context, proposal *Proposal) (*futures.Fut if err := node.terminalError(); err != nil { return nil, err } - if released := node.fsm.DrainReleaseIndex(); released > node.drainReleaseSeen { - node.drainReleaseSeen = released - node.drainGeneration++ - node.drainOutstanding = 0 + if node.fsm != nil { + if released := node.fsm.DrainReleaseIndex(); released > node.drainReleaseSeen { + node.drainReleaseSeen = released + node.drainGeneration++ + node.drainOutstanding = 0 + } } if !proposal.drainMarker && !proposal.releaseMarker && !proposal.membershipBarrier { - if _, index := node.fsm.Drain(); index != 0 || node.drainOutstanding != 0 { + var drainIndex uint64 + if node.fsm != nil { + _, drainIndex = node.fsm.Drain() + } + if drainIndex != 0 || node.drainOutstanding != 0 { return nil, domain.ErrMaintenanceMode } } From e45cdb0196b87cbb47dfd4f31bd27b44a9ea54f1 Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Fri, 30 Oct 2026 14:39:15 +0100 Subject: [PATCH 077/127] fix: register available learner command --- cmd/ledgerctl/cluster/command.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/ledgerctl/cluster/command.go b/cmd/ledgerctl/cluster/command.go index d50df528a8..14cbd1a2ff 100644 --- a/cmd/ledgerctl/cluster/command.go +++ b/cmd/ledgerctl/cluster/command.go @@ -15,7 +15,7 @@ func NewCommand() *cobra.Command { cmd.AddCommand(NewWatchCommand()) cmd.AddCommand(NewDiskUsageCommand()) cmd.AddCommand(NewTransferLeaderCommand()) - cmd.AddCommand(NewPromoteLearnerCommand()) + cmd.AddCommand(NewAddLearnerCommand()) cmd.AddCommand(NewRemoveNodeCommand()) cmd.AddCommand(NewMaintenanceCommand()) cmd.AddCommand(NewBeginDrainCommand()) From 8b5bae7cbb001a607ecf14e1aedb3c3ffabaf1c4 Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Fri, 30 Oct 2026 14:41:16 +0100 Subject: [PATCH 078/127] fix: respect current learner CLI surface --- cmd/ledgerctl/cluster/command.go | 1 - 1 file changed, 1 deletion(-) diff --git a/cmd/ledgerctl/cluster/command.go b/cmd/ledgerctl/cluster/command.go index 14cbd1a2ff..6fa06d7bd7 100644 --- a/cmd/ledgerctl/cluster/command.go +++ b/cmd/ledgerctl/cluster/command.go @@ -15,7 +15,6 @@ func NewCommand() *cobra.Command { cmd.AddCommand(NewWatchCommand()) cmd.AddCommand(NewDiskUsageCommand()) cmd.AddCommand(NewTransferLeaderCommand()) - cmd.AddCommand(NewAddLearnerCommand()) cmd.AddCommand(NewRemoveNodeCommand()) cmd.AddCommand(NewMaintenanceCommand()) cmd.AddCommand(NewBeginDrainCommand()) From 7fdb750a33315c63dea6e98c678f00b9f477706c Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Mon, 2 Nov 2026 23:36:00 +0100 Subject: [PATCH 079/127] fix(ledger): submit validated proposals once --- internal/infra/node/node.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/internal/infra/node/node.go b/internal/infra/node/node.go index e9ac18b798..172b4222fc 100644 --- a/internal/infra/node/node.go +++ b/internal/infra/node/node.go @@ -2497,8 +2497,6 @@ func (node *Node) handleProposal(p *Proposal) { if err == nil { if validationErr := capabilities.ValidateFinalProposal(p.data, p.requiredCapabilities); validationErr != nil { err = validationErr - } else { - err = node.rawNode.Propose(p.data) } } if err == nil { From 1e7870cc2682d4c2fce041ef58d59da44dad4c41 Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Mon, 2 Nov 2026 23:39:48 +0100 Subject: [PATCH 080/127] fix(ledger): drop legacy refresh compatibility --- .../subsystems/consensus/capabilities.md | 7 +++---- internal/bootstrap/module.go | 15 +++------------ .../bootstrap/module_refresh_membership_test.go | 10 +++++----- 3 files changed, 11 insertions(+), 21 deletions(-) diff --git a/docs/technical/architecture/subsystems/consensus/capabilities.md b/docs/technical/architecture/subsystems/consensus/capabilities.md index 23cf575480..aae508eb31 100644 --- a/docs/technical/architecture/subsystems/consensus/capabilities.md +++ b/docs/technical/architecture/subsystems/consensus/capabilities.md @@ -156,10 +156,9 @@ the node and its Raft gRPC server are started, a node whose WAL carries `CLUSTER_JOINED` sends an internal `RefreshMembership` request to a configured peer. Followers forward that request to the current leader over the Raft gRPC server. The node can therefore receive inbound Raft traffic before the refresh -commits; the refresh is an evidence update, not a pre-traffic barrier. During -the rc.1/rc.2 rolling upgrade, an older peer that returns `Unimplemented` is -treated as transient and the node keeps retrying in the background until a -compatible peer is available. +commits; the refresh is an evidence update, not a pre-traffic barrier. A +temporarily unavailable leader or quorum is retried until the cluster can +commit the refresh. The leader validates the identity and capability evidence against the active policy, then proposes a `ConfChangeUpdateNode` containing the current endpoints and capabilities. diff --git a/internal/bootstrap/module.go b/internal/bootstrap/module.go index fc1021ad82..9e1ba595fd 100644 --- a/internal/bootstrap/module.go +++ b/internal/bootstrap/module.go @@ -1432,9 +1432,8 @@ func membershipRefreshHook(cfg Config, logger logging.Logger) fx.Hook { // permanent rejection (for example a removed incarnation or invalid // identity) must abort startup rather than letting stale WAL membership // evidence escape into the cluster. Transient connectivity failures - // (including Unavailable) and rolling-upgrade compatibility responses - // are handed to the background retry loop; permanent rejections remain - // startup failures. + // (including Unavailable) are handed to the background retry loop; + // permanent rejections remain startup failures. err := tryRefreshMembershipForStartup(ctx, cfg, cfg.TLSConfig, logger) if err == nil { return nil @@ -1447,9 +1446,6 @@ func membershipRefreshHook(cfg Config, logger logging.Logger) fx.Hook { cancel = refreshCancel go func() { if err := tryRefreshMembership(refreshCtx, cfg, cfg.TLSConfig, logger); err != nil { - // TODO(rc.3): remove this compatibility fallback once every - // supported peer implements RefreshMembership. The leader-side - // reconciler keeps retrying transient evidence until convergence. logger.WithFields(map[string]any{"error": err}).Errorf("Membership capability refresh failed; continuing with conservative evidence") } }() @@ -1765,12 +1761,7 @@ func isRetryableMembershipRefreshError(err error) bool { return true } - // TODO(rc.3): remove this compatibility retry once every supported peer - // implements RefreshMembership. During the rc.1/rc.2 rolling upgrade, an - // older peer returns Unimplemented rather than Unavailable for this newly - // introduced RPC; treating it as transient lets the node keep trying newer - // peers without making the startup hook fail immediately. - return st.Code() == codes.Unimplemented + return false } // isStaleRaftProgress reports whether a FailedPrecondition status carries the diff --git a/internal/bootstrap/module_refresh_membership_test.go b/internal/bootstrap/module_refresh_membership_test.go index c42269b11e..c61a95e61e 100644 --- a/internal/bootstrap/module_refresh_membership_test.go +++ b/internal/bootstrap/module_refresh_membership_test.go @@ -27,7 +27,7 @@ func TestIsRetryableMembershipRefreshError(t *testing.T) { want bool }{ {name: "unavailable", code: codes.Unavailable, want: true}, - {name: "old peer missing rpc", code: codes.Unimplemented, want: true}, + {name: "unsupported peer", code: codes.Unimplemented, want: false}, {name: "invalid request", code: codes.InvalidArgument, want: false}, } { t.Run(tc.name, func(t *testing.T) { @@ -89,16 +89,16 @@ func refreshTestConfig(t *testing.T, peerAddress string) Config { } } -func TestTryRefreshMembershipRetriesUnimplementedUntilPeerSupportsRPC(t *testing.T) { +func TestTryRefreshMembershipDoesNotRetryUnsupportedPeer(t *testing.T) { t.Parallel() - peer := &retryingRefreshServer{firstErr: status.Error(codes.Unimplemented, "old peer")} + peer := &retryingRefreshServer{firstErr: status.Error(codes.Unimplemented, "unsupported peer")} cfg := refreshTestConfig(t, serveRefreshMembership(t, peer)) ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) t.Cleanup(cancel) - require.NoError(t, tryRefreshMembership(ctx, cfg, cfg.TLSConfig, logging.Testing())) - require.Equal(t, int32(2), peer.attempts.Load()) + require.ErrorContains(t, tryRefreshMembership(ctx, cfg, cfg.TLSConfig, logging.Testing()), "unsupported peer") + require.Equal(t, int32(1), peer.attempts.Load()) } func TestTryRefreshMembershipStopsRetryingWhenContextIsCanceled(t *testing.T) { From e0697fbfa6fc9537182ef37f3f4bf6b54892adcd Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Tue, 3 Nov 2026 11:47:35 +0100 Subject: [PATCH 081/127] Revert "fix(ledger): drop legacy refresh compatibility" This reverts commit 8ad4a22cb379d3e65f059630bf5025527b31904f. --- .../subsystems/consensus/capabilities.md | 7 ++++--- internal/bootstrap/module.go | 15 ++++++++++++--- .../bootstrap/module_refresh_membership_test.go | 10 +++++----- 3 files changed, 21 insertions(+), 11 deletions(-) diff --git a/docs/technical/architecture/subsystems/consensus/capabilities.md b/docs/technical/architecture/subsystems/consensus/capabilities.md index aae508eb31..23cf575480 100644 --- a/docs/technical/architecture/subsystems/consensus/capabilities.md +++ b/docs/technical/architecture/subsystems/consensus/capabilities.md @@ -156,9 +156,10 @@ the node and its Raft gRPC server are started, a node whose WAL carries `CLUSTER_JOINED` sends an internal `RefreshMembership` request to a configured peer. Followers forward that request to the current leader over the Raft gRPC server. The node can therefore receive inbound Raft traffic before the refresh -commits; the refresh is an evidence update, not a pre-traffic barrier. A -temporarily unavailable leader or quorum is retried until the cluster can -commit the refresh. +commits; the refresh is an evidence update, not a pre-traffic barrier. During +the rc.1/rc.2 rolling upgrade, an older peer that returns `Unimplemented` is +treated as transient and the node keeps retrying in the background until a +compatible peer is available. The leader validates the identity and capability evidence against the active policy, then proposes a `ConfChangeUpdateNode` containing the current endpoints and capabilities. diff --git a/internal/bootstrap/module.go b/internal/bootstrap/module.go index 9e1ba595fd..fc1021ad82 100644 --- a/internal/bootstrap/module.go +++ b/internal/bootstrap/module.go @@ -1432,8 +1432,9 @@ func membershipRefreshHook(cfg Config, logger logging.Logger) fx.Hook { // permanent rejection (for example a removed incarnation or invalid // identity) must abort startup rather than letting stale WAL membership // evidence escape into the cluster. Transient connectivity failures - // (including Unavailable) are handed to the background retry loop; - // permanent rejections remain startup failures. + // (including Unavailable) and rolling-upgrade compatibility responses + // are handed to the background retry loop; permanent rejections remain + // startup failures. err := tryRefreshMembershipForStartup(ctx, cfg, cfg.TLSConfig, logger) if err == nil { return nil @@ -1446,6 +1447,9 @@ func membershipRefreshHook(cfg Config, logger logging.Logger) fx.Hook { cancel = refreshCancel go func() { if err := tryRefreshMembership(refreshCtx, cfg, cfg.TLSConfig, logger); err != nil { + // TODO(rc.3): remove this compatibility fallback once every + // supported peer implements RefreshMembership. The leader-side + // reconciler keeps retrying transient evidence until convergence. logger.WithFields(map[string]any{"error": err}).Errorf("Membership capability refresh failed; continuing with conservative evidence") } }() @@ -1761,7 +1765,12 @@ func isRetryableMembershipRefreshError(err error) bool { return true } - return false + // TODO(rc.3): remove this compatibility retry once every supported peer + // implements RefreshMembership. During the rc.1/rc.2 rolling upgrade, an + // older peer returns Unimplemented rather than Unavailable for this newly + // introduced RPC; treating it as transient lets the node keep trying newer + // peers without making the startup hook fail immediately. + return st.Code() == codes.Unimplemented } // isStaleRaftProgress reports whether a FailedPrecondition status carries the diff --git a/internal/bootstrap/module_refresh_membership_test.go b/internal/bootstrap/module_refresh_membership_test.go index c61a95e61e..c42269b11e 100644 --- a/internal/bootstrap/module_refresh_membership_test.go +++ b/internal/bootstrap/module_refresh_membership_test.go @@ -27,7 +27,7 @@ func TestIsRetryableMembershipRefreshError(t *testing.T) { want bool }{ {name: "unavailable", code: codes.Unavailable, want: true}, - {name: "unsupported peer", code: codes.Unimplemented, want: false}, + {name: "old peer missing rpc", code: codes.Unimplemented, want: true}, {name: "invalid request", code: codes.InvalidArgument, want: false}, } { t.Run(tc.name, func(t *testing.T) { @@ -89,16 +89,16 @@ func refreshTestConfig(t *testing.T, peerAddress string) Config { } } -func TestTryRefreshMembershipDoesNotRetryUnsupportedPeer(t *testing.T) { +func TestTryRefreshMembershipRetriesUnimplementedUntilPeerSupportsRPC(t *testing.T) { t.Parallel() - peer := &retryingRefreshServer{firstErr: status.Error(codes.Unimplemented, "unsupported peer")} + peer := &retryingRefreshServer{firstErr: status.Error(codes.Unimplemented, "old peer")} cfg := refreshTestConfig(t, serveRefreshMembership(t, peer)) ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) t.Cleanup(cancel) - require.ErrorContains(t, tryRefreshMembership(ctx, cfg, cfg.TLSConfig, logging.Testing()), "unsupported peer") - require.Equal(t, int32(1), peer.attempts.Load()) + require.NoError(t, tryRefreshMembership(ctx, cfg, cfg.TLSConfig, logging.Testing())) + require.Equal(t, int32(2), peer.attempts.Load()) } func TestTryRefreshMembershipStopsRetryingWhenContextIsCanceled(t *testing.T) { From 376e6421544678a438f96e525bc78b1b89c9df73 Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Tue, 3 Nov 2026 11:48:42 +0100 Subject: [PATCH 082/127] docs(ledger): clarify rc capability rollout boundary --- .../architecture/subsystems/consensus/capabilities.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/technical/architecture/subsystems/consensus/capabilities.md b/docs/technical/architecture/subsystems/consensus/capabilities.md index 23cf575480..2aab7d5fe9 100644 --- a/docs/technical/architecture/subsystems/consensus/capabilities.md +++ b/docs/technical/architecture/subsystems/consensus/capabilities.md @@ -157,9 +157,11 @@ the node and its Raft gRPC server are started, a node whose WAL carries peer. Followers forward that request to the current leader over the Raft gRPC server. The node can therefore receive inbound Raft traffic before the refresh commits; the refresh is an evidence update, not a pre-traffic barrier. During -the rc.1/rc.2 rolling upgrade, an older peer that returns `Unimplemented` is +the rc.2/rc.3 rolling upgrade, an older peer that returns `Unimplemented` is treated as transient and the node keeps retrying in the background until a -compatible peer is available. +compatible peer is available. The capability-dependent protocol cutover is +reserved for the coordinated rc.3/rc.4 restart, after every node runs the +barrier-aware binary. The leader validates the identity and capability evidence against the active policy, then proposes a `ConfChangeUpdateNode` containing the current endpoints and capabilities. From 21b6cbcee663b48a15230b39cd5ea9c3f7254055 Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Tue, 3 Nov 2026 11:51:45 +0100 Subject: [PATCH 083/127] fix(ledger): remove obsolete proposal policy wrapper --- internal/infra/plan/runner.go | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/internal/infra/plan/runner.go b/internal/infra/plan/runner.go index 73620a1723..127b5efb8a 100644 --- a/internal/infra/plan/runner.go +++ b/internal/infra/plan/runner.go @@ -143,16 +143,6 @@ func (p *Builder) Run( return nil, ErrUnsupportedTechnicalComposition } - // Every producer that uses the shared runner (mirror, events, backup and - // bootstrap maintenance) gets the same final policy check. Admission wraps - // its business writes separately with businessWrite=true; this default - // wrapper covers technical/control-plane proposals. - if p.store != nil { - proposer = NewCapabilityPolicyProposer(proposer, func() (*commonpb.ClusterPolicy, error) { - return query.ReadClusterPolicy(p.store) - }, p.readMembers, false) - } - // Guarantee a unique command ID. The applier keys FSM futures by // `cmd.Id` (used by the node proposal factory); two in-flight // proposals sharing Id=0 would overwrite each other in From d80396db19c16c238e528c41ae5885511556de7f Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Tue, 3 Nov 2026 11:52:01 +0100 Subject: [PATCH 084/127] test(ledger): adapt proposal composition tests --- internal/infra/plan/proposal_composition_test.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/internal/infra/plan/proposal_composition_test.go b/internal/infra/plan/proposal_composition_test.go index f0305cc6fe..fb61b9e839 100644 --- a/internal/infra/plan/proposal_composition_test.go +++ b/internal/infra/plan/proposal_composition_test.go @@ -46,10 +46,10 @@ func TestRun_RejectsUnsupportedTechnicalComposition(t *testing.T) { } proposer := &recordingProposer{tracker: tracker} marshalled := false - result, err := builder.Run(t.Context(), cmd, build, func(p *raftcmdpb.Proposal) ([]byte, error) { + result, err := builder.Run(t.Context(), cmd, build, func(p *raftcmdpb.Proposal) error { marshalled = true - return p.MarshalVT() + return nil }, proposer) require.ErrorIs(t, err, ErrUnsupportedTechnicalComposition) require.Nil(t, result) @@ -104,7 +104,7 @@ func TestRun_AllowsSupportedTechnicalComposition(t *testing.T) { cmd.Orders = []*raftcmdpb.Order{{}} } proposeErr := errors.New("reached proposer") - _, err = builder.Run(t.Context(), cmd, build, (*raftcmdpb.Proposal).MarshalVT, stubFailingProposer{err: proposeErr}) + _, err = builder.Run(t.Context(), cmd, build, func(*raftcmdpb.Proposal) error { return nil }, stubFailingProposer{err: proposeErr}) require.ErrorIs(t, err, proposeErr) }) } @@ -129,7 +129,7 @@ func TestRun_AllowsStandaloneOperationalUpdate(t *testing.T) { require.NoError(t, err) cmd := &raftcmdpb.Proposal{TechnicalUpdates: []*raftcmdpb.TechnicalUpdate{update}} proposeErr := errors.New("reached proposer") - _, err = builder.Run(t.Context(), cmd, build, (*raftcmdpb.Proposal).MarshalVT, stubFailingProposer{err: proposeErr}) + _, err = builder.Run(t.Context(), cmd, build, func(*raftcmdpb.Proposal) error { return nil }, stubFailingProposer{err: proposeErr}) require.ErrorIs(t, err, proposeErr) }) } From 783438abbb6807c712cb1311018be4c7e87b80f3 Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Tue, 3 Nov 2026 11:52:16 +0100 Subject: [PATCH 085/127] test(ledger): update recovery fixture dependencies --- internal/bootstrap/join_transport_recovery_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/bootstrap/join_transport_recovery_test.go b/internal/bootstrap/join_transport_recovery_test.go index fed29fd516..aa4fa91980 100644 --- a/internal/bootstrap/join_transport_recovery_test.go +++ b/internal/bootstrap/join_transport_recovery_test.go @@ -136,12 +136,12 @@ func newJoinRecoveryNode(t *testing.T, id uint64, addr string, peers []node.Peer t.Cleanup(func() { require.NoError(t, pool.Close()) }) t.Cleanup(func() { require.NoError(t, servicePool.Close()) }) tr := node.NewTransport(logger, pool, meters, id, node.TransportConfig{Reception: []int{32, 32, 32}, Send: []int{32, 32, 32}}, "join-recovery", 1024, addr, addr) - member, err := membership.NewMembership(membership.NewPeerStore(store), tr, servicePool, id, addr, addr, instanceID, logger) + member, err := membership.NewMembership(membership.NewPeerStore(store), tr, servicePool, id, addr, addr, instanceID, logger, nil) require.NoError(t, err) cacheStore, err := cache.New(1000, meter) require.NoError(t, err) registry := state.NewStateRegistry(cacheStore, attributes.New()) - machine, err := state.NewMachine(logger, registry, state.NewCacheSnapshotter(logger, registry, nil), store, dal.NewSentinelFactory(store, false), meters, nil, state.NewSharedState(), signal.NewNotifications(), nil, "join-recovery", 0, member.WriteConfChange) + machine, err := state.NewMachine(logger, registry, state.NewCacheSnapshotter(logger, registry, nil), store, dal.NewSentinelFactory(store, false), meters, nil, state.NewSharedState(), signal.NewNotifications(), nil, "join-recovery", 0, member.WriteConfChange, state.NoopCapabilityPolicyCommitHandler) require.NoError(t, err) t.Cleanup(machine.Close) recovery := state.NewRecovery(machine, store) From 10e3ab64755fe79758b339055ac29145890a1447 Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Tue, 3 Nov 2026 11:52:40 +0100 Subject: [PATCH 086/127] test(ledger): provide membership policy fixture --- internal/bootstrap/join_transport_recovery_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/bootstrap/join_transport_recovery_test.go b/internal/bootstrap/join_transport_recovery_test.go index aa4fa91980..dafadfaeab 100644 --- a/internal/bootstrap/join_transport_recovery_test.go +++ b/internal/bootstrap/join_transport_recovery_test.go @@ -136,7 +136,7 @@ func newJoinRecoveryNode(t *testing.T, id uint64, addr string, peers []node.Peer t.Cleanup(func() { require.NoError(t, pool.Close()) }) t.Cleanup(func() { require.NoError(t, servicePool.Close()) }) tr := node.NewTransport(logger, pool, meters, id, node.TransportConfig{Reception: []int{32, 32, 32}, Send: []int{32, 32, 32}}, "join-recovery", 1024, addr, addr) - member, err := membership.NewMembership(membership.NewPeerStore(store), tr, servicePool, id, addr, addr, instanceID, logger, nil) + member, err := membership.NewMembership(membership.NewPeerStore(store), tr, servicePool, id, addr, addr, instanceID, logger, membership.NoopClusterPolicyProvider) require.NoError(t, err) cacheStore, err := cache.New(1000, meter) require.NoError(t, err) From 1b8b35548e33cdae093051b21478e1a296f7bbf6 Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Tue, 3 Nov 2026 12:04:49 +0100 Subject: [PATCH 087/127] test(ledger): update store and mirror fixtures --- cmd/ledgerctl/store/bootstrap_retry_test.go | 2 +- internal/application/mirror/worker_postgres_integration_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/ledgerctl/store/bootstrap_retry_test.go b/cmd/ledgerctl/store/bootstrap_retry_test.go index 1ba971a681..3bb946868d 100644 --- a/cmd/ledgerctl/store/bootstrap_retry_test.go +++ b/cmd/ledgerctl/store/bootstrap_retry_test.go @@ -61,7 +61,7 @@ func newBootstrapRetryAdmission(t *testing.T, store *dal.Store) *admission.Admis notifier.EXPECT().NotifyConfigChanged().AnyTimes() machine, err := state.NewMachine(logger, registry, state.NewCacheSnapshotter(logger, registry, nil), store, dal.NewSentinelFactory(store, false), meters, keys, shared, notifier, nil, - "", 0, func(*raftpb.Entry, *dal.WriteSession) error { return nil }) + "", 0, func(*raftpb.Entry, *dal.WriteSession) error { return nil }, state.NoopCapabilityPolicyCommitHandler) require.NoError(t, err) require.NoError(t, state.NewRecovery(machine, store).RecoverState()) next := machine.State.LastAppliedIndex + 1 diff --git a/internal/application/mirror/worker_postgres_integration_test.go b/internal/application/mirror/worker_postgres_integration_test.go index e1412c54cf..2d59164fc5 100644 --- a/internal/application/mirror/worker_postgres_integration_test.go +++ b/internal/application/mirror/worker_postgres_integration_test.go @@ -65,7 +65,7 @@ func TestWorker_PostgresGapCommitAndRollback(t *testing.T) { c, err := cache.New(100, meters.Meter("test")) require.NoError(t, err) registry := state.NewStateRegistry(c, attrs) - machine, err := state.NewMachine(logger, registry, state.NewCacheSnapshotter(logger, registry, nil), store, dal.NewSentinelFactory(store, false), meters, keystore.NewKeyStore(), state.NewSharedState(), signal.NewNotifications(), nil, "test-cluster", 0, func(*raftpb.Entry, *dal.WriteSession) error { return nil }) + machine, err := state.NewMachine(logger, registry, state.NewCacheSnapshotter(logger, registry, nil), store, dal.NewSentinelFactory(store, false), meters, keystore.NewKeyStore(), state.NewSharedState(), signal.NewNotifications(), nil, "test-cluster", 0, func(*raftpb.Entry, *dal.WriteSession) error { return nil }, state.NoopCapabilityPolicyCommitHandler) require.NoError(t, err) writeMirrorMetadataPolicy(t, store, mirrorMetadataPolicy()) require.NoError(t, state.NewRecovery(machine, store).RecoverState()) From b02dd91f53cdc5965cb23ada8bd792d4df221135 Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Tue, 3 Nov 2026 18:30:50 +0100 Subject: [PATCH 088/127] test: use existing learner registration method --- internal/application/membership/membership_node_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/application/membership/membership_node_test.go b/internal/application/membership/membership_node_test.go index 40e0cf953e..61a4757deb 100644 --- a/internal/application/membership/membership_node_test.go +++ b/internal/application/membership/membership_node_test.go @@ -150,7 +150,7 @@ func TestService_LearnerRegistrationCommitsAndPreservesCallerIntent(t *testing.T if bootJoin { return s.JoinAsLearner(ctx, nodeID, raftAddr, serviceAddr, instanceID, capabilities) } - return s.AddLearnerWithCapabilities(ctx, nodeID, raftAddr, serviceAddr, instanceID, capabilities) + return s.AddLearner(ctx, nodeID, raftAddr, serviceAddr, instanceID, capabilities) } require.NoError(t, register(ctx, 2, "peer:7777", "peer:8888", []byte("peer-instance-id"), nil)) peers, err := s.ListPeers(ctx) @@ -227,7 +227,7 @@ func TestService_AddLearnerWithCapabilitiesPersistsEvidence(t *testing.T) { ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second) defer cancel() - require.NoError(t, s.AddLearnerWithCapabilities(ctx, 2, "peer:7777", "peer:8888", []byte("peer-instance-id"), []uint32{7, 9})) + require.NoError(t, s.AddLearner(ctx, 2, "peer:7777", "peer:8888", []byte("peer-instance-id"), []uint32{7, 9})) peer := s.infraMembership.PeerAddresses()[2] require.Equal(t, []uint32{7, 9}, peer.Capabilities) From 4f7548f166c2541dde893e3a5b5868cfdbe65c4a Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Tue, 3 Nov 2026 18:33:48 +0100 Subject: [PATCH 089/127] chore: reconcile raft audit manifest --- docs/technical/audits/raft-membership-leadership.json | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/docs/technical/audits/raft-membership-leadership.json b/docs/technical/audits/raft-membership-leadership.json index 258971cdf6..fe50e3ae78 100644 --- a/docs/technical/audits/raft-membership-leadership.json +++ b/docs/technical/audits/raft-membership-leadership.json @@ -93,7 +93,8 @@ "Every MsgSnap the leader hands to the transport must be reported to its RawNode with ReportSnapshot for the destination peer. etcd/raft pauses all appends to a peer in StateSnapshot, so a lost MsgSnap or an acknowledgement that arrives after the leader compacted past the sent snapshot must not strand the peer without entries for the rest of the leader's term.", "Controlled drain preflights every current member for protocol support, fences prior membership changes before marker enqueue, and rejects marker or release proposals if leadership term changed since preflight. It blocks normal membership changes from enqueue through applied release. Its completion certificate requires matching current membership and each captured member locally persisting the same cutoff; leader Match alone is insufficient.", "After a full-stop upgrade, production Raft writers serialize ExecutionBatch and SetClusterPolicyProposal payloads directly. No source binary may coexist with the target or reopen its WAL after new writes. Historical replay must preserve exact audit bytes and hashes. Native policy controls are unaudited operational state and must not be lowered to orders. Compare final state across encodings, verify each audit chain and exact policy omission, and preserve the historical prefix byte-for-byte. Incremental manifests must preserve policy-only changes as well as mixed legacy/native deltas. Both forms preserve coverage targets, checkpoint boundaries, technical-only HLC behavior, and post-checkpoint policy restore. Preserve legacy decoding and test reopening the released store/WAL, replaying retained legacy entries, writing new payloads, and restarting.", - "Maintenance must fsync a snapshot file, persist its matching WAL snapshot record, and only then publish the snapshot in memory or compact. A WAL-record failure leaves the file orphaned, keeps the previous snapshot authoritative, and allows an idle retry; compaction remains behind the minimum of the durable FSM cursor and the Raft apply-ack cursor." + "Maintenance must fsync a snapshot file, persist its matching WAL snapshot record, and only then publish the snapshot in memory or compact. A WAL-record failure leaves the file orphaned, keeps the previous snapshot authoritative, and allows an idle retry; compaction remains behind the minimum of the durable FSM cursor and the Raft apply-ack cursor.", + "Capability evidence and policy transitions are replicated membership state: refresh, bootstrap join, promotion, and policy activation must preserve capability evidence, identity, and rolling-upgrade compatibility. An old peer returning Unimplemented for RefreshMembership must not prevent startup, while an active policy must not admit a member or proposal without committed evidence." ], "adversarial_questions": [ "When production learner join preflight commits membership before the joining Raft listener starts, does a failed first optional-TLS probe retain both the committed endpoint and a live retry owner? After listener startup, can ordinary traffic recover replication, catch-up and automatic promotion without re-registration or voter restart, while removal and shutdown terminate pending retries?", @@ -116,7 +117,8 @@ "Can snapshot installation discard entries carried by the same Ready, and what in-memory snapshot/cache state and durable WAL prefix remain if candidate WAL preparation or either directory rename fails before storage acknowledgement?", "After the leader sends a MsgSnap, can the peer's Progress remain in StateSnapshot indefinitely because the snapshot was lost, the peer restarted, or leader maintenance compacted past the sent index before the acknowledgement, leaving a node that reports a normal sync status but never advances?", "Can a mixed-version member pass preflight, or can a membership change slip between preflight and marker application? If leadership is lost and regained after preflight while another leader changes membership, does the old-term marker or release get rejected before Raft append? Can a timed-out marker, lost member, leader transfer or force removal make the operator accept an obsolete captured set or a follower that has not persisted the cutoff?", - "If snapshot-file persistence succeeds but the matching WAL snapshot-record append fails, does the in-memory snapshot remain unchanged, does an idle retry recreate the same snapshot, and can any maintenance tick compact before the record is durable?" + "If snapshot-file persistence succeeds but the matching WAL snapshot-record append fails, does the in-memory snapshot remain unchanged, does an idle retry recreate the same snapshot, and can any maintenance tick compact before the record is durable?", + "During a follower-first rolling upgrade, what happens when RefreshMembership reaches an older peer that returns Unimplemented? Does startup remain available with conservative evidence and eventual retry, and does a later committed refresh converge the membership row? Can capability activation or declaration be applied against stale or pre-refresh evidence, or bypass the authoritative policy gate?" ], "dynamic_checks_to_consider": [ "hold a fresh learner's listener closed through production join preflight with optional TLS, record the current term, committed addition index, durable membership and valid instance identity, then open the listener and require autonomous replication and promotion without another registration; independently test initial probe fail-then-success, actual Raft stream delivery, endpoint replacement, and pending-owner removal/shutdown (internal/bootstrap/join_transport_recovery_test.go and internal/infra/node/transport_initial_probe_test.go)", @@ -141,6 +143,7 @@ "drive a real leader RawNode through sendReadyMessages and finishReady while it sends a MsgSnap, compact its log past that snapshot, then deliver the late acknowledgement and require a fresh MsgSnap instead of a paused peer (internal/infra/node/leader_snapshot_report_test.go)", "Exercise old-member Unimplemented preflight, concurrent add/promotion/removal, lost marker response, leader loss plus membership change plus re-election, and member restart; assert old-term proposals are rejected before Raft append, marker/cutoff identity, and each member persisted-index certificate before Drained.", "stop the original checkpoint donor permanently while a syncing voter is in PrepareSnapshot, keep another checkpointable voter alive, force the syncing voter to win on its log, retain the serving gate until restore completes, then compare literal business balances on both survivors and acknowledge a new write (tests/e2e/cluster/checkpoint_donor_loss_test.go); separately lose a donor during PrepareSnapshot and after partial FetchFile bytes reach disk, require complete-session fallback, exact attempt counts, staging and progress reset, correct final bytes, and temporary-file cleanup (internal/application/ctrl/snapshot_fetcher_donor_loss_test.go)", - "inject a successful snapshot-file write followed by a failed WAL snapshot-record append, run an idle maintenance retry, and assert that the previous snapshot remains authoritative until the record is durable and that no required WAL prefix is compacted (internal/storage/wal/wal_default_test.go and internal/infra/node/maintenance_test.go)" + "inject a successful snapshot-file write followed by a failed WAL snapshot-record append, run an idle maintenance retry, and assert that the previous snapshot remains authoritative until the record is durable and that no required WAL prefix is compacted (internal/storage/wal/wal_default_test.go and internal/infra/node/maintenance_test.go)", + "Run a follower-first rolling upgrade against a peer that returns Unimplemented for RefreshMembership; assert readiness opens with conservative evidence, retries continue, and a later compatible peer commits the refresh. Then exercise capability declaration/activation and membership ConfChange in the same or adjacent FSM batch, recording committed policy, evidence, ConfState, and final admission decisions on every replica." ] } From 838b899ad0390c105a63aa8a3b3fdeb405947998 Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Thu, 5 Nov 2026 20:09:58 +0100 Subject: [PATCH 090/127] test: reconcile post-rebase constructor coverage --- internal/application/mirror/worker_diagnostic_test.go | 2 +- internal/infra/node/proposal_test.go | 7 ++----- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/internal/application/mirror/worker_diagnostic_test.go b/internal/application/mirror/worker_diagnostic_test.go index f642e26b90..c086b3805b 100644 --- a/internal/application/mirror/worker_diagnostic_test.go +++ b/internal/application/mirror/worker_diagnostic_test.go @@ -238,7 +238,7 @@ func TestWorker_TransportErrorsDoNotDiscloseCredentials(t *testing.T) { registry := state.NewStateRegistry(c, attrs) keys := keystore.NewKeyStore() shared := state.NewSharedState() - machine, err := state.NewMachine(logger, registry, state.NewCacheSnapshotter(logger, registry, nil), store, dal.NewSentinelFactory(store, false), meters, keys, shared, signal.NewNotifications(), nil, "test-cluster", 0, func(*raftpb.Entry, *dal.WriteSession) error { return nil }) + machine, err := state.NewMachine(logger, registry, state.NewCacheSnapshotter(logger, registry, nil), store, dal.NewSentinelFactory(store, false), meters, keys, shared, signal.NewNotifications(), nil, "test-cluster", 0, func(*raftpb.Entry, *dal.WriteSession) error { return nil }, state.NoopCapabilityPolicyCommitHandler) require.NoError(t, err) writeMirrorMetadataPolicy(t, store, mirrorMetadataPolicy()) require.NoError(t, state.NewRecovery(machine, store).RecoverState()) diff --git a/internal/infra/node/proposal_test.go b/internal/infra/node/proposal_test.go index df16d83d20..7e27f763f5 100644 --- a/internal/infra/node/proposal_test.go +++ b/internal/infra/node/proposal_test.go @@ -35,19 +35,16 @@ func TestProposalSetPredictedIndexPatchesSerializedPayload(t *testing.T) { require.Equal(t, uint64(42), decoded.GetPredictedIndex()) } -func TestNewProposalReservesPredictedIndexSuffix(t *testing.T) { +func TestNewProposalAppendsPredictedIndexWithoutMutatingPrefix(t *testing.T) { command := &raftcmdpb.Proposal{Id: 1} proposal, err := NewProposal(1, command) require.NoError(t, err) before := proposal.Data() - reserved := cap(proposal.data) - require.GreaterOrEqual(t, reserved-len(proposal.data), predictedIndexEncodedSize) - proposal.SetPredictedIndex(42) after := proposal.Data() + require.Greater(t, len(after), len(before)) require.Equal(t, before, after[:len(before)]) - require.Equal(t, reserved, cap(proposal.data), "predicted-index patch must not grow the backing buffer") } func TestNewProposalDerivesActivePolicyCapabilities(t *testing.T) { From 629be60e1cc6755d2980554bbfe984e9f0d70fbb Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Thu, 5 Nov 2026 21:56:02 +0100 Subject: [PATCH 091/127] chore: fix imports after rebase --- internal/infra/plan/runner.go | 1 - 1 file changed, 1 deletion(-) diff --git a/internal/infra/plan/runner.go b/internal/infra/plan/runner.go index 127b5efb8a..29b45864fc 100644 --- a/internal/infra/plan/runner.go +++ b/internal/infra/plan/runner.go @@ -11,7 +11,6 @@ import ( "github.com/formancehq/ledger/v3/internal/pkg/commands" "github.com/formancehq/ledger/v3/internal/pkg/futures" "github.com/formancehq/ledger/v3/internal/pkg/proposals" - "github.com/formancehq/ledger/v3/internal/proto/commonpb" "github.com/formancehq/ledger/v3/internal/proto/raftcmdpb" ) From 550331b5eb16fe1f07a82bea0d139be0d799c520 Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Thu, 5 Nov 2026 22:09:20 +0100 Subject: [PATCH 092/127] test: reconcile post-rebase CI expectations --- internal/infra/plan/proposal_composition_test.go | 4 ++-- scripts/agentcheckpr/selector_test.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/infra/plan/proposal_composition_test.go b/internal/infra/plan/proposal_composition_test.go index fb61b9e839..9181c27471 100644 --- a/internal/infra/plan/proposal_composition_test.go +++ b/internal/infra/plan/proposal_composition_test.go @@ -75,10 +75,10 @@ func TestRun_RejectsUnsupportedNativeTechnicalComposition(t *testing.T) { cmd := &raftcmdpb.Proposal{Payload: &raftcmdpb.Proposal_Batch{Batch: batch}} proposer := &recordingProposer{tracker: tracker} marshalled := false - result, err := builder.Run(t.Context(), cmd, build, func(p *raftcmdpb.Proposal) ([]byte, error) { + result, err := builder.Run(t.Context(), cmd, build, func(p *raftcmdpb.Proposal) error { marshalled = true - return p.MarshalVT() + return nil }, proposer) require.ErrorIs(t, err, ErrUnsupportedTechnicalComposition) require.Nil(t, result) diff --git a/scripts/agentcheckpr/selector_test.go b/scripts/agentcheckpr/selector_test.go index a2972a220c..8c8c93cbd3 100644 --- a/scripts/agentcheckpr/selector_test.go +++ b/scripts/agentcheckpr/selector_test.go @@ -86,7 +86,7 @@ func TestSelectsLocalValidationGatesFromCompleteDiff(t *testing.T) { { name: "Raft protocol change requires capability contract", paths: []string{"misc/proto/common.proto"}, - expected: []string{"agent-check-full", "test-e2e", "protocol-revision", "capability-contract"}, + expected: []string{"agent-check-full", "test-e2e", "protocol-revision", "proto-lint", "capability-contract"}, }, { name: "FSM behavior change requires capability contract", From 0c8fbedbe86fe81b1f28bd87c0ed97216be6dc99 Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Thu, 5 Nov 2026 22:33:07 +0100 Subject: [PATCH 093/127] chore(proto): regenerate with CI toolchain --- internal/proto/auditpb/audit.pb.go | 2 +- internal/proto/auditpb/audit_vtproto.pb.go | 14 +- .../cluster_bootstrap.pb.go | 2 +- .../cluster_bootstrap_grpc.pb.go | 2 +- .../cluster_bootstrap_vtproto.pb.go | 2 +- internal/proto/clusterpb/cluster.pb.go | 2 +- internal/proto/clusterpb/cluster_grpc.pb.go | 2 +- .../proto/clusterpb/cluster_vtproto.pb.go | 2 +- internal/proto/commonpb/common_vtproto.pb.go | 1756 +---------------- internal/proto/eventspb/events.pb.go | 2 +- internal/proto/eventspb/events_vtproto.pb.go | 2 +- internal/proto/proposalpb/proposal.pb.go | 2 +- .../proto/proposalpb/proposal_vtproto.pb.go | 2 +- internal/proto/raftcmdpb/raft_cmd.pb.go | 2 +- .../proto/raftcmdpb/raft_cmd_vtproto.pb.go | 356 +--- .../rafttransportpb/raft_transport.pb.go | 2 +- .../rafttransportpb/raft_transport_grpc.pb.go | 2 +- .../raft_transport_vtproto.pb.go | 26 +- internal/proto/restorepb/restore.pb.go | 2 +- internal/proto/restorepb/restore_grpc.pb.go | 2 +- .../proto/restorepb/restore_vtproto.pb.go | 14 +- internal/proto/servicepb/bucket.pb.go | 2 +- internal/proto/servicepb/bucket_grpc.pb.go | 2 +- internal/proto/servicepb/bucket_vtproto.pb.go | 308 +-- internal/proto/signaturepb/signature.pb.go | 2 +- .../proto/signaturepb/signature_vtproto.pb.go | 2 +- internal/proto/snapshotpb/snapshot.pb.go | 2 +- internal/proto/snapshotpb/snapshot_grpc.pb.go | 2 +- .../proto/snapshotpb/snapshot_vtproto.pb.go | 2 +- 29 files changed, 29 insertions(+), 2491 deletions(-) diff --git a/internal/proto/auditpb/audit.pb.go b/internal/proto/auditpb/audit.pb.go index 898ca0222e..6b3586577d 100644 --- a/internal/proto/auditpb/audit.pb.go +++ b/internal/proto/auditpb/audit.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.12 -// protoc v7.36.2 +// protoc v7.34.2 // source: audit.proto package auditpb diff --git a/internal/proto/auditpb/audit_vtproto.pb.go b/internal/proto/auditpb/audit_vtproto.pb.go index 92a855946e..f2f0abab4b 100644 --- a/internal/proto/auditpb/audit_vtproto.pb.go +++ b/internal/proto/auditpb/audit_vtproto.pb.go @@ -1,5 +1,5 @@ // Code generated by protoc-gen-go-vtproto. DO NOT EDIT. -// protoc-gen-go-vtproto version: v0.6.1-0.20240319094008-0393e58bdf10 +// protoc-gen-go-vtproto version: (devel) // source: audit.proto package auditpb @@ -488,10 +488,6 @@ func (m *AuditEntry_Success) MarshalToSizedBufferVT(dAtA []byte) (int, error) { i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x22 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x22 } return len(dAtA) - i, nil } @@ -511,10 +507,6 @@ func (m *AuditEntry_Failure) MarshalToSizedBufferVT(dAtA []byte) (int, error) { i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x2a - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x2a } return len(dAtA) - i, nil } @@ -752,8 +744,6 @@ func (m *AuditEntry_Success) SizeVT() (n int) { if m.Success != nil { l = m.Success.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -766,8 +756,6 @@ func (m *AuditEntry_Failure) SizeVT() (n int) { if m.Failure != nil { l = m.Failure.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } diff --git a/internal/proto/clusterbootstrappb/cluster_bootstrap.pb.go b/internal/proto/clusterbootstrappb/cluster_bootstrap.pb.go index 76ae2b342f..2da477a78f 100644 --- a/internal/proto/clusterbootstrappb/cluster_bootstrap.pb.go +++ b/internal/proto/clusterbootstrappb/cluster_bootstrap.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.12 -// protoc v7.36.2 +// protoc v7.34.2 // source: cluster_bootstrap.proto package clusterbootstrappb diff --git a/internal/proto/clusterbootstrappb/cluster_bootstrap_grpc.pb.go b/internal/proto/clusterbootstrappb/cluster_bootstrap_grpc.pb.go index e0cf9f4033..f626353548 100644 --- a/internal/proto/clusterbootstrappb/cluster_bootstrap_grpc.pb.go +++ b/internal/proto/clusterbootstrappb/cluster_bootstrap_grpc.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: // - protoc-gen-go-grpc v1.6.2 -// - protoc v7.36.2 +// - protoc v7.34.2 // source: cluster_bootstrap.proto package clusterbootstrappb diff --git a/internal/proto/clusterbootstrappb/cluster_bootstrap_vtproto.pb.go b/internal/proto/clusterbootstrappb/cluster_bootstrap_vtproto.pb.go index 0fd577baa8..8124847378 100644 --- a/internal/proto/clusterbootstrappb/cluster_bootstrap_vtproto.pb.go +++ b/internal/proto/clusterbootstrappb/cluster_bootstrap_vtproto.pb.go @@ -1,5 +1,5 @@ // Code generated by protoc-gen-go-vtproto. DO NOT EDIT. -// protoc-gen-go-vtproto version: v0.6.1-0.20240319094008-0393e58bdf10 +// protoc-gen-go-vtproto version: (devel) // source: cluster_bootstrap.proto package clusterbootstrappb diff --git a/internal/proto/clusterpb/cluster.pb.go b/internal/proto/clusterpb/cluster.pb.go index 290fa17735..1de4622a87 100644 --- a/internal/proto/clusterpb/cluster.pb.go +++ b/internal/proto/clusterpb/cluster.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.12 -// protoc v7.36.2 +// protoc v7.34.2 // source: cluster.proto package clusterpb diff --git a/internal/proto/clusterpb/cluster_grpc.pb.go b/internal/proto/clusterpb/cluster_grpc.pb.go index 16b45bd11c..ee73c7400e 100644 --- a/internal/proto/clusterpb/cluster_grpc.pb.go +++ b/internal/proto/clusterpb/cluster_grpc.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: // - protoc-gen-go-grpc v1.6.2 -// - protoc v7.36.2 +// - protoc v7.34.2 // source: cluster.proto package clusterpb diff --git a/internal/proto/clusterpb/cluster_vtproto.pb.go b/internal/proto/clusterpb/cluster_vtproto.pb.go index 7bbbcc9c9f..36a91e11e2 100644 --- a/internal/proto/clusterpb/cluster_vtproto.pb.go +++ b/internal/proto/clusterpb/cluster_vtproto.pb.go @@ -1,5 +1,5 @@ // Code generated by protoc-gen-go-vtproto. DO NOT EDIT. -// protoc-gen-go-vtproto version: v0.6.1-0.20240319094008-0393e58bdf10 +// protoc-gen-go-vtproto version: (devel) // source: cluster.proto package clusterpb diff --git a/internal/proto/commonpb/common_vtproto.pb.go b/internal/proto/commonpb/common_vtproto.pb.go index 3d585858c1..97e8ab6af9 100644 --- a/internal/proto/commonpb/common_vtproto.pb.go +++ b/internal/proto/commonpb/common_vtproto.pb.go @@ -1,5 +1,5 @@ // Code generated by protoc-gen-go-vtproto. DO NOT EDIT. -// protoc-gen-go-vtproto version: v0.6.1-0.20240319094008-0393e58bdf10 +// protoc-gen-go-vtproto version: (devel) // source: common.proto package commonpb @@ -23627,10 +23627,6 @@ func (m *MetadataValue_NullValue) MarshalToSizedBufferVT(dAtA []byte) (int, erro i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x22 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x22 } return len(dAtA) - i, nil } @@ -24781,10 +24777,6 @@ func (m *Target_Account) MarshalToSizedBufferVT(dAtA []byte) (int, error) { i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0xa - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0xa } return len(dAtA) - i, nil } @@ -25127,10 +25119,6 @@ func (m *IndexID_Metadata) MarshalToSizedBufferVT(dAtA []byte) (int, error) { i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x22 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x22 } return len(dAtA) - i, nil } @@ -25362,10 +25350,6 @@ func (m *LogPayload_CreateLedger) MarshalToSizedBufferVT(dAtA []byte) (int, erro i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0xa - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0xa } return len(dAtA) - i, nil } @@ -25385,10 +25369,6 @@ func (m *LogPayload_DeleteLedger) MarshalToSizedBufferVT(dAtA []byte) (int, erro i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x12 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x12 } return len(dAtA) - i, nil } @@ -25408,10 +25388,6 @@ func (m *LogPayload_Apply) MarshalToSizedBufferVT(dAtA []byte) (int, error) { i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x1a - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x1a } return len(dAtA) - i, nil } @@ -25431,10 +25407,6 @@ func (m *LogPayload_RegisterSigningKey) MarshalToSizedBufferVT(dAtA []byte) (int i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x22 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x22 } return len(dAtA) - i, nil } @@ -25454,10 +25426,6 @@ func (m *LogPayload_RevokeSigningKey) MarshalToSizedBufferVT(dAtA []byte) (int, i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x2a - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x2a } return len(dAtA) - i, nil } @@ -25477,10 +25445,6 @@ func (m *LogPayload_SetSigningConfig) MarshalToSizedBufferVT(dAtA []byte) (int, i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x32 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x32 } return len(dAtA) - i, nil } @@ -25500,10 +25464,6 @@ func (m *LogPayload_AddedEventsSink) MarshalToSizedBufferVT(dAtA []byte) (int, e i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x3a - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x3a } return len(dAtA) - i, nil } @@ -25523,10 +25483,6 @@ func (m *LogPayload_RemovedEventsSink) MarshalToSizedBufferVT(dAtA []byte) (int, i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x42 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x42 } return len(dAtA) - i, nil } @@ -25546,10 +25502,6 @@ func (m *LogPayload_SetMaintenanceMode) MarshalToSizedBufferVT(dAtA []byte) (int i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x4a - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x4a } return len(dAtA) - i, nil } @@ -25569,10 +25521,6 @@ func (m *LogPayload_PromoteLedger) MarshalToSizedBufferVT(dAtA []byte) (int, err i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x52 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x52 } return len(dAtA) - i, nil } @@ -25592,10 +25540,6 @@ func (m *LogPayload_CreatedPreparedQuery) MarshalToSizedBufferVT(dAtA []byte) (i i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x5a - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x5a } return len(dAtA) - i, nil } @@ -25615,10 +25559,6 @@ func (m *LogPayload_UpdatedPreparedQuery) MarshalToSizedBufferVT(dAtA []byte) (i i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x62 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x62 } return len(dAtA) - i, nil } @@ -25638,10 +25578,6 @@ func (m *LogPayload_DeletedPreparedQuery) MarshalToSizedBufferVT(dAtA []byte) (i i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x6a - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x6a } return len(dAtA) - i, nil } @@ -25661,10 +25597,6 @@ func (m *LogPayload_SavedNumscript) MarshalToSizedBufferVT(dAtA []byte) (int, er i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x72 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x72 } return len(dAtA) - i, nil } @@ -25684,10 +25616,6 @@ func (m *LogPayload_CreatedQueryCheckpoint) MarshalToSizedBufferVT(dAtA []byte) i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x7a - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x7a } return len(dAtA) - i, nil } @@ -25709,12 +25637,6 @@ func (m *LogPayload_DeletedQueryCheckpoint) MarshalToSizedBufferVT(dAtA []byte) dAtA[i] = 0x1 i-- dAtA[i] = 0x82 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x1 - i-- - dAtA[i] = 0x82 } return len(dAtA) - i, nil } @@ -25736,12 +25658,6 @@ func (m *LogPayload_SetQueryCheckpointSchedule) MarshalToSizedBufferVT(dAtA []by dAtA[i] = 0x1 i-- dAtA[i] = 0x8a - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x1 - i-- - dAtA[i] = 0x8a } return len(dAtA) - i, nil } @@ -25763,12 +25679,6 @@ func (m *LogPayload_DeleteQueryCheckpointSchedule) MarshalToSizedBufferVT(dAtA [ dAtA[i] = 0x1 i-- dAtA[i] = 0x92 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x1 - i-- - dAtA[i] = 0x92 } return len(dAtA) - i, nil } @@ -25790,12 +25700,6 @@ func (m *LogPayload_SavedLedgerMetadata) MarshalToSizedBufferVT(dAtA []byte) (in dAtA[i] = 0x1 i-- dAtA[i] = 0x9a - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x1 - i-- - dAtA[i] = 0x9a } return len(dAtA) - i, nil } @@ -25817,12 +25721,6 @@ func (m *LogPayload_DeletedLedgerMetadata) MarshalToSizedBufferVT(dAtA []byte) ( dAtA[i] = 0x1 i-- dAtA[i] = 0xa2 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x1 - i-- - dAtA[i] = 0xa2 } return len(dAtA) - i, nil } @@ -25844,12 +25742,6 @@ func (m *LogPayload_SetClusterPolicy) MarshalToSizedBufferVT(dAtA []byte) (int, dAtA[i] = 0x1 i-- dAtA[i] = 0xaa - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x1 - i-- - dAtA[i] = 0xaa } return len(dAtA) - i, nil } @@ -27390,10 +27282,6 @@ func (m *SinkConfig_Nats) MarshalToSizedBufferVT(dAtA []byte) (int, error) { i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x12 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x12 } return len(dAtA) - i, nil } @@ -27413,10 +27301,6 @@ func (m *SinkConfig_Clickhouse) MarshalToSizedBufferVT(dAtA []byte) (int, error) i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x1a - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x1a } return len(dAtA) - i, nil } @@ -27436,10 +27320,6 @@ func (m *SinkConfig_Kafka) MarshalToSizedBufferVT(dAtA []byte) (int, error) { i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x22 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x22 } return len(dAtA) - i, nil } @@ -27459,10 +27339,6 @@ func (m *SinkConfig_Http) MarshalToSizedBufferVT(dAtA []byte) (int, error) { i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x2a - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x2a } return len(dAtA) - i, nil } @@ -27482,10 +27358,6 @@ func (m *SinkConfig_Databricks) MarshalToSizedBufferVT(dAtA []byte) (int, error) i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x32 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x32 } return len(dAtA) - i, nil } @@ -27928,10 +27800,6 @@ func (m *DatabricksSinkConfig_OauthM2M) MarshalToSizedBufferVT(dAtA []byte) (int i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x42 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x42 } return len(dAtA) - i, nil } @@ -28418,10 +28286,6 @@ func (m *LedgerLogPayload_CreatedTransaction) MarshalToSizedBufferVT(dAtA []byte i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0xa - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0xa } return len(dAtA) - i, nil } @@ -28441,10 +28305,6 @@ func (m *LedgerLogPayload_RevertedTransaction) MarshalToSizedBufferVT(dAtA []byt i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x12 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x12 } return len(dAtA) - i, nil } @@ -28464,10 +28324,6 @@ func (m *LedgerLogPayload_SavedMetadata) MarshalToSizedBufferVT(dAtA []byte) (in i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x1a - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x1a } return len(dAtA) - i, nil } @@ -28487,10 +28343,6 @@ func (m *LedgerLogPayload_DeletedMetadata) MarshalToSizedBufferVT(dAtA []byte) ( i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x22 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x22 } return len(dAtA) - i, nil } @@ -28510,10 +28362,6 @@ func (m *LedgerLogPayload_SetMetadataFieldType) MarshalToSizedBufferVT(dAtA []by i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x2a - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x2a } return len(dAtA) - i, nil } @@ -28533,10 +28381,6 @@ func (m *LedgerLogPayload_RemovedMetadataFieldType) MarshalToSizedBufferVT(dAtA i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x32 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x32 } return len(dAtA) - i, nil } @@ -28556,10 +28400,6 @@ func (m *LedgerLogPayload_FillGap) MarshalToSizedBufferVT(dAtA []byte) (int, err i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x3a - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x3a } return len(dAtA) - i, nil } @@ -28579,10 +28419,6 @@ func (m *LedgerLogPayload_CreateIndex) MarshalToSizedBufferVT(dAtA []byte) (int, i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x42 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x42 } return len(dAtA) - i, nil } @@ -28602,10 +28438,6 @@ func (m *LedgerLogPayload_DropIndex) MarshalToSizedBufferVT(dAtA []byte) (int, e i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x4a - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x4a } return len(dAtA) - i, nil } @@ -28625,10 +28457,6 @@ func (m *LedgerLogPayload_AddedAccountType) MarshalToSizedBufferVT(dAtA []byte) i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x52 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x52 } return len(dAtA) - i, nil } @@ -28648,10 +28476,6 @@ func (m *LedgerLogPayload_RemovedAccountType) MarshalToSizedBufferVT(dAtA []byte i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x5a - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x5a } return len(dAtA) - i, nil } @@ -28671,10 +28495,6 @@ func (m *LedgerLogPayload_UpdatedDefaultEnforcementMode) MarshalToSizedBufferVT( i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x62 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x62 } return len(dAtA) - i, nil } @@ -28694,10 +28514,6 @@ func (m *LedgerLogPayload_OrderSkipped) MarshalToSizedBufferVT(dAtA []byte) (int i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x6a - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x6a } return len(dAtA) - i, nil } @@ -28759,10 +28575,6 @@ func (m *FailureDetails_AccountAddressEmpty) MarshalToSizedBufferVT(dAtA []byte) i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0xa - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0xa } return len(dAtA) - i, nil } @@ -28782,10 +28594,6 @@ func (m *FailureDetails_AccountAddressEmptySegment) MarshalToSizedBufferVT(dAtA i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x12 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x12 } return len(dAtA) - i, nil } @@ -28805,10 +28613,6 @@ func (m *FailureDetails_AccountAddressInvalidChar) MarshalToSizedBufferVT(dAtA [ i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x1a - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x1a } return len(dAtA) - i, nil } @@ -28828,10 +28632,6 @@ func (m *FailureDetails_AccountAddressTooLong) MarshalToSizedBufferVT(dAtA []byt i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x22 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x22 } return len(dAtA) - i, nil } @@ -28851,10 +28651,6 @@ func (m *FailureDetails_AccountNotMatchingType) MarshalToSizedBufferVT(dAtA []by i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x2a - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x2a } return len(dAtA) - i, nil } @@ -28874,10 +28670,6 @@ func (m *FailureDetails_AccountTypeAlreadyExists) MarshalToSizedBufferVT(dAtA [] i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x32 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x32 } return len(dAtA) - i, nil } @@ -28897,10 +28689,6 @@ func (m *FailureDetails_AccountTypeConflict) MarshalToSizedBufferVT(dAtA []byte) i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x3a - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x3a } return len(dAtA) - i, nil } @@ -28920,10 +28708,6 @@ func (m *FailureDetails_AccountTypeHasAccounts) MarshalToSizedBufferVT(dAtA []by i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x42 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x42 } return len(dAtA) - i, nil } @@ -28943,10 +28727,6 @@ func (m *FailureDetails_AccountTypeNameTooLong) MarshalToSizedBufferVT(dAtA []by i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x4a - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x4a } return len(dAtA) - i, nil } @@ -28966,10 +28746,6 @@ func (m *FailureDetails_AccountTypeNotFound) MarshalToSizedBufferVT(dAtA []byte) i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x52 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x52 } return len(dAtA) - i, nil } @@ -28989,10 +28765,6 @@ func (m *FailureDetails_AccountTypePatternTooLong) MarshalToSizedBufferVT(dAtA [ i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x5a - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x5a } return len(dAtA) - i, nil } @@ -29012,10 +28784,6 @@ func (m *FailureDetails_AssetInvalid) MarshalToSizedBufferVT(dAtA []byte) (int, i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x62 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x62 } return len(dAtA) - i, nil } @@ -29035,10 +28803,6 @@ func (m *FailureDetails_BalanceNotFound) MarshalToSizedBufferVT(dAtA []byte) (in i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x6a - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x6a } return len(dAtA) - i, nil } @@ -29058,10 +28822,6 @@ func (m *FailureDetails_CheckpointIdRequired) MarshalToSizedBufferVT(dAtA []byte i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x72 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x72 } return len(dAtA) - i, nil } @@ -29081,10 +28841,6 @@ func (m *FailureDetails_CheckpointLimitReached) MarshalToSizedBufferVT(dAtA []by i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x7a - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x7a } return len(dAtA) - i, nil } @@ -29106,12 +28862,6 @@ func (m *FailureDetails_CheckpointNotFound) MarshalToSizedBufferVT(dAtA []byte) dAtA[i] = 0x1 i-- dAtA[i] = 0x82 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x1 - i-- - dAtA[i] = 0x82 } return len(dAtA) - i, nil } @@ -29133,12 +28883,6 @@ func (m *FailureDetails_ColorInvalid) MarshalToSizedBufferVT(dAtA []byte) (int, dAtA[i] = 0x1 i-- dAtA[i] = 0x8a - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x1 - i-- - dAtA[i] = 0x8a } return len(dAtA) - i, nil } @@ -29160,12 +28904,6 @@ func (m *FailureDetails_ColorTooLong) MarshalToSizedBufferVT(dAtA []byte) (int, dAtA[i] = 0x1 i-- dAtA[i] = 0x92 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x1 - i-- - dAtA[i] = 0x92 } return len(dAtA) - i, nil } @@ -29187,12 +28925,6 @@ func (m *FailureDetails_EmptyTransaction) MarshalToSizedBufferVT(dAtA []byte) (i dAtA[i] = 0x1 i-- dAtA[i] = 0x9a - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x1 - i-- - dAtA[i] = 0x9a } return len(dAtA) - i, nil } @@ -29214,12 +28946,6 @@ func (m *FailureDetails_IdempotencyKeyConflict) MarshalToSizedBufferVT(dAtA []by dAtA[i] = 0x1 i-- dAtA[i] = 0xa2 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x1 - i-- - dAtA[i] = 0xa2 } return len(dAtA) - i, nil } @@ -29241,12 +28967,6 @@ func (m *FailureDetails_IdRequired) MarshalToSizedBufferVT(dAtA []byte) (int, er dAtA[i] = 0x1 i-- dAtA[i] = 0xaa - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x1 - i-- - dAtA[i] = 0xaa } return len(dAtA) - i, nil } @@ -29268,12 +28988,6 @@ func (m *FailureDetails_IndexAlreadyExists) MarshalToSizedBufferVT(dAtA []byte) dAtA[i] = 0x1 i-- dAtA[i] = 0xb2 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x1 - i-- - dAtA[i] = 0xb2 } return len(dAtA) - i, nil } @@ -29295,12 +29009,6 @@ func (m *FailureDetails_IndexNotFound) MarshalToSizedBufferVT(dAtA []byte) (int, dAtA[i] = 0x1 i-- dAtA[i] = 0xba - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x1 - i-- - dAtA[i] = 0xba } return len(dAtA) - i, nil } @@ -29322,12 +29030,6 @@ func (m *FailureDetails_IndexTargetUnsupported) MarshalToSizedBufferVT(dAtA []by dAtA[i] = 0x1 i-- dAtA[i] = 0xc2 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x1 - i-- - dAtA[i] = 0xc2 } return len(dAtA) - i, nil } @@ -29349,12 +29051,6 @@ func (m *FailureDetails_InsufficientFunds) MarshalToSizedBufferVT(dAtA []byte) ( dAtA[i] = 0x1 i-- dAtA[i] = 0xca - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x1 - i-- - dAtA[i] = 0xca } return len(dAtA) - i, nil } @@ -29376,12 +29072,6 @@ func (m *FailureDetails_InvalidCronExpression) MarshalToSizedBufferVT(dAtA []byt dAtA[i] = 0x1 i-- dAtA[i] = 0xd2 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x1 - i-- - dAtA[i] = 0xd2 } return len(dAtA) - i, nil } @@ -29403,12 +29093,6 @@ func (m *FailureDetails_InvalidPattern) MarshalToSizedBufferVT(dAtA []byte) (int dAtA[i] = 0x1 i-- dAtA[i] = 0xda - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x1 - i-- - dAtA[i] = 0xda } return len(dAtA) - i, nil } @@ -29430,12 +29114,6 @@ func (m *FailureDetails_InvalidSkippableReason) MarshalToSizedBufferVT(dAtA []by dAtA[i] = 0x1 i-- dAtA[i] = 0xe2 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x1 - i-- - dAtA[i] = 0xe2 } return len(dAtA) - i, nil } @@ -29457,12 +29135,6 @@ func (m *FailureDetails_LedgerAlreadyExists) MarshalToSizedBufferVT(dAtA []byte) dAtA[i] = 0x1 i-- dAtA[i] = 0xea - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x1 - i-- - dAtA[i] = 0xea } return len(dAtA) - i, nil } @@ -29484,12 +29156,6 @@ func (m *FailureDetails_LedgerDeleted) MarshalToSizedBufferVT(dAtA []byte) (int, dAtA[i] = 0x1 i-- dAtA[i] = 0xf2 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x1 - i-- - dAtA[i] = 0xf2 } return len(dAtA) - i, nil } @@ -29511,12 +29177,6 @@ func (m *FailureDetails_LedgerInMirrorMode) MarshalToSizedBufferVT(dAtA []byte) dAtA[i] = 0x1 i-- dAtA[i] = 0xfa - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x1 - i-- - dAtA[i] = 0xfa } return len(dAtA) - i, nil } @@ -29538,12 +29198,6 @@ func (m *FailureDetails_LedgerNameInvalidChar) MarshalToSizedBufferVT(dAtA []byt dAtA[i] = 0x2 i-- dAtA[i] = 0x82 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x2 - i-- - dAtA[i] = 0x82 } return len(dAtA) - i, nil } @@ -29565,12 +29219,6 @@ func (m *FailureDetails_LedgerNameRequired) MarshalToSizedBufferVT(dAtA []byte) dAtA[i] = 0x2 i-- dAtA[i] = 0x8a - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x2 - i-- - dAtA[i] = 0x8a } return len(dAtA) - i, nil } @@ -29592,12 +29240,6 @@ func (m *FailureDetails_LedgerNameReservedPrefix) MarshalToSizedBufferVT(dAtA [] dAtA[i] = 0x2 i-- dAtA[i] = 0x92 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x2 - i-- - dAtA[i] = 0x92 } return len(dAtA) - i, nil } @@ -29619,12 +29261,6 @@ func (m *FailureDetails_LedgerNameTooLong) MarshalToSizedBufferVT(dAtA []byte) ( dAtA[i] = 0x2 i-- dAtA[i] = 0x9a - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x2 - i-- - dAtA[i] = 0x9a } return len(dAtA) - i, nil } @@ -29646,12 +29282,6 @@ func (m *FailureDetails_LedgerNotFound) MarshalToSizedBufferVT(dAtA []byte) (int dAtA[i] = 0x2 i-- dAtA[i] = 0xa2 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x2 - i-- - dAtA[i] = 0xa2 } return len(dAtA) - i, nil } @@ -29673,12 +29303,6 @@ func (m *FailureDetails_LedgerNotInMirrorMode) MarshalToSizedBufferVT(dAtA []byt dAtA[i] = 0x2 i-- dAtA[i] = 0xaa - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x2 - i-- - dAtA[i] = 0xaa } return len(dAtA) - i, nil } @@ -29700,12 +29324,6 @@ func (m *FailureDetails_MetadataFieldNotInSchema) MarshalToSizedBufferVT(dAtA [] dAtA[i] = 0x2 i-- dAtA[i] = 0xb2 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x2 - i-- - dAtA[i] = 0xb2 } return len(dAtA) - i, nil } @@ -29727,12 +29345,6 @@ func (m *FailureDetails_MetadataKeyEmpty) MarshalToSizedBufferVT(dAtA []byte) (i dAtA[i] = 0x2 i-- dAtA[i] = 0xba - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x2 - i-- - dAtA[i] = 0xba } return len(dAtA) - i, nil } @@ -29754,12 +29366,6 @@ func (m *FailureDetails_MetadataKeyInvalidChar) MarshalToSizedBufferVT(dAtA []by dAtA[i] = 0x2 i-- dAtA[i] = 0xc2 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x2 - i-- - dAtA[i] = 0xc2 } return len(dAtA) - i, nil } @@ -29781,12 +29387,6 @@ func (m *FailureDetails_MetadataKeyRequired) MarshalToSizedBufferVT(dAtA []byte) dAtA[i] = 0x2 i-- dAtA[i] = 0xca - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x2 - i-- - dAtA[i] = 0xca } return len(dAtA) - i, nil } @@ -29808,12 +29408,6 @@ func (m *FailureDetails_MetadataLimitExceeded) MarshalToSizedBufferVT(dAtA []byt dAtA[i] = 0x2 i-- dAtA[i] = 0xd2 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x2 - i-- - dAtA[i] = 0xd2 } return len(dAtA) - i, nil } @@ -29835,12 +29429,6 @@ func (m *FailureDetails_MetadataNotFound) MarshalToSizedBufferVT(dAtA []byte) (i dAtA[i] = 0x2 i-- dAtA[i] = 0xda - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x2 - i-- - dAtA[i] = 0xda } return len(dAtA) - i, nil } @@ -29862,12 +29450,6 @@ func (m *FailureDetails_MetadataValueContainsNullByte) MarshalToSizedBufferVT(dA dAtA[i] = 0x2 i-- dAtA[i] = 0xe2 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x2 - i-- - dAtA[i] = 0xe2 } return len(dAtA) - i, nil } @@ -29889,12 +29471,6 @@ func (m *FailureDetails_MirrorHttpUrlInvalid) MarshalToSizedBufferVT(dAtA []byte dAtA[i] = 0x2 i-- dAtA[i] = 0xea - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x2 - i-- - dAtA[i] = 0xea } return len(dAtA) - i, nil } @@ -29916,12 +29492,6 @@ func (m *FailureDetails_MirrorIamRegionRequired) MarshalToSizedBufferVT(dAtA []b dAtA[i] = 0x2 i-- dAtA[i] = 0xf2 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x2 - i-- - dAtA[i] = 0xf2 } return len(dAtA) - i, nil } @@ -29943,12 +29513,6 @@ func (m *FailureDetails_MirrorIamRequiresTls) MarshalToSizedBufferVT(dAtA []byte dAtA[i] = 0x2 i-- dAtA[i] = 0xfa - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x2 - i-- - dAtA[i] = 0xfa } return len(dAtA) - i, nil } @@ -29970,12 +29534,6 @@ func (m *FailureDetails_MirrorRewriteRuleInvalid) MarshalToSizedBufferVT(dAtA [] dAtA[i] = 0x3 i-- dAtA[i] = 0x82 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x3 - i-- - dAtA[i] = 0x82 } return len(dAtA) - i, nil } @@ -29997,12 +29555,6 @@ func (m *FailureDetails_NumscriptAssetMismatch) MarshalToSizedBufferVT(dAtA []by dAtA[i] = 0x3 i-- dAtA[i] = 0x8a - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x3 - i-- - dAtA[i] = 0x8a } return len(dAtA) - i, nil } @@ -30024,12 +29576,6 @@ func (m *FailureDetails_NumscriptBadArity) MarshalToSizedBufferVT(dAtA []byte) ( dAtA[i] = 0x3 i-- dAtA[i] = 0x92 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x3 - i-- - dAtA[i] = 0x92 } return len(dAtA) - i, nil } @@ -30051,12 +29597,6 @@ func (m *FailureDetails_NumscriptBadMetaValue) MarshalToSizedBufferVT(dAtA []byt dAtA[i] = 0x3 i-- dAtA[i] = 0x9a - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x3 - i-- - dAtA[i] = 0x9a } return len(dAtA) - i, nil } @@ -30078,12 +29618,6 @@ func (m *FailureDetails_NumscriptBadPortion) MarshalToSizedBufferVT(dAtA []byte) dAtA[i] = 0x3 i-- dAtA[i] = 0xa2 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x3 - i-- - dAtA[i] = 0xa2 } return len(dAtA) - i, nil } @@ -30105,12 +29639,6 @@ func (m *FailureDetails_NumscriptCannotCastScopedAccount) MarshalToSizedBufferVT dAtA[i] = 0x3 i-- dAtA[i] = 0xaa - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x3 - i-- - dAtA[i] = 0xaa } return len(dAtA) - i, nil } @@ -30132,12 +29660,6 @@ func (m *FailureDetails_NumscriptCannotCastToString) MarshalToSizedBufferVT(dAtA dAtA[i] = 0x3 i-- dAtA[i] = 0xb2 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x3 - i-- - dAtA[i] = 0xb2 } return len(dAtA) - i, nil } @@ -30159,12 +29681,6 @@ func (m *FailureDetails_NumscriptCannotStoreScopedAccount) MarshalToSizedBufferV dAtA[i] = 0x3 i-- dAtA[i] = 0xba - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x3 - i-- - dAtA[i] = 0xba } return len(dAtA) - i, nil } @@ -30186,12 +29702,6 @@ func (m *FailureDetails_NumscriptCompileBadArity) MarshalToSizedBufferVT(dAtA [] dAtA[i] = 0x3 i-- dAtA[i] = 0xc2 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x3 - i-- - dAtA[i] = 0xc2 } return len(dAtA) - i, nil } @@ -30213,12 +29723,6 @@ func (m *FailureDetails_NumscriptCompileCannotCastToString) MarshalToSizedBuffer dAtA[i] = 0x3 i-- dAtA[i] = 0xca - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x3 - i-- - dAtA[i] = 0xca } return len(dAtA) - i, nil } @@ -30240,12 +29744,6 @@ func (m *FailureDetails_NumscriptCompileCannotStoreScopedAccount) MarshalToSized dAtA[i] = 0x3 i-- dAtA[i] = 0xd2 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x3 - i-- - dAtA[i] = 0xd2 } return len(dAtA) - i, nil } @@ -30267,12 +29765,6 @@ func (m *FailureDetails_NumscriptCompileDuplicateRemaining) MarshalToSizedBuffer dAtA[i] = 0x3 i-- dAtA[i] = 0xda - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x3 - i-- - dAtA[i] = 0xda } return len(dAtA) - i, nil } @@ -30294,12 +29786,6 @@ func (m *FailureDetails_NumscriptCompileDuplicateVariable) MarshalToSizedBufferV dAtA[i] = 0x3 i-- dAtA[i] = 0xe2 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x3 - i-- - dAtA[i] = 0xe2 } return len(dAtA) - i, nil } @@ -30321,12 +29807,6 @@ func (m *FailureDetails_NumscriptCompileExperimentalFeature) MarshalToSizedBuffe dAtA[i] = 0x3 i-- dAtA[i] = 0xea - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x3 - i-- - dAtA[i] = 0xea } return len(dAtA) - i, nil } @@ -30348,12 +29828,6 @@ func (m *FailureDetails_NumscriptCompileInvalidFeature) MarshalToSizedBufferVT(d dAtA[i] = 0x3 i-- dAtA[i] = 0xf2 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x3 - i-- - dAtA[i] = 0xf2 } return len(dAtA) - i, nil } @@ -30375,12 +29849,6 @@ func (m *FailureDetails_NumscriptCompileInvalidMetaPosition) MarshalToSizedBuffe dAtA[i] = 0x3 i-- dAtA[i] = 0xfa - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x3 - i-- - dAtA[i] = 0xfa } return len(dAtA) - i, nil } @@ -30402,12 +29870,6 @@ func (m *FailureDetails_NumscriptCompileInvalidType) MarshalToSizedBufferVT(dAtA dAtA[i] = 0x4 i-- dAtA[i] = 0x82 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x4 - i-- - dAtA[i] = 0x82 } return len(dAtA) - i, nil } @@ -30429,12 +29891,6 @@ func (m *FailureDetails_NumscriptCompileInvalidUncappedSource) MarshalToSizedBuf dAtA[i] = 0x4 i-- dAtA[i] = 0x8a - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x4 - i-- - dAtA[i] = 0x8a } return len(dAtA) - i, nil } @@ -30456,12 +29912,6 @@ func (m *FailureDetails_NumscriptCompileInvalidVariableValue) MarshalToSizedBuff dAtA[i] = 0x4 i-- dAtA[i] = 0x92 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x4 - i-- - dAtA[i] = 0x92 } return len(dAtA) - i, nil } @@ -30483,12 +29933,6 @@ func (m *FailureDetails_NumscriptCompileMissingVariable) MarshalToSizedBufferVT( dAtA[i] = 0x4 i-- dAtA[i] = 0x9a - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x4 - i-- - dAtA[i] = 0x9a } return len(dAtA) - i, nil } @@ -30510,12 +29954,6 @@ func (m *FailureDetails_NumscriptCompileTypeMismatch) MarshalToSizedBufferVT(dAt dAtA[i] = 0x4 i-- dAtA[i] = 0xa2 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x4 - i-- - dAtA[i] = 0xa2 } return len(dAtA) - i, nil } @@ -30537,12 +29975,6 @@ func (m *FailureDetails_NumscriptCompileUnboundVariable) MarshalToSizedBufferVT( dAtA[i] = 0x4 i-- dAtA[i] = 0xaa - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x4 - i-- - dAtA[i] = 0xaa } return len(dAtA) - i, nil } @@ -30564,12 +29996,6 @@ func (m *FailureDetails_NumscriptCompileUnknownFunction) MarshalToSizedBufferVT( dAtA[i] = 0x4 i-- dAtA[i] = 0xb2 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x4 - i-- - dAtA[i] = 0xb2 } return len(dAtA) - i, nil } @@ -30591,12 +30017,6 @@ func (m *FailureDetails_NumscriptContentRequired) MarshalToSizedBufferVT(dAtA [] dAtA[i] = 0x4 i-- dAtA[i] = 0xba - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x4 - i-- - dAtA[i] = 0xba } return len(dAtA) - i, nil } @@ -30618,12 +30038,6 @@ func (m *FailureDetails_NumscriptCurrencyMismatch) MarshalToSizedBufferVT(dAtA [ dAtA[i] = 0x4 i-- dAtA[i] = 0xc2 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x4 - i-- - dAtA[i] = 0xc2 } return len(dAtA) - i, nil } @@ -30645,12 +30059,6 @@ func (m *FailureDetails_NumscriptDivideByZero) MarshalToSizedBufferVT(dAtA []byt dAtA[i] = 0x4 i-- dAtA[i] = 0xca - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x4 - i-- - dAtA[i] = 0xca } return len(dAtA) - i, nil } @@ -30672,12 +30080,6 @@ func (m *FailureDetails_NumscriptExperimentalFeature) MarshalToSizedBufferVT(dAt dAtA[i] = 0x4 i-- dAtA[i] = 0xd2 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x4 - i-- - dAtA[i] = 0xd2 } return len(dAtA) - i, nil } @@ -30699,12 +30101,6 @@ func (m *FailureDetails_NumscriptInvalidAccountName) MarshalToSizedBufferVT(dAtA dAtA[i] = 0x4 i-- dAtA[i] = 0xda - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x4 - i-- - dAtA[i] = 0xda } return len(dAtA) - i, nil } @@ -30726,12 +30122,6 @@ func (m *FailureDetails_NumscriptInvalidAllotmentInSendAll) MarshalToSizedBuffer dAtA[i] = 0x4 i-- dAtA[i] = 0xe2 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x4 - i-- - dAtA[i] = 0xe2 } return len(dAtA) - i, nil } @@ -30753,12 +30143,6 @@ func (m *FailureDetails_NumscriptInvalidAllotmentSum) MarshalToSizedBufferVT(dAt dAtA[i] = 0x4 i-- dAtA[i] = 0xea - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x4 - i-- - dAtA[i] = 0xea } return len(dAtA) - i, nil } @@ -30780,12 +30164,6 @@ func (m *FailureDetails_NumscriptInvalidAsset) MarshalToSizedBufferVT(dAtA []byt dAtA[i] = 0x4 i-- dAtA[i] = 0xf2 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x4 - i-- - dAtA[i] = 0xf2 } return len(dAtA) - i, nil } @@ -30807,12 +30185,6 @@ func (m *FailureDetails_NumscriptInvalidColor) MarshalToSizedBufferVT(dAtA []byt dAtA[i] = 0x4 i-- dAtA[i] = 0xfa - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x4 - i-- - dAtA[i] = 0xfa } return len(dAtA) - i, nil } @@ -30834,12 +30206,6 @@ func (m *FailureDetails_NumscriptInvalidFeature) MarshalToSizedBufferVT(dAtA []b dAtA[i] = 0x5 i-- dAtA[i] = 0x82 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x5 - i-- - dAtA[i] = 0x82 } return len(dAtA) - i, nil } @@ -30861,12 +30227,6 @@ func (m *FailureDetails_NumscriptInvalidMonetaryLiteral) MarshalToSizedBufferVT( dAtA[i] = 0x5 i-- dAtA[i] = 0x8a - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x5 - i-- - dAtA[i] = 0x8a } return len(dAtA) - i, nil } @@ -30888,12 +30248,6 @@ func (m *FailureDetails_NumscriptInvalidNestedMeta) MarshalToSizedBufferVT(dAtA dAtA[i] = 0x5 i-- dAtA[i] = 0x92 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x5 - i-- - dAtA[i] = 0x92 } return len(dAtA) - i, nil } @@ -30915,12 +30269,6 @@ func (m *FailureDetails_NumscriptInvalidNumberLiteral) MarshalToSizedBufferVT(dA dAtA[i] = 0x5 i-- dAtA[i] = 0x9a - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x5 - i-- - dAtA[i] = 0x9a } return len(dAtA) - i, nil } @@ -30942,12 +30290,6 @@ func (m *FailureDetails_NumscriptInvalidRemainingAllotment) MarshalToSizedBuffer dAtA[i] = 0x5 i-- dAtA[i] = 0xa2 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x5 - i-- - dAtA[i] = 0xa2 } return len(dAtA) - i, nil } @@ -30969,12 +30311,6 @@ func (m *FailureDetails_NumscriptInvalidScope) MarshalToSizedBufferVT(dAtA []byt dAtA[i] = 0x5 i-- dAtA[i] = 0xaa - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x5 - i-- - dAtA[i] = 0xaa } return len(dAtA) - i, nil } @@ -30996,12 +30332,6 @@ func (m *FailureDetails_NumscriptInvalidType) MarshalToSizedBufferVT(dAtA []byte dAtA[i] = 0x5 i-- dAtA[i] = 0xb2 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x5 - i-- - dAtA[i] = 0xb2 } return len(dAtA) - i, nil } @@ -31023,12 +30353,6 @@ func (m *FailureDetails_NumscriptInvalidUnboundedScalingAddress) MarshalToSizedB dAtA[i] = 0x5 i-- dAtA[i] = 0xba - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x5 - i-- - dAtA[i] = 0xba } return len(dAtA) - i, nil } @@ -31050,12 +30374,6 @@ func (m *FailureDetails_NumscriptInvalidUnboundedSendAll) MarshalToSizedBufferVT dAtA[i] = 0x5 i-- dAtA[i] = 0xc2 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x5 - i-- - dAtA[i] = 0xc2 } return len(dAtA) - i, nil } @@ -31077,12 +30395,6 @@ func (m *FailureDetails_NumscriptInvalidUncappedSource) MarshalToSizedBufferVT(d dAtA[i] = 0x5 i-- dAtA[i] = 0xca - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x5 - i-- - dAtA[i] = 0xca } return len(dAtA) - i, nil } @@ -31104,12 +30416,6 @@ func (m *FailureDetails_NumscriptInvalidVersion) MarshalToSizedBufferVT(dAtA []b dAtA[i] = 0x5 i-- dAtA[i] = 0xd2 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x5 - i-- - dAtA[i] = 0xd2 } return len(dAtA) - i, nil } @@ -31131,12 +30437,6 @@ func (m *FailureDetails_NumscriptMetadataNotFound) MarshalToSizedBufferVT(dAtA [ dAtA[i] = 0x5 i-- dAtA[i] = 0xda - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x5 - i-- - dAtA[i] = 0xda } return len(dAtA) - i, nil } @@ -31158,12 +30458,6 @@ func (m *FailureDetails_NumscriptMissingFunds) MarshalToSizedBufferVT(dAtA []byt dAtA[i] = 0x5 i-- dAtA[i] = 0xe2 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x5 - i-- - dAtA[i] = 0xe2 } return len(dAtA) - i, nil } @@ -31185,12 +30479,6 @@ func (m *FailureDetails_NumscriptMissingVariable) MarshalToSizedBufferVT(dAtA [] dAtA[i] = 0x5 i-- dAtA[i] = 0xea - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x5 - i-- - dAtA[i] = 0xea } return len(dAtA) - i, nil } @@ -31212,12 +30500,6 @@ func (m *FailureDetails_NumscriptNameInvalidChar) MarshalToSizedBufferVT(dAtA [] dAtA[i] = 0x5 i-- dAtA[i] = 0xf2 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x5 - i-- - dAtA[i] = 0xf2 } return len(dAtA) - i, nil } @@ -31239,12 +30521,6 @@ func (m *FailureDetails_NumscriptNameRequired) MarshalToSizedBufferVT(dAtA []byt dAtA[i] = 0x5 i-- dAtA[i] = 0xfa - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x5 - i-- - dAtA[i] = 0xfa } return len(dAtA) - i, nil } @@ -31266,12 +30542,6 @@ func (m *FailureDetails_NumscriptNameTooLong) MarshalToSizedBufferVT(dAtA []byte dAtA[i] = 0x6 i-- dAtA[i] = 0x82 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x6 - i-- - dAtA[i] = 0x82 } return len(dAtA) - i, nil } @@ -31293,12 +30563,6 @@ func (m *FailureDetails_NumscriptNegativeAmount) MarshalToSizedBufferVT(dAtA []b dAtA[i] = 0x6 i-- dAtA[i] = 0x8a - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x6 - i-- - dAtA[i] = 0x8a } return len(dAtA) - i, nil } @@ -31320,12 +30584,6 @@ func (m *FailureDetails_NumscriptNegativeBalance) MarshalToSizedBufferVT(dAtA [] dAtA[i] = 0x6 i-- dAtA[i] = 0x92 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x6 - i-- - dAtA[i] = 0x92 } return len(dAtA) - i, nil } @@ -31347,12 +30605,6 @@ func (m *FailureDetails_NumscriptNegativePortion) MarshalToSizedBufferVT(dAtA [] dAtA[i] = 0x6 i-- dAtA[i] = 0x9a - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x6 - i-- - dAtA[i] = 0x9a } return len(dAtA) - i, nil } @@ -31374,12 +30626,6 @@ func (m *FailureDetails_NumscriptNotFound) MarshalToSizedBufferVT(dAtA []byte) ( dAtA[i] = 0x6 i-- dAtA[i] = 0xa2 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x6 - i-- - dAtA[i] = 0xa2 } return len(dAtA) - i, nil } @@ -31401,12 +30647,6 @@ func (m *FailureDetails_NumscriptParseError) MarshalToSizedBufferVT(dAtA []byte) dAtA[i] = 0x6 i-- dAtA[i] = 0xaa - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x6 - i-- - dAtA[i] = 0xaa } return len(dAtA) - i, nil } @@ -31428,12 +30668,6 @@ func (m *FailureDetails_NumscriptPostingAmountOverflow) MarshalToSizedBufferVT(d dAtA[i] = 0x6 i-- dAtA[i] = 0xb2 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x6 - i-- - dAtA[i] = 0xb2 } return len(dAtA) - i, nil } @@ -31455,12 +30689,6 @@ func (m *FailureDetails_NumscriptPostingNegativeAmount) MarshalToSizedBufferVT(d dAtA[i] = 0x6 i-- dAtA[i] = 0xba - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x6 - i-- - dAtA[i] = 0xba } return len(dAtA) - i, nil } @@ -31482,12 +30710,6 @@ func (m *FailureDetails_NumscriptScalingUnsupported) MarshalToSizedBufferVT(dAtA dAtA[i] = 0x6 i-- dAtA[i] = 0xc2 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x6 - i-- - dAtA[i] = 0xc2 } return len(dAtA) - i, nil } @@ -31509,12 +30731,6 @@ func (m *FailureDetails_NumscriptTypeError) MarshalToSizedBufferVT(dAtA []byte) dAtA[i] = 0x6 i-- dAtA[i] = 0xca - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x6 - i-- - dAtA[i] = 0xca } return len(dAtA) - i, nil } @@ -31536,12 +30752,6 @@ func (m *FailureDetails_NumscriptUnboundFunction) MarshalToSizedBufferVT(dAtA [] dAtA[i] = 0x6 i-- dAtA[i] = 0xd2 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x6 - i-- - dAtA[i] = 0xd2 } return len(dAtA) - i, nil } @@ -31563,12 +30773,6 @@ func (m *FailureDetails_NumscriptUnboundVariable) MarshalToSizedBufferVT(dAtA [] dAtA[i] = 0x6 i-- dAtA[i] = 0xda - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x6 - i-- - dAtA[i] = 0xda } return len(dAtA) - i, nil } @@ -31590,12 +30794,6 @@ func (m *FailureDetails_NumscriptVersionAlreadyExists) MarshalToSizedBufferVT(dA dAtA[i] = 0x6 i-- dAtA[i] = 0xe2 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x6 - i-- - dAtA[i] = 0xe2 } return len(dAtA) - i, nil } @@ -31617,12 +30815,6 @@ func (m *FailureDetails_PostingsAndScriptConflict) MarshalToSizedBufferVT(dAtA [ dAtA[i] = 0x6 i-- dAtA[i] = 0xea - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x6 - i-- - dAtA[i] = 0xea } return len(dAtA) - i, nil } @@ -31644,12 +30836,6 @@ func (m *FailureDetails_PreparedQueryAlreadyExists) MarshalToSizedBufferVT(dAtA dAtA[i] = 0x6 i-- dAtA[i] = 0xf2 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x6 - i-- - dAtA[i] = 0xf2 } return len(dAtA) - i, nil } @@ -31671,12 +30857,6 @@ func (m *FailureDetails_PreparedQueryNameInvalidChar) MarshalToSizedBufferVT(dAt dAtA[i] = 0x6 i-- dAtA[i] = 0xfa - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x6 - i-- - dAtA[i] = 0xfa } return len(dAtA) - i, nil } @@ -31698,12 +30878,6 @@ func (m *FailureDetails_PreparedQueryNameRequired) MarshalToSizedBufferVT(dAtA [ dAtA[i] = 0x7 i-- dAtA[i] = 0x82 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x7 - i-- - dAtA[i] = 0x82 } return len(dAtA) - i, nil } @@ -31725,12 +30899,6 @@ func (m *FailureDetails_PreparedQueryNameTooLong) MarshalToSizedBufferVT(dAtA [] dAtA[i] = 0x7 i-- dAtA[i] = 0x8a - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x7 - i-- - dAtA[i] = 0x8a } return len(dAtA) - i, nil } @@ -31752,12 +30920,6 @@ func (m *FailureDetails_PreparedQueryNotFound) MarshalToSizedBufferVT(dAtA []byt dAtA[i] = 0x7 i-- dAtA[i] = 0x92 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x7 - i-- - dAtA[i] = 0x92 } return len(dAtA) - i, nil } @@ -31779,12 +30941,6 @@ func (m *FailureDetails_PreparedQueryRequired) MarshalToSizedBufferVT(dAtA []byt dAtA[i] = 0x7 i-- dAtA[i] = 0x9a - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x7 - i-- - dAtA[i] = 0x9a } return len(dAtA) - i, nil } @@ -31806,12 +30962,6 @@ func (m *FailureDetails_PreparedQueryTargetUnsupported) MarshalToSizedBufferVT(d dAtA[i] = 0x7 i-- dAtA[i] = 0xa2 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x7 - i-- - dAtA[i] = 0xa2 } return len(dAtA) - i, nil } @@ -31833,12 +30983,6 @@ func (m *FailureDetails_RevertTargetCreatedInBatch) MarshalToSizedBufferVT(dAtA dAtA[i] = 0x7 i-- dAtA[i] = 0xaa - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x7 - i-- - dAtA[i] = 0xaa } return len(dAtA) - i, nil } @@ -31860,12 +31004,6 @@ func (m *FailureDetails_ScopedBalanceUnsupported) MarshalToSizedBufferVT(dAtA [] dAtA[i] = 0x7 i-- dAtA[i] = 0xb2 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x7 - i-- - dAtA[i] = 0xb2 } return len(dAtA) - i, nil } @@ -31887,12 +31025,6 @@ func (m *FailureDetails_ScriptAndReferenceConflict) MarshalToSizedBufferVT(dAtA dAtA[i] = 0x7 i-- dAtA[i] = 0xba - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x7 - i-- - dAtA[i] = 0xba } return len(dAtA) - i, nil } @@ -31914,12 +31046,6 @@ func (m *FailureDetails_ScriptRequired) MarshalToSizedBufferVT(dAtA []byte) (int dAtA[i] = 0x7 i-- dAtA[i] = 0xc2 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x7 - i-- - dAtA[i] = 0xc2 } return len(dAtA) - i, nil } @@ -31941,12 +31067,6 @@ func (m *FailureDetails_SigningKeyIdInvalidChar) MarshalToSizedBufferVT(dAtA []b dAtA[i] = 0x7 i-- dAtA[i] = 0xca - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x7 - i-- - dAtA[i] = 0xca } return len(dAtA) - i, nil } @@ -31968,12 +31088,6 @@ func (m *FailureDetails_SigningKeyIdRequired) MarshalToSizedBufferVT(dAtA []byte dAtA[i] = 0x7 i-- dAtA[i] = 0xd2 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x7 - i-- - dAtA[i] = 0xd2 } return len(dAtA) - i, nil } @@ -31995,12 +31109,6 @@ func (m *FailureDetails_SigningKeyIdTooLong) MarshalToSizedBufferVT(dAtA []byte) dAtA[i] = 0x7 i-- dAtA[i] = 0xda - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x7 - i-- - dAtA[i] = 0xda } return len(dAtA) - i, nil } @@ -32022,12 +31130,6 @@ func (m *FailureDetails_SigningKeyInvalidLength) MarshalToSizedBufferVT(dAtA []b dAtA[i] = 0x7 i-- dAtA[i] = 0xe2 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x7 - i-- - dAtA[i] = 0xe2 } return len(dAtA) - i, nil } @@ -32049,12 +31151,6 @@ func (m *FailureDetails_SinkAlreadyExists) MarshalToSizedBufferVT(dAtA []byte) ( dAtA[i] = 0x7 i-- dAtA[i] = 0xea - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x7 - i-- - dAtA[i] = 0xea } return len(dAtA) - i, nil } @@ -32076,12 +31172,6 @@ func (m *FailureDetails_SinkBatchSizeTooLarge) MarshalToSizedBufferVT(dAtA []byt dAtA[i] = 0x7 i-- dAtA[i] = 0xf2 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x7 - i-- - dAtA[i] = 0xf2 } return len(dAtA) - i, nil } @@ -32103,12 +31193,6 @@ func (m *FailureDetails_SinkControllerMismatch) MarshalToSizedBufferVT(dAtA []by dAtA[i] = 0x7 i-- dAtA[i] = 0xfa - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x7 - i-- - dAtA[i] = 0xfa } return len(dAtA) - i, nil } @@ -32130,12 +31214,6 @@ func (m *FailureDetails_SinkNotFound) MarshalToSizedBufferVT(dAtA []byte) (int, dAtA[i] = 0x8 i-- dAtA[i] = 0x82 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x8 - i-- - dAtA[i] = 0x82 } return len(dAtA) - i, nil } @@ -32157,12 +31235,6 @@ func (m *FailureDetails_TargetRequired) MarshalToSizedBufferVT(dAtA []byte) (int dAtA[i] = 0x8 i-- dAtA[i] = 0x8a - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x8 - i-- - dAtA[i] = 0x8a } return len(dAtA) - i, nil } @@ -32184,12 +31256,6 @@ func (m *FailureDetails_TransactionAlreadyReverted) MarshalToSizedBufferVT(dAtA dAtA[i] = 0x8 i-- dAtA[i] = 0x92 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x8 - i-- - dAtA[i] = 0x92 } return len(dAtA) - i, nil } @@ -32211,12 +31277,6 @@ func (m *FailureDetails_TransactionNotFound) MarshalToSizedBufferVT(dAtA []byte) dAtA[i] = 0x8 i-- dAtA[i] = 0x9a - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x8 - i-- - dAtA[i] = 0x9a } return len(dAtA) - i, nil } @@ -32238,12 +31298,6 @@ func (m *FailureDetails_TransactionReferenceConflict) MarshalToSizedBufferVT(dAt dAtA[i] = 0x8 i-- dAtA[i] = 0xa2 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x8 - i-- - dAtA[i] = 0xa2 } return len(dAtA) - i, nil } @@ -32265,12 +31319,6 @@ func (m *FailureDetails_TransactionReferenceNotFound) MarshalToSizedBufferVT(dAt dAtA[i] = 0x8 i-- dAtA[i] = 0xaa - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x8 - i-- - dAtA[i] = 0xaa } return len(dAtA) - i, nil } @@ -32292,12 +31340,6 @@ func (m *FailureDetails_TransactionReferenceTooLong) MarshalToSizedBufferVT(dAtA dAtA[i] = 0x8 i-- dAtA[i] = 0xb2 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x8 - i-- - dAtA[i] = 0xb2 } return len(dAtA) - i, nil } @@ -32319,12 +31361,6 @@ func (m *FailureDetails_TransactionTargetMissing) MarshalToSizedBufferVT(dAtA [] dAtA[i] = 0x8 i-- dAtA[i] = 0xba - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x8 - i-- - dAtA[i] = 0xba } return len(dAtA) - i, nil } @@ -32346,12 +31382,6 @@ func (m *FailureDetails_TransientAccountNonZero) MarshalToSizedBufferVT(dAtA []b dAtA[i] = 0x8 i-- dAtA[i] = 0xc2 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x8 - i-- - dAtA[i] = 0xc2 } return len(dAtA) - i, nil } @@ -32373,12 +31403,6 @@ func (m *FailureDetails_VolumeOverflow) MarshalToSizedBufferVT(dAtA []byte) (int dAtA[i] = 0x8 i-- dAtA[i] = 0xca - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x8 - i-- - dAtA[i] = 0xca } return len(dAtA) - i, nil } @@ -32400,12 +31424,6 @@ func (m *FailureDetails_AccountMetadataKeyEmpty) MarshalToSizedBufferVT(dAtA []b dAtA[i] = 0x8 i-- dAtA[i] = 0xd2 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x8 - i-- - dAtA[i] = 0xd2 } return len(dAtA) - i, nil } @@ -32427,12 +31445,6 @@ func (m *FailureDetails_AccountMetadataKeyInvalidChar) MarshalToSizedBufferVT(dA dAtA[i] = 0x8 i-- dAtA[i] = 0xda - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x8 - i-- - dAtA[i] = 0xda } return len(dAtA) - i, nil } @@ -32454,12 +31466,6 @@ func (m *FailureDetails_AccountMetadataValueContainsNullByte) MarshalToSizedBuff dAtA[i] = 0x8 i-- dAtA[i] = 0xe2 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x8 - i-- - dAtA[i] = 0xe2 } return len(dAtA) - i, nil } @@ -32481,12 +31487,6 @@ func (m *FailureDetails_AccountMetadataLimitExceeded) MarshalToSizedBufferVT(dAt dAtA[i] = 0x8 i-- dAtA[i] = 0xea - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x8 - i-- - dAtA[i] = 0xea } return len(dAtA) - i, nil } @@ -32508,12 +31508,6 @@ func (m *FailureDetails_MetadataKeyLimitExceeded) MarshalToSizedBufferVT(dAtA [] dAtA[i] = 0x8 i-- dAtA[i] = 0xf2 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x8 - i-- - dAtA[i] = 0xf2 } return len(dAtA) - i, nil } @@ -32535,12 +31529,6 @@ func (m *FailureDetails_AccountMetadataKeyLimitExceeded) MarshalToSizedBufferVT( dAtA[i] = 0x8 i-- dAtA[i] = 0xfa - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x8 - i-- - dAtA[i] = 0xfa } return len(dAtA) - i, nil } @@ -32562,12 +31550,6 @@ func (m *FailureDetails_NumscriptInsufficientFunds) MarshalToSizedBufferVT(dAtA dAtA[i] = 0x9 i-- dAtA[i] = 0x82 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x9 - i-- - dAtA[i] = 0x82 } return len(dAtA) - i, nil } @@ -32589,12 +31571,6 @@ func (m *FailureDetails_NumscriptDivideByZeroWithNumerator) MarshalToSizedBuffer dAtA[i] = 0x9 i-- dAtA[i] = 0x8a - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x9 - i-- - dAtA[i] = 0x8a } return len(dAtA) - i, nil } @@ -32616,12 +31592,6 @@ func (m *FailureDetails_NumscriptScopedMetadataNotFound) MarshalToSizedBufferVT( dAtA[i] = 0x9 i-- dAtA[i] = 0x92 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x9 - i-- - dAtA[i] = 0x92 } return len(dAtA) - i, nil } @@ -32643,12 +31613,6 @@ func (m *FailureDetails_NumscriptScopedNegativeBalance) MarshalToSizedBufferVT(d dAtA[i] = 0x9 i-- dAtA[i] = 0x9a - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x9 - i-- - dAtA[i] = 0x9a } return len(dAtA) - i, nil } @@ -32670,12 +31634,6 @@ func (m *FailureDetails_NumscriptVersionNotFound) MarshalToSizedBufferVT(dAtA [] dAtA[i] = 0x9 i-- dAtA[i] = 0xa2 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x9 - i-- - dAtA[i] = 0xa2 } return len(dAtA) - i, nil } @@ -39273,10 +38231,6 @@ func (m *MirrorSourceConfig_Http) MarshalToSizedBufferVT(dAtA []byte) (int, erro i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x12 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x12 } return len(dAtA) - i, nil } @@ -39296,10 +38250,6 @@ func (m *MirrorSourceConfig_Postgres) MarshalToSizedBufferVT(dAtA []byte) (int, i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x1a - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x1a } return len(dAtA) - i, nil } @@ -39425,10 +38375,6 @@ func (m *MirrorRewriteRule_CreatedTransaction) MarshalToSizedBufferVT(dAtA []byt i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0xa - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0xa } return len(dAtA) - i, nil } @@ -39448,10 +38394,6 @@ func (m *MirrorRewriteRule_RevertedTransaction) MarshalToSizedBufferVT(dAtA []by i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x12 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x12 } return len(dAtA) - i, nil } @@ -39471,10 +38413,6 @@ func (m *MirrorRewriteRule_SavedMetadata) MarshalToSizedBufferVT(dAtA []byte) (i i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x1a - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x1a } return len(dAtA) - i, nil } @@ -39494,10 +38432,6 @@ func (m *MirrorRewriteRule_DeletedMetadata) MarshalToSizedBufferVT(dAtA []byte) i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x22 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x22 } return len(dAtA) - i, nil } @@ -39517,10 +38451,6 @@ func (m *MirrorRewriteRule_AnyVariant) MarshalToSizedBufferVT(dAtA []byte) (int, i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x2a - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x2a } return len(dAtA) - i, nil } @@ -39842,10 +38772,6 @@ func (m *CreatedTransactionAction_RewriteAddress) MarshalToSizedBufferVT(dAtA [] i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0xa - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0xa } return len(dAtA) - i, nil } @@ -39865,10 +38791,6 @@ func (m *CreatedTransactionAction_SetMetadata) MarshalToSizedBufferVT(dAtA []byt i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x12 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x12 } return len(dAtA) - i, nil } @@ -39888,10 +38810,6 @@ func (m *CreatedTransactionAction_DeleteMetadata) MarshalToSizedBufferVT(dAtA [] i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x1a - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x1a } return len(dAtA) - i, nil } @@ -39911,10 +38829,6 @@ func (m *CreatedTransactionAction_SetAccountMetadata) MarshalToSizedBufferVT(dAt i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x22 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x22 } return len(dAtA) - i, nil } @@ -39934,10 +38848,6 @@ func (m *CreatedTransactionAction_DeleteAccountMetadata) MarshalToSizedBufferVT( i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x2a - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x2a } return len(dAtA) - i, nil } @@ -39957,10 +38867,6 @@ func (m *CreatedTransactionAction_SetAccountMetadataFromAddress) MarshalToSizedB i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x32 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x32 } return len(dAtA) - i, nil } @@ -39980,10 +38886,6 @@ func (m *CreatedTransactionAction_Drop) MarshalToSizedBufferVT(dAtA []byte) (int i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x3a - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x3a } return len(dAtA) - i, nil } @@ -40045,10 +38947,6 @@ func (m *RevertedTransactionAction_RewriteAddress) MarshalToSizedBufferVT(dAtA [ i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0xa - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0xa } return len(dAtA) - i, nil } @@ -40068,10 +38966,6 @@ func (m *RevertedTransactionAction_SetMetadata) MarshalToSizedBufferVT(dAtA []by i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x12 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x12 } return len(dAtA) - i, nil } @@ -40091,10 +38985,6 @@ func (m *RevertedTransactionAction_DeleteMetadata) MarshalToSizedBufferVT(dAtA [ i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x1a - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x1a } return len(dAtA) - i, nil } @@ -40114,10 +39004,6 @@ func (m *RevertedTransactionAction_Drop) MarshalToSizedBufferVT(dAtA []byte) (in i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x22 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x22 } return len(dAtA) - i, nil } @@ -40179,10 +39065,6 @@ func (m *SavedMetadataAction_RewriteAddress) MarshalToSizedBufferVT(dAtA []byte) i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0xa - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0xa } return len(dAtA) - i, nil } @@ -40202,10 +39084,6 @@ func (m *SavedMetadataAction_SetMetadata) MarshalToSizedBufferVT(dAtA []byte) (i i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x12 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x12 } return len(dAtA) - i, nil } @@ -40225,10 +39103,6 @@ func (m *SavedMetadataAction_DeleteMetadata) MarshalToSizedBufferVT(dAtA []byte) i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x1a - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x1a } return len(dAtA) - i, nil } @@ -40248,10 +39122,6 @@ func (m *SavedMetadataAction_Drop) MarshalToSizedBufferVT(dAtA []byte) (int, err i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x22 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x22 } return len(dAtA) - i, nil } @@ -40313,10 +39183,6 @@ func (m *DeletedMetadataAction_RewriteAddress) MarshalToSizedBufferVT(dAtA []byt i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0xa - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0xa } return len(dAtA) - i, nil } @@ -40336,10 +39202,6 @@ func (m *DeletedMetadataAction_Drop) MarshalToSizedBufferVT(dAtA []byte) (int, e i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x12 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x12 } return len(dAtA) - i, nil } @@ -40401,10 +39263,6 @@ func (m *AnyVariantAction_RewriteAddress) MarshalToSizedBufferVT(dAtA []byte) (i i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0xa - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0xa } return len(dAtA) - i, nil } @@ -40424,10 +39282,6 @@ func (m *AnyVariantAction_Drop) MarshalToSizedBufferVT(dAtA []byte) (int, error) i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x12 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x12 } return len(dAtA) - i, nil } @@ -41846,10 +40700,6 @@ func (m *SegmentType_Uuid) MarshalToSizedBufferVT(dAtA []byte) (int, error) { i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x12 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x12 } return len(dAtA) - i, nil } @@ -41869,10 +40719,6 @@ func (m *SegmentType_Uint64) MarshalToSizedBufferVT(dAtA []byte) (int, error) { i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x1a - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x1a } return len(dAtA) - i, nil } @@ -41892,10 +40738,6 @@ func (m *SegmentType_Bytes) MarshalToSizedBufferVT(dAtA []byte) (int, error) { i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x22 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x22 } return len(dAtA) - i, nil } @@ -42251,10 +41093,6 @@ func (m *QueryFilter_Field) MarshalToSizedBufferVT(dAtA []byte) (int, error) { i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0xa - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0xa } return len(dAtA) - i, nil } @@ -42274,10 +41112,6 @@ func (m *QueryFilter_Address) MarshalToSizedBufferVT(dAtA []byte) (int, error) { i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x12 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x12 } return len(dAtA) - i, nil } @@ -42297,10 +41131,6 @@ func (m *QueryFilter_And) MarshalToSizedBufferVT(dAtA []byte) (int, error) { i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x1a - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x1a } return len(dAtA) - i, nil } @@ -42320,10 +41150,6 @@ func (m *QueryFilter_Or) MarshalToSizedBufferVT(dAtA []byte) (int, error) { i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x22 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x22 } return len(dAtA) - i, nil } @@ -42343,10 +41169,6 @@ func (m *QueryFilter_Not) MarshalToSizedBufferVT(dAtA []byte) (int, error) { i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x2a - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x2a } return len(dAtA) - i, nil } @@ -42366,10 +41188,6 @@ func (m *QueryFilter_Reference) MarshalToSizedBufferVT(dAtA []byte) (int, error) i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x32 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x32 } return len(dAtA) - i, nil } @@ -42389,10 +41207,6 @@ func (m *QueryFilter_BuiltinUint) MarshalToSizedBufferVT(dAtA []byte) (int, erro i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x3a - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x3a } return len(dAtA) - i, nil } @@ -42412,10 +41226,6 @@ func (m *QueryFilter_Ledger) MarshalToSizedBufferVT(dAtA []byte) (int, error) { i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x42 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x42 } return len(dAtA) - i, nil } @@ -42435,10 +41245,6 @@ func (m *QueryFilter_LogId) MarshalToSizedBufferVT(dAtA []byte) (int, error) { i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x4a - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x4a } return len(dAtA) - i, nil } @@ -42458,10 +41264,6 @@ func (m *QueryFilter_LogBuiltinUint) MarshalToSizedBufferVT(dAtA []byte) (int, e i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x52 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x52 } return len(dAtA) - i, nil } @@ -42481,10 +41283,6 @@ func (m *QueryFilter_AccountHasAsset) MarshalToSizedBufferVT(dAtA []byte) (int, i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x5a - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x5a } return len(dAtA) - i, nil } @@ -42504,10 +41302,6 @@ func (m *QueryFilter_Reverted) MarshalToSizedBufferVT(dAtA []byte) (int, error) i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x62 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x62 } return len(dAtA) - i, nil } @@ -42527,10 +41321,6 @@ func (m *QueryFilter_Audit) MarshalToSizedBufferVT(dAtA []byte) (int, error) { i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x6a - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x6a } return len(dAtA) - i, nil } @@ -42683,10 +41473,6 @@ func (m *AuditCondition_StringCond) MarshalToSizedBufferVT(dAtA []byte) (int, er i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x12 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x12 } return len(dAtA) - i, nil } @@ -42706,10 +41492,6 @@ func (m *AuditCondition_UintCond) MarshalToSizedBufferVT(dAtA []byte) (int, erro i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x1a - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x1a } return len(dAtA) - i, nil } @@ -43195,10 +41977,6 @@ func (m *FieldCondition_StringCond) MarshalToSizedBufferVT(dAtA []byte) (int, er i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x12 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x12 } return len(dAtA) - i, nil } @@ -43218,10 +41996,6 @@ func (m *FieldCondition_IntCond) MarshalToSizedBufferVT(dAtA []byte) (int, error i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x1a - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x1a } return len(dAtA) - i, nil } @@ -43241,10 +42015,6 @@ func (m *FieldCondition_UintCond) MarshalToSizedBufferVT(dAtA []byte) (int, erro i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x22 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x22 } return len(dAtA) - i, nil } @@ -43264,10 +42034,6 @@ func (m *FieldCondition_BoolCond) MarshalToSizedBufferVT(dAtA []byte) (int, erro i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x2a - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x2a } return len(dAtA) - i, nil } @@ -43287,10 +42053,6 @@ func (m *FieldCondition_ExistsCond) MarshalToSizedBufferVT(dAtA []byte) (int, er i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x32 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x32 } return len(dAtA) - i, nil } @@ -44517,10 +43279,6 @@ func (m *CallerSnapshot_Authenticated) MarshalToSizedBufferVT(dAtA []byte) (int, i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0xa - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0xa } return len(dAtA) - i, nil } @@ -44540,10 +43298,6 @@ func (m *CallerSnapshot_Anonymous) MarshalToSizedBufferVT(dAtA []byte) (int, err i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x12 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x12 } return len(dAtA) - i, nil } @@ -44563,10 +43317,6 @@ func (m *CallerSnapshot_System) MarshalToSizedBufferVT(dAtA []byte) (int, error) i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x1a - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x1a } return len(dAtA) - i, nil } @@ -44586,10 +43336,6 @@ func (m *CallerSnapshot_AuthDisabled) MarshalToSizedBufferVT(dAtA []byte) (int, i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x22 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x22 } return len(dAtA) - i, nil } @@ -44780,10 +43526,6 @@ func (m *BackupStorage_S3) MarshalToSizedBufferVT(dAtA []byte) (int, error) { i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0xa - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0xa } return len(dAtA) - i, nil } @@ -44803,10 +43545,6 @@ func (m *BackupStorage_Azure) MarshalToSizedBufferVT(dAtA []byte) (int, error) { i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x12 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x12 } return len(dAtA) - i, nil } @@ -45090,8 +43828,6 @@ func (m *MetadataValue_NullValue) SizeVT() (n int) { if m.NullValue != nil { l = m.NullValue.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -45564,8 +44300,6 @@ func (m *Target_Account) SizeVT() (n int) { if m.Account != nil { l = m.Account.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -45726,8 +44460,6 @@ func (m *IndexID_Metadata) SizeVT() (n int) { if m.Metadata != nil { l = m.Metadata.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -45816,8 +44548,6 @@ func (m *LogPayload_CreateLedger) SizeVT() (n int) { if m.CreateLedger != nil { l = m.CreateLedger.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -45830,8 +44560,6 @@ func (m *LogPayload_DeleteLedger) SizeVT() (n int) { if m.DeleteLedger != nil { l = m.DeleteLedger.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -45844,8 +44572,6 @@ func (m *LogPayload_Apply) SizeVT() (n int) { if m.Apply != nil { l = m.Apply.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -45858,8 +44584,6 @@ func (m *LogPayload_RegisterSigningKey) SizeVT() (n int) { if m.RegisterSigningKey != nil { l = m.RegisterSigningKey.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -45872,8 +44596,6 @@ func (m *LogPayload_RevokeSigningKey) SizeVT() (n int) { if m.RevokeSigningKey != nil { l = m.RevokeSigningKey.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -45886,8 +44608,6 @@ func (m *LogPayload_SetSigningConfig) SizeVT() (n int) { if m.SetSigningConfig != nil { l = m.SetSigningConfig.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -45900,8 +44620,6 @@ func (m *LogPayload_AddedEventsSink) SizeVT() (n int) { if m.AddedEventsSink != nil { l = m.AddedEventsSink.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -45914,8 +44632,6 @@ func (m *LogPayload_RemovedEventsSink) SizeVT() (n int) { if m.RemovedEventsSink != nil { l = m.RemovedEventsSink.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -45928,8 +44644,6 @@ func (m *LogPayload_SetMaintenanceMode) SizeVT() (n int) { if m.SetMaintenanceMode != nil { l = m.SetMaintenanceMode.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -45942,8 +44656,6 @@ func (m *LogPayload_PromoteLedger) SizeVT() (n int) { if m.PromoteLedger != nil { l = m.PromoteLedger.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -45956,8 +44668,6 @@ func (m *LogPayload_CreatedPreparedQuery) SizeVT() (n int) { if m.CreatedPreparedQuery != nil { l = m.CreatedPreparedQuery.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -45970,8 +44680,6 @@ func (m *LogPayload_UpdatedPreparedQuery) SizeVT() (n int) { if m.UpdatedPreparedQuery != nil { l = m.UpdatedPreparedQuery.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -45984,8 +44692,6 @@ func (m *LogPayload_DeletedPreparedQuery) SizeVT() (n int) { if m.DeletedPreparedQuery != nil { l = m.DeletedPreparedQuery.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -45998,8 +44704,6 @@ func (m *LogPayload_SavedNumscript) SizeVT() (n int) { if m.SavedNumscript != nil { l = m.SavedNumscript.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -46012,8 +44716,6 @@ func (m *LogPayload_CreatedQueryCheckpoint) SizeVT() (n int) { if m.CreatedQueryCheckpoint != nil { l = m.CreatedQueryCheckpoint.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -46026,8 +44728,6 @@ func (m *LogPayload_DeletedQueryCheckpoint) SizeVT() (n int) { if m.DeletedQueryCheckpoint != nil { l = m.DeletedQueryCheckpoint.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -46040,8 +44740,6 @@ func (m *LogPayload_SetQueryCheckpointSchedule) SizeVT() (n int) { if m.SetQueryCheckpointSchedule != nil { l = m.SetQueryCheckpointSchedule.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -46054,8 +44752,6 @@ func (m *LogPayload_DeleteQueryCheckpointSchedule) SizeVT() (n int) { if m.DeleteQueryCheckpointSchedule != nil { l = m.DeleteQueryCheckpointSchedule.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -46068,8 +44764,6 @@ func (m *LogPayload_SavedLedgerMetadata) SizeVT() (n int) { if m.SavedLedgerMetadata != nil { l = m.SavedLedgerMetadata.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -46082,8 +44776,6 @@ func (m *LogPayload_DeletedLedgerMetadata) SizeVT() (n int) { if m.DeletedLedgerMetadata != nil { l = m.DeletedLedgerMetadata.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -46096,8 +44788,6 @@ func (m *LogPayload_SetClusterPolicy) SizeVT() (n int) { if m.SetClusterPolicy != nil { l = m.SetClusterPolicy.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -46683,8 +45373,6 @@ func (m *SinkConfig_Nats) SizeVT() (n int) { if m.Nats != nil { l = m.Nats.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -46697,8 +45385,6 @@ func (m *SinkConfig_Clickhouse) SizeVT() (n int) { if m.Clickhouse != nil { l = m.Clickhouse.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -46711,8 +45397,6 @@ func (m *SinkConfig_Kafka) SizeVT() (n int) { if m.Kafka != nil { l = m.Kafka.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -46725,8 +45409,6 @@ func (m *SinkConfig_Http) SizeVT() (n int) { if m.Http != nil { l = m.Http.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -46739,8 +45421,6 @@ func (m *SinkConfig_Databricks) SizeVT() (n int) { if m.Databricks != nil { l = m.Databricks.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -46927,8 +45607,6 @@ func (m *DatabricksSinkConfig_OauthM2M) SizeVT() (n int) { if m.OauthM2M != nil { l = m.OauthM2M.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -47130,8 +45808,6 @@ func (m *LedgerLogPayload_CreatedTransaction) SizeVT() (n int) { if m.CreatedTransaction != nil { l = m.CreatedTransaction.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -47144,8 +45820,6 @@ func (m *LedgerLogPayload_RevertedTransaction) SizeVT() (n int) { if m.RevertedTransaction != nil { l = m.RevertedTransaction.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -47158,8 +45832,6 @@ func (m *LedgerLogPayload_SavedMetadata) SizeVT() (n int) { if m.SavedMetadata != nil { l = m.SavedMetadata.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -47172,8 +45844,6 @@ func (m *LedgerLogPayload_DeletedMetadata) SizeVT() (n int) { if m.DeletedMetadata != nil { l = m.DeletedMetadata.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -47186,8 +45856,6 @@ func (m *LedgerLogPayload_SetMetadataFieldType) SizeVT() (n int) { if m.SetMetadataFieldType != nil { l = m.SetMetadataFieldType.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -47200,8 +45868,6 @@ func (m *LedgerLogPayload_RemovedMetadataFieldType) SizeVT() (n int) { if m.RemovedMetadataFieldType != nil { l = m.RemovedMetadataFieldType.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -47214,8 +45880,6 @@ func (m *LedgerLogPayload_FillGap) SizeVT() (n int) { if m.FillGap != nil { l = m.FillGap.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -47228,8 +45892,6 @@ func (m *LedgerLogPayload_CreateIndex) SizeVT() (n int) { if m.CreateIndex != nil { l = m.CreateIndex.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -47242,8 +45904,6 @@ func (m *LedgerLogPayload_DropIndex) SizeVT() (n int) { if m.DropIndex != nil { l = m.DropIndex.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -47256,8 +45916,6 @@ func (m *LedgerLogPayload_AddedAccountType) SizeVT() (n int) { if m.AddedAccountType != nil { l = m.AddedAccountType.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -47270,8 +45928,6 @@ func (m *LedgerLogPayload_RemovedAccountType) SizeVT() (n int) { if m.RemovedAccountType != nil { l = m.RemovedAccountType.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -47284,8 +45940,6 @@ func (m *LedgerLogPayload_UpdatedDefaultEnforcementMode) SizeVT() (n int) { if m.UpdatedDefaultEnforcementMode != nil { l = m.UpdatedDefaultEnforcementMode.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -47298,8 +45952,6 @@ func (m *LedgerLogPayload_OrderSkipped) SizeVT() (n int) { if m.OrderSkipped != nil { l = m.OrderSkipped.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -47325,8 +45977,6 @@ func (m *FailureDetails_AccountAddressEmpty) SizeVT() (n int) { if m.AccountAddressEmpty != nil { l = m.AccountAddressEmpty.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -47339,8 +45989,6 @@ func (m *FailureDetails_AccountAddressEmptySegment) SizeVT() (n int) { if m.AccountAddressEmptySegment != nil { l = m.AccountAddressEmptySegment.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -47353,8 +46001,6 @@ func (m *FailureDetails_AccountAddressInvalidChar) SizeVT() (n int) { if m.AccountAddressInvalidChar != nil { l = m.AccountAddressInvalidChar.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -47367,8 +46013,6 @@ func (m *FailureDetails_AccountAddressTooLong) SizeVT() (n int) { if m.AccountAddressTooLong != nil { l = m.AccountAddressTooLong.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -47381,8 +46025,6 @@ func (m *FailureDetails_AccountNotMatchingType) SizeVT() (n int) { if m.AccountNotMatchingType != nil { l = m.AccountNotMatchingType.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -47395,8 +46037,6 @@ func (m *FailureDetails_AccountTypeAlreadyExists) SizeVT() (n int) { if m.AccountTypeAlreadyExists != nil { l = m.AccountTypeAlreadyExists.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -47409,8 +46049,6 @@ func (m *FailureDetails_AccountTypeConflict) SizeVT() (n int) { if m.AccountTypeConflict != nil { l = m.AccountTypeConflict.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -47423,8 +46061,6 @@ func (m *FailureDetails_AccountTypeHasAccounts) SizeVT() (n int) { if m.AccountTypeHasAccounts != nil { l = m.AccountTypeHasAccounts.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -47437,8 +46073,6 @@ func (m *FailureDetails_AccountTypeNameTooLong) SizeVT() (n int) { if m.AccountTypeNameTooLong != nil { l = m.AccountTypeNameTooLong.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -47451,8 +46085,6 @@ func (m *FailureDetails_AccountTypeNotFound) SizeVT() (n int) { if m.AccountTypeNotFound != nil { l = m.AccountTypeNotFound.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -47465,8 +46097,6 @@ func (m *FailureDetails_AccountTypePatternTooLong) SizeVT() (n int) { if m.AccountTypePatternTooLong != nil { l = m.AccountTypePatternTooLong.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -47479,8 +46109,6 @@ func (m *FailureDetails_AssetInvalid) SizeVT() (n int) { if m.AssetInvalid != nil { l = m.AssetInvalid.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -47493,8 +46121,6 @@ func (m *FailureDetails_BalanceNotFound) SizeVT() (n int) { if m.BalanceNotFound != nil { l = m.BalanceNotFound.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -47507,8 +46133,6 @@ func (m *FailureDetails_CheckpointIdRequired) SizeVT() (n int) { if m.CheckpointIdRequired != nil { l = m.CheckpointIdRequired.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -47521,8 +46145,6 @@ func (m *FailureDetails_CheckpointLimitReached) SizeVT() (n int) { if m.CheckpointLimitReached != nil { l = m.CheckpointLimitReached.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -47535,8 +46157,6 @@ func (m *FailureDetails_CheckpointNotFound) SizeVT() (n int) { if m.CheckpointNotFound != nil { l = m.CheckpointNotFound.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -47549,8 +46169,6 @@ func (m *FailureDetails_ColorInvalid) SizeVT() (n int) { if m.ColorInvalid != nil { l = m.ColorInvalid.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -47563,8 +46181,6 @@ func (m *FailureDetails_ColorTooLong) SizeVT() (n int) { if m.ColorTooLong != nil { l = m.ColorTooLong.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -47577,8 +46193,6 @@ func (m *FailureDetails_EmptyTransaction) SizeVT() (n int) { if m.EmptyTransaction != nil { l = m.EmptyTransaction.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -47591,8 +46205,6 @@ func (m *FailureDetails_IdempotencyKeyConflict) SizeVT() (n int) { if m.IdempotencyKeyConflict != nil { l = m.IdempotencyKeyConflict.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -47605,8 +46217,6 @@ func (m *FailureDetails_IdRequired) SizeVT() (n int) { if m.IdRequired != nil { l = m.IdRequired.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -47619,8 +46229,6 @@ func (m *FailureDetails_IndexAlreadyExists) SizeVT() (n int) { if m.IndexAlreadyExists != nil { l = m.IndexAlreadyExists.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -47633,8 +46241,6 @@ func (m *FailureDetails_IndexNotFound) SizeVT() (n int) { if m.IndexNotFound != nil { l = m.IndexNotFound.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -47647,8 +46253,6 @@ func (m *FailureDetails_IndexTargetUnsupported) SizeVT() (n int) { if m.IndexTargetUnsupported != nil { l = m.IndexTargetUnsupported.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -47661,8 +46265,6 @@ func (m *FailureDetails_InsufficientFunds) SizeVT() (n int) { if m.InsufficientFunds != nil { l = m.InsufficientFunds.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -47675,8 +46277,6 @@ func (m *FailureDetails_InvalidCronExpression) SizeVT() (n int) { if m.InvalidCronExpression != nil { l = m.InvalidCronExpression.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -47689,8 +46289,6 @@ func (m *FailureDetails_InvalidPattern) SizeVT() (n int) { if m.InvalidPattern != nil { l = m.InvalidPattern.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -47703,8 +46301,6 @@ func (m *FailureDetails_InvalidSkippableReason) SizeVT() (n int) { if m.InvalidSkippableReason != nil { l = m.InvalidSkippableReason.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -47717,8 +46313,6 @@ func (m *FailureDetails_LedgerAlreadyExists) SizeVT() (n int) { if m.LedgerAlreadyExists != nil { l = m.LedgerAlreadyExists.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -47731,8 +46325,6 @@ func (m *FailureDetails_LedgerDeleted) SizeVT() (n int) { if m.LedgerDeleted != nil { l = m.LedgerDeleted.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -47745,8 +46337,6 @@ func (m *FailureDetails_LedgerInMirrorMode) SizeVT() (n int) { if m.LedgerInMirrorMode != nil { l = m.LedgerInMirrorMode.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -47759,8 +46349,6 @@ func (m *FailureDetails_LedgerNameInvalidChar) SizeVT() (n int) { if m.LedgerNameInvalidChar != nil { l = m.LedgerNameInvalidChar.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -47773,8 +46361,6 @@ func (m *FailureDetails_LedgerNameRequired) SizeVT() (n int) { if m.LedgerNameRequired != nil { l = m.LedgerNameRequired.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -47787,8 +46373,6 @@ func (m *FailureDetails_LedgerNameReservedPrefix) SizeVT() (n int) { if m.LedgerNameReservedPrefix != nil { l = m.LedgerNameReservedPrefix.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -47801,8 +46385,6 @@ func (m *FailureDetails_LedgerNameTooLong) SizeVT() (n int) { if m.LedgerNameTooLong != nil { l = m.LedgerNameTooLong.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -47815,8 +46397,6 @@ func (m *FailureDetails_LedgerNotFound) SizeVT() (n int) { if m.LedgerNotFound != nil { l = m.LedgerNotFound.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -47829,8 +46409,6 @@ func (m *FailureDetails_LedgerNotInMirrorMode) SizeVT() (n int) { if m.LedgerNotInMirrorMode != nil { l = m.LedgerNotInMirrorMode.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -47843,8 +46421,6 @@ func (m *FailureDetails_MetadataFieldNotInSchema) SizeVT() (n int) { if m.MetadataFieldNotInSchema != nil { l = m.MetadataFieldNotInSchema.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -47857,8 +46433,6 @@ func (m *FailureDetails_MetadataKeyEmpty) SizeVT() (n int) { if m.MetadataKeyEmpty != nil { l = m.MetadataKeyEmpty.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -47871,8 +46445,6 @@ func (m *FailureDetails_MetadataKeyInvalidChar) SizeVT() (n int) { if m.MetadataKeyInvalidChar != nil { l = m.MetadataKeyInvalidChar.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -47885,8 +46457,6 @@ func (m *FailureDetails_MetadataKeyRequired) SizeVT() (n int) { if m.MetadataKeyRequired != nil { l = m.MetadataKeyRequired.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -47899,8 +46469,6 @@ func (m *FailureDetails_MetadataLimitExceeded) SizeVT() (n int) { if m.MetadataLimitExceeded != nil { l = m.MetadataLimitExceeded.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -47913,8 +46481,6 @@ func (m *FailureDetails_MetadataNotFound) SizeVT() (n int) { if m.MetadataNotFound != nil { l = m.MetadataNotFound.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -47927,8 +46493,6 @@ func (m *FailureDetails_MetadataValueContainsNullByte) SizeVT() (n int) { if m.MetadataValueContainsNullByte != nil { l = m.MetadataValueContainsNullByte.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -47941,8 +46505,6 @@ func (m *FailureDetails_MirrorHttpUrlInvalid) SizeVT() (n int) { if m.MirrorHttpUrlInvalid != nil { l = m.MirrorHttpUrlInvalid.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -47955,8 +46517,6 @@ func (m *FailureDetails_MirrorIamRegionRequired) SizeVT() (n int) { if m.MirrorIamRegionRequired != nil { l = m.MirrorIamRegionRequired.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -47969,8 +46529,6 @@ func (m *FailureDetails_MirrorIamRequiresTls) SizeVT() (n int) { if m.MirrorIamRequiresTls != nil { l = m.MirrorIamRequiresTls.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -47983,8 +46541,6 @@ func (m *FailureDetails_MirrorRewriteRuleInvalid) SizeVT() (n int) { if m.MirrorRewriteRuleInvalid != nil { l = m.MirrorRewriteRuleInvalid.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -47997,8 +46553,6 @@ func (m *FailureDetails_NumscriptAssetMismatch) SizeVT() (n int) { if m.NumscriptAssetMismatch != nil { l = m.NumscriptAssetMismatch.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -48011,8 +46565,6 @@ func (m *FailureDetails_NumscriptBadArity) SizeVT() (n int) { if m.NumscriptBadArity != nil { l = m.NumscriptBadArity.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -48025,8 +46577,6 @@ func (m *FailureDetails_NumscriptBadMetaValue) SizeVT() (n int) { if m.NumscriptBadMetaValue != nil { l = m.NumscriptBadMetaValue.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -48039,8 +46589,6 @@ func (m *FailureDetails_NumscriptBadPortion) SizeVT() (n int) { if m.NumscriptBadPortion != nil { l = m.NumscriptBadPortion.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -48053,8 +46601,6 @@ func (m *FailureDetails_NumscriptCannotCastScopedAccount) SizeVT() (n int) { if m.NumscriptCannotCastScopedAccount != nil { l = m.NumscriptCannotCastScopedAccount.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -48067,8 +46613,6 @@ func (m *FailureDetails_NumscriptCannotCastToString) SizeVT() (n int) { if m.NumscriptCannotCastToString != nil { l = m.NumscriptCannotCastToString.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -48081,8 +46625,6 @@ func (m *FailureDetails_NumscriptCannotStoreScopedAccount) SizeVT() (n int) { if m.NumscriptCannotStoreScopedAccount != nil { l = m.NumscriptCannotStoreScopedAccount.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -48095,8 +46637,6 @@ func (m *FailureDetails_NumscriptCompileBadArity) SizeVT() (n int) { if m.NumscriptCompileBadArity != nil { l = m.NumscriptCompileBadArity.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -48109,8 +46649,6 @@ func (m *FailureDetails_NumscriptCompileCannotCastToString) SizeVT() (n int) { if m.NumscriptCompileCannotCastToString != nil { l = m.NumscriptCompileCannotCastToString.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -48123,8 +46661,6 @@ func (m *FailureDetails_NumscriptCompileCannotStoreScopedAccount) SizeVT() (n in if m.NumscriptCompileCannotStoreScopedAccount != nil { l = m.NumscriptCompileCannotStoreScopedAccount.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -48137,8 +46673,6 @@ func (m *FailureDetails_NumscriptCompileDuplicateRemaining) SizeVT() (n int) { if m.NumscriptCompileDuplicateRemaining != nil { l = m.NumscriptCompileDuplicateRemaining.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -48151,8 +46685,6 @@ func (m *FailureDetails_NumscriptCompileDuplicateVariable) SizeVT() (n int) { if m.NumscriptCompileDuplicateVariable != nil { l = m.NumscriptCompileDuplicateVariable.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -48165,8 +46697,6 @@ func (m *FailureDetails_NumscriptCompileExperimentalFeature) SizeVT() (n int) { if m.NumscriptCompileExperimentalFeature != nil { l = m.NumscriptCompileExperimentalFeature.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -48179,8 +46709,6 @@ func (m *FailureDetails_NumscriptCompileInvalidFeature) SizeVT() (n int) { if m.NumscriptCompileInvalidFeature != nil { l = m.NumscriptCompileInvalidFeature.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -48193,8 +46721,6 @@ func (m *FailureDetails_NumscriptCompileInvalidMetaPosition) SizeVT() (n int) { if m.NumscriptCompileInvalidMetaPosition != nil { l = m.NumscriptCompileInvalidMetaPosition.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -48207,8 +46733,6 @@ func (m *FailureDetails_NumscriptCompileInvalidType) SizeVT() (n int) { if m.NumscriptCompileInvalidType != nil { l = m.NumscriptCompileInvalidType.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -48221,8 +46745,6 @@ func (m *FailureDetails_NumscriptCompileInvalidUncappedSource) SizeVT() (n int) if m.NumscriptCompileInvalidUncappedSource != nil { l = m.NumscriptCompileInvalidUncappedSource.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -48235,8 +46757,6 @@ func (m *FailureDetails_NumscriptCompileInvalidVariableValue) SizeVT() (n int) { if m.NumscriptCompileInvalidVariableValue != nil { l = m.NumscriptCompileInvalidVariableValue.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -48249,8 +46769,6 @@ func (m *FailureDetails_NumscriptCompileMissingVariable) SizeVT() (n int) { if m.NumscriptCompileMissingVariable != nil { l = m.NumscriptCompileMissingVariable.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -48263,8 +46781,6 @@ func (m *FailureDetails_NumscriptCompileTypeMismatch) SizeVT() (n int) { if m.NumscriptCompileTypeMismatch != nil { l = m.NumscriptCompileTypeMismatch.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -48277,8 +46793,6 @@ func (m *FailureDetails_NumscriptCompileUnboundVariable) SizeVT() (n int) { if m.NumscriptCompileUnboundVariable != nil { l = m.NumscriptCompileUnboundVariable.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -48291,8 +46805,6 @@ func (m *FailureDetails_NumscriptCompileUnknownFunction) SizeVT() (n int) { if m.NumscriptCompileUnknownFunction != nil { l = m.NumscriptCompileUnknownFunction.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -48305,8 +46817,6 @@ func (m *FailureDetails_NumscriptContentRequired) SizeVT() (n int) { if m.NumscriptContentRequired != nil { l = m.NumscriptContentRequired.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -48319,8 +46829,6 @@ func (m *FailureDetails_NumscriptCurrencyMismatch) SizeVT() (n int) { if m.NumscriptCurrencyMismatch != nil { l = m.NumscriptCurrencyMismatch.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -48333,8 +46841,6 @@ func (m *FailureDetails_NumscriptDivideByZero) SizeVT() (n int) { if m.NumscriptDivideByZero != nil { l = m.NumscriptDivideByZero.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -48347,8 +46853,6 @@ func (m *FailureDetails_NumscriptExperimentalFeature) SizeVT() (n int) { if m.NumscriptExperimentalFeature != nil { l = m.NumscriptExperimentalFeature.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -48361,8 +46865,6 @@ func (m *FailureDetails_NumscriptInvalidAccountName) SizeVT() (n int) { if m.NumscriptInvalidAccountName != nil { l = m.NumscriptInvalidAccountName.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -48375,8 +46877,6 @@ func (m *FailureDetails_NumscriptInvalidAllotmentInSendAll) SizeVT() (n int) { if m.NumscriptInvalidAllotmentInSendAll != nil { l = m.NumscriptInvalidAllotmentInSendAll.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -48389,8 +46889,6 @@ func (m *FailureDetails_NumscriptInvalidAllotmentSum) SizeVT() (n int) { if m.NumscriptInvalidAllotmentSum != nil { l = m.NumscriptInvalidAllotmentSum.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -48403,8 +46901,6 @@ func (m *FailureDetails_NumscriptInvalidAsset) SizeVT() (n int) { if m.NumscriptInvalidAsset != nil { l = m.NumscriptInvalidAsset.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -48417,8 +46913,6 @@ func (m *FailureDetails_NumscriptInvalidColor) SizeVT() (n int) { if m.NumscriptInvalidColor != nil { l = m.NumscriptInvalidColor.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -48431,8 +46925,6 @@ func (m *FailureDetails_NumscriptInvalidFeature) SizeVT() (n int) { if m.NumscriptInvalidFeature != nil { l = m.NumscriptInvalidFeature.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -48445,8 +46937,6 @@ func (m *FailureDetails_NumscriptInvalidMonetaryLiteral) SizeVT() (n int) { if m.NumscriptInvalidMonetaryLiteral != nil { l = m.NumscriptInvalidMonetaryLiteral.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -48459,8 +46949,6 @@ func (m *FailureDetails_NumscriptInvalidNestedMeta) SizeVT() (n int) { if m.NumscriptInvalidNestedMeta != nil { l = m.NumscriptInvalidNestedMeta.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -48473,8 +46961,6 @@ func (m *FailureDetails_NumscriptInvalidNumberLiteral) SizeVT() (n int) { if m.NumscriptInvalidNumberLiteral != nil { l = m.NumscriptInvalidNumberLiteral.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -48487,8 +46973,6 @@ func (m *FailureDetails_NumscriptInvalidRemainingAllotment) SizeVT() (n int) { if m.NumscriptInvalidRemainingAllotment != nil { l = m.NumscriptInvalidRemainingAllotment.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -48501,8 +46985,6 @@ func (m *FailureDetails_NumscriptInvalidScope) SizeVT() (n int) { if m.NumscriptInvalidScope != nil { l = m.NumscriptInvalidScope.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -48515,8 +46997,6 @@ func (m *FailureDetails_NumscriptInvalidType) SizeVT() (n int) { if m.NumscriptInvalidType != nil { l = m.NumscriptInvalidType.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -48529,8 +47009,6 @@ func (m *FailureDetails_NumscriptInvalidUnboundedScalingAddress) SizeVT() (n int if m.NumscriptInvalidUnboundedScalingAddress != nil { l = m.NumscriptInvalidUnboundedScalingAddress.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -48543,8 +47021,6 @@ func (m *FailureDetails_NumscriptInvalidUnboundedSendAll) SizeVT() (n int) { if m.NumscriptInvalidUnboundedSendAll != nil { l = m.NumscriptInvalidUnboundedSendAll.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -48557,8 +47033,6 @@ func (m *FailureDetails_NumscriptInvalidUncappedSource) SizeVT() (n int) { if m.NumscriptInvalidUncappedSource != nil { l = m.NumscriptInvalidUncappedSource.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -48571,8 +47045,6 @@ func (m *FailureDetails_NumscriptInvalidVersion) SizeVT() (n int) { if m.NumscriptInvalidVersion != nil { l = m.NumscriptInvalidVersion.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -48585,8 +47057,6 @@ func (m *FailureDetails_NumscriptMetadataNotFound) SizeVT() (n int) { if m.NumscriptMetadataNotFound != nil { l = m.NumscriptMetadataNotFound.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -48599,8 +47069,6 @@ func (m *FailureDetails_NumscriptMissingFunds) SizeVT() (n int) { if m.NumscriptMissingFunds != nil { l = m.NumscriptMissingFunds.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -48613,8 +47081,6 @@ func (m *FailureDetails_NumscriptMissingVariable) SizeVT() (n int) { if m.NumscriptMissingVariable != nil { l = m.NumscriptMissingVariable.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -48627,8 +47093,6 @@ func (m *FailureDetails_NumscriptNameInvalidChar) SizeVT() (n int) { if m.NumscriptNameInvalidChar != nil { l = m.NumscriptNameInvalidChar.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -48641,8 +47105,6 @@ func (m *FailureDetails_NumscriptNameRequired) SizeVT() (n int) { if m.NumscriptNameRequired != nil { l = m.NumscriptNameRequired.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -48655,8 +47117,6 @@ func (m *FailureDetails_NumscriptNameTooLong) SizeVT() (n int) { if m.NumscriptNameTooLong != nil { l = m.NumscriptNameTooLong.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -48669,8 +47129,6 @@ func (m *FailureDetails_NumscriptNegativeAmount) SizeVT() (n int) { if m.NumscriptNegativeAmount != nil { l = m.NumscriptNegativeAmount.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -48683,8 +47141,6 @@ func (m *FailureDetails_NumscriptNegativeBalance) SizeVT() (n int) { if m.NumscriptNegativeBalance != nil { l = m.NumscriptNegativeBalance.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -48697,8 +47153,6 @@ func (m *FailureDetails_NumscriptNegativePortion) SizeVT() (n int) { if m.NumscriptNegativePortion != nil { l = m.NumscriptNegativePortion.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -48711,8 +47165,6 @@ func (m *FailureDetails_NumscriptNotFound) SizeVT() (n int) { if m.NumscriptNotFound != nil { l = m.NumscriptNotFound.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -48725,8 +47177,6 @@ func (m *FailureDetails_NumscriptParseError) SizeVT() (n int) { if m.NumscriptParseError != nil { l = m.NumscriptParseError.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -48739,8 +47189,6 @@ func (m *FailureDetails_NumscriptPostingAmountOverflow) SizeVT() (n int) { if m.NumscriptPostingAmountOverflow != nil { l = m.NumscriptPostingAmountOverflow.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -48753,8 +47201,6 @@ func (m *FailureDetails_NumscriptPostingNegativeAmount) SizeVT() (n int) { if m.NumscriptPostingNegativeAmount != nil { l = m.NumscriptPostingNegativeAmount.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -48767,8 +47213,6 @@ func (m *FailureDetails_NumscriptScalingUnsupported) SizeVT() (n int) { if m.NumscriptScalingUnsupported != nil { l = m.NumscriptScalingUnsupported.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -48781,8 +47225,6 @@ func (m *FailureDetails_NumscriptTypeError) SizeVT() (n int) { if m.NumscriptTypeError != nil { l = m.NumscriptTypeError.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -48795,8 +47237,6 @@ func (m *FailureDetails_NumscriptUnboundFunction) SizeVT() (n int) { if m.NumscriptUnboundFunction != nil { l = m.NumscriptUnboundFunction.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -48809,8 +47249,6 @@ func (m *FailureDetails_NumscriptUnboundVariable) SizeVT() (n int) { if m.NumscriptUnboundVariable != nil { l = m.NumscriptUnboundVariable.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -48823,8 +47261,6 @@ func (m *FailureDetails_NumscriptVersionAlreadyExists) SizeVT() (n int) { if m.NumscriptVersionAlreadyExists != nil { l = m.NumscriptVersionAlreadyExists.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -48837,8 +47273,6 @@ func (m *FailureDetails_PostingsAndScriptConflict) SizeVT() (n int) { if m.PostingsAndScriptConflict != nil { l = m.PostingsAndScriptConflict.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -48851,8 +47285,6 @@ func (m *FailureDetails_PreparedQueryAlreadyExists) SizeVT() (n int) { if m.PreparedQueryAlreadyExists != nil { l = m.PreparedQueryAlreadyExists.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -48865,8 +47297,6 @@ func (m *FailureDetails_PreparedQueryNameInvalidChar) SizeVT() (n int) { if m.PreparedQueryNameInvalidChar != nil { l = m.PreparedQueryNameInvalidChar.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -48879,8 +47309,6 @@ func (m *FailureDetails_PreparedQueryNameRequired) SizeVT() (n int) { if m.PreparedQueryNameRequired != nil { l = m.PreparedQueryNameRequired.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -48893,8 +47321,6 @@ func (m *FailureDetails_PreparedQueryNameTooLong) SizeVT() (n int) { if m.PreparedQueryNameTooLong != nil { l = m.PreparedQueryNameTooLong.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -48907,8 +47333,6 @@ func (m *FailureDetails_PreparedQueryNotFound) SizeVT() (n int) { if m.PreparedQueryNotFound != nil { l = m.PreparedQueryNotFound.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -48921,8 +47345,6 @@ func (m *FailureDetails_PreparedQueryRequired) SizeVT() (n int) { if m.PreparedQueryRequired != nil { l = m.PreparedQueryRequired.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -48935,8 +47357,6 @@ func (m *FailureDetails_PreparedQueryTargetUnsupported) SizeVT() (n int) { if m.PreparedQueryTargetUnsupported != nil { l = m.PreparedQueryTargetUnsupported.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -48949,8 +47369,6 @@ func (m *FailureDetails_RevertTargetCreatedInBatch) SizeVT() (n int) { if m.RevertTargetCreatedInBatch != nil { l = m.RevertTargetCreatedInBatch.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -48963,8 +47381,6 @@ func (m *FailureDetails_ScopedBalanceUnsupported) SizeVT() (n int) { if m.ScopedBalanceUnsupported != nil { l = m.ScopedBalanceUnsupported.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -48977,8 +47393,6 @@ func (m *FailureDetails_ScriptAndReferenceConflict) SizeVT() (n int) { if m.ScriptAndReferenceConflict != nil { l = m.ScriptAndReferenceConflict.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -48991,8 +47405,6 @@ func (m *FailureDetails_ScriptRequired) SizeVT() (n int) { if m.ScriptRequired != nil { l = m.ScriptRequired.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -49005,8 +47417,6 @@ func (m *FailureDetails_SigningKeyIdInvalidChar) SizeVT() (n int) { if m.SigningKeyIdInvalidChar != nil { l = m.SigningKeyIdInvalidChar.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -49019,8 +47429,6 @@ func (m *FailureDetails_SigningKeyIdRequired) SizeVT() (n int) { if m.SigningKeyIdRequired != nil { l = m.SigningKeyIdRequired.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -49033,8 +47441,6 @@ func (m *FailureDetails_SigningKeyIdTooLong) SizeVT() (n int) { if m.SigningKeyIdTooLong != nil { l = m.SigningKeyIdTooLong.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -49047,8 +47453,6 @@ func (m *FailureDetails_SigningKeyInvalidLength) SizeVT() (n int) { if m.SigningKeyInvalidLength != nil { l = m.SigningKeyInvalidLength.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -49061,8 +47465,6 @@ func (m *FailureDetails_SinkAlreadyExists) SizeVT() (n int) { if m.SinkAlreadyExists != nil { l = m.SinkAlreadyExists.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -49075,8 +47477,6 @@ func (m *FailureDetails_SinkBatchSizeTooLarge) SizeVT() (n int) { if m.SinkBatchSizeTooLarge != nil { l = m.SinkBatchSizeTooLarge.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -49089,8 +47489,6 @@ func (m *FailureDetails_SinkControllerMismatch) SizeVT() (n int) { if m.SinkControllerMismatch != nil { l = m.SinkControllerMismatch.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -49103,8 +47501,6 @@ func (m *FailureDetails_SinkNotFound) SizeVT() (n int) { if m.SinkNotFound != nil { l = m.SinkNotFound.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -49117,8 +47513,6 @@ func (m *FailureDetails_TargetRequired) SizeVT() (n int) { if m.TargetRequired != nil { l = m.TargetRequired.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -49131,8 +47525,6 @@ func (m *FailureDetails_TransactionAlreadyReverted) SizeVT() (n int) { if m.TransactionAlreadyReverted != nil { l = m.TransactionAlreadyReverted.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -49145,8 +47537,6 @@ func (m *FailureDetails_TransactionNotFound) SizeVT() (n int) { if m.TransactionNotFound != nil { l = m.TransactionNotFound.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -49159,8 +47549,6 @@ func (m *FailureDetails_TransactionReferenceConflict) SizeVT() (n int) { if m.TransactionReferenceConflict != nil { l = m.TransactionReferenceConflict.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -49173,8 +47561,6 @@ func (m *FailureDetails_TransactionReferenceNotFound) SizeVT() (n int) { if m.TransactionReferenceNotFound != nil { l = m.TransactionReferenceNotFound.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -49187,8 +47573,6 @@ func (m *FailureDetails_TransactionReferenceTooLong) SizeVT() (n int) { if m.TransactionReferenceTooLong != nil { l = m.TransactionReferenceTooLong.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -49201,8 +47585,6 @@ func (m *FailureDetails_TransactionTargetMissing) SizeVT() (n int) { if m.TransactionTargetMissing != nil { l = m.TransactionTargetMissing.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -49215,8 +47597,6 @@ func (m *FailureDetails_TransientAccountNonZero) SizeVT() (n int) { if m.TransientAccountNonZero != nil { l = m.TransientAccountNonZero.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -49229,8 +47609,6 @@ func (m *FailureDetails_VolumeOverflow) SizeVT() (n int) { if m.VolumeOverflow != nil { l = m.VolumeOverflow.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -49243,8 +47621,6 @@ func (m *FailureDetails_AccountMetadataKeyEmpty) SizeVT() (n int) { if m.AccountMetadataKeyEmpty != nil { l = m.AccountMetadataKeyEmpty.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -49257,8 +47633,6 @@ func (m *FailureDetails_AccountMetadataKeyInvalidChar) SizeVT() (n int) { if m.AccountMetadataKeyInvalidChar != nil { l = m.AccountMetadataKeyInvalidChar.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -49271,8 +47645,6 @@ func (m *FailureDetails_AccountMetadataValueContainsNullByte) SizeVT() (n int) { if m.AccountMetadataValueContainsNullByte != nil { l = m.AccountMetadataValueContainsNullByte.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -49285,8 +47657,6 @@ func (m *FailureDetails_AccountMetadataLimitExceeded) SizeVT() (n int) { if m.AccountMetadataLimitExceeded != nil { l = m.AccountMetadataLimitExceeded.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -49299,8 +47669,6 @@ func (m *FailureDetails_MetadataKeyLimitExceeded) SizeVT() (n int) { if m.MetadataKeyLimitExceeded != nil { l = m.MetadataKeyLimitExceeded.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -49313,8 +47681,6 @@ func (m *FailureDetails_AccountMetadataKeyLimitExceeded) SizeVT() (n int) { if m.AccountMetadataKeyLimitExceeded != nil { l = m.AccountMetadataKeyLimitExceeded.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -49327,8 +47693,6 @@ func (m *FailureDetails_NumscriptInsufficientFunds) SizeVT() (n int) { if m.NumscriptInsufficientFunds != nil { l = m.NumscriptInsufficientFunds.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -49341,8 +47705,6 @@ func (m *FailureDetails_NumscriptDivideByZeroWithNumerator) SizeVT() (n int) { if m.NumscriptDivideByZeroWithNumerator != nil { l = m.NumscriptDivideByZeroWithNumerator.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -49355,8 +47717,6 @@ func (m *FailureDetails_NumscriptScopedMetadataNotFound) SizeVT() (n int) { if m.NumscriptScopedMetadataNotFound != nil { l = m.NumscriptScopedMetadataNotFound.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -49369,8 +47729,6 @@ func (m *FailureDetails_NumscriptScopedNegativeBalance) SizeVT() (n int) { if m.NumscriptScopedNegativeBalance != nil { l = m.NumscriptScopedNegativeBalance.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -49383,8 +47741,6 @@ func (m *FailureDetails_NumscriptVersionNotFound) SizeVT() (n int) { if m.NumscriptVersionNotFound != nil { l = m.NumscriptVersionNotFound.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -51722,8 +50078,6 @@ func (m *MirrorSourceConfig_Http) SizeVT() (n int) { if m.Http != nil { l = m.Http.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -51736,8 +50090,6 @@ func (m *MirrorSourceConfig_Postgres) SizeVT() (n int) { if m.Postgres != nil { l = m.Postgres.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -51788,8 +50140,6 @@ func (m *MirrorRewriteRule_CreatedTransaction) SizeVT() (n int) { if m.CreatedTransaction != nil { l = m.CreatedTransaction.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -51802,8 +50152,6 @@ func (m *MirrorRewriteRule_RevertedTransaction) SizeVT() (n int) { if m.RevertedTransaction != nil { l = m.RevertedTransaction.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -51816,8 +50164,6 @@ func (m *MirrorRewriteRule_SavedMetadata) SizeVT() (n int) { if m.SavedMetadata != nil { l = m.SavedMetadata.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -51830,8 +50176,6 @@ func (m *MirrorRewriteRule_DeletedMetadata) SizeVT() (n int) { if m.DeletedMetadata != nil { l = m.DeletedMetadata.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -51844,8 +50188,6 @@ func (m *MirrorRewriteRule_AnyVariant) SizeVT() (n int) { if m.AnyVariant != nil { l = m.AnyVariant.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -51971,8 +50313,6 @@ func (m *CreatedTransactionAction_RewriteAddress) SizeVT() (n int) { if m.RewriteAddress != nil { l = m.RewriteAddress.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -51985,8 +50325,6 @@ func (m *CreatedTransactionAction_SetMetadata) SizeVT() (n int) { if m.SetMetadata != nil { l = m.SetMetadata.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -51999,8 +50337,6 @@ func (m *CreatedTransactionAction_DeleteMetadata) SizeVT() (n int) { if m.DeleteMetadata != nil { l = m.DeleteMetadata.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -52013,8 +50349,6 @@ func (m *CreatedTransactionAction_SetAccountMetadata) SizeVT() (n int) { if m.SetAccountMetadata != nil { l = m.SetAccountMetadata.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -52027,8 +50361,6 @@ func (m *CreatedTransactionAction_DeleteAccountMetadata) SizeVT() (n int) { if m.DeleteAccountMetadata != nil { l = m.DeleteAccountMetadata.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -52041,8 +50373,6 @@ func (m *CreatedTransactionAction_SetAccountMetadataFromAddress) SizeVT() (n int if m.SetAccountMetadataFromAddress != nil { l = m.SetAccountMetadataFromAddress.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -52055,8 +50385,6 @@ func (m *CreatedTransactionAction_Drop) SizeVT() (n int) { if m.Drop != nil { l = m.Drop.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -52082,8 +50410,6 @@ func (m *RevertedTransactionAction_RewriteAddress) SizeVT() (n int) { if m.RewriteAddress != nil { l = m.RewriteAddress.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -52096,8 +50422,6 @@ func (m *RevertedTransactionAction_SetMetadata) SizeVT() (n int) { if m.SetMetadata != nil { l = m.SetMetadata.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -52110,8 +50434,6 @@ func (m *RevertedTransactionAction_DeleteMetadata) SizeVT() (n int) { if m.DeleteMetadata != nil { l = m.DeleteMetadata.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -52124,8 +50446,6 @@ func (m *RevertedTransactionAction_Drop) SizeVT() (n int) { if m.Drop != nil { l = m.Drop.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -52151,8 +50471,6 @@ func (m *SavedMetadataAction_RewriteAddress) SizeVT() (n int) { if m.RewriteAddress != nil { l = m.RewriteAddress.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -52165,8 +50483,6 @@ func (m *SavedMetadataAction_SetMetadata) SizeVT() (n int) { if m.SetMetadata != nil { l = m.SetMetadata.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -52179,8 +50495,6 @@ func (m *SavedMetadataAction_DeleteMetadata) SizeVT() (n int) { if m.DeleteMetadata != nil { l = m.DeleteMetadata.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -52193,8 +50507,6 @@ func (m *SavedMetadataAction_Drop) SizeVT() (n int) { if m.Drop != nil { l = m.Drop.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -52220,8 +50532,6 @@ func (m *DeletedMetadataAction_RewriteAddress) SizeVT() (n int) { if m.RewriteAddress != nil { l = m.RewriteAddress.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -52234,8 +50544,6 @@ func (m *DeletedMetadataAction_Drop) SizeVT() (n int) { if m.Drop != nil { l = m.Drop.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -52261,8 +50569,6 @@ func (m *AnyVariantAction_RewriteAddress) SizeVT() (n int) { if m.RewriteAddress != nil { l = m.RewriteAddress.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -52275,8 +50581,6 @@ func (m *AnyVariantAction_Drop) SizeVT() (n int) { if m.Drop != nil { l = m.Drop.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -52860,8 +51164,6 @@ func (m *SegmentType_Uuid) SizeVT() (n int) { if m.Uuid != nil { l = m.Uuid.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -52874,8 +51176,6 @@ func (m *SegmentType_Uint64) SizeVT() (n int) { if m.Uint64 != nil { l = m.Uint64.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -52888,8 +51188,6 @@ func (m *SegmentType_Bytes) SizeVT() (n int) { if m.Bytes != nil { l = m.Bytes.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -53020,8 +51318,6 @@ func (m *QueryFilter_Field) SizeVT() (n int) { if m.Field != nil { l = m.Field.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -53034,8 +51330,6 @@ func (m *QueryFilter_Address) SizeVT() (n int) { if m.Address != nil { l = m.Address.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -53048,8 +51342,6 @@ func (m *QueryFilter_And) SizeVT() (n int) { if m.And != nil { l = m.And.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -53062,8 +51354,6 @@ func (m *QueryFilter_Or) SizeVT() (n int) { if m.Or != nil { l = m.Or.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -53076,8 +51366,6 @@ func (m *QueryFilter_Not) SizeVT() (n int) { if m.Not != nil { l = m.Not.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -53090,8 +51378,6 @@ func (m *QueryFilter_Reference) SizeVT() (n int) { if m.Reference != nil { l = m.Reference.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -53104,8 +51390,6 @@ func (m *QueryFilter_BuiltinUint) SizeVT() (n int) { if m.BuiltinUint != nil { l = m.BuiltinUint.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -53118,8 +51402,6 @@ func (m *QueryFilter_Ledger) SizeVT() (n int) { if m.Ledger != nil { l = m.Ledger.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -53132,8 +51414,6 @@ func (m *QueryFilter_LogId) SizeVT() (n int) { if m.LogId != nil { l = m.LogId.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -53146,8 +51426,6 @@ func (m *QueryFilter_LogBuiltinUint) SizeVT() (n int) { if m.LogBuiltinUint != nil { l = m.LogBuiltinUint.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -53160,8 +51438,6 @@ func (m *QueryFilter_AccountHasAsset) SizeVT() (n int) { if m.AccountHasAsset != nil { l = m.AccountHasAsset.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -53174,8 +51450,6 @@ func (m *QueryFilter_Reverted) SizeVT() (n int) { if m.Reverted != nil { l = m.Reverted.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -53188,8 +51462,6 @@ func (m *QueryFilter_Audit) SizeVT() (n int) { if m.Audit != nil { l = m.Audit.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -53245,8 +51517,6 @@ func (m *AuditCondition_StringCond) SizeVT() (n int) { if m.StringCond != nil { l = m.StringCond.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -53259,8 +51529,6 @@ func (m *AuditCondition_UintCond) SizeVT() (n int) { if m.UintCond != nil { l = m.UintCond.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -53439,8 +51707,6 @@ func (m *FieldCondition_StringCond) SizeVT() (n int) { if m.StringCond != nil { l = m.StringCond.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -53453,8 +51719,6 @@ func (m *FieldCondition_IntCond) SizeVT() (n int) { if m.IntCond != nil { l = m.IntCond.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -53467,8 +51731,6 @@ func (m *FieldCondition_UintCond) SizeVT() (n int) { if m.UintCond != nil { l = m.UintCond.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -53481,8 +51743,6 @@ func (m *FieldCondition_BoolCond) SizeVT() (n int) { if m.BoolCond != nil { l = m.BoolCond.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -53495,8 +51755,6 @@ func (m *FieldCondition_ExistsCond) SizeVT() (n int) { if m.ExistsCond != nil { l = m.ExistsCond.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -54004,8 +52262,6 @@ func (m *CallerSnapshot_Authenticated) SizeVT() (n int) { if m.Authenticated != nil { l = m.Authenticated.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -54018,8 +52274,6 @@ func (m *CallerSnapshot_Anonymous) SizeVT() (n int) { if m.Anonymous != nil { l = m.Anonymous.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -54032,8 +52286,6 @@ func (m *CallerSnapshot_System) SizeVT() (n int) { if m.System != nil { l = m.System.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -54046,8 +52298,6 @@ func (m *CallerSnapshot_AuthDisabled) SizeVT() (n int) { if m.AuthDisabled != nil { l = m.AuthDisabled.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -54129,8 +52379,6 @@ func (m *BackupStorage_S3) SizeVT() (n int) { if m.S3 != nil { l = m.S3.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -54143,8 +52391,6 @@ func (m *BackupStorage_Azure) SizeVT() (n int) { if m.Azure != nil { l = m.Azure.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } diff --git a/internal/proto/eventspb/events.pb.go b/internal/proto/eventspb/events.pb.go index 3f055a53aa..3fbadb9282 100644 --- a/internal/proto/eventspb/events.pb.go +++ b/internal/proto/eventspb/events.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.12 -// protoc v7.36.2 +// protoc v7.34.2 // source: events.proto package eventspb diff --git a/internal/proto/eventspb/events_vtproto.pb.go b/internal/proto/eventspb/events_vtproto.pb.go index e6877aec2c..afa329ab7f 100644 --- a/internal/proto/eventspb/events_vtproto.pb.go +++ b/internal/proto/eventspb/events_vtproto.pb.go @@ -1,5 +1,5 @@ // Code generated by protoc-gen-go-vtproto. DO NOT EDIT. -// protoc-gen-go-vtproto version: v0.6.1-0.20240319094008-0393e58bdf10 +// protoc-gen-go-vtproto version: (devel) // source: events.proto package eventspb diff --git a/internal/proto/proposalpb/proposal.pb.go b/internal/proto/proposalpb/proposal.pb.go index 769dfc761d..d4b8affd1e 100644 --- a/internal/proto/proposalpb/proposal.pb.go +++ b/internal/proto/proposalpb/proposal.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.12 -// protoc v7.36.2 +// protoc v7.34.2 // source: proposal.proto package proposalpb diff --git a/internal/proto/proposalpb/proposal_vtproto.pb.go b/internal/proto/proposalpb/proposal_vtproto.pb.go index aa7da5e375..996f6fc0b3 100644 --- a/internal/proto/proposalpb/proposal_vtproto.pb.go +++ b/internal/proto/proposalpb/proposal_vtproto.pb.go @@ -1,5 +1,5 @@ // Code generated by protoc-gen-go-vtproto. DO NOT EDIT. -// protoc-gen-go-vtproto version: v0.6.1-0.20240319094008-0393e58bdf10 +// protoc-gen-go-vtproto version: (devel) // source: proposal.proto package proposalpb diff --git a/internal/proto/raftcmdpb/raft_cmd.pb.go b/internal/proto/raftcmdpb/raft_cmd.pb.go index c9e50c8a99..12ab706338 100644 --- a/internal/proto/raftcmdpb/raft_cmd.pb.go +++ b/internal/proto/raftcmdpb/raft_cmd.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.12 -// protoc v7.36.2 +// protoc v7.34.2 // source: raft_cmd.proto package raftcmdpb diff --git a/internal/proto/raftcmdpb/raft_cmd_vtproto.pb.go b/internal/proto/raftcmdpb/raft_cmd_vtproto.pb.go index 76adedf891..0579b2fe96 100644 --- a/internal/proto/raftcmdpb/raft_cmd_vtproto.pb.go +++ b/internal/proto/raftcmdpb/raft_cmd_vtproto.pb.go @@ -1,5 +1,5 @@ // Code generated by protoc-gen-go-vtproto. DO NOT EDIT. -// protoc-gen-go-vtproto version: v0.6.1-0.20240319094008-0393e58bdf10 +// protoc-gen-go-vtproto version: (devel) // source: raft_cmd.proto package raftcmdpb @@ -6087,10 +6087,6 @@ func (m *Order_LedgerScoped) MarshalToSizedBufferVT(dAtA []byte) (int, error) { i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0xa - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0xa } return len(dAtA) - i, nil } @@ -6110,10 +6106,6 @@ func (m *Order_SystemScoped) MarshalToSizedBufferVT(dAtA []byte) (int, error) { i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x12 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x12 } return len(dAtA) - i, nil } @@ -6246,10 +6238,6 @@ func (m *LedgerScopedOrder_Apply) MarshalToSizedBufferVT(dAtA []byte) (int, erro i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x12 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x12 } return len(dAtA) - i, nil } @@ -6269,10 +6257,6 @@ func (m *LedgerScopedOrder_CreateLedger) MarshalToSizedBufferVT(dAtA []byte) (in i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x1a - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x1a } return len(dAtA) - i, nil } @@ -6292,10 +6276,6 @@ func (m *LedgerScopedOrder_DeleteLedger) MarshalToSizedBufferVT(dAtA []byte) (in i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x22 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x22 } return len(dAtA) - i, nil } @@ -6315,10 +6295,6 @@ func (m *LedgerScopedOrder_MirrorIngest) MarshalToSizedBufferVT(dAtA []byte) (in i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x2a - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x2a } return len(dAtA) - i, nil } @@ -6338,10 +6314,6 @@ func (m *LedgerScopedOrder_PromoteLedger) MarshalToSizedBufferVT(dAtA []byte) (i i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x32 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x32 } return len(dAtA) - i, nil } @@ -6361,10 +6333,6 @@ func (m *LedgerScopedOrder_SaveLedgerMetadata) MarshalToSizedBufferVT(dAtA []byt i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x3a - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x3a } return len(dAtA) - i, nil } @@ -6384,10 +6352,6 @@ func (m *LedgerScopedOrder_DeleteLedgerMetadata) MarshalToSizedBufferVT(dAtA []b i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x42 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x42 } return len(dAtA) - i, nil } @@ -6407,10 +6371,6 @@ func (m *LedgerScopedOrder_SaveNumscript) MarshalToSizedBufferVT(dAtA []byte) (i i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x4a - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x4a } return len(dAtA) - i, nil } @@ -6430,10 +6390,6 @@ func (m *LedgerScopedOrder_CreatePreparedQuery) MarshalToSizedBufferVT(dAtA []by i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x52 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x52 } return len(dAtA) - i, nil } @@ -6453,10 +6409,6 @@ func (m *LedgerScopedOrder_UpdatePreparedQuery) MarshalToSizedBufferVT(dAtA []by i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x5a - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x5a } return len(dAtA) - i, nil } @@ -6476,10 +6428,6 @@ func (m *LedgerScopedOrder_DeletePreparedQuery) MarshalToSizedBufferVT(dAtA []by i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x62 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x62 } return len(dAtA) - i, nil } @@ -6541,10 +6489,6 @@ func (m *SystemScopedOrder_RegisterSigningKey) MarshalToSizedBufferVT(dAtA []byt i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0xa - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0xa } return len(dAtA) - i, nil } @@ -6564,10 +6508,6 @@ func (m *SystemScopedOrder_RevokeSigningKey) MarshalToSizedBufferVT(dAtA []byte) i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x12 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x12 } return len(dAtA) - i, nil } @@ -6587,10 +6527,6 @@ func (m *SystemScopedOrder_SetSigningConfig) MarshalToSizedBufferVT(dAtA []byte) i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x1a - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x1a } return len(dAtA) - i, nil } @@ -6610,10 +6546,6 @@ func (m *SystemScopedOrder_AddEventsSink) MarshalToSizedBufferVT(dAtA []byte) (i i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x22 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x22 } return len(dAtA) - i, nil } @@ -6633,10 +6565,6 @@ func (m *SystemScopedOrder_RemoveEventsSink) MarshalToSizedBufferVT(dAtA []byte) i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x2a - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x2a } return len(dAtA) - i, nil } @@ -6656,10 +6584,6 @@ func (m *SystemScopedOrder_SetMaintenanceMode) MarshalToSizedBufferVT(dAtA []byt i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x32 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x32 } return len(dAtA) - i, nil } @@ -6679,10 +6603,6 @@ func (m *SystemScopedOrder_CreateQueryCheckpoint) MarshalToSizedBufferVT(dAtA [] i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x3a - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x3a } return len(dAtA) - i, nil } @@ -6702,10 +6622,6 @@ func (m *SystemScopedOrder_DeleteQueryCheckpoint) MarshalToSizedBufferVT(dAtA [] i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x42 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x42 } return len(dAtA) - i, nil } @@ -6725,10 +6641,6 @@ func (m *SystemScopedOrder_SetQueryCheckpointSchedule) MarshalToSizedBufferVT(dA i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x4a - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x4a } return len(dAtA) - i, nil } @@ -6748,10 +6660,6 @@ func (m *SystemScopedOrder_DeleteQueryCheckpointSchedule) MarshalToSizedBufferVT i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x52 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x52 } return len(dAtA) - i, nil } @@ -6771,10 +6679,6 @@ func (m *SystemScopedOrder_SetClusterPolicy) MarshalToSizedBufferVT(dAtA []byte) i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x5a - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x5a } return len(dAtA) - i, nil } @@ -7735,10 +7639,6 @@ func (m *MirrorLogEntry_CreatedTransaction) MarshalToSizedBufferVT(dAtA []byte) i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x1a - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x1a } return len(dAtA) - i, nil } @@ -7758,10 +7658,6 @@ func (m *MirrorLogEntry_SavedMetadata) MarshalToSizedBufferVT(dAtA []byte) (int, i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x22 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x22 } return len(dAtA) - i, nil } @@ -7781,10 +7677,6 @@ func (m *MirrorLogEntry_RevertedTransaction) MarshalToSizedBufferVT(dAtA []byte) i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x2a - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x2a } return len(dAtA) - i, nil } @@ -7804,10 +7696,6 @@ func (m *MirrorLogEntry_DeletedMetadata) MarshalToSizedBufferVT(dAtA []byte) (in i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x32 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x32 } return len(dAtA) - i, nil } @@ -7827,10 +7715,6 @@ func (m *MirrorLogEntry_FillGap) MarshalToSizedBufferVT(dAtA []byte) (int, error i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x3a - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x3a } return len(dAtA) - i, nil } @@ -8337,10 +8221,6 @@ func (m *LedgerApplyOrder_CreateTransaction) MarshalToSizedBufferVT(dAtA []byte) i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0xa - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0xa } return len(dAtA) - i, nil } @@ -8360,10 +8240,6 @@ func (m *LedgerApplyOrder_AddMetadata) MarshalToSizedBufferVT(dAtA []byte) (int, i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x12 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x12 } return len(dAtA) - i, nil } @@ -8383,10 +8259,6 @@ func (m *LedgerApplyOrder_RevertTransaction) MarshalToSizedBufferVT(dAtA []byte) i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x1a - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x1a } return len(dAtA) - i, nil } @@ -8406,10 +8278,6 @@ func (m *LedgerApplyOrder_DeleteMetadata) MarshalToSizedBufferVT(dAtA []byte) (i i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x22 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x22 } return len(dAtA) - i, nil } @@ -8429,10 +8297,6 @@ func (m *LedgerApplyOrder_SetMetadataFieldType) MarshalToSizedBufferVT(dAtA []by i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x2a - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x2a } return len(dAtA) - i, nil } @@ -8452,10 +8316,6 @@ func (m *LedgerApplyOrder_RemoveMetadataFieldType) MarshalToSizedBufferVT(dAtA [ i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x32 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x32 } return len(dAtA) - i, nil } @@ -8475,10 +8335,6 @@ func (m *LedgerApplyOrder_CreateIndex) MarshalToSizedBufferVT(dAtA []byte) (int, i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x3a - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x3a } return len(dAtA) - i, nil } @@ -8498,10 +8354,6 @@ func (m *LedgerApplyOrder_DropIndex) MarshalToSizedBufferVT(dAtA []byte) (int, e i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x42 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x42 } return len(dAtA) - i, nil } @@ -8521,10 +8373,6 @@ func (m *LedgerApplyOrder_AddAccountType) MarshalToSizedBufferVT(dAtA []byte) (i i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x4a - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x4a } return len(dAtA) - i, nil } @@ -8544,10 +8392,6 @@ func (m *LedgerApplyOrder_RemoveAccountType) MarshalToSizedBufferVT(dAtA []byte) i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x52 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x52 } return len(dAtA) - i, nil } @@ -8567,10 +8411,6 @@ func (m *LedgerApplyOrder_UpdateDefaultEnforcementMode) MarshalToSizedBufferVT(d i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x5a - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x5a } return len(dAtA) - i, nil } @@ -9622,10 +9462,6 @@ func (m *Proposal_Batch) MarshalToSizedBufferVT(dAtA []byte) (int, error) { i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x5a - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x5a } return len(dAtA) - i, nil } @@ -9645,10 +9481,6 @@ func (m *Proposal_SetClusterPolicy) MarshalToSizedBufferVT(dAtA []byte) (int, er i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x62 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x62 } return len(dAtA) - i, nil } @@ -9717,10 +9549,6 @@ func (m *TechnicalUpdate_MirrorSync) MarshalToSizedBufferVT(dAtA []byte) (int, e i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x12 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x12 } return len(dAtA) - i, nil } @@ -9740,10 +9568,6 @@ func (m *TechnicalUpdate_EventsSink) MarshalToSizedBufferVT(dAtA []byte) (int, e i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x1a - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x1a } return len(dAtA) - i, nil } @@ -9763,10 +9587,6 @@ func (m *TechnicalUpdate_IdempotencyEviction) MarshalToSizedBufferVT(dAtA []byte i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x22 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x22 } return len(dAtA) - i, nil } @@ -9786,10 +9606,6 @@ func (m *TechnicalUpdate_ClusterConfig) MarshalToSizedBufferVT(dAtA []byte) (int i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x2a - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x2a } return len(dAtA) - i, nil } @@ -9809,10 +9625,6 @@ func (m *TechnicalUpdate_BackupOrder) MarshalToSizedBufferVT(dAtA []byte) (int, i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x32 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x32 } return len(dAtA) - i, nil } @@ -9832,10 +9644,6 @@ func (m *TechnicalUpdate_IncrementalBackupOrder) MarshalToSizedBufferVT(dAtA []b i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x3a - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x3a } return len(dAtA) - i, nil } @@ -9869,10 +9677,6 @@ func (m *TechnicalUpdate_DrainMarker) MarshalToSizedBufferVT(dAtA []byte) (int, i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x4a - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x4a } return len(dAtA) - i, nil } @@ -9892,10 +9696,6 @@ func (m *TechnicalUpdate_DrainRelease) MarshalToSizedBufferVT(dAtA []byte) (int, i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x52 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x52 } return len(dAtA) - i, nil } @@ -10152,10 +9952,6 @@ func (m *BackupDestination_S3) MarshalToSizedBufferVT(dAtA []byte) (int, error) i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x1a - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x1a } return len(dAtA) - i, nil } @@ -10175,10 +9971,6 @@ func (m *BackupDestination_Azure) MarshalToSizedBufferVT(dAtA []byte) (int, erro i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x22 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x22 } return len(dAtA) - i, nil } @@ -10461,10 +10253,6 @@ func (m *BackupOrder_Start) MarshalToSizedBufferVT(dAtA []byte) (int, error) { i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0xa - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0xa } return len(dAtA) - i, nil } @@ -10484,10 +10272,6 @@ func (m *BackupOrder_Complete) MarshalToSizedBufferVT(dAtA []byte) (int, error) i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x12 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x12 } return len(dAtA) - i, nil } @@ -10507,10 +10291,6 @@ func (m *BackupOrder_Fail) MarshalToSizedBufferVT(dAtA []byte) (int, error) { i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x1a - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x1a } return len(dAtA) - i, nil } @@ -10572,10 +10352,6 @@ func (m *IncrementalBackupOrder_Start) MarshalToSizedBufferVT(dAtA []byte) (int, i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0xa - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0xa } return len(dAtA) - i, nil } @@ -10595,10 +10371,6 @@ func (m *IncrementalBackupOrder_Complete) MarshalToSizedBufferVT(dAtA []byte) (i i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x12 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x12 } return len(dAtA) - i, nil } @@ -10618,10 +10390,6 @@ func (m *IncrementalBackupOrder_Fail) MarshalToSizedBufferVT(dAtA []byte) (int, i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x1a - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x1a } return len(dAtA) - i, nil } @@ -11034,10 +10802,6 @@ func (m *CreatedLogOrReference_CreatedLog) MarshalToSizedBufferVT(dAtA []byte) ( i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0xa - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0xa } return len(dAtA) - i, nil } @@ -11863,8 +11627,6 @@ func (m *Order_LedgerScoped) SizeVT() (n int) { if m.LedgerScoped != nil { l = m.LedgerScoped.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -11877,8 +11639,6 @@ func (m *Order_SystemScoped) SizeVT() (n int) { if m.SystemScoped != nil { l = m.SystemScoped.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -11933,8 +11693,6 @@ func (m *LedgerScopedOrder_Apply) SizeVT() (n int) { if m.Apply != nil { l = m.Apply.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -11947,8 +11705,6 @@ func (m *LedgerScopedOrder_CreateLedger) SizeVT() (n int) { if m.CreateLedger != nil { l = m.CreateLedger.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -11961,8 +11717,6 @@ func (m *LedgerScopedOrder_DeleteLedger) SizeVT() (n int) { if m.DeleteLedger != nil { l = m.DeleteLedger.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -11975,8 +11729,6 @@ func (m *LedgerScopedOrder_MirrorIngest) SizeVT() (n int) { if m.MirrorIngest != nil { l = m.MirrorIngest.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -11989,8 +11741,6 @@ func (m *LedgerScopedOrder_PromoteLedger) SizeVT() (n int) { if m.PromoteLedger != nil { l = m.PromoteLedger.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -12003,8 +11753,6 @@ func (m *LedgerScopedOrder_SaveLedgerMetadata) SizeVT() (n int) { if m.SaveLedgerMetadata != nil { l = m.SaveLedgerMetadata.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -12017,8 +11765,6 @@ func (m *LedgerScopedOrder_DeleteLedgerMetadata) SizeVT() (n int) { if m.DeleteLedgerMetadata != nil { l = m.DeleteLedgerMetadata.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -12031,8 +11777,6 @@ func (m *LedgerScopedOrder_SaveNumscript) SizeVT() (n int) { if m.SaveNumscript != nil { l = m.SaveNumscript.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -12045,8 +11789,6 @@ func (m *LedgerScopedOrder_CreatePreparedQuery) SizeVT() (n int) { if m.CreatePreparedQuery != nil { l = m.CreatePreparedQuery.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -12059,8 +11801,6 @@ func (m *LedgerScopedOrder_UpdatePreparedQuery) SizeVT() (n int) { if m.UpdatePreparedQuery != nil { l = m.UpdatePreparedQuery.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -12073,8 +11813,6 @@ func (m *LedgerScopedOrder_DeletePreparedQuery) SizeVT() (n int) { if m.DeletePreparedQuery != nil { l = m.DeletePreparedQuery.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -12100,8 +11838,6 @@ func (m *SystemScopedOrder_RegisterSigningKey) SizeVT() (n int) { if m.RegisterSigningKey != nil { l = m.RegisterSigningKey.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -12114,8 +11850,6 @@ func (m *SystemScopedOrder_RevokeSigningKey) SizeVT() (n int) { if m.RevokeSigningKey != nil { l = m.RevokeSigningKey.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -12128,8 +11862,6 @@ func (m *SystemScopedOrder_SetSigningConfig) SizeVT() (n int) { if m.SetSigningConfig != nil { l = m.SetSigningConfig.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -12142,8 +11874,6 @@ func (m *SystemScopedOrder_AddEventsSink) SizeVT() (n int) { if m.AddEventsSink != nil { l = m.AddEventsSink.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -12156,8 +11886,6 @@ func (m *SystemScopedOrder_RemoveEventsSink) SizeVT() (n int) { if m.RemoveEventsSink != nil { l = m.RemoveEventsSink.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -12170,8 +11898,6 @@ func (m *SystemScopedOrder_SetMaintenanceMode) SizeVT() (n int) { if m.SetMaintenanceMode != nil { l = m.SetMaintenanceMode.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -12184,8 +11910,6 @@ func (m *SystemScopedOrder_CreateQueryCheckpoint) SizeVT() (n int) { if m.CreateQueryCheckpoint != nil { l = m.CreateQueryCheckpoint.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -12198,8 +11922,6 @@ func (m *SystemScopedOrder_DeleteQueryCheckpoint) SizeVT() (n int) { if m.DeleteQueryCheckpoint != nil { l = m.DeleteQueryCheckpoint.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -12212,8 +11934,6 @@ func (m *SystemScopedOrder_SetQueryCheckpointSchedule) SizeVT() (n int) { if m.SetQueryCheckpointSchedule != nil { l = m.SetQueryCheckpointSchedule.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -12226,8 +11946,6 @@ func (m *SystemScopedOrder_DeleteQueryCheckpointSchedule) SizeVT() (n int) { if m.DeleteQueryCheckpointSchedule != nil { l = m.DeleteQueryCheckpointSchedule.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -12240,8 +11958,6 @@ func (m *SystemScopedOrder_SetClusterPolicy) SizeVT() (n int) { if m.SetClusterPolicy != nil { l = m.SetClusterPolicy.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -12595,8 +12311,6 @@ func (m *MirrorLogEntry_CreatedTransaction) SizeVT() (n int) { if m.CreatedTransaction != nil { l = m.CreatedTransaction.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -12609,8 +12323,6 @@ func (m *MirrorLogEntry_SavedMetadata) SizeVT() (n int) { if m.SavedMetadata != nil { l = m.SavedMetadata.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -12623,8 +12335,6 @@ func (m *MirrorLogEntry_RevertedTransaction) SizeVT() (n int) { if m.RevertedTransaction != nil { l = m.RevertedTransaction.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -12637,8 +12347,6 @@ func (m *MirrorLogEntry_DeletedMetadata) SizeVT() (n int) { if m.DeletedMetadata != nil { l = m.DeletedMetadata.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -12651,8 +12359,6 @@ func (m *MirrorLogEntry_FillGap) SizeVT() (n int) { if m.FillGap != nil { l = m.FillGap.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -12855,8 +12561,6 @@ func (m *LedgerApplyOrder_CreateTransaction) SizeVT() (n int) { if m.CreateTransaction != nil { l = m.CreateTransaction.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -12869,8 +12573,6 @@ func (m *LedgerApplyOrder_AddMetadata) SizeVT() (n int) { if m.AddMetadata != nil { l = m.AddMetadata.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -12883,8 +12585,6 @@ func (m *LedgerApplyOrder_RevertTransaction) SizeVT() (n int) { if m.RevertTransaction != nil { l = m.RevertTransaction.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -12897,8 +12597,6 @@ func (m *LedgerApplyOrder_DeleteMetadata) SizeVT() (n int) { if m.DeleteMetadata != nil { l = m.DeleteMetadata.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -12911,8 +12609,6 @@ func (m *LedgerApplyOrder_SetMetadataFieldType) SizeVT() (n int) { if m.SetMetadataFieldType != nil { l = m.SetMetadataFieldType.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -12925,8 +12621,6 @@ func (m *LedgerApplyOrder_RemoveMetadataFieldType) SizeVT() (n int) { if m.RemoveMetadataFieldType != nil { l = m.RemoveMetadataFieldType.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -12939,8 +12633,6 @@ func (m *LedgerApplyOrder_CreateIndex) SizeVT() (n int) { if m.CreateIndex != nil { l = m.CreateIndex.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -12953,8 +12645,6 @@ func (m *LedgerApplyOrder_DropIndex) SizeVT() (n int) { if m.DropIndex != nil { l = m.DropIndex.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -12967,8 +12657,6 @@ func (m *LedgerApplyOrder_AddAccountType) SizeVT() (n int) { if m.AddAccountType != nil { l = m.AddAccountType.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -12981,8 +12669,6 @@ func (m *LedgerApplyOrder_RemoveAccountType) SizeVT() (n int) { if m.RemoveAccountType != nil { l = m.RemoveAccountType.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -12995,8 +12681,6 @@ func (m *LedgerApplyOrder_UpdateDefaultEnforcementMode) SizeVT() (n int) { if m.UpdateDefaultEnforcementMode != nil { l = m.UpdateDefaultEnforcementMode.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -13406,8 +13090,6 @@ func (m *Proposal_Batch) SizeVT() (n int) { if m.Batch != nil { l = m.Batch.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -13420,8 +13102,6 @@ func (m *Proposal_SetClusterPolicy) SizeVT() (n int) { if m.SetClusterPolicy != nil { l = m.SetClusterPolicy.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -13451,8 +13131,6 @@ func (m *TechnicalUpdate_MirrorSync) SizeVT() (n int) { if m.MirrorSync != nil { l = m.MirrorSync.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -13465,8 +13143,6 @@ func (m *TechnicalUpdate_EventsSink) SizeVT() (n int) { if m.EventsSink != nil { l = m.EventsSink.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -13479,8 +13155,6 @@ func (m *TechnicalUpdate_IdempotencyEviction) SizeVT() (n int) { if m.IdempotencyEviction != nil { l = m.IdempotencyEviction.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -13493,8 +13167,6 @@ func (m *TechnicalUpdate_ClusterConfig) SizeVT() (n int) { if m.ClusterConfig != nil { l = m.ClusterConfig.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -13507,8 +13179,6 @@ func (m *TechnicalUpdate_BackupOrder) SizeVT() (n int) { if m.BackupOrder != nil { l = m.BackupOrder.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -13521,8 +13191,6 @@ func (m *TechnicalUpdate_IncrementalBackupOrder) SizeVT() (n int) { if m.IncrementalBackupOrder != nil { l = m.IncrementalBackupOrder.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -13545,8 +13213,6 @@ func (m *TechnicalUpdate_DrainMarker) SizeVT() (n int) { if m.DrainMarker != nil { l = m.DrainMarker.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -13559,8 +13225,6 @@ func (m *TechnicalUpdate_DrainRelease) SizeVT() (n int) { if m.DrainRelease != nil { l = m.DrainRelease.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -13662,8 +13326,6 @@ func (m *BackupDestination_S3) SizeVT() (n int) { if m.S3 != nil { l = m.S3.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -13676,8 +13338,6 @@ func (m *BackupDestination_Azure) SizeVT() (n int) { if m.Azure != nil { l = m.Azure.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -13802,8 +13462,6 @@ func (m *BackupOrder_Start) SizeVT() (n int) { if m.Start != nil { l = m.Start.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -13816,8 +13474,6 @@ func (m *BackupOrder_Complete) SizeVT() (n int) { if m.Complete != nil { l = m.Complete.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -13830,8 +13486,6 @@ func (m *BackupOrder_Fail) SizeVT() (n int) { if m.Fail != nil { l = m.Fail.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -13857,8 +13511,6 @@ func (m *IncrementalBackupOrder_Start) SizeVT() (n int) { if m.Start != nil { l = m.Start.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -13871,8 +13523,6 @@ func (m *IncrementalBackupOrder_Complete) SizeVT() (n int) { if m.Complete != nil { l = m.Complete.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -13885,8 +13535,6 @@ func (m *IncrementalBackupOrder_Fail) SizeVT() (n int) { if m.Fail != nil { l = m.Fail.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -14048,8 +13696,6 @@ func (m *CreatedLogOrReference_CreatedLog) SizeVT() (n int) { if m.CreatedLog != nil { l = m.CreatedLog.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } diff --git a/internal/proto/rafttransportpb/raft_transport.pb.go b/internal/proto/rafttransportpb/raft_transport.pb.go index e3b4e8b5fa..1a4e713e42 100644 --- a/internal/proto/rafttransportpb/raft_transport.pb.go +++ b/internal/proto/rafttransportpb/raft_transport.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.12 -// protoc v7.36.2 +// protoc v7.34.2 // source: raft_transport.proto package rafttransportpb diff --git a/internal/proto/rafttransportpb/raft_transport_grpc.pb.go b/internal/proto/rafttransportpb/raft_transport_grpc.pb.go index 7ccd746846..5a2bec35d2 100644 --- a/internal/proto/rafttransportpb/raft_transport_grpc.pb.go +++ b/internal/proto/rafttransportpb/raft_transport_grpc.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: // - protoc-gen-go-grpc v1.6.2 -// - protoc v7.36.2 +// - protoc v7.34.2 // source: raft_transport.proto package rafttransportpb diff --git a/internal/proto/rafttransportpb/raft_transport_vtproto.pb.go b/internal/proto/rafttransportpb/raft_transport_vtproto.pb.go index c33b070551..c147702af3 100644 --- a/internal/proto/rafttransportpb/raft_transport_vtproto.pb.go +++ b/internal/proto/rafttransportpb/raft_transport_vtproto.pb.go @@ -1,5 +1,5 @@ // Code generated by protoc-gen-go-vtproto. DO NOT EDIT. -// protoc-gen-go-vtproto version: v0.6.1-0.20240319094008-0393e58bdf10 +// protoc-gen-go-vtproto version: (devel) // source: raft_transport.proto package rafttransportpb @@ -1078,10 +1078,6 @@ func (m *SendMessageRequest_Raft) MarshalToSizedBufferVT(dAtA []byte) (int, erro i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0xa - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0xa } return len(dAtA) - i, nil } @@ -1101,10 +1097,6 @@ func (m *SendMessageRequest_Ping) MarshalToSizedBufferVT(dAtA []byte) (int, erro i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x12 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x12 } return len(dAtA) - i, nil } @@ -1166,10 +1158,6 @@ func (m *SendMessageResponse_Raft) MarshalToSizedBufferVT(dAtA []byte) (int, err i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0xa - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0xa } return len(dAtA) - i, nil } @@ -1189,10 +1177,6 @@ func (m *SendMessageResponse_Pong) MarshalToSizedBufferVT(dAtA []byte) (int, err i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x12 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x12 } return len(dAtA) - i, nil } @@ -1361,8 +1345,6 @@ func (m *SendMessageRequest_Raft) SizeVT() (n int) { if m.Raft != nil { l = m.Raft.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -1375,8 +1357,6 @@ func (m *SendMessageRequest_Ping) SizeVT() (n int) { if m.Ping != nil { l = m.Ping.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -1402,8 +1382,6 @@ func (m *SendMessageResponse_Raft) SizeVT() (n int) { if m.Raft != nil { l = m.Raft.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -1416,8 +1394,6 @@ func (m *SendMessageResponse_Pong) SizeVT() (n int) { if m.Pong != nil { l = m.Pong.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } diff --git a/internal/proto/restorepb/restore.pb.go b/internal/proto/restorepb/restore.pb.go index 1593fd5753..c881e82769 100644 --- a/internal/proto/restorepb/restore.pb.go +++ b/internal/proto/restorepb/restore.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.12 -// protoc v7.36.2 +// protoc v7.34.2 // source: restore.proto package restorepb diff --git a/internal/proto/restorepb/restore_grpc.pb.go b/internal/proto/restorepb/restore_grpc.pb.go index 1b06f5b71a..51b2ef4603 100644 --- a/internal/proto/restorepb/restore_grpc.pb.go +++ b/internal/proto/restorepb/restore_grpc.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: // - protoc-gen-go-grpc v1.6.2 -// - protoc v7.36.2 +// - protoc v7.34.2 // source: restore.proto package restorepb diff --git a/internal/proto/restorepb/restore_vtproto.pb.go b/internal/proto/restorepb/restore_vtproto.pb.go index 8f879105cf..3b069df5fc 100644 --- a/internal/proto/restorepb/restore_vtproto.pb.go +++ b/internal/proto/restorepb/restore_vtproto.pb.go @@ -1,5 +1,5 @@ // Code generated by protoc-gen-go-vtproto. DO NOT EDIT. -// protoc-gen-go-vtproto version: v0.6.1-0.20240319094008-0393e58bdf10 +// protoc-gen-go-vtproto version: (devel) // source: restore.proto package restorepb @@ -1038,10 +1038,6 @@ func (m *ValidateRestoreEvent_Error) MarshalToSizedBufferVT(dAtA []byte) (int, e i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0xa - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0xa } return len(dAtA) - i, nil } @@ -1061,10 +1057,6 @@ func (m *ValidateRestoreEvent_Progress) MarshalToSizedBufferVT(dAtA []byte) (int i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x12 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x12 } return len(dAtA) - i, nil } @@ -1465,8 +1457,6 @@ func (m *ValidateRestoreEvent_Error) SizeVT() (n int) { if m.Error != nil { l = m.Error.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -1479,8 +1469,6 @@ func (m *ValidateRestoreEvent_Progress) SizeVT() (n int) { if m.Progress != nil { l = m.Progress.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } diff --git a/internal/proto/servicepb/bucket.pb.go b/internal/proto/servicepb/bucket.pb.go index 3c0f643c36..6ff8f2f98b 100644 --- a/internal/proto/servicepb/bucket.pb.go +++ b/internal/proto/servicepb/bucket.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.12 -// protoc v7.36.2 +// protoc v7.34.2 // source: bucket.proto package servicepb diff --git a/internal/proto/servicepb/bucket_grpc.pb.go b/internal/proto/servicepb/bucket_grpc.pb.go index 9f4f2e6305..f7b6b63aa7 100644 --- a/internal/proto/servicepb/bucket_grpc.pb.go +++ b/internal/proto/servicepb/bucket_grpc.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: // - protoc-gen-go-grpc v1.6.2 -// - protoc v7.36.2 +// - protoc v7.34.2 // source: bucket.proto package servicepb diff --git a/internal/proto/servicepb/bucket_vtproto.pb.go b/internal/proto/servicepb/bucket_vtproto.pb.go index 1994cbe441..008f41bbac 100644 --- a/internal/proto/servicepb/bucket_vtproto.pb.go +++ b/internal/proto/servicepb/bucket_vtproto.pb.go @@ -1,5 +1,5 @@ // Code generated by protoc-gen-go-vtproto. DO NOT EDIT. -// protoc-gen-go-vtproto version: v0.6.1-0.20240319094008-0393e58bdf10 +// protoc-gen-go-vtproto version: (devel) // source: bucket.proto package servicepb @@ -8509,10 +8509,6 @@ func (m *ApplyRequest_Unsigned) MarshalToSizedBufferVT(dAtA []byte) (int, error) i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0xa - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0xa } return len(dAtA) - i, nil } @@ -8532,10 +8528,6 @@ func (m *ApplyRequest_Signed) MarshalToSizedBufferVT(dAtA []byte) (int, error) { i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x12 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x12 } return len(dAtA) - i, nil } @@ -8694,10 +8686,6 @@ func (m *Request_Apply) MarshalToSizedBufferVT(dAtA []byte) (int, error) { i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0xa - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0xa } return len(dAtA) - i, nil } @@ -8717,10 +8705,6 @@ func (m *Request_CreateLedger) MarshalToSizedBufferVT(dAtA []byte) (int, error) i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x12 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x12 } return len(dAtA) - i, nil } @@ -8740,10 +8724,6 @@ func (m *Request_DeleteLedger) MarshalToSizedBufferVT(dAtA []byte) (int, error) i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x1a - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x1a } return len(dAtA) - i, nil } @@ -8763,10 +8743,6 @@ func (m *Request_RegisterSigningKey) MarshalToSizedBufferVT(dAtA []byte) (int, e i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x22 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x22 } return len(dAtA) - i, nil } @@ -8786,10 +8762,6 @@ func (m *Request_RevokeSigningKey) MarshalToSizedBufferVT(dAtA []byte) (int, err i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x2a - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x2a } return len(dAtA) - i, nil } @@ -8809,10 +8781,6 @@ func (m *Request_SetSigningConfig) MarshalToSizedBufferVT(dAtA []byte) (int, err i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x32 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x32 } return len(dAtA) - i, nil } @@ -8832,10 +8800,6 @@ func (m *Request_AddEventsSink) MarshalToSizedBufferVT(dAtA []byte) (int, error) i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x3a - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x3a } return len(dAtA) - i, nil } @@ -8855,10 +8819,6 @@ func (m *Request_RemoveEventsSink) MarshalToSizedBufferVT(dAtA []byte) (int, err i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x42 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x42 } return len(dAtA) - i, nil } @@ -8878,10 +8838,6 @@ func (m *Request_SetMaintenanceMode) MarshalToSizedBufferVT(dAtA []byte) (int, e i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x4a - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x4a } return len(dAtA) - i, nil } @@ -8901,10 +8857,6 @@ func (m *Request_SetMetadataFieldType) MarshalToSizedBufferVT(dAtA []byte) (int, i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x52 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x52 } return len(dAtA) - i, nil } @@ -8924,10 +8876,6 @@ func (m *Request_RemoveMetadataFieldType) MarshalToSizedBufferVT(dAtA []byte) (i i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x5a - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x5a } return len(dAtA) - i, nil } @@ -8947,10 +8895,6 @@ func (m *Request_PromoteLedger) MarshalToSizedBufferVT(dAtA []byte) (int, error) i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x62 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x62 } return len(dAtA) - i, nil } @@ -8970,10 +8914,6 @@ func (m *Request_CreatePreparedQuery) MarshalToSizedBufferVT(dAtA []byte) (int, i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x6a - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x6a } return len(dAtA) - i, nil } @@ -8993,10 +8933,6 @@ func (m *Request_UpdatePreparedQuery) MarshalToSizedBufferVT(dAtA []byte) (int, i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x72 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x72 } return len(dAtA) - i, nil } @@ -9016,10 +8952,6 @@ func (m *Request_DeletePreparedQuery) MarshalToSizedBufferVT(dAtA []byte) (int, i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x7a - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x7a } return len(dAtA) - i, nil } @@ -9041,12 +8973,6 @@ func (m *Request_CreateIndex) MarshalToSizedBufferVT(dAtA []byte) (int, error) { dAtA[i] = 0x1 i-- dAtA[i] = 0x82 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x1 - i-- - dAtA[i] = 0x82 } return len(dAtA) - i, nil } @@ -9068,12 +8994,6 @@ func (m *Request_DropIndex) MarshalToSizedBufferVT(dAtA []byte) (int, error) { dAtA[i] = 0x1 i-- dAtA[i] = 0x8a - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x1 - i-- - dAtA[i] = 0x8a } return len(dAtA) - i, nil } @@ -9095,12 +9015,6 @@ func (m *Request_SaveNumscript) MarshalToSizedBufferVT(dAtA []byte) (int, error) dAtA[i] = 0x1 i-- dAtA[i] = 0x92 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x1 - i-- - dAtA[i] = 0x92 } return len(dAtA) - i, nil } @@ -9122,12 +9036,6 @@ func (m *Request_AddAccountType) MarshalToSizedBufferVT(dAtA []byte) (int, error dAtA[i] = 0x1 i-- dAtA[i] = 0x9a - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x1 - i-- - dAtA[i] = 0x9a } return len(dAtA) - i, nil } @@ -9149,12 +9057,6 @@ func (m *Request_RemoveAccountType) MarshalToSizedBufferVT(dAtA []byte) (int, er dAtA[i] = 0x1 i-- dAtA[i] = 0xa2 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x1 - i-- - dAtA[i] = 0xa2 } return len(dAtA) - i, nil } @@ -9176,12 +9078,6 @@ func (m *Request_SetDefaultEnforcementMode) MarshalToSizedBufferVT(dAtA []byte) dAtA[i] = 0x1 i-- dAtA[i] = 0xaa - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x1 - i-- - dAtA[i] = 0xaa } return len(dAtA) - i, nil } @@ -9203,12 +9099,6 @@ func (m *Request_CreateQueryCheckpoint) MarshalToSizedBufferVT(dAtA []byte) (int dAtA[i] = 0x1 i-- dAtA[i] = 0xb2 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x1 - i-- - dAtA[i] = 0xb2 } return len(dAtA) - i, nil } @@ -9230,12 +9120,6 @@ func (m *Request_DeleteQueryCheckpoint) MarshalToSizedBufferVT(dAtA []byte) (int dAtA[i] = 0x1 i-- dAtA[i] = 0xba - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x1 - i-- - dAtA[i] = 0xba } return len(dAtA) - i, nil } @@ -9257,12 +9141,6 @@ func (m *Request_SetQueryCheckpointSchedule) MarshalToSizedBufferVT(dAtA []byte) dAtA[i] = 0x1 i-- dAtA[i] = 0xc2 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x1 - i-- - dAtA[i] = 0xc2 } return len(dAtA) - i, nil } @@ -9284,12 +9162,6 @@ func (m *Request_DeleteQueryCheckpointSchedule) MarshalToSizedBufferVT(dAtA []by dAtA[i] = 0x1 i-- dAtA[i] = 0xca - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x1 - i-- - dAtA[i] = 0xca } return len(dAtA) - i, nil } @@ -9311,12 +9183,6 @@ func (m *Request_SaveLedgerMetadata) MarshalToSizedBufferVT(dAtA []byte) (int, e dAtA[i] = 0x1 i-- dAtA[i] = 0xd2 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x1 - i-- - dAtA[i] = 0xd2 } return len(dAtA) - i, nil } @@ -9338,12 +9204,6 @@ func (m *Request_DeleteLedgerMetadata) MarshalToSizedBufferVT(dAtA []byte) (int, dAtA[i] = 0x1 i-- dAtA[i] = 0xda - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x1 - i-- - dAtA[i] = 0xda } return len(dAtA) - i, nil } @@ -11272,10 +11132,6 @@ func (m *LedgerAction_CreateTransaction) MarshalToSizedBufferVT(dAtA []byte) (in i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0xa - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0xa } return len(dAtA) - i, nil } @@ -11295,10 +11151,6 @@ func (m *LedgerAction_AddMetadata) MarshalToSizedBufferVT(dAtA []byte) (int, err i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x12 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x12 } return len(dAtA) - i, nil } @@ -11318,10 +11170,6 @@ func (m *LedgerAction_RevertTransaction) MarshalToSizedBufferVT(dAtA []byte) (in i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x1a - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x1a } return len(dAtA) - i, nil } @@ -11341,10 +11189,6 @@ func (m *LedgerAction_DeleteMetadata) MarshalToSizedBufferVT(dAtA []byte) (int, i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x22 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x22 } return len(dAtA) - i, nil } @@ -11364,10 +11208,6 @@ func (m *LedgerAction_AddAccountType) MarshalToSizedBufferVT(dAtA []byte) (int, i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x2a - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x2a } return len(dAtA) - i, nil } @@ -11387,10 +11227,6 @@ func (m *LedgerAction_RemoveAccountType) MarshalToSizedBufferVT(dAtA []byte) (in i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x32 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x32 } return len(dAtA) - i, nil } @@ -11410,10 +11246,6 @@ func (m *LedgerAction_SetDefaultEnforcementMode) MarshalToSizedBufferVT(dAtA []b i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x3a - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x3a } return len(dAtA) - i, nil } @@ -12813,10 +12645,6 @@ func (m *CheckStoreEvent_Error) MarshalToSizedBufferVT(dAtA []byte) (int, error) i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0xa - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0xa } return len(dAtA) - i, nil } @@ -12836,10 +12664,6 @@ func (m *CheckStoreEvent_Progress) MarshalToSizedBufferVT(dAtA []byte) (int, err i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x12 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x12 } return len(dAtA) - i, nil } @@ -13616,10 +13440,6 @@ func (m *AnalyzeAccountsEvent_Progress) MarshalToSizedBufferVT(dAtA []byte) (int i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0xa - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0xa } return len(dAtA) - i, nil } @@ -13639,10 +13459,6 @@ func (m *AnalyzeAccountsEvent_Result) MarshalToSizedBufferVT(dAtA []byte) (int, i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x12 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x12 } return len(dAtA) - i, nil } @@ -13704,10 +13520,6 @@ func (m *AnalyzeTransactionsEvent_Progress) MarshalToSizedBufferVT(dAtA []byte) i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0xa - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0xa } return len(dAtA) - i, nil } @@ -13727,10 +13539,6 @@ func (m *AnalyzeTransactionsEvent_Result) MarshalToSizedBufferVT(dAtA []byte) (i i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x12 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x12 } return len(dAtA) - i, nil } @@ -14733,10 +14541,6 @@ func (m *ExecutePreparedQueryResponse_Cursor) MarshalToSizedBufferVT(dAtA []byte i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0xa - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0xa } return len(dAtA) - i, nil } @@ -14756,10 +14560,6 @@ func (m *ExecutePreparedQueryResponse_Aggregate) MarshalToSizedBufferVT(dAtA []b i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x12 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x12 } return len(dAtA) - i, nil } @@ -15561,10 +15361,6 @@ func (m *InspectIndexResponse_DistinctValues) MarshalToSizedBufferVT(dAtA []byte i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0xa - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0xa } return len(dAtA) - i, nil } @@ -15584,10 +15380,6 @@ func (m *InspectIndexResponse_Facets) MarshalToSizedBufferVT(dAtA []byte) (int, i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x12 - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x12 } return len(dAtA) - i, nil } @@ -15607,10 +15399,6 @@ func (m *InspectIndexResponse_Summary) MarshalToSizedBufferVT(dAtA []byte) (int, i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- dAtA[i] = 0x1a - } else { - i = protohelpers.EncodeVarint(dAtA, i, 0) - i-- - dAtA[i] = 0x1a } return len(dAtA) - i, nil } @@ -16179,8 +15967,6 @@ func (m *ApplyRequest_Unsigned) SizeVT() (n int) { if m.Unsigned != nil { l = m.Unsigned.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -16193,8 +15979,6 @@ func (m *ApplyRequest_Signed) SizeVT() (n int) { if m.Signed != nil { l = m.Signed.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -16256,8 +16040,6 @@ func (m *Request_Apply) SizeVT() (n int) { if m.Apply != nil { l = m.Apply.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -16270,8 +16052,6 @@ func (m *Request_CreateLedger) SizeVT() (n int) { if m.CreateLedger != nil { l = m.CreateLedger.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -16284,8 +16064,6 @@ func (m *Request_DeleteLedger) SizeVT() (n int) { if m.DeleteLedger != nil { l = m.DeleteLedger.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -16298,8 +16076,6 @@ func (m *Request_RegisterSigningKey) SizeVT() (n int) { if m.RegisterSigningKey != nil { l = m.RegisterSigningKey.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -16312,8 +16088,6 @@ func (m *Request_RevokeSigningKey) SizeVT() (n int) { if m.RevokeSigningKey != nil { l = m.RevokeSigningKey.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -16326,8 +16100,6 @@ func (m *Request_SetSigningConfig) SizeVT() (n int) { if m.SetSigningConfig != nil { l = m.SetSigningConfig.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -16340,8 +16112,6 @@ func (m *Request_AddEventsSink) SizeVT() (n int) { if m.AddEventsSink != nil { l = m.AddEventsSink.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -16354,8 +16124,6 @@ func (m *Request_RemoveEventsSink) SizeVT() (n int) { if m.RemoveEventsSink != nil { l = m.RemoveEventsSink.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -16368,8 +16136,6 @@ func (m *Request_SetMaintenanceMode) SizeVT() (n int) { if m.SetMaintenanceMode != nil { l = m.SetMaintenanceMode.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -16382,8 +16148,6 @@ func (m *Request_SetMetadataFieldType) SizeVT() (n int) { if m.SetMetadataFieldType != nil { l = m.SetMetadataFieldType.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -16396,8 +16160,6 @@ func (m *Request_RemoveMetadataFieldType) SizeVT() (n int) { if m.RemoveMetadataFieldType != nil { l = m.RemoveMetadataFieldType.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -16410,8 +16172,6 @@ func (m *Request_PromoteLedger) SizeVT() (n int) { if m.PromoteLedger != nil { l = m.PromoteLedger.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -16424,8 +16184,6 @@ func (m *Request_CreatePreparedQuery) SizeVT() (n int) { if m.CreatePreparedQuery != nil { l = m.CreatePreparedQuery.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -16438,8 +16196,6 @@ func (m *Request_UpdatePreparedQuery) SizeVT() (n int) { if m.UpdatePreparedQuery != nil { l = m.UpdatePreparedQuery.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -16452,8 +16208,6 @@ func (m *Request_DeletePreparedQuery) SizeVT() (n int) { if m.DeletePreparedQuery != nil { l = m.DeletePreparedQuery.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -16466,8 +16220,6 @@ func (m *Request_CreateIndex) SizeVT() (n int) { if m.CreateIndex != nil { l = m.CreateIndex.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -16480,8 +16232,6 @@ func (m *Request_DropIndex) SizeVT() (n int) { if m.DropIndex != nil { l = m.DropIndex.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -16494,8 +16244,6 @@ func (m *Request_SaveNumscript) SizeVT() (n int) { if m.SaveNumscript != nil { l = m.SaveNumscript.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -16508,8 +16256,6 @@ func (m *Request_AddAccountType) SizeVT() (n int) { if m.AddAccountType != nil { l = m.AddAccountType.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -16522,8 +16268,6 @@ func (m *Request_RemoveAccountType) SizeVT() (n int) { if m.RemoveAccountType != nil { l = m.RemoveAccountType.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -16536,8 +16280,6 @@ func (m *Request_SetDefaultEnforcementMode) SizeVT() (n int) { if m.SetDefaultEnforcementMode != nil { l = m.SetDefaultEnforcementMode.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -16550,8 +16292,6 @@ func (m *Request_CreateQueryCheckpoint) SizeVT() (n int) { if m.CreateQueryCheckpoint != nil { l = m.CreateQueryCheckpoint.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -16564,8 +16304,6 @@ func (m *Request_DeleteQueryCheckpoint) SizeVT() (n int) { if m.DeleteQueryCheckpoint != nil { l = m.DeleteQueryCheckpoint.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -16578,8 +16316,6 @@ func (m *Request_SetQueryCheckpointSchedule) SizeVT() (n int) { if m.SetQueryCheckpointSchedule != nil { l = m.SetQueryCheckpointSchedule.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -16592,8 +16328,6 @@ func (m *Request_DeleteQueryCheckpointSchedule) SizeVT() (n int) { if m.DeleteQueryCheckpointSchedule != nil { l = m.DeleteQueryCheckpointSchedule.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -16606,8 +16340,6 @@ func (m *Request_SaveLedgerMetadata) SizeVT() (n int) { if m.SaveLedgerMetadata != nil { l = m.SaveLedgerMetadata.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -16620,8 +16352,6 @@ func (m *Request_DeleteLedgerMetadata) SizeVT() (n int) { if m.DeleteLedgerMetadata != nil { l = m.DeleteLedgerMetadata.SizeVT() n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -17361,8 +17091,6 @@ func (m *LedgerAction_CreateTransaction) SizeVT() (n int) { if m.CreateTransaction != nil { l = m.CreateTransaction.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -17375,8 +17103,6 @@ func (m *LedgerAction_AddMetadata) SizeVT() (n int) { if m.AddMetadata != nil { l = m.AddMetadata.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -17389,8 +17115,6 @@ func (m *LedgerAction_RevertTransaction) SizeVT() (n int) { if m.RevertTransaction != nil { l = m.RevertTransaction.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -17403,8 +17127,6 @@ func (m *LedgerAction_DeleteMetadata) SizeVT() (n int) { if m.DeleteMetadata != nil { l = m.DeleteMetadata.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -17417,8 +17139,6 @@ func (m *LedgerAction_AddAccountType) SizeVT() (n int) { if m.AddAccountType != nil { l = m.AddAccountType.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -17431,8 +17151,6 @@ func (m *LedgerAction_RemoveAccountType) SizeVT() (n int) { if m.RemoveAccountType != nil { l = m.RemoveAccountType.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -17445,8 +17163,6 @@ func (m *LedgerAction_SetDefaultEnforcementMode) SizeVT() (n int) { if m.SetDefaultEnforcementMode != nil { l = m.SetDefaultEnforcementMode.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -17990,8 +17706,6 @@ func (m *CheckStoreEvent_Error) SizeVT() (n int) { if m.Error != nil { l = m.Error.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -18004,8 +17718,6 @@ func (m *CheckStoreEvent_Progress) SizeVT() (n int) { if m.Progress != nil { l = m.Progress.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -18307,8 +18019,6 @@ func (m *AnalyzeAccountsEvent_Progress) SizeVT() (n int) { if m.Progress != nil { l = m.Progress.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -18321,8 +18031,6 @@ func (m *AnalyzeAccountsEvent_Result) SizeVT() (n int) { if m.Result != nil { l = m.Result.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -18348,8 +18056,6 @@ func (m *AnalyzeTransactionsEvent_Progress) SizeVT() (n int) { if m.Progress != nil { l = m.Progress.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -18362,8 +18068,6 @@ func (m *AnalyzeTransactionsEvent_Result) SizeVT() (n int) { if m.Result != nil { l = m.Result.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -18779,8 +18483,6 @@ func (m *ExecutePreparedQueryResponse_Cursor) SizeVT() (n int) { if m.Cursor != nil { l = m.Cursor.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -18793,8 +18495,6 @@ func (m *ExecutePreparedQueryResponse_Aggregate) SizeVT() (n int) { if m.Aggregate != nil { l = m.Aggregate.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -19128,8 +18828,6 @@ func (m *InspectIndexResponse_DistinctValues) SizeVT() (n int) { if m.DistinctValues != nil { l = m.DistinctValues.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -19142,8 +18840,6 @@ func (m *InspectIndexResponse_Facets) SizeVT() (n int) { if m.Facets != nil { l = m.Facets.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } @@ -19156,8 +18852,6 @@ func (m *InspectIndexResponse_Summary) SizeVT() (n int) { if m.Summary != nil { l = m.Summary.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } else { - n += 3 } return n } diff --git a/internal/proto/signaturepb/signature.pb.go b/internal/proto/signaturepb/signature.pb.go index bec53d4112..0eb98fa36a 100644 --- a/internal/proto/signaturepb/signature.pb.go +++ b/internal/proto/signaturepb/signature.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.12 -// protoc v7.36.2 +// protoc v7.34.2 // source: signature.proto package signaturepb diff --git a/internal/proto/signaturepb/signature_vtproto.pb.go b/internal/proto/signaturepb/signature_vtproto.pb.go index 114dcca3c9..dae3335ca1 100644 --- a/internal/proto/signaturepb/signature_vtproto.pb.go +++ b/internal/proto/signaturepb/signature_vtproto.pb.go @@ -1,5 +1,5 @@ // Code generated by protoc-gen-go-vtproto. DO NOT EDIT. -// protoc-gen-go-vtproto version: v0.6.1-0.20240319094008-0393e58bdf10 +// protoc-gen-go-vtproto version: (devel) // source: signature.proto package signaturepb diff --git a/internal/proto/snapshotpb/snapshot.pb.go b/internal/proto/snapshotpb/snapshot.pb.go index 3014abe92f..a8b9831947 100644 --- a/internal/proto/snapshotpb/snapshot.pb.go +++ b/internal/proto/snapshotpb/snapshot.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.12 -// protoc v7.36.2 +// protoc v7.34.2 // source: snapshot.proto package snapshotpb diff --git a/internal/proto/snapshotpb/snapshot_grpc.pb.go b/internal/proto/snapshotpb/snapshot_grpc.pb.go index 2871a54bff..99d5388f6e 100644 --- a/internal/proto/snapshotpb/snapshot_grpc.pb.go +++ b/internal/proto/snapshotpb/snapshot_grpc.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: // - protoc-gen-go-grpc v1.6.2 -// - protoc v7.36.2 +// - protoc v7.34.2 // source: snapshot.proto package snapshotpb diff --git a/internal/proto/snapshotpb/snapshot_vtproto.pb.go b/internal/proto/snapshotpb/snapshot_vtproto.pb.go index 60eae1c42f..e09c27dbd2 100644 --- a/internal/proto/snapshotpb/snapshot_vtproto.pb.go +++ b/internal/proto/snapshotpb/snapshot_vtproto.pb.go @@ -1,5 +1,5 @@ // Code generated by protoc-gen-go-vtproto. DO NOT EDIT. -// protoc-gen-go-vtproto version: v0.6.1-0.20240319094008-0393e58bdf10 +// protoc-gen-go-vtproto version: (devel) // source: snapshot.proto package snapshotpb From 955d8887e7997a15a2a9536d9157a64cf5854013 Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Thu, 5 Nov 2026 22:48:54 +0100 Subject: [PATCH 094/127] chore: apply CI lint formatting --- internal/application/membership/membership_node_test.go | 1 + internal/bootstrap/cluster_policy_reconciler_test.go | 2 ++ internal/bootstrap/module.go | 3 +++ internal/bootstrap/module_refresh_membership_test.go | 1 + internal/infra/capabilities/capabilities_bench_test.go | 2 +- internal/infra/membership/membership.go | 1 - internal/infra/state/machine.go | 1 + internal/proto/auditpb/audit.pb.go | 4 +++- 8 files changed, 12 insertions(+), 3 deletions(-) diff --git a/internal/application/membership/membership_node_test.go b/internal/application/membership/membership_node_test.go index 61a4757deb..b1a4675bff 100644 --- a/internal/application/membership/membership_node_test.go +++ b/internal/application/membership/membership_node_test.go @@ -150,6 +150,7 @@ func TestService_LearnerRegistrationCommitsAndPreservesCallerIntent(t *testing.T if bootJoin { return s.JoinAsLearner(ctx, nodeID, raftAddr, serviceAddr, instanceID, capabilities) } + return s.AddLearner(ctx, nodeID, raftAddr, serviceAddr, instanceID, capabilities) } require.NoError(t, register(ctx, 2, "peer:7777", "peer:8888", []byte("peer-instance-id"), nil)) diff --git a/internal/bootstrap/cluster_policy_reconciler_test.go b/internal/bootstrap/cluster_policy_reconciler_test.go index fa0071e58e..9fb93ad514 100644 --- a/internal/bootstrap/cluster_policy_reconciler_test.go +++ b/internal/bootstrap/cluster_policy_reconciler_test.go @@ -175,6 +175,7 @@ func TestReconcileClusterPolicy(t *testing.T) { var memberReads atomic.Int32 reconcileClusterPolicy(context.Background(), admission, store, cfg, func() bool { return true }, logging.Testing(), func() []capabilities.Member { memberReads.Add(1) + return []capabilities.Member{{NodeID: 1, Role: capabilities.Voter}} }) @@ -198,6 +199,7 @@ func TestReconcileClusterPolicy(t *testing.T) { var memberReads atomic.Int32 reconcileClusterPolicy(context.Background(), admission, store, cfg, func() bool { return true }, logging.Testing(), func() []capabilities.Member { memberReads.Add(1) + return []capabilities.Member{{NodeID: 1, Role: capabilities.Voter}} }) diff --git a/internal/bootstrap/module.go b/internal/bootstrap/module.go index fc1021ad82..2528e31fe3 100644 --- a/internal/bootstrap/module.go +++ b/internal/bootstrap/module.go @@ -344,6 +344,7 @@ func Module() fx.Option { if machine != nil { policy = machine.ClusterPolicy() } + return membership.WriteConfChangeWithPolicy(entry, session, policy) }, func(policy *commonpb.ClusterPolicy) { @@ -409,6 +410,7 @@ func Module() fx.Option { // gate runs under the serialized proposal/index boundary; taking two // snapshots here needlessly doubles lock, map-copy and allocation cost. members := raftNode.CapabilityMembers() + return validateProposalCapabilityAdmission(policy, proposal, members) } @@ -1752,6 +1754,7 @@ func tryRefreshMembershipOnce(ctx context.Context, cfg Config, tlsCfg TLSConfig, if lastTransient != nil { return fmt.Errorf("%w: %w", errMembershipRefreshTransient, lastTransient) } + return errMembershipRefreshTransient } diff --git a/internal/bootstrap/module_refresh_membership_test.go b/internal/bootstrap/module_refresh_membership_test.go index c42269b11e..7f8bf65872 100644 --- a/internal/bootstrap/module_refresh_membership_test.go +++ b/internal/bootstrap/module_refresh_membership_test.go @@ -50,6 +50,7 @@ type blockingRefreshServer struct { func (blockingRefreshServer) RefreshMembership(ctx context.Context, _ *clusterbootstrappb.RefreshMembershipRequest) (*clusterbootstrappb.RefreshMembershipResponse, error) { <-ctx.Done() + return nil, ctx.Err() } diff --git a/internal/infra/capabilities/capabilities_bench_test.go b/internal/infra/capabilities/capabilities_bench_test.go index 32bb8621b9..3d09e0dc69 100644 --- a/internal/infra/capabilities/capabilities_bench_test.go +++ b/internal/infra/capabilities/capabilities_bench_test.go @@ -33,7 +33,7 @@ func BenchmarkEvaluate(b *testing.B) { b.ReportAllocs() b.ResetTimer() - for i := 0; i < b.N; i++ { + for range b.N { if !Evaluate(state, required, false, members).Allowed { b.Fatal("representative capability state was rejected") } diff --git a/internal/infra/membership/membership.go b/internal/infra/membership/membership.go index 987c3943a5..61160b792e 100644 --- a/internal/infra/membership/membership.go +++ b/internal/infra/membership/membership.go @@ -253,7 +253,6 @@ func (m *Membership) SnapshotDonorCandidates() []uint64 { slices.Sort(candidates) return candidates - } // SetCapabilityGate publishes the committed activation gate used by diff --git a/internal/infra/state/machine.go b/internal/infra/state/machine.go index 76dde17a16..3357fc3390 100644 --- a/internal/infra/state/machine.go +++ b/internal/infra/state/machine.go @@ -332,6 +332,7 @@ func (fsm *Machine) ClusterPolicy() *commonpb.ClusterPolicy { if fsm.State.ClusterPolicy == nil { return nil } + return fsm.State.ClusterPolicy.CloneVT() } diff --git a/internal/proto/auditpb/audit.pb.go b/internal/proto/auditpb/audit.pb.go index 6b3586577d..5742645a84 100644 --- a/internal/proto/auditpb/audit.pb.go +++ b/internal/proto/auditpb/audit.pb.go @@ -47,7 +47,8 @@ type AuditEntry struct { Ledgers []string `protobuf:"bytes,8,rep,name=ledgers,proto3" json:"ledgers,omitempty"` // Batch-level integrity hash chaining audit entries. // Formula: H(key, header_payload || concat(per_item_payload) || - // previous_audit_hash) + // + // previous_audit_hash) // where: // - `key` is derived from the once-committed random audit secret via // domain-separated BLAKE3, independently of the operational cluster ID @@ -62,6 +63,7 @@ type AuditEntry struct { // via state.BuildPerItemPayload // - `previous_audit_hash` is `hash` of the predecessor entry, or empty for // the first entry + // // Same formula for success and failure entries. The hash binds every field // of AuditEntry and AuditItem except `hash` itself. Tampering with any // other field is detected by checker.verifyAuditHashChain. From 2af0cacc7e0d955f093330718a29d2faf2f5909f Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Thu, 5 Nov 2026 22:59:21 +0100 Subject: [PATCH 095/127] chore(proto): match CI comment formatting --- internal/proto/auditpb/audit.pb.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/proto/auditpb/audit.pb.go b/internal/proto/auditpb/audit.pb.go index 5742645a84..83d8509417 100644 --- a/internal/proto/auditpb/audit.pb.go +++ b/internal/proto/auditpb/audit.pb.go @@ -64,6 +64,7 @@ type AuditEntry struct { // - `previous_audit_hash` is `hash` of the predecessor entry, or empty for // the first entry // + // // Same formula for success and failure entries. The hash binds every field // of AuditEntry and AuditItem except `hash` itself. Tampering with any // other field is detected by checker.verifyAuditHashChain. From d94fb2918beb6662596343e5472991acb45e3f57 Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Fri, 6 Nov 2026 00:35:33 +0100 Subject: [PATCH 096/127] chore(proto): align audit comment formatting --- internal/proto/auditpb/audit.pb.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/proto/auditpb/audit.pb.go b/internal/proto/auditpb/audit.pb.go index 83d8509417..ffa8870b4c 100644 --- a/internal/proto/auditpb/audit.pb.go +++ b/internal/proto/auditpb/audit.pb.go @@ -49,6 +49,7 @@ type AuditEntry struct { // Formula: H(key, header_payload || concat(per_item_payload) || // // previous_audit_hash) + // // where: // - `key` is derived from the once-committed random audit secret via // domain-separated BLAKE3, independently of the operational cluster ID @@ -64,7 +65,6 @@ type AuditEntry struct { // - `previous_audit_hash` is `hash` of the predecessor entry, or empty for // the first entry // - // // Same formula for success and failure entries. The hash binds every field // of AuditEntry and AuditItem except `hash` itself. Tampering with any // other field is detected by checker.verifyAuditHashChain. From cfaa4935e2915e4210d00f97f0ee6ec777f9db6a Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Fri, 6 Nov 2026 09:53:31 +0100 Subject: [PATCH 097/127] simplify capability activation for coordinated upgrades --- docs/ops/upgrades.md | 15 +++---- .../subsystems/consensus/capabilities.md | 30 ++++++------- .../cluster_policy_reconciler_test.go | 44 +----------------- internal/bootstrap/module.go | 45 ++----------------- internal/infra/node/node.go | 9 ---- internal/infra/node/observer.go | 5 --- 6 files changed, 27 insertions(+), 121 deletions(-) diff --git a/docs/ops/upgrades.md b/docs/ops/upgrades.md index fd641072e9..97b3c72b7a 100644 --- a/docs/ops/upgrades.md +++ b/docs/ops/upgrades.md @@ -342,14 +342,13 @@ validation passes. For RC.1 to RC.2, follow the [maintenance-barrier rolling procedure](#account-metadata-address-validation). For RC.3 to RC.4, use the [operator-required stopped restart](#operator-required-stopped-restart). -Capability barriers are the prerequisite for any future incompatible Raft -entry, execution-plan, replay, or snapshot format. Deploy barrier-aware Ledger -servers to every voter and learner first. This release only deploys the -capability-evidence mechanism; it does not provide an operator-facing arming -procedure or activate a new capability automatically. An old leader that -predates the barrier is not made safe by upgrading other nodes. See the -[capability contract](../technical/architecture/subsystems/consensus/capabilities.md) -for membership evidence and the downgrade boundary. +Capability metadata is the prerequisite for any future incompatible Raft entry, +execution-plan, replay, or snapshot format. This release does not support +mixed-version activation: stop every Ledger node, deploy the barrier-aware +binary everywhere, restart the complete cluster, and only then enable the new +feature. The capability reconciler does not arm features from membership +advertisements. See the [capability contract](../technical/architecture/subsystems/consensus/capabilities.md) +for the persisted replay boundary and coordinated rollout. Use this for RC updates after the mixed-version validation above passes: diff --git a/docs/technical/architecture/subsystems/consensus/capabilities.md b/docs/technical/architecture/subsystems/consensus/capabilities.md index 2aab7d5fe9..b84cef4408 100644 --- a/docs/technical/architecture/subsystems/consensus/capabilities.md +++ b/docs/technical/architecture/subsystems/consensus/capabilities.md @@ -1,7 +1,9 @@ -# Rolling-upgrade capabilities +# Coordinated-upgrade capabilities -Ledger's capability barrier is the protocol foundation for a future mixed-version -rolling upgrade. It is separate from the client-facing gRPC protocol revision. +Ledger's capability metadata is the protocol foundation for future incompatible +features deployed with a coordinated stop-and-restart. It is separate from the +client-facing gRPC protocol revision. This release does not implement a +mixed-version rolling activation path. ## Wire contract @@ -103,11 +105,11 @@ their old behavior while new orders use a different rule. ## Rollout and recovery -Deploy barrier-aware binaries to every eligible member before arming an -incompatible feature. An old leader that does not understand the barrier cannot -be made safe by upgrading only the other nodes. Restart, snapshot install, -restore, and downgrade code must preserve the policy. Membership changes racing activation are -serialized at the proposal boundary. `Node.Propose` evaluates the authoritative +Deploy the barrier-aware binary to every member, stop the cluster, and restart +the complete cluster before enabling an incompatible feature. An old leader +cannot be made safe by upgrading only the other nodes. Restart, snapshot +install, restore, and downgrade code must preserve the policy. Membership +changes are not used as a rolling activation mechanism. `Node.Propose` evaluates the authoritative policy while holding the proposal and index-tracker guards, before reserving a predicted index or queueing work; this prevents a rejected proposal from leaving a hole for queued successors. Removing a member remains allowed; @@ -171,11 +173,7 @@ Raft and applied to every member's peer store; a local restart-time upsert is not sufficient to activate a capability for admission. A fresh WAL instead uses `JoinAsLearner` and `ConfChangeAddLearnerNode`. -The cluster-policy reconciler maintains the replicated policy and automatically -arms newly advertised capabilities only when the candidate is present in the -intersection of the local binary registry and every current voter and learner. -An empty or incomplete membership set activates nothing. Membership commits -trigger reconciliation immediately, and the periodic retry remains as a -self-healing fallback for leadership or proposal failures. Once an activation -is committed, it is monotonic and a new member that does not advertise it is -rejected. +The cluster-policy reconciler preserves committed capability activations while +reconciling ordinary policy configuration; it does not arm capabilities from +membership advertisements. A future feature must define its activation policy +explicitly and enable it only after the coordinated restart has completed. diff --git a/internal/bootstrap/cluster_policy_reconciler_test.go b/internal/bootstrap/cluster_policy_reconciler_test.go index 9fb93ad514..57db9a6f0b 100644 --- a/internal/bootstrap/cluster_policy_reconciler_test.go +++ b/internal/bootstrap/cluster_policy_reconciler_test.go @@ -3,7 +3,6 @@ package bootstrap import ( "context" "sync" - "sync/atomic" "testing" "github.com/stretchr/testify/require" @@ -12,7 +11,6 @@ import ( logging "github.com/formancehq/go-libs/v5/pkg/observe/log" "github.com/formancehq/ledger/v3/internal/domain" - "github.com/formancehq/ledger/v3/internal/infra/capabilities" "github.com/formancehq/ledger/v3/internal/infra/state" "github.com/formancehq/ledger/v3/internal/proto/commonpb" "github.com/formancehq/ledger/v3/internal/proto/servicepb" @@ -153,36 +151,6 @@ func TestReconcileClusterPolicy(t *testing.T) { require.Zero(t, admission.callCount(), "a newer applied revision must not be overwritten") }) - t.Run("does not manufacture a revision when a member reader is present", func(t *testing.T) { - t.Parallel() - - store := newReconcilerTestStore(t) - cfg := validBaseConfig() - batch := store.OpenWriteSession() - require.NoError(t, state.SaveClusterPolicy(batch, &commonpb.ClusterPolicy{ - Revision: cfg.ClusterPolicyRevision + 1, - IdempotencyTtlMicros: uint64(cfg.IdempotencyTTL.Microseconds()), - QueryCheckpointLimit: cfg.QueryCheckpointLimit, - MetadataMaxEntriesPerEntity: cfg.MetadataMaxEntriesPerEntity, - MetadataMaxKeyBytes: cfg.MetadataMaxKeyBytes, - MetadataMaxValueBytes: cfg.MetadataMaxValueBytes, - MetadataMaxEntityBytes: cfg.MetadataMaxEntityBytes, - MetadataMaxCommandBytes: cfg.MetadataMaxCommandBytes, - })) - require.NoError(t, batch.Commit()) - - admission := &fakeAdmission{} - var memberReads atomic.Int32 - reconcileClusterPolicy(context.Background(), admission, store, cfg, func() bool { return true }, logging.Testing(), func() []capabilities.Member { - memberReads.Add(1) - - return []capabilities.Member{{NodeID: 1, Role: capabilities.Voter}} - }) - - require.Zero(t, admission.callCount(), "member evidence must not bypass the stale revision guard") - require.EqualValues(t, 1, memberReads.Load(), "matching operator configuration must still allow capability reconciliation") - }) - t.Run("rejects a newer revision with a configuration conflict", func(t *testing.T) { t.Parallel() @@ -196,15 +164,9 @@ func TestReconcileClusterPolicy(t *testing.T) { require.NoError(t, batch.Commit()) admission := &fakeAdmission{} - var memberReads atomic.Int32 - reconcileClusterPolicy(context.Background(), admission, store, cfg, func() bool { return true }, logging.Testing(), func() []capabilities.Member { - memberReads.Add(1) - - return []capabilities.Member{{NodeID: 1, Role: capabilities.Voter}} - }) + reconcileClusterPolicy(context.Background(), admission, store, cfg, func() bool { return true }, logging.Testing()) require.Zero(t, admission.callCount(), "a newer conflicting configuration must not be re-proposed") - require.Zero(t, memberReads.Load(), "configuration conflicts must stop before capability reconciliation") }) t.Run("does not replace a same-revision configuration conflict with a capability bump", func(t *testing.T) { @@ -220,9 +182,7 @@ func TestReconcileClusterPolicy(t *testing.T) { require.NoError(t, batch.Commit()) admission := &fakeAdmission{} - reconcileClusterPolicy(context.Background(), admission, store, cfg, func() bool { return true }, logging.Testing(), func() []capabilities.Member { - return []capabilities.Member{{NodeID: 1, Role: capabilities.Voter}} - }) + reconcileClusterPolicy(context.Background(), admission, store, cfg, func() bool { return true }, logging.Testing()) require.Zero(t, admission.callCount(), "same-revision configuration conflicts must remain rejected") }) diff --git a/internal/bootstrap/module.go b/internal/bootstrap/module.go index 2528e31fe3..6fc1e12cc5 100644 --- a/internal/bootstrap/module.go +++ b/internal/bootstrap/module.go @@ -128,7 +128,7 @@ func newClusterPolicyReconciler(admission ctrl.Admission, store *dal.Store, buil return } - reconcileClusterPolicy(ctx, admission, store, cfg, raftNode.IsLeader, logger, raftNode.CapabilityMembers) + reconcileClusterPolicy(ctx, admission, store, cfg, raftNode.IsLeader, logger) }, clusterPolicyReconcileInterval) } @@ -938,9 +938,8 @@ func Module() fx.Option { RequestShutdown: shutdownRequester(shutdowner), })) }, - // Wire Observer: handle LeadershipChange, LeaderReady and capability - // membership events. Membership owns transport / service-pool wiring; - // the capability event wakes policy reconciliation immediately. + // Wire Observer: handle LeadershipChange and LeaderReady events. + // Membership owns transport / service-pool wiring. func( n *node.Node, store *dal.Store, @@ -983,8 +982,6 @@ func Module() fx.Option { // can block on its own commit. handleLeaderReadyEvent(eventsManager, mirrorManager, logger) proposeClusterConfigIfNeeded(e.Context, n, builder, store, cfg, logger) - case node.CapabilityMembershipChangedEvent: - reconciler.Trigger() default: logger.Errorf("Unknown observer event type: %T", event) } @@ -1902,7 +1899,7 @@ const clusterPolicyReconcileInterval = time.Second // through AdmitClusterPolicy as an internal audited SetClusterPolicy order so // the checker can re-derive it; the write-readiness gate exempts this // non-business proposal so it is never blocked by its own cluster-policy gate. -func reconcileClusterPolicy(ctx context.Context, admission ctrl.Admission, store *dal.Store, cfg Config, isLeader func() bool, logger logging.Logger, memberReaders ...func() []capabilities.Member) { +func reconcileClusterPolicy(ctx context.Context, admission ctrl.Admission, store *dal.Store, cfg Config, isLeader func() bool, logger logging.Logger) { if !isLeader() { return } @@ -1950,40 +1947,6 @@ func reconcileClusterPolicy(ctx context.Context, admission ctrl.Admission, store return } - if len(memberReaders) > 0 { - ready, readyErr := capabilities.Ready(capabilities.SupportedIDs(), memberReaders[0]()) - if readyErr != nil { - logger.WithFields(map[string]any{"error": readyErr}).Errorf("Capability evidence is invalid; delaying policy reconciliation") - - return - } - active := make(map[uint32]bool, len(desired.GetCapabilityActivations())) - for _, activation := range desired.GetCapabilityActivations() { - if activation != nil { - active[activation.GetId()] = activation.GetActive() - } - } - for _, id := range ready { - if enabled, exists := active[uint32(id)]; exists { - if !enabled { - for _, activation := range desired.GetCapabilityActivations() { - if activation != nil && activation.GetId() == uint32(id) { - activation.Active = true - - break - } - } - } - - continue - } - desired.CapabilityActivations = append(desired.CapabilityActivations, &commonpb.CapabilityActivation{Id: uint32(id), Active: true}) - } - if applied != nil && !sameClusterPolicyPayload(applied, desired) && desired.GetRevision() <= applied.GetRevision() { - desired.Revision = applied.GetRevision() + 1 - } - } - switch { case appliedRev > desired.GetRevision(): // A newer policy is already applied; the FSM would reject this as stale. diff --git a/internal/infra/node/node.go b/internal/infra/node/node.go index 172b4222fc..5bcc305df7 100644 --- a/internal/infra/node/node.go +++ b/internal/infra/node/node.go @@ -1749,7 +1749,6 @@ func (node *Node) finishReady(result readyResult, stop chan struct{}) error { // callers waiting on ConfChange commit (AddLearner, PromoteLearner, etc.) // don't resume before the WAL is consistent. var pendingFutures []committedConfChangeFuture - capabilityMembershipChanged := false for _, committed := range result.confChanges { cc := committed.change @@ -1801,8 +1800,6 @@ func (node *Node) finishReady(result readyResult, stop chan struct{}) error { if err != nil { return err } - capabilityMembershipChanged = true - pending, err := node.takePendingConfChange(cc, committed.index) if err != nil { return err @@ -1820,12 +1817,6 @@ func (node *Node) finishReady(result readyResult, stop chan struct{}) error { }) } } - if capabilityMembershipChanged { - if node.observer != nil { - node.observer.Emit(CapabilityMembershipChangedEvent{}) - } - } - // If the ConfState changed (e.g. a learner was added), update the WAL // snapshot's ConfState immediately. Without this, etcd/raft would send // the stale snapshot (which lacks the new node) before the applier's diff --git a/internal/infra/node/observer.go b/internal/infra/node/observer.go index 5a8f1a3a05..ecdc3bc28f 100644 --- a/internal/infra/node/observer.go +++ b/internal/infra/node/observer.go @@ -18,11 +18,6 @@ type LeaderReadyEvent struct { Context context.Context } -// CapabilityMembershipChangedEvent is emitted after a committed membership -// change has updated the local capability evidence cache. A leader can use it -// to immediately reconcile capability activations without polling the cache. -type CapabilityMembershipChangedEvent struct{} - // EventHandler is a callback invoked synchronously for each emitted event. // Consumers use a type switch on the event to handle concrete types. type EventHandler func(event any) From c3a273b2dba17669059e2fda7483bc52acdd4bcf Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Fri, 6 Nov 2026 10:02:05 +0100 Subject: [PATCH 098/127] chore(proto): regenerate after main rebase --- internal/proto/clusterpb/cluster.pb.go | 13 +- .../proto/clusterpb/cluster_ledger_json.pb.go | 7 + internal/proto/commonpb/common.pb.go | 871 ++++++++++-------- .../proto/commonpb/common_ledger_json.pb.go | 7 + 4 files changed, 495 insertions(+), 403 deletions(-) diff --git a/internal/proto/clusterpb/cluster.pb.go b/internal/proto/clusterpb/cluster.pb.go index 1de4622a87..f090087bc5 100644 --- a/internal/proto/clusterpb/cluster.pb.go +++ b/internal/proto/clusterpb/cluster.pb.go @@ -1310,6 +1310,7 @@ type AddLearnerRequest struct { RaftAddress string `protobuf:"bytes,2,opt,name=raft_address,json=raftAddress,proto3" json:"raft_address,omitempty"` // Raft transport address of the learner ServiceAddress string `protobuf:"bytes,3,opt,name=service_address,json=serviceAddress,proto3" json:"service_address,omitempty"` // Service API address of the learner InstanceId []byte `protobuf:"bytes,4,opt,name=instance_id,json=instanceId,proto3" json:"instance_id,omitempty"` // 16-byte UUID from the learner's persisted INSTANCE_ID marker; required + Capabilities []uint32 `protobuf:"fixed32,5,rep,packed,name=capabilities,proto3" json:"capabilities,omitempty"` // Capabilities supported by the learner binary unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -1372,6 +1373,13 @@ func (x *AddLearnerRequest) GetInstanceId() []byte { return nil } +func (x *AddLearnerRequest) GetCapabilities() []uint32 { + if x != nil { + return x.Capabilities + } + return nil +} + type AddLearnerResponse struct { state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields @@ -2524,13 +2532,14 @@ const file_cluster_proto_rawDesc = "" + "\n" + "wal_volume\x18\x01 \x01(\v2\x14.cluster.VolumeUsageR\twalVolume\x125\n" + "\vdata_volume\x18\x02 \x01(\v2\x14.cluster.VolumeUsageR\n" + - "dataVolume:\x04\xd8\xd5\"\x01\"\x99\x01\n" + + "dataVolume:\x04\xd8\xd5\"\x01\"\xbd\x01\n" + "\x11AddLearnerRequest\x12\x17\n" + "\anode_id\x18\x01 \x01(\x04R\x06nodeId\x12!\n" + "\fraft_address\x18\x02 \x01(\tR\vraftAddress\x12'\n" + "\x0fservice_address\x18\x03 \x01(\tR\x0eserviceAddress\x12\x1f\n" + "\vinstance_id\x18\x04 \x01(\fR\n" + - "instanceId\"\x14\n" + + "instanceId\x12\"\n" + + "\fcapabilities\x18\x05 \x03(\aR\fcapabilities\"\x14\n" + "\x12AddLearnerResponse\"0\n" + "\x15PromoteLearnerRequest\x12\x17\n" + "\anode_id\x18\x01 \x01(\x04R\x06nodeId\"\x18\n" + diff --git a/internal/proto/clusterpb/cluster_ledger_json.pb.go b/internal/proto/clusterpb/cluster_ledger_json.pb.go index dec8a02e65..523280bcd0 100644 --- a/internal/proto/clusterpb/cluster_ledger_json.pb.go +++ b/internal/proto/clusterpb/cluster_ledger_json.pb.go @@ -286,11 +286,18 @@ func (x *AddLearnerRequest) MarshalJSON() ([]byte, error) { RaftAddress string `json:"raftAddress"` ServiceAddress string `json:"serviceAddress"` InstanceId []byte `json:"instanceId"` + Capabilities any `json:"capabilities"` }{ NodeId: x.NodeId, RaftAddress: x.RaftAddress, ServiceAddress: x.ServiceAddress, InstanceId: x.InstanceId, + Capabilities: func() any { + if x.Capabilities == nil { + return []any{} + } + return x.Capabilities + }(), }) } diff --git a/internal/proto/commonpb/common.pb.go b/internal/proto/commonpb/common.pb.go index 15c3fc1fe5..2c6f21f593 100644 --- a/internal/proto/commonpb/common.pb.go +++ b/internal/proto/commonpb/common.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.12 -// protoc v7.34.2 +// protoc v7.36.2 // source: common.proto package commonpb @@ -4461,8 +4461,10 @@ type ClusterPolicy struct { MetadataMaxValueBytes uint64 `protobuf:"fixed64,6,opt,name=metadata_max_value_bytes,json=metadataMaxValueBytes,proto3" json:"metadata_max_value_bytes,omitempty"` MetadataMaxEntityBytes uint64 `protobuf:"fixed64,7,opt,name=metadata_max_entity_bytes,json=metadataMaxEntityBytes,proto3" json:"metadata_max_entity_bytes,omitempty"` MetadataMaxCommandBytes uint64 `protobuf:"fixed64,8,opt,name=metadata_max_command_bytes,json=metadataMaxCommandBytes,proto3" json:"metadata_max_command_bytes,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // Capability activation is replicated policy, never a node-local flag. + CapabilityActivations []*CapabilityActivation `protobuf:"bytes,13,rep,name=capability_activations,json=capabilityActivations,proto3" json:"capability_activations,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *ClusterPolicy) Reset() { @@ -4551,6 +4553,13 @@ func (x *ClusterPolicy) GetMetadataMaxCommandBytes() uint64 { return 0 } +func (x *ClusterPolicy) GetCapabilityActivations() []*CapabilityActivation { + if x != nil { + return x.CapabilityActivations + } + return nil +} + // CreatedPreparedQueryLog records the creation of a prepared query. type CreatedPreparedQueryLog struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -22169,6 +22178,60 @@ func (x *ListOptions) GetFilter() *QueryFilter { return nil } +// CapabilityActivation is intentionally small and wire-stable. IDs are fixed +// integers owned by the producer, not hashes of implementation names. +type CapabilityActivation struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id uint32 `protobuf:"fixed32,1,opt,name=id,proto3" json:"id,omitempty"` + Active bool `protobuf:"varint,2,opt,name=active,proto3" json:"active,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CapabilityActivation) Reset() { + *x = CapabilityActivation{} + mi := &file_common_proto_msgTypes[312] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CapabilityActivation) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CapabilityActivation) ProtoMessage() {} + +func (x *CapabilityActivation) ProtoReflect() protoreflect.Message { + mi := &file_common_proto_msgTypes[312] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CapabilityActivation.ProtoReflect.Descriptor instead. +func (*CapabilityActivation) Descriptor() ([]byte, []int) { + return file_common_proto_rawDescGZIP(), []int{312} +} + +func (x *CapabilityActivation) GetId() uint32 { + if x != nil { + return x.Id + } + return 0 +} + +func (x *CapabilityActivation) GetActive() bool { + if x != nil { + return x.Active + } + return false +} + var file_common_proto_extTypes = []protoimpl.ExtensionInfo{ { ExtendedType: (*descriptorpb.MethodOptions)(nil), @@ -22544,7 +22607,7 @@ const file_common_proto_rawDesc = "" + "\x15PersistedClusterState\x12-\n" + "\x06config\x18\x01 \x01(\v2\x15.common.ClusterConfigR\x06config\x12\x1f\n" + "\vcache_epoch\x18\x02 \x01(\x06R\n" + - "cacheEpoch\"\xc9\x03\n" + + "cacheEpoch\"\x9e\x04\n" + "\rClusterPolicy\x12\x1a\n" + "\brevision\x18\x01 \x01(\x06R\brevision\x124\n" + "\x16idempotency_ttl_micros\x18\x02 \x01(\x06R\x14idempotencyTtlMicros\x124\n" + @@ -22553,7 +22616,8 @@ const file_common_proto_rawDesc = "" + "\x16metadata_max_key_bytes\x18\x05 \x01(\x06R\x13metadataMaxKeyBytes\x127\n" + "\x18metadata_max_value_bytes\x18\x06 \x01(\x06R\x15metadataMaxValueBytes\x129\n" + "\x19metadata_max_entity_bytes\x18\a \x01(\x06R\x16metadataMaxEntityBytes\x12;\n" + - "\x1ametadata_max_command_bytes\x18\b \x01(\x06R\x17metadataMaxCommandBytes:\x04\xd8\xd5\"\x01\"d\n" + + "\x1ametadata_max_command_bytes\x18\b \x01(\x06R\x17metadataMaxCommandBytes\x12S\n" + + "\x16capability_activations\x18\r \x03(\v2\x1c.common.CapabilityActivationR\x15capabilityActivations:\x04\xd8\xd5\"\x01\"d\n" + "\x17CreatedPreparedQueryLog\x12\x16\n" + "\x06ledger\x18\x01 \x01(\tR\x06ledger\x12+\n" + "\x05query\x18\x02 \x01(\v2\x15.common.PreparedQueryR\x05query:\x04\xd8\xd5\"\x01\"\xdc\x02\n" + @@ -23798,7 +23862,10 @@ const file_common_proto_rawDesc = "" + "\tpage_size\x18\x02 \x01(\rR\bpageSize\x12\x16\n" + "\x06cursor\x18\x03 \x01(\tR\x06cursor\x12\x18\n" + "\areverse\x18\x04 \x01(\bR\areverse\x12+\n" + - "\x06filter\x18\x05 \x01(\v2\x13.common.QueryFilterR\x06filter*\xbb\x03\n" + + "\x06filter\x18\x05 \x01(\v2\x13.common.QueryFilterR\x06filter\">\n" + + "\x14CapabilityActivation\x12\x0e\n" + + "\x02id\x18\x01 \x01(\aR\x02id\x12\x16\n" + + "\x06active\x18\x02 \x01(\bR\x06active*\xbb\x03\n" + "\tAuthScope\x12\x1a\n" + "\x16AUTH_SCOPE_UNSPECIFIED\x10\x00\x12\x1a\n" + "\x16AUTH_SCOPE_LEDGER_READ\x10\x01\x12\x1b\n" + @@ -24007,7 +24074,7 @@ func file_common_proto_rawDescGZIP() []byte { } var file_common_proto_enumTypes = make([]protoimpl.EnumInfo, 18) -var file_common_proto_msgTypes = make([]protoimpl.MessageInfo, 330) +var file_common_proto_msgTypes = make([]protoimpl.MessageInfo, 331) var file_common_proto_goTypes = []any{ (AuthScope)(0), // 0: common.AuthScope (DynamicAuthResolver)(0), // 1: common.DynamicAuthResolver @@ -24339,44 +24406,45 @@ var file_common_proto_goTypes = []any{ (*BackupStorage)(nil), // 327: common.BackupStorage (*ReadOptions)(nil), // 328: common.ReadOptions (*ListOptions)(nil), // 329: common.ListOptions - nil, // 330: common.MetadataMap.ValuesEntry - nil, // 331: common.Transaction.MetadataEntry - nil, // 332: common.Script.VarsEntry - nil, // 333: common.PostCommitVolumes.VolumesByAccountEntry - nil, // 334: common.Account.MetadataEntry - nil, // 335: common.MetadataSchema.AccountFieldsEntry - nil, // 336: common.MetadataSchema.TransactionFieldsEntry - nil, // 337: common.MetadataSchema.LedgerFieldsEntry - nil, // 338: common.SavedLedgerMetadataLog.MetadataEntry - nil, // 339: common.CreatedLedgerLog.AccountTypesEntry - nil, // 340: common.CreatedLedgerLog.MetadataEntry - nil, // 341: common.CreatedTransaction.AccountMetadataEntry - nil, // 342: common.SavedMetadata.MetadataEntry - nil, // 343: common.LedgerInfo.AccountTypesEntry - nil, // 344: common.LedgerInfo.MetadataEntry - nil, // 345: common.SaveMetadataCommand.MetadataEntry - nil, // 346: common.TransactionState.MetadataEntry - nil, // 347: common.AccountType.SegmentTypesEntry - (*signaturepb.SignedLog)(nil), // 348: signature.SignedLog - (*descriptorpb.MethodOptions)(nil), // 349: google.protobuf.MethodOptions - (*descriptorpb.MessageOptions)(nil), // 350: google.protobuf.MessageOptions - (*descriptorpb.FileOptions)(nil), // 351: google.protobuf.FileOptions - (*descriptorpb.FieldOptions)(nil), // 352: google.protobuf.FieldOptions + (*CapabilityActivation)(nil), // 330: common.CapabilityActivation + nil, // 331: common.MetadataMap.ValuesEntry + nil, // 332: common.Transaction.MetadataEntry + nil, // 333: common.Script.VarsEntry + nil, // 334: common.PostCommitVolumes.VolumesByAccountEntry + nil, // 335: common.Account.MetadataEntry + nil, // 336: common.MetadataSchema.AccountFieldsEntry + nil, // 337: common.MetadataSchema.TransactionFieldsEntry + nil, // 338: common.MetadataSchema.LedgerFieldsEntry + nil, // 339: common.SavedLedgerMetadataLog.MetadataEntry + nil, // 340: common.CreatedLedgerLog.AccountTypesEntry + nil, // 341: common.CreatedLedgerLog.MetadataEntry + nil, // 342: common.CreatedTransaction.AccountMetadataEntry + nil, // 343: common.SavedMetadata.MetadataEntry + nil, // 344: common.LedgerInfo.AccountTypesEntry + nil, // 345: common.LedgerInfo.MetadataEntry + nil, // 346: common.SaveMetadataCommand.MetadataEntry + nil, // 347: common.TransactionState.MetadataEntry + nil, // 348: common.AccountType.SegmentTypesEntry + (*signaturepb.SignedLog)(nil), // 349: signature.SignedLog + (*descriptorpb.MethodOptions)(nil), // 350: google.protobuf.MethodOptions + (*descriptorpb.MessageOptions)(nil), // 351: google.protobuf.MessageOptions + (*descriptorpb.FileOptions)(nil), // 352: google.protobuf.FileOptions + (*descriptorpb.FieldOptions)(nil), // 353: google.protobuf.FieldOptions } var file_common_proto_depIdxs = []int32{ 0, // 0: common.MethodAuthPolicy.fixed_scope:type_name -> common.AuthScope 1, // 1: common.MethodAuthPolicy.dynamic_resolver:type_name -> common.DynamicAuthResolver 20, // 2: common.MetadataValue.null_value:type_name -> common.NullValue - 330, // 3: common.MetadataMap.values:type_name -> common.MetadataMap.ValuesEntry + 331, // 3: common.MetadataMap.values:type_name -> common.MetadataMap.ValuesEntry 24, // 4: common.Posting.amount:type_name -> common.Uint256 25, // 5: common.Transaction.postings:type_name -> common.Posting - 331, // 6: common.Transaction.metadata:type_name -> common.Transaction.MetadataEntry + 332, // 6: common.Transaction.metadata:type_name -> common.Transaction.MetadataEntry 19, // 7: common.Transaction.timestamp:type_name -> common.Timestamp 19, // 8: common.Transaction.inserted_at:type_name -> common.Timestamp 19, // 9: common.Transaction.updated_at:type_name -> common.Timestamp 19, // 10: common.Transaction.reverted_at:type_name -> common.Timestamp 34, // 11: common.Transaction.post_commit_volumes:type_name -> common.PostCommitVolumes - 332, // 12: common.Script.vars:type_name -> common.Script.VarsEntry + 333, // 12: common.Script.vars:type_name -> common.Script.VarsEntry 28, // 13: common.SignedBigInt.magnitude:type_name -> common.BigUint 28, // 14: common.Volumes.input:type_name -> common.BigUint 28, // 15: common.Volumes.output:type_name -> common.BigUint @@ -24385,18 +24453,18 @@ var file_common_proto_depIdxs = []int32{ 29, // 18: common.VolumesWithBalance.balance:type_name -> common.SignedBigInt 33, // 19: common.VolumesByAssets.volumes:type_name -> common.VolumeEntry 30, // 20: common.VolumeEntry.volumes:type_name -> common.Volumes - 333, // 21: common.PostCommitVolumes.volumes_by_account:type_name -> common.PostCommitVolumes.VolumesByAccountEntry + 334, // 21: common.PostCommitVolumes.volumes_by_account:type_name -> common.PostCommitVolumes.VolumesByAccountEntry 31, // 22: common.AccountVolume.volumes:type_name -> common.VolumesWithBalance - 334, // 23: common.Account.metadata:type_name -> common.Account.MetadataEntry + 335, // 23: common.Account.metadata:type_name -> common.Account.MetadataEntry 19, // 24: common.Account.first_usage:type_name -> common.Timestamp 19, // 25: common.Account.insertion_date:type_name -> common.Timestamp 19, // 26: common.Account.updated_at:type_name -> common.Timestamp 35, // 27: common.Account.volumes:type_name -> common.AccountVolume 37, // 28: common.Target.account:type_name -> common.TargetAccount 3, // 29: common.MetadataFieldSchema.type:type_name -> common.MetadataType - 335, // 30: common.MetadataSchema.account_fields:type_name -> common.MetadataSchema.AccountFieldsEntry - 336, // 31: common.MetadataSchema.transaction_fields:type_name -> common.MetadataSchema.TransactionFieldsEntry - 337, // 32: common.MetadataSchema.ledger_fields:type_name -> common.MetadataSchema.LedgerFieldsEntry + 336, // 30: common.MetadataSchema.account_fields:type_name -> common.MetadataSchema.AccountFieldsEntry + 337, // 31: common.MetadataSchema.transaction_fields:type_name -> common.MetadataSchema.TransactionFieldsEntry + 338, // 32: common.MetadataSchema.ledger_fields:type_name -> common.MetadataSchema.LedgerFieldsEntry 2, // 33: common.SetMetadataFieldTypeCommand.target_type:type_name -> common.TargetType 3, // 34: common.SetMetadataFieldTypeCommand.type:type_name -> common.MetadataType 2, // 35: common.MetadataIndexID.target:type_name -> common.TargetType @@ -24407,7 +24475,7 @@ var file_common_proto_depIdxs = []int32{ 43, // 40: common.Index.id:type_name -> common.IndexID 19, // 41: common.Index.created_at:type_name -> common.Timestamp 47, // 42: common.Log.payload:type_name -> common.LogPayload - 348, // 43: common.Log.response_signature:type_name -> signature.SignedLog + 349, // 43: common.Log.response_signature:type_name -> signature.SignedLog 83, // 44: common.LogPayload.create_ledger:type_name -> common.CreatedLedgerLog 84, // 45: common.LogPayload.delete_ledger:type_name -> common.DeletedLedgerLog 85, // 46: common.LogPayload.apply:type_name -> common.ApplyLedgerLog @@ -24445,367 +24513,368 @@ var file_common_proto_depIdxs = []int32{ 57, // 78: common.ClusterConfig.bloom_prepared_queries:type_name -> common.BloomTypeConfig 57, // 79: common.ClusterConfig.bloom_indexes:type_name -> common.BloomTypeConfig 58, // 80: common.PersistedClusterState.config:type_name -> common.ClusterConfig - 312, // 81: common.CreatedPreparedQueryLog.query:type_name -> common.PreparedQuery - 292, // 82: common.UpdatedPreparedQueryLog.previous_filter:type_name -> common.QueryFilter - 292, // 83: common.UpdatedPreparedQueryLog.new_filter:type_name -> common.QueryFilter - 16, // 84: common.UpdatedPreparedQueryLog.previous_target:type_name -> common.QueryTarget - 16, // 85: common.UpdatedPreparedQueryLog.new_target:type_name -> common.QueryTarget - 338, // 86: common.SavedLedgerMetadataLog.metadata:type_name -> common.SavedLedgerMetadataLog.MetadataEntry - 19, // 87: common.NumscriptInfo.created_at:type_name -> common.Timestamp - 66, // 88: common.SavedNumscriptLog.info:type_name -> common.NumscriptInfo - 19, // 89: common.NumscriptVersionEntry.created_at:type_name -> common.Timestamp - 19, // 90: common.TemplateUsage.last_used:type_name -> common.Timestamp - 19, // 91: common.CreatedQueryCheckpointLog.created_at:type_name -> common.Timestamp - 77, // 92: common.SinkConfig.nats:type_name -> common.NatsSinkConfig - 78, // 93: common.SinkConfig.clickhouse:type_name -> common.ClickHouseSinkConfig - 79, // 94: common.SinkConfig.kafka:type_name -> common.KafkaSinkConfig - 80, // 95: common.SinkConfig.http:type_name -> common.HttpSinkConfig - 81, // 96: common.SinkConfig.databricks:type_name -> common.DatabricksSinkConfig - 8, // 97: common.SinkConfig.event_types:type_name -> common.EventType - 76, // 98: common.SinkStatus.error:type_name -> common.SinkError - 19, // 99: common.SinkError.occurred_at:type_name -> common.Timestamp - 82, // 100: common.DatabricksSinkConfig.oauth_m2m:type_name -> common.DatabricksOAuthM2M - 19, // 101: common.CreatedLedgerLog.created_at:type_name -> common.Timestamp - 40, // 102: common.CreatedLedgerLog.metadata_schema:type_name -> common.MetadataSchema - 9, // 103: common.CreatedLedgerLog.mode:type_name -> common.LedgerMode - 249, // 104: common.CreatedLedgerLog.mirror_source:type_name -> common.MirrorSourceConfig - 339, // 105: common.CreatedLedgerLog.account_types:type_name -> common.CreatedLedgerLog.AccountTypesEntry - 12, // 106: common.CreatedLedgerLog.default_enforcement_mode:type_name -> common.ChartEnforcementMode - 340, // 107: common.CreatedLedgerLog.metadata:type_name -> common.CreatedLedgerLog.MetadataEntry - 19, // 108: common.DeletedLedgerLog.deleted_at:type_name -> common.Timestamp - 86, // 109: common.ApplyLedgerLog.log:type_name -> common.LedgerLog - 88, // 110: common.LedgerLog.data:type_name -> common.LedgerLogPayload - 19, // 111: common.LedgerLog.date:type_name -> common.Timestamp - 87, // 112: common.LedgerLog.purged_volumes:type_name -> common.TouchedVolume - 87, // 113: common.LedgerLog.new_kept_volumes:type_name -> common.TouchedVolume - 87, // 114: common.LedgerLog.ephemeral_volumes:type_name -> common.TouchedVolume - 243, // 115: common.LedgerLogPayload.created_transaction:type_name -> common.CreatedTransaction - 244, // 116: common.LedgerLogPayload.reverted_transaction:type_name -> common.RevertedTransaction - 245, // 117: common.LedgerLogPayload.saved_metadata:type_name -> common.SavedMetadata - 246, // 118: common.LedgerLogPayload.deleted_metadata:type_name -> common.DeletedMetadata - 247, // 119: common.LedgerLogPayload.set_metadata_field_type:type_name -> common.SetMetadataFieldTypeLog - 248, // 120: common.LedgerLogPayload.removed_metadata_field_type:type_name -> common.RemovedMetadataFieldTypeLog - 242, // 121: common.LedgerLogPayload.fill_gap:type_name -> common.FilledGapLog - 240, // 122: common.LedgerLogPayload.create_index:type_name -> common.CreatedIndexLog - 241, // 123: common.LedgerLogPayload.drop_index:type_name -> common.DroppedIndexLog - 289, // 124: common.LedgerLogPayload.added_account_type:type_name -> common.AddedAccountTypeLog - 290, // 125: common.LedgerLogPayload.removed_account_type:type_name -> common.RemovedAccountTypeLog - 291, // 126: common.LedgerLogPayload.updated_default_enforcement_mode:type_name -> common.UpdatedDefaultEnforcementModeLog - 239, // 127: common.LedgerLogPayload.order_skipped:type_name -> common.OrderSkippedLog - 90, // 128: common.FailureDetails.account_address_empty:type_name -> common.AccountAddressEmptyDetails - 91, // 129: common.FailureDetails.account_address_empty_segment:type_name -> common.AccountAddressEmptySegmentDetails - 92, // 130: common.FailureDetails.account_address_invalid_char:type_name -> common.AccountAddressInvalidCharDetails - 93, // 131: common.FailureDetails.account_address_too_long:type_name -> common.AccountAddressTooLongDetails - 94, // 132: common.FailureDetails.account_not_matching_type:type_name -> common.AccountNotMatchingTypeDetails - 95, // 133: common.FailureDetails.account_type_already_exists:type_name -> common.AccountTypeAlreadyExistsDetails - 96, // 134: common.FailureDetails.account_type_conflict:type_name -> common.AccountTypeConflictDetails - 97, // 135: common.FailureDetails.account_type_has_accounts:type_name -> common.AccountTypeHasAccountsDetails - 98, // 136: common.FailureDetails.account_type_name_too_long:type_name -> common.AccountTypeNameTooLongDetails - 99, // 137: common.FailureDetails.account_type_not_found:type_name -> common.AccountTypeNotFoundDetails - 100, // 138: common.FailureDetails.account_type_pattern_too_long:type_name -> common.AccountTypePatternTooLongDetails - 101, // 139: common.FailureDetails.asset_invalid:type_name -> common.AssetInvalidDetails - 102, // 140: common.FailureDetails.balance_not_found:type_name -> common.BalanceNotFoundDetails - 103, // 141: common.FailureDetails.checkpoint_id_required:type_name -> common.CheckpointIdRequiredDetails - 104, // 142: common.FailureDetails.checkpoint_limit_reached:type_name -> common.CheckpointLimitReachedDetails - 105, // 143: common.FailureDetails.checkpoint_not_found:type_name -> common.CheckpointNotFoundDetails - 106, // 144: common.FailureDetails.color_invalid:type_name -> common.ColorInvalidDetails - 107, // 145: common.FailureDetails.color_too_long:type_name -> common.ColorTooLongDetails - 108, // 146: common.FailureDetails.empty_transaction:type_name -> common.EmptyTransactionDetails - 109, // 147: common.FailureDetails.idempotency_key_conflict:type_name -> common.IdempotencyKeyConflictDetails - 110, // 148: common.FailureDetails.id_required:type_name -> common.IdRequiredDetails - 111, // 149: common.FailureDetails.index_already_exists:type_name -> common.IndexAlreadyExistsDetails - 112, // 150: common.FailureDetails.index_not_found:type_name -> common.IndexNotFoundDetails - 113, // 151: common.FailureDetails.index_target_unsupported:type_name -> common.IndexTargetUnsupportedDetails - 114, // 152: common.FailureDetails.insufficient_funds:type_name -> common.InsufficientFundsDetails - 116, // 153: common.FailureDetails.invalid_cron_expression:type_name -> common.InvalidCronExpressionDetails - 117, // 154: common.FailureDetails.invalid_pattern:type_name -> common.InvalidPatternDetails - 118, // 155: common.FailureDetails.invalid_skippable_reason:type_name -> common.InvalidSkippableReasonDetails - 119, // 156: common.FailureDetails.ledger_already_exists:type_name -> common.LedgerAlreadyExistsDetails - 120, // 157: common.FailureDetails.ledger_deleted:type_name -> common.LedgerDeletedDetails - 121, // 158: common.FailureDetails.ledger_in_mirror_mode:type_name -> common.LedgerInMirrorModeDetails - 122, // 159: common.FailureDetails.ledger_name_invalid_char:type_name -> common.LedgerNameInvalidCharDetails - 123, // 160: common.FailureDetails.ledger_name_required:type_name -> common.LedgerNameRequiredDetails - 124, // 161: common.FailureDetails.ledger_name_reserved_prefix:type_name -> common.LedgerNameReservedPrefixDetails - 125, // 162: common.FailureDetails.ledger_name_too_long:type_name -> common.LedgerNameTooLongDetails - 126, // 163: common.FailureDetails.ledger_not_found:type_name -> common.LedgerNotFoundDetails - 127, // 164: common.FailureDetails.ledger_not_in_mirror_mode:type_name -> common.LedgerNotInMirrorModeDetails - 128, // 165: common.FailureDetails.metadata_field_not_in_schema:type_name -> common.MetadataFieldNotInSchemaDetails - 129, // 166: common.FailureDetails.metadata_key_empty:type_name -> common.MetadataKeyEmptyDetails - 131, // 167: common.FailureDetails.metadata_key_invalid_char:type_name -> common.MetadataKeyInvalidCharDetails - 133, // 168: common.FailureDetails.metadata_key_required:type_name -> common.MetadataKeyRequiredDetails - 134, // 169: common.FailureDetails.metadata_limit_exceeded:type_name -> common.MetadataLimitExceededDetails - 138, // 170: common.FailureDetails.metadata_not_found:type_name -> common.MetadataNotFoundDetails - 139, // 171: common.FailureDetails.metadata_value_contains_null_byte:type_name -> common.MetadataValueContainsNullByteDetails - 141, // 172: common.FailureDetails.mirror_http_url_invalid:type_name -> common.MirrorHttpUrlInvalidDetails - 142, // 173: common.FailureDetails.mirror_iam_region_required:type_name -> common.MirrorIamRegionRequiredDetails - 143, // 174: common.FailureDetails.mirror_iam_requires_tls:type_name -> common.MirrorIamRequiresTlsDetails - 144, // 175: common.FailureDetails.mirror_rewrite_rule_invalid:type_name -> common.MirrorRewriteRuleInvalidDetails - 146, // 176: common.FailureDetails.numscript_asset_mismatch:type_name -> common.NumscriptAssetMismatchDetails - 147, // 177: common.FailureDetails.numscript_bad_arity:type_name -> common.NumscriptBadArityDetails - 148, // 178: common.FailureDetails.numscript_bad_meta_value:type_name -> common.NumscriptBadMetaValueDetails - 149, // 179: common.FailureDetails.numscript_bad_portion:type_name -> common.NumscriptBadPortionDetails - 150, // 180: common.FailureDetails.numscript_cannot_cast_scoped_account:type_name -> common.NumscriptCannotCastScopedAccountDetails - 151, // 181: common.FailureDetails.numscript_cannot_cast_to_string:type_name -> common.NumscriptCannotCastToStringDetails - 152, // 182: common.FailureDetails.numscript_cannot_store_scoped_account:type_name -> common.NumscriptCannotStoreScopedAccountDetails - 153, // 183: common.FailureDetails.numscript_compile_bad_arity:type_name -> common.NumscriptCompileBadArityDetails - 154, // 184: common.FailureDetails.numscript_compile_cannot_cast_to_string:type_name -> common.NumscriptCompileCannotCastToStringDetails - 155, // 185: common.FailureDetails.numscript_compile_cannot_store_scoped_account:type_name -> common.NumscriptCompileCannotStoreScopedAccountDetails - 156, // 186: common.FailureDetails.numscript_compile_duplicate_remaining:type_name -> common.NumscriptCompileDuplicateRemainingDetails - 157, // 187: common.FailureDetails.numscript_compile_duplicate_variable:type_name -> common.NumscriptCompileDuplicateVariableDetails - 158, // 188: common.FailureDetails.numscript_compile_experimental_feature:type_name -> common.NumscriptCompileExperimentalFeatureDetails - 159, // 189: common.FailureDetails.numscript_compile_invalid_feature:type_name -> common.NumscriptCompileInvalidFeatureDetails - 160, // 190: common.FailureDetails.numscript_compile_invalid_meta_position:type_name -> common.NumscriptCompileInvalidMetaPositionDetails - 161, // 191: common.FailureDetails.numscript_compile_invalid_type:type_name -> common.NumscriptCompileInvalidTypeDetails - 162, // 192: common.FailureDetails.numscript_compile_invalid_uncapped_source:type_name -> common.NumscriptCompileInvalidUncappedSourceDetails - 163, // 193: common.FailureDetails.numscript_compile_invalid_variable_value:type_name -> common.NumscriptCompileInvalidVariableValueDetails - 164, // 194: common.FailureDetails.numscript_compile_missing_variable:type_name -> common.NumscriptCompileMissingVariableDetails - 165, // 195: common.FailureDetails.numscript_compile_type_mismatch:type_name -> common.NumscriptCompileTypeMismatchDetails - 166, // 196: common.FailureDetails.numscript_compile_unbound_variable:type_name -> common.NumscriptCompileUnboundVariableDetails - 167, // 197: common.FailureDetails.numscript_compile_unknown_function:type_name -> common.NumscriptCompileUnknownFunctionDetails - 168, // 198: common.FailureDetails.numscript_content_required:type_name -> common.NumscriptContentRequiredDetails - 169, // 199: common.FailureDetails.numscript_currency_mismatch:type_name -> common.NumscriptCurrencyMismatchDetails - 170, // 200: common.FailureDetails.numscript_divide_by_zero:type_name -> common.NumscriptDivideByZeroDetails - 172, // 201: common.FailureDetails.numscript_experimental_feature:type_name -> common.NumscriptExperimentalFeatureDetails - 173, // 202: common.FailureDetails.numscript_invalid_account_name:type_name -> common.NumscriptInvalidAccountNameDetails - 174, // 203: common.FailureDetails.numscript_invalid_allotment_in_send_all:type_name -> common.NumscriptInvalidAllotmentInSendAllDetails - 175, // 204: common.FailureDetails.numscript_invalid_allotment_sum:type_name -> common.NumscriptInvalidAllotmentSumDetails - 176, // 205: common.FailureDetails.numscript_invalid_asset:type_name -> common.NumscriptInvalidAssetDetails - 177, // 206: common.FailureDetails.numscript_invalid_color:type_name -> common.NumscriptInvalidColorDetails - 178, // 207: common.FailureDetails.numscript_invalid_feature:type_name -> common.NumscriptInvalidFeatureDetails - 179, // 208: common.FailureDetails.numscript_invalid_monetary_literal:type_name -> common.NumscriptInvalidMonetaryLiteralDetails - 180, // 209: common.FailureDetails.numscript_invalid_nested_meta:type_name -> common.NumscriptInvalidNestedMetaDetails - 181, // 210: common.FailureDetails.numscript_invalid_number_literal:type_name -> common.NumscriptInvalidNumberLiteralDetails - 182, // 211: common.FailureDetails.numscript_invalid_remaining_allotment:type_name -> common.NumscriptInvalidRemainingAllotmentDetails - 183, // 212: common.FailureDetails.numscript_invalid_scope:type_name -> common.NumscriptInvalidScopeDetails - 184, // 213: common.FailureDetails.numscript_invalid_type:type_name -> common.NumscriptInvalidTypeDetails - 185, // 214: common.FailureDetails.numscript_invalid_unbounded_scaling_address:type_name -> common.NumscriptInvalidUnboundedScalingAddressDetails - 186, // 215: common.FailureDetails.numscript_invalid_unbounded_send_all:type_name -> common.NumscriptInvalidUnboundedSendAllDetails - 187, // 216: common.FailureDetails.numscript_invalid_uncapped_source:type_name -> common.NumscriptInvalidUncappedSourceDetails - 188, // 217: common.FailureDetails.numscript_invalid_version:type_name -> common.NumscriptInvalidVersionDetails - 189, // 218: common.FailureDetails.numscript_metadata_not_found:type_name -> common.NumscriptMetadataNotFoundDetails - 191, // 219: common.FailureDetails.numscript_missing_funds:type_name -> common.NumscriptMissingFundsDetails - 192, // 220: common.FailureDetails.numscript_missing_variable:type_name -> common.NumscriptMissingVariableDetails - 193, // 221: common.FailureDetails.numscript_name_invalid_char:type_name -> common.NumscriptNameInvalidCharDetails - 194, // 222: common.FailureDetails.numscript_name_required:type_name -> common.NumscriptNameRequiredDetails - 195, // 223: common.FailureDetails.numscript_name_too_long:type_name -> common.NumscriptNameTooLongDetails - 196, // 224: common.FailureDetails.numscript_negative_amount:type_name -> common.NumscriptNegativeAmountDetails - 197, // 225: common.FailureDetails.numscript_negative_balance:type_name -> common.NumscriptNegativeBalanceDetails - 199, // 226: common.FailureDetails.numscript_negative_portion:type_name -> common.NumscriptNegativePortionDetails - 200, // 227: common.FailureDetails.numscript_not_found:type_name -> common.NumscriptNotFoundDetails - 202, // 228: common.FailureDetails.numscript_parse_error:type_name -> common.NumscriptParseErrorDetails - 203, // 229: common.FailureDetails.numscript_posting_amount_overflow:type_name -> common.NumscriptPostingAmountOverflowDetails - 204, // 230: common.FailureDetails.numscript_posting_negative_amount:type_name -> common.NumscriptPostingNegativeAmountDetails - 205, // 231: common.FailureDetails.numscript_scaling_unsupported:type_name -> common.NumscriptScalingUnsupportedDetails - 206, // 232: common.FailureDetails.numscript_type_error:type_name -> common.NumscriptTypeErrorDetails - 207, // 233: common.FailureDetails.numscript_unbound_function:type_name -> common.NumscriptUnboundFunctionDetails - 208, // 234: common.FailureDetails.numscript_unbound_variable:type_name -> common.NumscriptUnboundVariableDetails - 209, // 235: common.FailureDetails.numscript_version_already_exists:type_name -> common.NumscriptVersionAlreadyExistsDetails - 210, // 236: common.FailureDetails.postings_and_script_conflict:type_name -> common.PostingsAndScriptConflictDetails - 211, // 237: common.FailureDetails.prepared_query_already_exists:type_name -> common.PreparedQueryAlreadyExistsDetails - 212, // 238: common.FailureDetails.prepared_query_name_invalid_char:type_name -> common.PreparedQueryNameInvalidCharDetails - 213, // 239: common.FailureDetails.prepared_query_name_required:type_name -> common.PreparedQueryNameRequiredDetails - 214, // 240: common.FailureDetails.prepared_query_name_too_long:type_name -> common.PreparedQueryNameTooLongDetails - 215, // 241: common.FailureDetails.prepared_query_not_found:type_name -> common.PreparedQueryNotFoundDetails - 216, // 242: common.FailureDetails.prepared_query_required:type_name -> common.PreparedQueryRequiredDetails - 217, // 243: common.FailureDetails.prepared_query_target_unsupported:type_name -> common.PreparedQueryTargetUnsupportedDetails - 218, // 244: common.FailureDetails.revert_target_created_in_batch:type_name -> common.RevertTargetCreatedInBatchDetails - 219, // 245: common.FailureDetails.scoped_balance_unsupported:type_name -> common.ScopedBalanceUnsupportedDetails - 220, // 246: common.FailureDetails.script_and_reference_conflict:type_name -> common.ScriptAndReferenceConflictDetails - 221, // 247: common.FailureDetails.script_required:type_name -> common.ScriptRequiredDetails - 222, // 248: common.FailureDetails.signing_key_id_invalid_char:type_name -> common.SigningKeyIdInvalidCharDetails - 223, // 249: common.FailureDetails.signing_key_id_required:type_name -> common.SigningKeyIdRequiredDetails - 224, // 250: common.FailureDetails.signing_key_id_too_long:type_name -> common.SigningKeyIdTooLongDetails - 225, // 251: common.FailureDetails.signing_key_invalid_length:type_name -> common.SigningKeyInvalidLengthDetails - 226, // 252: common.FailureDetails.sink_already_exists:type_name -> common.SinkAlreadyExistsDetails - 227, // 253: common.FailureDetails.sink_batch_size_too_large:type_name -> common.SinkBatchSizeTooLargeDetails - 228, // 254: common.FailureDetails.sink_controller_mismatch:type_name -> common.SinkControllerMismatchDetails - 229, // 255: common.FailureDetails.sink_not_found:type_name -> common.SinkNotFoundDetails - 230, // 256: common.FailureDetails.target_required:type_name -> common.TargetRequiredDetails - 231, // 257: common.FailureDetails.transaction_already_reverted:type_name -> common.TransactionAlreadyRevertedDetails - 232, // 258: common.FailureDetails.transaction_not_found:type_name -> common.TransactionNotFoundDetails - 233, // 259: common.FailureDetails.transaction_reference_conflict:type_name -> common.TransactionReferenceConflictDetails - 234, // 260: common.FailureDetails.transaction_reference_not_found:type_name -> common.TransactionReferenceNotFoundDetails - 235, // 261: common.FailureDetails.transaction_reference_too_long:type_name -> common.TransactionReferenceTooLongDetails - 236, // 262: common.FailureDetails.transaction_target_missing:type_name -> common.TransactionTargetMissingDetails - 237, // 263: common.FailureDetails.transient_account_non_zero:type_name -> common.TransientAccountNonZeroDetails - 238, // 264: common.FailureDetails.volume_overflow:type_name -> common.VolumeOverflowDetails - 130, // 265: common.FailureDetails.account_metadata_key_empty:type_name -> common.AccountMetadataKeyEmptyDetails - 132, // 266: common.FailureDetails.account_metadata_key_invalid_char:type_name -> common.AccountMetadataKeyInvalidCharDetails - 140, // 267: common.FailureDetails.account_metadata_value_contains_null_byte:type_name -> common.AccountMetadataValueContainsNullByteDetails - 135, // 268: common.FailureDetails.account_metadata_limit_exceeded:type_name -> common.AccountMetadataLimitExceededDetails - 136, // 269: common.FailureDetails.metadata_key_limit_exceeded:type_name -> common.MetadataKeyLimitExceededDetails - 137, // 270: common.FailureDetails.account_metadata_key_limit_exceeded:type_name -> common.AccountMetadataKeyLimitExceededDetails - 115, // 271: common.FailureDetails.numscript_insufficient_funds:type_name -> common.NumscriptInsufficientFundsDetails - 171, // 272: common.FailureDetails.numscript_divide_by_zero_with_numerator:type_name -> common.NumscriptDivideByZeroWithNumeratorDetails - 190, // 273: common.FailureDetails.numscript_scoped_metadata_not_found:type_name -> common.NumscriptScopedMetadataNotFoundDetails - 198, // 274: common.FailureDetails.numscript_scoped_negative_balance:type_name -> common.NumscriptScopedNegativeBalanceDetails - 201, // 275: common.FailureDetails.numscript_version_not_found:type_name -> common.NumscriptVersionNotFoundDetails - 145, // 276: common.FailureDetails.mirror_asset_mapping_invalid:type_name -> common.MirrorAssetMappingInvalidDetails - 89, // 277: common.OrderSkippedLog.details:type_name -> common.FailureDetails - 43, // 278: common.CreatedIndexLog.id:type_name -> common.IndexID - 3, // 279: common.CreatedIndexLog.bound_type:type_name -> common.MetadataType - 43, // 280: common.DroppedIndexLog.id:type_name -> common.IndexID - 26, // 281: common.CreatedTransaction.transaction:type_name -> common.Transaction - 341, // 282: common.CreatedTransaction.account_metadata:type_name -> common.CreatedTransaction.AccountMetadataEntry - 26, // 283: common.RevertedTransaction.revert_transaction:type_name -> common.Transaction - 38, // 284: common.SavedMetadata.target:type_name -> common.Target - 342, // 285: common.SavedMetadata.metadata:type_name -> common.SavedMetadata.MetadataEntry - 38, // 286: common.DeletedMetadata.target:type_name -> common.Target - 2, // 287: common.SetMetadataFieldTypeLog.target_type:type_name -> common.TargetType - 3, // 288: common.SetMetadataFieldTypeLog.type:type_name -> common.MetadataType - 2, // 289: common.RemovedMetadataFieldTypeLog.target_type:type_name -> common.TargetType - 43, // 290: common.RemovedMetadataFieldTypeLog.dropped_index:type_name -> common.IndexID - 270, // 291: common.MirrorSourceConfig.http:type_name -> common.HttpMirrorSourceConfig - 272, // 292: common.MirrorSourceConfig.postgres:type_name -> common.PostgresMirrorSourceConfig - 251, // 293: common.MirrorSourceConfig.rewrite_rules:type_name -> common.MirrorRewriteRule - 250, // 294: common.MirrorSourceConfig.asset_mappings:type_name -> common.MirrorAssetMapping - 252, // 295: common.MirrorRewriteRule.created_transaction:type_name -> common.CreatedTransactionRule - 253, // 296: common.MirrorRewriteRule.reverted_transaction:type_name -> common.RevertedTransactionRule - 254, // 297: common.MirrorRewriteRule.saved_metadata:type_name -> common.SavedMetadataRule - 255, // 298: common.MirrorRewriteRule.deleted_metadata:type_name -> common.DeletedMetadataRule - 256, // 299: common.MirrorRewriteRule.any_variant:type_name -> common.AnyVariantRule - 257, // 300: common.CreatedTransactionRule.actions:type_name -> common.CreatedTransactionAction - 258, // 301: common.RevertedTransactionRule.actions:type_name -> common.RevertedTransactionAction - 259, // 302: common.SavedMetadataRule.actions:type_name -> common.SavedMetadataAction - 260, // 303: common.DeletedMetadataRule.actions:type_name -> common.DeletedMetadataAction - 261, // 304: common.AnyVariantRule.actions:type_name -> common.AnyVariantAction - 262, // 305: common.CreatedTransactionAction.rewrite_address:type_name -> common.RewriteAddressAction - 263, // 306: common.CreatedTransactionAction.set_metadata:type_name -> common.SetMetadataAction - 264, // 307: common.CreatedTransactionAction.delete_metadata:type_name -> common.DeleteMetadataAction - 265, // 308: common.CreatedTransactionAction.set_account_metadata:type_name -> common.SetAccountMetadataAction - 266, // 309: common.CreatedTransactionAction.delete_account_metadata:type_name -> common.DeleteAccountMetadataAction - 267, // 310: common.CreatedTransactionAction.set_account_metadata_from_address:type_name -> common.SetAccountMetadataFromAddressAction - 269, // 311: common.CreatedTransactionAction.drop:type_name -> common.DropAction - 262, // 312: common.RevertedTransactionAction.rewrite_address:type_name -> common.RewriteAddressAction - 263, // 313: common.RevertedTransactionAction.set_metadata:type_name -> common.SetMetadataAction - 264, // 314: common.RevertedTransactionAction.delete_metadata:type_name -> common.DeleteMetadataAction - 269, // 315: common.RevertedTransactionAction.drop:type_name -> common.DropAction - 262, // 316: common.SavedMetadataAction.rewrite_address:type_name -> common.RewriteAddressAction - 263, // 317: common.SavedMetadataAction.set_metadata:type_name -> common.SetMetadataAction - 264, // 318: common.SavedMetadataAction.delete_metadata:type_name -> common.DeleteMetadataAction - 269, // 319: common.SavedMetadataAction.drop:type_name -> common.DropAction - 262, // 320: common.DeletedMetadataAction.rewrite_address:type_name -> common.RewriteAddressAction - 269, // 321: common.DeletedMetadataAction.drop:type_name -> common.DropAction - 262, // 322: common.AnyVariantAction.rewrite_address:type_name -> common.RewriteAddressAction - 269, // 323: common.AnyVariantAction.drop:type_name -> common.DropAction - 268, // 324: common.SetAccountMetadataFromAddressAction.replacements:type_name -> common.SetAccountMetadataFromAddressReplacement - 271, // 325: common.HttpMirrorSourceConfig.oauth2_client_credentials:type_name -> common.OAuth2ClientCredentials - 273, // 326: common.PostgresMirrorSourceConfig.aws_iam_auth:type_name -> common.PostgresAwsIamAuth - 19, // 327: common.MirrorSyncError.occurred_at:type_name -> common.Timestamp - 10, // 328: common.MirrorSyncProgress.state:type_name -> common.MirrorSyncState - 274, // 329: common.MirrorSyncProgress.error:type_name -> common.MirrorSyncError - 19, // 330: common.LedgerInfo.created_at:type_name -> common.Timestamp - 19, // 331: common.LedgerInfo.deleted_at:type_name -> common.Timestamp - 40, // 332: common.LedgerInfo.metadata_schema:type_name -> common.MetadataSchema - 9, // 333: common.LedgerInfo.mode:type_name -> common.LedgerMode - 249, // 334: common.LedgerInfo.mirror_source:type_name -> common.MirrorSourceConfig - 275, // 335: common.LedgerInfo.mirror_sync_progress:type_name -> common.MirrorSyncProgress - 343, // 336: common.LedgerInfo.account_types:type_name -> common.LedgerInfo.AccountTypesEntry - 12, // 337: common.LedgerInfo.default_enforcement_mode:type_name -> common.ChartEnforcementMode - 344, // 338: common.LedgerInfo.metadata:type_name -> common.LedgerInfo.MetadataEntry - 38, // 339: common.SaveMetadataCommand.target:type_name -> common.Target - 345, // 340: common.SaveMetadataCommand.metadata:type_name -> common.SaveMetadataCommand.MetadataEntry - 38, // 341: common.DeleteMetadataCommand.target:type_name -> common.Target - 346, // 342: common.TransactionState.metadata:type_name -> common.TransactionState.MetadataEntry - 19, // 343: common.TransactionState.timestamp:type_name -> common.Timestamp - 25, // 344: common.TransactionState.postings:type_name -> common.Posting - 19, // 345: common.TransactionState.reverted_at:type_name -> common.Timestamp - 281, // 346: common.IdempotencyKeyValue.failure:type_name -> common.IdempotencyFailure - 89, // 347: common.IdempotencyFailure.details:type_name -> common.FailureDetails - 285, // 348: common.SegmentType.uuid:type_name -> common.UUIDConstraint - 286, // 349: common.SegmentType.uint64:type_name -> common.Uint64Constraint - 287, // 350: common.SegmentType.bytes:type_name -> common.BytesConstraint - 13, // 351: common.AccountType.persistence:type_name -> common.AccountTypePersistence - 347, // 352: common.AccountType.segment_types:type_name -> common.AccountType.SegmentTypesEntry - 288, // 353: common.AddedAccountTypeLog.account_type:type_name -> common.AccountType - 12, // 354: common.UpdatedDefaultEnforcementModeLog.enforcement_mode:type_name -> common.ChartEnforcementMode - 305, // 355: common.QueryFilter.field:type_name -> common.FieldCondition - 311, // 356: common.QueryFilter.address:type_name -> common.AddressMatch - 301, // 357: common.QueryFilter.and:type_name -> common.AndFilter - 302, // 358: common.QueryFilter.or:type_name -> common.OrFilter - 303, // 359: common.QueryFilter.not:type_name -> common.NotFilter - 293, // 360: common.QueryFilter.reference:type_name -> common.ReferenceCondition - 298, // 361: common.QueryFilter.builtin_uint:type_name -> common.BuiltinUintCondition - 296, // 362: common.QueryFilter.ledger:type_name -> common.LedgerCondition - 297, // 363: common.QueryFilter.log_id:type_name -> common.LogIdCondition - 299, // 364: common.QueryFilter.log_builtin_uint:type_name -> common.LogBuiltinUintCondition - 300, // 365: common.QueryFilter.account_has_asset:type_name -> common.AccountHasAssetCondition - 294, // 366: common.QueryFilter.reverted:type_name -> common.RevertedCondition - 295, // 367: common.QueryFilter.audit:type_name -> common.AuditCondition - 306, // 368: common.ReferenceCondition.cond:type_name -> common.StringCondition - 14, // 369: common.AuditCondition.field:type_name -> common.AuditField - 306, // 370: common.AuditCondition.string_cond:type_name -> common.StringCondition - 308, // 371: common.AuditCondition.uint_cond:type_name -> common.UintCondition - 306, // 372: common.LedgerCondition.cond:type_name -> common.StringCondition - 308, // 373: common.LogIdCondition.cond:type_name -> common.UintCondition - 4, // 374: common.BuiltinUintCondition.field:type_name -> common.TransactionBuiltinIndex - 308, // 375: common.BuiltinUintCondition.cond:type_name -> common.UintCondition - 6, // 376: common.LogBuiltinUintCondition.field:type_name -> common.LogBuiltinIndex - 308, // 377: common.LogBuiltinUintCondition.cond:type_name -> common.UintCondition - 292, // 378: common.AndFilter.filters:type_name -> common.QueryFilter - 292, // 379: common.OrFilter.filters:type_name -> common.QueryFilter - 292, // 380: common.NotFilter.filter:type_name -> common.QueryFilter - 304, // 381: common.FieldCondition.field:type_name -> common.FieldRef - 306, // 382: common.FieldCondition.string_cond:type_name -> common.StringCondition - 307, // 383: common.FieldCondition.int_cond:type_name -> common.IntCondition - 308, // 384: common.FieldCondition.uint_cond:type_name -> common.UintCondition - 309, // 385: common.FieldCondition.bool_cond:type_name -> common.BoolCondition - 310, // 386: common.FieldCondition.exists_cond:type_name -> common.ExistsCondition - 15, // 387: common.AddressMatch.role:type_name -> common.AddressRole - 292, // 388: common.PreparedQuery.filter:type_name -> common.QueryFilter - 16, // 389: common.PreparedQuery.target:type_name -> common.QueryTarget - 24, // 390: common.AggregatedVolume.input:type_name -> common.Uint256 - 24, // 391: common.AggregatedVolume.output:type_name -> common.Uint256 - 313, // 392: common.AggregateResult.volumes:type_name -> common.AggregatedVolume - 315, // 393: common.AggregateResult.groups:type_name -> common.GroupedAggregateResult - 313, // 394: common.GroupedAggregateResult.volumes:type_name -> common.AggregatedVolume - 36, // 395: common.PreparedQueryCursor.account_data:type_name -> common.Account - 26, // 396: common.PreparedQueryCursor.transaction_data:type_name -> common.Transaction - 46, // 397: common.PreparedQueryCursor.log_data:type_name -> common.Log - 319, // 398: common.AuthenticatedCaller.identity:type_name -> common.CallerIdentity - 320, // 399: common.CallerSnapshot.authenticated:type_name -> common.AuthenticatedCaller - 321, // 400: common.CallerSnapshot.anonymous:type_name -> common.AnonymousCaller - 322, // 401: common.CallerSnapshot.system:type_name -> common.SystemCaller - 323, // 402: common.CallerSnapshot.auth_disabled:type_name -> common.AuthDisabledCaller - 325, // 403: common.BackupStorage.s3:type_name -> common.S3StorageConfig - 326, // 404: common.BackupStorage.azure:type_name -> common.AzureStorageConfig - 328, // 405: common.ListOptions.read:type_name -> common.ReadOptions - 292, // 406: common.ListOptions.filter:type_name -> common.QueryFilter - 21, // 407: common.MetadataMap.ValuesEntry.value:type_name -> common.MetadataValue - 21, // 408: common.Transaction.MetadataEntry.value:type_name -> common.MetadataValue - 32, // 409: common.PostCommitVolumes.VolumesByAccountEntry.value:type_name -> common.VolumesByAssets - 21, // 410: common.Account.MetadataEntry.value:type_name -> common.MetadataValue - 39, // 411: common.MetadataSchema.AccountFieldsEntry.value:type_name -> common.MetadataFieldSchema - 39, // 412: common.MetadataSchema.TransactionFieldsEntry.value:type_name -> common.MetadataFieldSchema - 39, // 413: common.MetadataSchema.LedgerFieldsEntry.value:type_name -> common.MetadataFieldSchema - 21, // 414: common.SavedLedgerMetadataLog.MetadataEntry.value:type_name -> common.MetadataValue - 288, // 415: common.CreatedLedgerLog.AccountTypesEntry.value:type_name -> common.AccountType - 21, // 416: common.CreatedLedgerLog.MetadataEntry.value:type_name -> common.MetadataValue - 22, // 417: common.CreatedTransaction.AccountMetadataEntry.value:type_name -> common.MetadataMap - 21, // 418: common.SavedMetadata.MetadataEntry.value:type_name -> common.MetadataValue - 288, // 419: common.LedgerInfo.AccountTypesEntry.value:type_name -> common.AccountType - 21, // 420: common.LedgerInfo.MetadataEntry.value:type_name -> common.MetadataValue - 21, // 421: common.SaveMetadataCommand.MetadataEntry.value:type_name -> common.MetadataValue - 21, // 422: common.TransactionState.MetadataEntry.value:type_name -> common.MetadataValue - 284, // 423: common.AccountType.SegmentTypesEntry.value:type_name -> common.SegmentType - 349, // 424: common.auth_policy:extendee -> google.protobuf.MethodOptions - 350, // 425: common.ledger_json:extendee -> google.protobuf.MessageOptions - 351, // 426: common.ledger_json_default:extendee -> google.protobuf.FileOptions - 350, // 427: common.ledger_json_manual:extendee -> google.protobuf.MessageOptions - 352, // 428: common.allowed_query_targets:extendee -> google.protobuf.FieldOptions - 352, // 429: common.valid_on_no_query_target:extendee -> google.protobuf.FieldOptions - 352, // 430: common.ledger_log_is_history:extendee -> google.protobuf.FieldOptions - 352, // 431: common.sensitive:extendee -> google.protobuf.FieldOptions - 352, // 432: common.sensitive_url:extendee -> google.protobuf.FieldOptions - 352, // 433: common.failure_reason:extendee -> google.protobuf.FieldOptions - 18, // 434: common.auth_policy:type_name -> common.MethodAuthPolicy - 16, // 435: common.allowed_query_targets:type_name -> common.QueryTarget - 11, // 436: common.failure_reason:type_name -> common.ErrorReason - 437, // [437:437] is the sub-list for method output_type - 437, // [437:437] is the sub-list for method input_type - 434, // [434:437] is the sub-list for extension type_name - 424, // [424:434] is the sub-list for extension extendee - 0, // [0:424] is the sub-list for field type_name + 330, // 81: common.ClusterPolicy.capability_activations:type_name -> common.CapabilityActivation + 312, // 82: common.CreatedPreparedQueryLog.query:type_name -> common.PreparedQuery + 292, // 83: common.UpdatedPreparedQueryLog.previous_filter:type_name -> common.QueryFilter + 292, // 84: common.UpdatedPreparedQueryLog.new_filter:type_name -> common.QueryFilter + 16, // 85: common.UpdatedPreparedQueryLog.previous_target:type_name -> common.QueryTarget + 16, // 86: common.UpdatedPreparedQueryLog.new_target:type_name -> common.QueryTarget + 339, // 87: common.SavedLedgerMetadataLog.metadata:type_name -> common.SavedLedgerMetadataLog.MetadataEntry + 19, // 88: common.NumscriptInfo.created_at:type_name -> common.Timestamp + 66, // 89: common.SavedNumscriptLog.info:type_name -> common.NumscriptInfo + 19, // 90: common.NumscriptVersionEntry.created_at:type_name -> common.Timestamp + 19, // 91: common.TemplateUsage.last_used:type_name -> common.Timestamp + 19, // 92: common.CreatedQueryCheckpointLog.created_at:type_name -> common.Timestamp + 77, // 93: common.SinkConfig.nats:type_name -> common.NatsSinkConfig + 78, // 94: common.SinkConfig.clickhouse:type_name -> common.ClickHouseSinkConfig + 79, // 95: common.SinkConfig.kafka:type_name -> common.KafkaSinkConfig + 80, // 96: common.SinkConfig.http:type_name -> common.HttpSinkConfig + 81, // 97: common.SinkConfig.databricks:type_name -> common.DatabricksSinkConfig + 8, // 98: common.SinkConfig.event_types:type_name -> common.EventType + 76, // 99: common.SinkStatus.error:type_name -> common.SinkError + 19, // 100: common.SinkError.occurred_at:type_name -> common.Timestamp + 82, // 101: common.DatabricksSinkConfig.oauth_m2m:type_name -> common.DatabricksOAuthM2M + 19, // 102: common.CreatedLedgerLog.created_at:type_name -> common.Timestamp + 40, // 103: common.CreatedLedgerLog.metadata_schema:type_name -> common.MetadataSchema + 9, // 104: common.CreatedLedgerLog.mode:type_name -> common.LedgerMode + 249, // 105: common.CreatedLedgerLog.mirror_source:type_name -> common.MirrorSourceConfig + 340, // 106: common.CreatedLedgerLog.account_types:type_name -> common.CreatedLedgerLog.AccountTypesEntry + 12, // 107: common.CreatedLedgerLog.default_enforcement_mode:type_name -> common.ChartEnforcementMode + 341, // 108: common.CreatedLedgerLog.metadata:type_name -> common.CreatedLedgerLog.MetadataEntry + 19, // 109: common.DeletedLedgerLog.deleted_at:type_name -> common.Timestamp + 86, // 110: common.ApplyLedgerLog.log:type_name -> common.LedgerLog + 88, // 111: common.LedgerLog.data:type_name -> common.LedgerLogPayload + 19, // 112: common.LedgerLog.date:type_name -> common.Timestamp + 87, // 113: common.LedgerLog.purged_volumes:type_name -> common.TouchedVolume + 87, // 114: common.LedgerLog.new_kept_volumes:type_name -> common.TouchedVolume + 87, // 115: common.LedgerLog.ephemeral_volumes:type_name -> common.TouchedVolume + 243, // 116: common.LedgerLogPayload.created_transaction:type_name -> common.CreatedTransaction + 244, // 117: common.LedgerLogPayload.reverted_transaction:type_name -> common.RevertedTransaction + 245, // 118: common.LedgerLogPayload.saved_metadata:type_name -> common.SavedMetadata + 246, // 119: common.LedgerLogPayload.deleted_metadata:type_name -> common.DeletedMetadata + 247, // 120: common.LedgerLogPayload.set_metadata_field_type:type_name -> common.SetMetadataFieldTypeLog + 248, // 121: common.LedgerLogPayload.removed_metadata_field_type:type_name -> common.RemovedMetadataFieldTypeLog + 242, // 122: common.LedgerLogPayload.fill_gap:type_name -> common.FilledGapLog + 240, // 123: common.LedgerLogPayload.create_index:type_name -> common.CreatedIndexLog + 241, // 124: common.LedgerLogPayload.drop_index:type_name -> common.DroppedIndexLog + 289, // 125: common.LedgerLogPayload.added_account_type:type_name -> common.AddedAccountTypeLog + 290, // 126: common.LedgerLogPayload.removed_account_type:type_name -> common.RemovedAccountTypeLog + 291, // 127: common.LedgerLogPayload.updated_default_enforcement_mode:type_name -> common.UpdatedDefaultEnforcementModeLog + 239, // 128: common.LedgerLogPayload.order_skipped:type_name -> common.OrderSkippedLog + 90, // 129: common.FailureDetails.account_address_empty:type_name -> common.AccountAddressEmptyDetails + 91, // 130: common.FailureDetails.account_address_empty_segment:type_name -> common.AccountAddressEmptySegmentDetails + 92, // 131: common.FailureDetails.account_address_invalid_char:type_name -> common.AccountAddressInvalidCharDetails + 93, // 132: common.FailureDetails.account_address_too_long:type_name -> common.AccountAddressTooLongDetails + 94, // 133: common.FailureDetails.account_not_matching_type:type_name -> common.AccountNotMatchingTypeDetails + 95, // 134: common.FailureDetails.account_type_already_exists:type_name -> common.AccountTypeAlreadyExistsDetails + 96, // 135: common.FailureDetails.account_type_conflict:type_name -> common.AccountTypeConflictDetails + 97, // 136: common.FailureDetails.account_type_has_accounts:type_name -> common.AccountTypeHasAccountsDetails + 98, // 137: common.FailureDetails.account_type_name_too_long:type_name -> common.AccountTypeNameTooLongDetails + 99, // 138: common.FailureDetails.account_type_not_found:type_name -> common.AccountTypeNotFoundDetails + 100, // 139: common.FailureDetails.account_type_pattern_too_long:type_name -> common.AccountTypePatternTooLongDetails + 101, // 140: common.FailureDetails.asset_invalid:type_name -> common.AssetInvalidDetails + 102, // 141: common.FailureDetails.balance_not_found:type_name -> common.BalanceNotFoundDetails + 103, // 142: common.FailureDetails.checkpoint_id_required:type_name -> common.CheckpointIdRequiredDetails + 104, // 143: common.FailureDetails.checkpoint_limit_reached:type_name -> common.CheckpointLimitReachedDetails + 105, // 144: common.FailureDetails.checkpoint_not_found:type_name -> common.CheckpointNotFoundDetails + 106, // 145: common.FailureDetails.color_invalid:type_name -> common.ColorInvalidDetails + 107, // 146: common.FailureDetails.color_too_long:type_name -> common.ColorTooLongDetails + 108, // 147: common.FailureDetails.empty_transaction:type_name -> common.EmptyTransactionDetails + 109, // 148: common.FailureDetails.idempotency_key_conflict:type_name -> common.IdempotencyKeyConflictDetails + 110, // 149: common.FailureDetails.id_required:type_name -> common.IdRequiredDetails + 111, // 150: common.FailureDetails.index_already_exists:type_name -> common.IndexAlreadyExistsDetails + 112, // 151: common.FailureDetails.index_not_found:type_name -> common.IndexNotFoundDetails + 113, // 152: common.FailureDetails.index_target_unsupported:type_name -> common.IndexTargetUnsupportedDetails + 114, // 153: common.FailureDetails.insufficient_funds:type_name -> common.InsufficientFundsDetails + 116, // 154: common.FailureDetails.invalid_cron_expression:type_name -> common.InvalidCronExpressionDetails + 117, // 155: common.FailureDetails.invalid_pattern:type_name -> common.InvalidPatternDetails + 118, // 156: common.FailureDetails.invalid_skippable_reason:type_name -> common.InvalidSkippableReasonDetails + 119, // 157: common.FailureDetails.ledger_already_exists:type_name -> common.LedgerAlreadyExistsDetails + 120, // 158: common.FailureDetails.ledger_deleted:type_name -> common.LedgerDeletedDetails + 121, // 159: common.FailureDetails.ledger_in_mirror_mode:type_name -> common.LedgerInMirrorModeDetails + 122, // 160: common.FailureDetails.ledger_name_invalid_char:type_name -> common.LedgerNameInvalidCharDetails + 123, // 161: common.FailureDetails.ledger_name_required:type_name -> common.LedgerNameRequiredDetails + 124, // 162: common.FailureDetails.ledger_name_reserved_prefix:type_name -> common.LedgerNameReservedPrefixDetails + 125, // 163: common.FailureDetails.ledger_name_too_long:type_name -> common.LedgerNameTooLongDetails + 126, // 164: common.FailureDetails.ledger_not_found:type_name -> common.LedgerNotFoundDetails + 127, // 165: common.FailureDetails.ledger_not_in_mirror_mode:type_name -> common.LedgerNotInMirrorModeDetails + 128, // 166: common.FailureDetails.metadata_field_not_in_schema:type_name -> common.MetadataFieldNotInSchemaDetails + 129, // 167: common.FailureDetails.metadata_key_empty:type_name -> common.MetadataKeyEmptyDetails + 131, // 168: common.FailureDetails.metadata_key_invalid_char:type_name -> common.MetadataKeyInvalidCharDetails + 133, // 169: common.FailureDetails.metadata_key_required:type_name -> common.MetadataKeyRequiredDetails + 134, // 170: common.FailureDetails.metadata_limit_exceeded:type_name -> common.MetadataLimitExceededDetails + 138, // 171: common.FailureDetails.metadata_not_found:type_name -> common.MetadataNotFoundDetails + 139, // 172: common.FailureDetails.metadata_value_contains_null_byte:type_name -> common.MetadataValueContainsNullByteDetails + 141, // 173: common.FailureDetails.mirror_http_url_invalid:type_name -> common.MirrorHttpUrlInvalidDetails + 142, // 174: common.FailureDetails.mirror_iam_region_required:type_name -> common.MirrorIamRegionRequiredDetails + 143, // 175: common.FailureDetails.mirror_iam_requires_tls:type_name -> common.MirrorIamRequiresTlsDetails + 144, // 176: common.FailureDetails.mirror_rewrite_rule_invalid:type_name -> common.MirrorRewriteRuleInvalidDetails + 146, // 177: common.FailureDetails.numscript_asset_mismatch:type_name -> common.NumscriptAssetMismatchDetails + 147, // 178: common.FailureDetails.numscript_bad_arity:type_name -> common.NumscriptBadArityDetails + 148, // 179: common.FailureDetails.numscript_bad_meta_value:type_name -> common.NumscriptBadMetaValueDetails + 149, // 180: common.FailureDetails.numscript_bad_portion:type_name -> common.NumscriptBadPortionDetails + 150, // 181: common.FailureDetails.numscript_cannot_cast_scoped_account:type_name -> common.NumscriptCannotCastScopedAccountDetails + 151, // 182: common.FailureDetails.numscript_cannot_cast_to_string:type_name -> common.NumscriptCannotCastToStringDetails + 152, // 183: common.FailureDetails.numscript_cannot_store_scoped_account:type_name -> common.NumscriptCannotStoreScopedAccountDetails + 153, // 184: common.FailureDetails.numscript_compile_bad_arity:type_name -> common.NumscriptCompileBadArityDetails + 154, // 185: common.FailureDetails.numscript_compile_cannot_cast_to_string:type_name -> common.NumscriptCompileCannotCastToStringDetails + 155, // 186: common.FailureDetails.numscript_compile_cannot_store_scoped_account:type_name -> common.NumscriptCompileCannotStoreScopedAccountDetails + 156, // 187: common.FailureDetails.numscript_compile_duplicate_remaining:type_name -> common.NumscriptCompileDuplicateRemainingDetails + 157, // 188: common.FailureDetails.numscript_compile_duplicate_variable:type_name -> common.NumscriptCompileDuplicateVariableDetails + 158, // 189: common.FailureDetails.numscript_compile_experimental_feature:type_name -> common.NumscriptCompileExperimentalFeatureDetails + 159, // 190: common.FailureDetails.numscript_compile_invalid_feature:type_name -> common.NumscriptCompileInvalidFeatureDetails + 160, // 191: common.FailureDetails.numscript_compile_invalid_meta_position:type_name -> common.NumscriptCompileInvalidMetaPositionDetails + 161, // 192: common.FailureDetails.numscript_compile_invalid_type:type_name -> common.NumscriptCompileInvalidTypeDetails + 162, // 193: common.FailureDetails.numscript_compile_invalid_uncapped_source:type_name -> common.NumscriptCompileInvalidUncappedSourceDetails + 163, // 194: common.FailureDetails.numscript_compile_invalid_variable_value:type_name -> common.NumscriptCompileInvalidVariableValueDetails + 164, // 195: common.FailureDetails.numscript_compile_missing_variable:type_name -> common.NumscriptCompileMissingVariableDetails + 165, // 196: common.FailureDetails.numscript_compile_type_mismatch:type_name -> common.NumscriptCompileTypeMismatchDetails + 166, // 197: common.FailureDetails.numscript_compile_unbound_variable:type_name -> common.NumscriptCompileUnboundVariableDetails + 167, // 198: common.FailureDetails.numscript_compile_unknown_function:type_name -> common.NumscriptCompileUnknownFunctionDetails + 168, // 199: common.FailureDetails.numscript_content_required:type_name -> common.NumscriptContentRequiredDetails + 169, // 200: common.FailureDetails.numscript_currency_mismatch:type_name -> common.NumscriptCurrencyMismatchDetails + 170, // 201: common.FailureDetails.numscript_divide_by_zero:type_name -> common.NumscriptDivideByZeroDetails + 172, // 202: common.FailureDetails.numscript_experimental_feature:type_name -> common.NumscriptExperimentalFeatureDetails + 173, // 203: common.FailureDetails.numscript_invalid_account_name:type_name -> common.NumscriptInvalidAccountNameDetails + 174, // 204: common.FailureDetails.numscript_invalid_allotment_in_send_all:type_name -> common.NumscriptInvalidAllotmentInSendAllDetails + 175, // 205: common.FailureDetails.numscript_invalid_allotment_sum:type_name -> common.NumscriptInvalidAllotmentSumDetails + 176, // 206: common.FailureDetails.numscript_invalid_asset:type_name -> common.NumscriptInvalidAssetDetails + 177, // 207: common.FailureDetails.numscript_invalid_color:type_name -> common.NumscriptInvalidColorDetails + 178, // 208: common.FailureDetails.numscript_invalid_feature:type_name -> common.NumscriptInvalidFeatureDetails + 179, // 209: common.FailureDetails.numscript_invalid_monetary_literal:type_name -> common.NumscriptInvalidMonetaryLiteralDetails + 180, // 210: common.FailureDetails.numscript_invalid_nested_meta:type_name -> common.NumscriptInvalidNestedMetaDetails + 181, // 211: common.FailureDetails.numscript_invalid_number_literal:type_name -> common.NumscriptInvalidNumberLiteralDetails + 182, // 212: common.FailureDetails.numscript_invalid_remaining_allotment:type_name -> common.NumscriptInvalidRemainingAllotmentDetails + 183, // 213: common.FailureDetails.numscript_invalid_scope:type_name -> common.NumscriptInvalidScopeDetails + 184, // 214: common.FailureDetails.numscript_invalid_type:type_name -> common.NumscriptInvalidTypeDetails + 185, // 215: common.FailureDetails.numscript_invalid_unbounded_scaling_address:type_name -> common.NumscriptInvalidUnboundedScalingAddressDetails + 186, // 216: common.FailureDetails.numscript_invalid_unbounded_send_all:type_name -> common.NumscriptInvalidUnboundedSendAllDetails + 187, // 217: common.FailureDetails.numscript_invalid_uncapped_source:type_name -> common.NumscriptInvalidUncappedSourceDetails + 188, // 218: common.FailureDetails.numscript_invalid_version:type_name -> common.NumscriptInvalidVersionDetails + 189, // 219: common.FailureDetails.numscript_metadata_not_found:type_name -> common.NumscriptMetadataNotFoundDetails + 191, // 220: common.FailureDetails.numscript_missing_funds:type_name -> common.NumscriptMissingFundsDetails + 192, // 221: common.FailureDetails.numscript_missing_variable:type_name -> common.NumscriptMissingVariableDetails + 193, // 222: common.FailureDetails.numscript_name_invalid_char:type_name -> common.NumscriptNameInvalidCharDetails + 194, // 223: common.FailureDetails.numscript_name_required:type_name -> common.NumscriptNameRequiredDetails + 195, // 224: common.FailureDetails.numscript_name_too_long:type_name -> common.NumscriptNameTooLongDetails + 196, // 225: common.FailureDetails.numscript_negative_amount:type_name -> common.NumscriptNegativeAmountDetails + 197, // 226: common.FailureDetails.numscript_negative_balance:type_name -> common.NumscriptNegativeBalanceDetails + 199, // 227: common.FailureDetails.numscript_negative_portion:type_name -> common.NumscriptNegativePortionDetails + 200, // 228: common.FailureDetails.numscript_not_found:type_name -> common.NumscriptNotFoundDetails + 202, // 229: common.FailureDetails.numscript_parse_error:type_name -> common.NumscriptParseErrorDetails + 203, // 230: common.FailureDetails.numscript_posting_amount_overflow:type_name -> common.NumscriptPostingAmountOverflowDetails + 204, // 231: common.FailureDetails.numscript_posting_negative_amount:type_name -> common.NumscriptPostingNegativeAmountDetails + 205, // 232: common.FailureDetails.numscript_scaling_unsupported:type_name -> common.NumscriptScalingUnsupportedDetails + 206, // 233: common.FailureDetails.numscript_type_error:type_name -> common.NumscriptTypeErrorDetails + 207, // 234: common.FailureDetails.numscript_unbound_function:type_name -> common.NumscriptUnboundFunctionDetails + 208, // 235: common.FailureDetails.numscript_unbound_variable:type_name -> common.NumscriptUnboundVariableDetails + 209, // 236: common.FailureDetails.numscript_version_already_exists:type_name -> common.NumscriptVersionAlreadyExistsDetails + 210, // 237: common.FailureDetails.postings_and_script_conflict:type_name -> common.PostingsAndScriptConflictDetails + 211, // 238: common.FailureDetails.prepared_query_already_exists:type_name -> common.PreparedQueryAlreadyExistsDetails + 212, // 239: common.FailureDetails.prepared_query_name_invalid_char:type_name -> common.PreparedQueryNameInvalidCharDetails + 213, // 240: common.FailureDetails.prepared_query_name_required:type_name -> common.PreparedQueryNameRequiredDetails + 214, // 241: common.FailureDetails.prepared_query_name_too_long:type_name -> common.PreparedQueryNameTooLongDetails + 215, // 242: common.FailureDetails.prepared_query_not_found:type_name -> common.PreparedQueryNotFoundDetails + 216, // 243: common.FailureDetails.prepared_query_required:type_name -> common.PreparedQueryRequiredDetails + 217, // 244: common.FailureDetails.prepared_query_target_unsupported:type_name -> common.PreparedQueryTargetUnsupportedDetails + 218, // 245: common.FailureDetails.revert_target_created_in_batch:type_name -> common.RevertTargetCreatedInBatchDetails + 219, // 246: common.FailureDetails.scoped_balance_unsupported:type_name -> common.ScopedBalanceUnsupportedDetails + 220, // 247: common.FailureDetails.script_and_reference_conflict:type_name -> common.ScriptAndReferenceConflictDetails + 221, // 248: common.FailureDetails.script_required:type_name -> common.ScriptRequiredDetails + 222, // 249: common.FailureDetails.signing_key_id_invalid_char:type_name -> common.SigningKeyIdInvalidCharDetails + 223, // 250: common.FailureDetails.signing_key_id_required:type_name -> common.SigningKeyIdRequiredDetails + 224, // 251: common.FailureDetails.signing_key_id_too_long:type_name -> common.SigningKeyIdTooLongDetails + 225, // 252: common.FailureDetails.signing_key_invalid_length:type_name -> common.SigningKeyInvalidLengthDetails + 226, // 253: common.FailureDetails.sink_already_exists:type_name -> common.SinkAlreadyExistsDetails + 227, // 254: common.FailureDetails.sink_batch_size_too_large:type_name -> common.SinkBatchSizeTooLargeDetails + 228, // 255: common.FailureDetails.sink_controller_mismatch:type_name -> common.SinkControllerMismatchDetails + 229, // 256: common.FailureDetails.sink_not_found:type_name -> common.SinkNotFoundDetails + 230, // 257: common.FailureDetails.target_required:type_name -> common.TargetRequiredDetails + 231, // 258: common.FailureDetails.transaction_already_reverted:type_name -> common.TransactionAlreadyRevertedDetails + 232, // 259: common.FailureDetails.transaction_not_found:type_name -> common.TransactionNotFoundDetails + 233, // 260: common.FailureDetails.transaction_reference_conflict:type_name -> common.TransactionReferenceConflictDetails + 234, // 261: common.FailureDetails.transaction_reference_not_found:type_name -> common.TransactionReferenceNotFoundDetails + 235, // 262: common.FailureDetails.transaction_reference_too_long:type_name -> common.TransactionReferenceTooLongDetails + 236, // 263: common.FailureDetails.transaction_target_missing:type_name -> common.TransactionTargetMissingDetails + 237, // 264: common.FailureDetails.transient_account_non_zero:type_name -> common.TransientAccountNonZeroDetails + 238, // 265: common.FailureDetails.volume_overflow:type_name -> common.VolumeOverflowDetails + 130, // 266: common.FailureDetails.account_metadata_key_empty:type_name -> common.AccountMetadataKeyEmptyDetails + 132, // 267: common.FailureDetails.account_metadata_key_invalid_char:type_name -> common.AccountMetadataKeyInvalidCharDetails + 140, // 268: common.FailureDetails.account_metadata_value_contains_null_byte:type_name -> common.AccountMetadataValueContainsNullByteDetails + 135, // 269: common.FailureDetails.account_metadata_limit_exceeded:type_name -> common.AccountMetadataLimitExceededDetails + 136, // 270: common.FailureDetails.metadata_key_limit_exceeded:type_name -> common.MetadataKeyLimitExceededDetails + 137, // 271: common.FailureDetails.account_metadata_key_limit_exceeded:type_name -> common.AccountMetadataKeyLimitExceededDetails + 115, // 272: common.FailureDetails.numscript_insufficient_funds:type_name -> common.NumscriptInsufficientFundsDetails + 171, // 273: common.FailureDetails.numscript_divide_by_zero_with_numerator:type_name -> common.NumscriptDivideByZeroWithNumeratorDetails + 190, // 274: common.FailureDetails.numscript_scoped_metadata_not_found:type_name -> common.NumscriptScopedMetadataNotFoundDetails + 198, // 275: common.FailureDetails.numscript_scoped_negative_balance:type_name -> common.NumscriptScopedNegativeBalanceDetails + 201, // 276: common.FailureDetails.numscript_version_not_found:type_name -> common.NumscriptVersionNotFoundDetails + 145, // 277: common.FailureDetails.mirror_asset_mapping_invalid:type_name -> common.MirrorAssetMappingInvalidDetails + 89, // 278: common.OrderSkippedLog.details:type_name -> common.FailureDetails + 43, // 279: common.CreatedIndexLog.id:type_name -> common.IndexID + 3, // 280: common.CreatedIndexLog.bound_type:type_name -> common.MetadataType + 43, // 281: common.DroppedIndexLog.id:type_name -> common.IndexID + 26, // 282: common.CreatedTransaction.transaction:type_name -> common.Transaction + 342, // 283: common.CreatedTransaction.account_metadata:type_name -> common.CreatedTransaction.AccountMetadataEntry + 26, // 284: common.RevertedTransaction.revert_transaction:type_name -> common.Transaction + 38, // 285: common.SavedMetadata.target:type_name -> common.Target + 343, // 286: common.SavedMetadata.metadata:type_name -> common.SavedMetadata.MetadataEntry + 38, // 287: common.DeletedMetadata.target:type_name -> common.Target + 2, // 288: common.SetMetadataFieldTypeLog.target_type:type_name -> common.TargetType + 3, // 289: common.SetMetadataFieldTypeLog.type:type_name -> common.MetadataType + 2, // 290: common.RemovedMetadataFieldTypeLog.target_type:type_name -> common.TargetType + 43, // 291: common.RemovedMetadataFieldTypeLog.dropped_index:type_name -> common.IndexID + 270, // 292: common.MirrorSourceConfig.http:type_name -> common.HttpMirrorSourceConfig + 272, // 293: common.MirrorSourceConfig.postgres:type_name -> common.PostgresMirrorSourceConfig + 251, // 294: common.MirrorSourceConfig.rewrite_rules:type_name -> common.MirrorRewriteRule + 250, // 295: common.MirrorSourceConfig.asset_mappings:type_name -> common.MirrorAssetMapping + 252, // 296: common.MirrorRewriteRule.created_transaction:type_name -> common.CreatedTransactionRule + 253, // 297: common.MirrorRewriteRule.reverted_transaction:type_name -> common.RevertedTransactionRule + 254, // 298: common.MirrorRewriteRule.saved_metadata:type_name -> common.SavedMetadataRule + 255, // 299: common.MirrorRewriteRule.deleted_metadata:type_name -> common.DeletedMetadataRule + 256, // 300: common.MirrorRewriteRule.any_variant:type_name -> common.AnyVariantRule + 257, // 301: common.CreatedTransactionRule.actions:type_name -> common.CreatedTransactionAction + 258, // 302: common.RevertedTransactionRule.actions:type_name -> common.RevertedTransactionAction + 259, // 303: common.SavedMetadataRule.actions:type_name -> common.SavedMetadataAction + 260, // 304: common.DeletedMetadataRule.actions:type_name -> common.DeletedMetadataAction + 261, // 305: common.AnyVariantRule.actions:type_name -> common.AnyVariantAction + 262, // 306: common.CreatedTransactionAction.rewrite_address:type_name -> common.RewriteAddressAction + 263, // 307: common.CreatedTransactionAction.set_metadata:type_name -> common.SetMetadataAction + 264, // 308: common.CreatedTransactionAction.delete_metadata:type_name -> common.DeleteMetadataAction + 265, // 309: common.CreatedTransactionAction.set_account_metadata:type_name -> common.SetAccountMetadataAction + 266, // 310: common.CreatedTransactionAction.delete_account_metadata:type_name -> common.DeleteAccountMetadataAction + 267, // 311: common.CreatedTransactionAction.set_account_metadata_from_address:type_name -> common.SetAccountMetadataFromAddressAction + 269, // 312: common.CreatedTransactionAction.drop:type_name -> common.DropAction + 262, // 313: common.RevertedTransactionAction.rewrite_address:type_name -> common.RewriteAddressAction + 263, // 314: common.RevertedTransactionAction.set_metadata:type_name -> common.SetMetadataAction + 264, // 315: common.RevertedTransactionAction.delete_metadata:type_name -> common.DeleteMetadataAction + 269, // 316: common.RevertedTransactionAction.drop:type_name -> common.DropAction + 262, // 317: common.SavedMetadataAction.rewrite_address:type_name -> common.RewriteAddressAction + 263, // 318: common.SavedMetadataAction.set_metadata:type_name -> common.SetMetadataAction + 264, // 319: common.SavedMetadataAction.delete_metadata:type_name -> common.DeleteMetadataAction + 269, // 320: common.SavedMetadataAction.drop:type_name -> common.DropAction + 262, // 321: common.DeletedMetadataAction.rewrite_address:type_name -> common.RewriteAddressAction + 269, // 322: common.DeletedMetadataAction.drop:type_name -> common.DropAction + 262, // 323: common.AnyVariantAction.rewrite_address:type_name -> common.RewriteAddressAction + 269, // 324: common.AnyVariantAction.drop:type_name -> common.DropAction + 268, // 325: common.SetAccountMetadataFromAddressAction.replacements:type_name -> common.SetAccountMetadataFromAddressReplacement + 271, // 326: common.HttpMirrorSourceConfig.oauth2_client_credentials:type_name -> common.OAuth2ClientCredentials + 273, // 327: common.PostgresMirrorSourceConfig.aws_iam_auth:type_name -> common.PostgresAwsIamAuth + 19, // 328: common.MirrorSyncError.occurred_at:type_name -> common.Timestamp + 10, // 329: common.MirrorSyncProgress.state:type_name -> common.MirrorSyncState + 274, // 330: common.MirrorSyncProgress.error:type_name -> common.MirrorSyncError + 19, // 331: common.LedgerInfo.created_at:type_name -> common.Timestamp + 19, // 332: common.LedgerInfo.deleted_at:type_name -> common.Timestamp + 40, // 333: common.LedgerInfo.metadata_schema:type_name -> common.MetadataSchema + 9, // 334: common.LedgerInfo.mode:type_name -> common.LedgerMode + 249, // 335: common.LedgerInfo.mirror_source:type_name -> common.MirrorSourceConfig + 275, // 336: common.LedgerInfo.mirror_sync_progress:type_name -> common.MirrorSyncProgress + 344, // 337: common.LedgerInfo.account_types:type_name -> common.LedgerInfo.AccountTypesEntry + 12, // 338: common.LedgerInfo.default_enforcement_mode:type_name -> common.ChartEnforcementMode + 345, // 339: common.LedgerInfo.metadata:type_name -> common.LedgerInfo.MetadataEntry + 38, // 340: common.SaveMetadataCommand.target:type_name -> common.Target + 346, // 341: common.SaveMetadataCommand.metadata:type_name -> common.SaveMetadataCommand.MetadataEntry + 38, // 342: common.DeleteMetadataCommand.target:type_name -> common.Target + 347, // 343: common.TransactionState.metadata:type_name -> common.TransactionState.MetadataEntry + 19, // 344: common.TransactionState.timestamp:type_name -> common.Timestamp + 25, // 345: common.TransactionState.postings:type_name -> common.Posting + 19, // 346: common.TransactionState.reverted_at:type_name -> common.Timestamp + 281, // 347: common.IdempotencyKeyValue.failure:type_name -> common.IdempotencyFailure + 89, // 348: common.IdempotencyFailure.details:type_name -> common.FailureDetails + 285, // 349: common.SegmentType.uuid:type_name -> common.UUIDConstraint + 286, // 350: common.SegmentType.uint64:type_name -> common.Uint64Constraint + 287, // 351: common.SegmentType.bytes:type_name -> common.BytesConstraint + 13, // 352: common.AccountType.persistence:type_name -> common.AccountTypePersistence + 348, // 353: common.AccountType.segment_types:type_name -> common.AccountType.SegmentTypesEntry + 288, // 354: common.AddedAccountTypeLog.account_type:type_name -> common.AccountType + 12, // 355: common.UpdatedDefaultEnforcementModeLog.enforcement_mode:type_name -> common.ChartEnforcementMode + 305, // 356: common.QueryFilter.field:type_name -> common.FieldCondition + 311, // 357: common.QueryFilter.address:type_name -> common.AddressMatch + 301, // 358: common.QueryFilter.and:type_name -> common.AndFilter + 302, // 359: common.QueryFilter.or:type_name -> common.OrFilter + 303, // 360: common.QueryFilter.not:type_name -> common.NotFilter + 293, // 361: common.QueryFilter.reference:type_name -> common.ReferenceCondition + 298, // 362: common.QueryFilter.builtin_uint:type_name -> common.BuiltinUintCondition + 296, // 363: common.QueryFilter.ledger:type_name -> common.LedgerCondition + 297, // 364: common.QueryFilter.log_id:type_name -> common.LogIdCondition + 299, // 365: common.QueryFilter.log_builtin_uint:type_name -> common.LogBuiltinUintCondition + 300, // 366: common.QueryFilter.account_has_asset:type_name -> common.AccountHasAssetCondition + 294, // 367: common.QueryFilter.reverted:type_name -> common.RevertedCondition + 295, // 368: common.QueryFilter.audit:type_name -> common.AuditCondition + 306, // 369: common.ReferenceCondition.cond:type_name -> common.StringCondition + 14, // 370: common.AuditCondition.field:type_name -> common.AuditField + 306, // 371: common.AuditCondition.string_cond:type_name -> common.StringCondition + 308, // 372: common.AuditCondition.uint_cond:type_name -> common.UintCondition + 306, // 373: common.LedgerCondition.cond:type_name -> common.StringCondition + 308, // 374: common.LogIdCondition.cond:type_name -> common.UintCondition + 4, // 375: common.BuiltinUintCondition.field:type_name -> common.TransactionBuiltinIndex + 308, // 376: common.BuiltinUintCondition.cond:type_name -> common.UintCondition + 6, // 377: common.LogBuiltinUintCondition.field:type_name -> common.LogBuiltinIndex + 308, // 378: common.LogBuiltinUintCondition.cond:type_name -> common.UintCondition + 292, // 379: common.AndFilter.filters:type_name -> common.QueryFilter + 292, // 380: common.OrFilter.filters:type_name -> common.QueryFilter + 292, // 381: common.NotFilter.filter:type_name -> common.QueryFilter + 304, // 382: common.FieldCondition.field:type_name -> common.FieldRef + 306, // 383: common.FieldCondition.string_cond:type_name -> common.StringCondition + 307, // 384: common.FieldCondition.int_cond:type_name -> common.IntCondition + 308, // 385: common.FieldCondition.uint_cond:type_name -> common.UintCondition + 309, // 386: common.FieldCondition.bool_cond:type_name -> common.BoolCondition + 310, // 387: common.FieldCondition.exists_cond:type_name -> common.ExistsCondition + 15, // 388: common.AddressMatch.role:type_name -> common.AddressRole + 292, // 389: common.PreparedQuery.filter:type_name -> common.QueryFilter + 16, // 390: common.PreparedQuery.target:type_name -> common.QueryTarget + 24, // 391: common.AggregatedVolume.input:type_name -> common.Uint256 + 24, // 392: common.AggregatedVolume.output:type_name -> common.Uint256 + 313, // 393: common.AggregateResult.volumes:type_name -> common.AggregatedVolume + 315, // 394: common.AggregateResult.groups:type_name -> common.GroupedAggregateResult + 313, // 395: common.GroupedAggregateResult.volumes:type_name -> common.AggregatedVolume + 36, // 396: common.PreparedQueryCursor.account_data:type_name -> common.Account + 26, // 397: common.PreparedQueryCursor.transaction_data:type_name -> common.Transaction + 46, // 398: common.PreparedQueryCursor.log_data:type_name -> common.Log + 319, // 399: common.AuthenticatedCaller.identity:type_name -> common.CallerIdentity + 320, // 400: common.CallerSnapshot.authenticated:type_name -> common.AuthenticatedCaller + 321, // 401: common.CallerSnapshot.anonymous:type_name -> common.AnonymousCaller + 322, // 402: common.CallerSnapshot.system:type_name -> common.SystemCaller + 323, // 403: common.CallerSnapshot.auth_disabled:type_name -> common.AuthDisabledCaller + 325, // 404: common.BackupStorage.s3:type_name -> common.S3StorageConfig + 326, // 405: common.BackupStorage.azure:type_name -> common.AzureStorageConfig + 328, // 406: common.ListOptions.read:type_name -> common.ReadOptions + 292, // 407: common.ListOptions.filter:type_name -> common.QueryFilter + 21, // 408: common.MetadataMap.ValuesEntry.value:type_name -> common.MetadataValue + 21, // 409: common.Transaction.MetadataEntry.value:type_name -> common.MetadataValue + 32, // 410: common.PostCommitVolumes.VolumesByAccountEntry.value:type_name -> common.VolumesByAssets + 21, // 411: common.Account.MetadataEntry.value:type_name -> common.MetadataValue + 39, // 412: common.MetadataSchema.AccountFieldsEntry.value:type_name -> common.MetadataFieldSchema + 39, // 413: common.MetadataSchema.TransactionFieldsEntry.value:type_name -> common.MetadataFieldSchema + 39, // 414: common.MetadataSchema.LedgerFieldsEntry.value:type_name -> common.MetadataFieldSchema + 21, // 415: common.SavedLedgerMetadataLog.MetadataEntry.value:type_name -> common.MetadataValue + 288, // 416: common.CreatedLedgerLog.AccountTypesEntry.value:type_name -> common.AccountType + 21, // 417: common.CreatedLedgerLog.MetadataEntry.value:type_name -> common.MetadataValue + 22, // 418: common.CreatedTransaction.AccountMetadataEntry.value:type_name -> common.MetadataMap + 21, // 419: common.SavedMetadata.MetadataEntry.value:type_name -> common.MetadataValue + 288, // 420: common.LedgerInfo.AccountTypesEntry.value:type_name -> common.AccountType + 21, // 421: common.LedgerInfo.MetadataEntry.value:type_name -> common.MetadataValue + 21, // 422: common.SaveMetadataCommand.MetadataEntry.value:type_name -> common.MetadataValue + 21, // 423: common.TransactionState.MetadataEntry.value:type_name -> common.MetadataValue + 284, // 424: common.AccountType.SegmentTypesEntry.value:type_name -> common.SegmentType + 350, // 425: common.auth_policy:extendee -> google.protobuf.MethodOptions + 351, // 426: common.ledger_json:extendee -> google.protobuf.MessageOptions + 352, // 427: common.ledger_json_default:extendee -> google.protobuf.FileOptions + 351, // 428: common.ledger_json_manual:extendee -> google.protobuf.MessageOptions + 353, // 429: common.allowed_query_targets:extendee -> google.protobuf.FieldOptions + 353, // 430: common.valid_on_no_query_target:extendee -> google.protobuf.FieldOptions + 353, // 431: common.ledger_log_is_history:extendee -> google.protobuf.FieldOptions + 353, // 432: common.sensitive:extendee -> google.protobuf.FieldOptions + 353, // 433: common.sensitive_url:extendee -> google.protobuf.FieldOptions + 353, // 434: common.failure_reason:extendee -> google.protobuf.FieldOptions + 18, // 435: common.auth_policy:type_name -> common.MethodAuthPolicy + 16, // 436: common.allowed_query_targets:type_name -> common.QueryTarget + 11, // 437: common.failure_reason:type_name -> common.ErrorReason + 438, // [438:438] is the sub-list for method output_type + 438, // [438:438] is the sub-list for method input_type + 435, // [435:438] is the sub-list for extension type_name + 425, // [425:435] is the sub-list for extension extendee + 0, // [0:425] is the sub-list for field type_name } func init() { file_common_proto_init() } @@ -25253,7 +25322,7 @@ func file_common_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_common_proto_rawDesc), len(file_common_proto_rawDesc)), NumEnums: 18, - NumMessages: 330, + NumMessages: 331, NumExtensions: 10, NumServices: 0, }, diff --git a/internal/proto/commonpb/common_ledger_json.pb.go b/internal/proto/commonpb/common_ledger_json.pb.go index 90063f4507..e56d38b865 100644 --- a/internal/proto/commonpb/common_ledger_json.pb.go +++ b/internal/proto/commonpb/common_ledger_json.pb.go @@ -323,6 +323,7 @@ func (x *ClusterPolicy) MarshalJSON() ([]byte, error) { MetadataMaxValueBytes uint64 `json:"metadataMaxValueBytes"` MetadataMaxEntityBytes uint64 `json:"metadataMaxEntityBytes"` MetadataMaxCommandBytes uint64 `json:"metadataMaxCommandBytes"` + CapabilityActivations any `json:"capabilityActivations"` }{ Revision: x.Revision, IdempotencyTtlMicros: x.IdempotencyTtlMicros, @@ -332,6 +333,12 @@ func (x *ClusterPolicy) MarshalJSON() ([]byte, error) { MetadataMaxValueBytes: x.MetadataMaxValueBytes, MetadataMaxEntityBytes: x.MetadataMaxEntityBytes, MetadataMaxCommandBytes: x.MetadataMaxCommandBytes, + CapabilityActivations: func() any { + if x.CapabilityActivations == nil { + return []any{} + } + return x.CapabilityActivations + }(), }) } From ea10e69c4beb7d6d0f3b9fb892840e72f4d77a61 Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Fri, 6 Nov 2026 10:22:54 +0100 Subject: [PATCH 099/127] fix: reconcile capability policy cleanup --- internal/bootstrap/module.go | 16 ---------------- internal/proto/commonpb/ledger_info_json_test.go | 2 +- 2 files changed, 1 insertion(+), 17 deletions(-) diff --git a/internal/bootstrap/module.go b/internal/bootstrap/module.go index 6fc1e12cc5..6bb6e2b3b8 100644 --- a/internal/bootstrap/module.go +++ b/internal/bootstrap/module.go @@ -1993,22 +1993,6 @@ func sameClusterPolicyConfiguration(a, b *commonpb.ClusterPolicy) bool { return left.EqualVT(right) } -// sameClusterPolicyPayload compares the complete policy while deliberately -// ignoring only the revision. A revision-only difference is not a reason to -// manufacture another revision during capability reconciliation. -func sameClusterPolicyPayload(a, b *commonpb.ClusterPolicy) bool { - if a == nil || b == nil { - return a == b - } - - left := a.CloneVT() - right := b.CloneVT() - left.Revision = 0 - right.Revision = 0 - - return left.EqualVT(right) -} - // buildResponseSigner returns the Ed25519 response signer for the gRPC // service plane. // diff --git a/internal/proto/commonpb/ledger_info_json_test.go b/internal/proto/commonpb/ledger_info_json_test.go index c1892bf0e1..41be037204 100644 --- a/internal/proto/commonpb/ledger_info_json_test.go +++ b/internal/proto/commonpb/ledger_info_json_test.go @@ -94,5 +94,5 @@ func TestSetClusterPolicyLogJSONProjectsNestedPolicy(t *testing.T) { MetadataMaxCommandBytes: 16_384, }}) require.NoError(t, err) - require.JSONEq(t, `{"policy":{"revision":7,"idempotencyTtlMicros":86400000000,"queryCheckpointLimit":0,"metadataMaxEntriesPerEntity":0,"metadataMaxKeyBytes":256,"metadataMaxValueBytes":0,"metadataMaxEntityBytes":0,"metadataMaxCommandBytes":16384}}`, string(data)) + require.JSONEq(t, `{"policy":{"revision":7,"idempotencyTtlMicros":86400000000,"queryCheckpointLimit":0,"metadataMaxEntriesPerEntity":0,"metadataMaxKeyBytes":256,"metadataMaxValueBytes":0,"metadataMaxEntityBytes":0,"metadataMaxCommandBytes":16384,"capabilityActivations":[]}}`, string(data)) } From 1543e09beb8c1bcd2aee4f261da78bd9fa8bac5f Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Fri, 6 Nov 2026 10:49:26 +0100 Subject: [PATCH 100/127] test: update admission replay machine setup --- internal/application/admission/metadata_replay_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/application/admission/metadata_replay_test.go b/internal/application/admission/metadata_replay_test.go index 17cdb98307..9069255bb3 100644 --- a/internal/application/admission/metadata_replay_test.go +++ b/internal/application/admission/metadata_replay_test.go @@ -72,7 +72,7 @@ func newMetadataReplayHarness(t testing.TB) *metadataReplayHarness { registry := state.NewStateRegistry(c, attrs) ks := keystore.NewKeyStore() shared := state.NewSharedState() - machine, err := state.NewMachine(logger, registry, state.NewCacheSnapshotter(logger, registry, nil), store, dal.NewSentinelFactory(store, false), meter, ks, shared, signal.NewNotifications(), nil, "test-cluster", 0, func(*raftpb.Entry, *dal.WriteSession) error { return nil }) + machine, err := state.NewMachine(logger, registry, state.NewCacheSnapshotter(logger, registry, nil), store, dal.NewSentinelFactory(store, false), meter, ks, shared, signal.NewNotifications(), nil, "test-cluster", 0, func(*raftpb.Entry, *dal.WriteSession) error { return nil }, state.NoopCapabilityPolicyCommitHandler) require.NoError(t, err) require.NoError(t, state.NewRecovery(machine, store).RecoverState()) tracker := node.NewIndexTracker(1) From 143996890334d9f00bc149afcf4184986894e1f3 Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Mon, 9 Nov 2026 09:34:14 +0100 Subject: [PATCH 101/127] fix(ledger): validate native policy activations --- internal/infra/node/proposal.go | 11 +++++++++-- internal/infra/node/proposal_test.go | 18 ++++++++++++++++++ 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/internal/infra/node/proposal.go b/internal/infra/node/proposal.go index 51b5827eaf..e91929d28d 100644 --- a/internal/infra/node/proposal.go +++ b/internal/infra/node/proposal.go @@ -30,7 +30,7 @@ type Proposal struct { expectedTerm uint64 requiredCapabilities []capabilities.CapabilityID // activePolicyCapabilities is populated by the proposal factory for - // SetClusterPolicy orders. Keeping the typed candidate alongside the + // SetClusterPolicy proposals and orders. Keeping the typed candidate alongside the // serialized command lets the leader validate activation evidence before // handing the bytes to Raft without decoding the command a second time. activePolicyCapabilities []capabilities.CapabilityID @@ -60,6 +60,13 @@ func NewProposal(commandID uint64, command *raftcmdpb.Proposal) (*Proposal, erro } proposal := newProposal(commandID, data, prepared.RequiredCapabilities()) + if policy := command.GetSetClusterPolicy().GetPolicy(); policy != nil { + for _, activation := range policy.GetCapabilityActivations() { + if activation != nil && activation.GetActive() { + proposal.activePolicyCapabilities = append(proposal.activePolicyCapabilities, capabilities.CapabilityID(activation.GetId())) + } + } + } for _, order := range command.GetOrders() { policy := order.GetSystemScoped().GetSetClusterPolicy().GetPolicy() if policy == nil { @@ -100,7 +107,7 @@ func (p *Proposal) RequiredCapabilities() []capabilities.CapabilityID { } // ActivePolicyCapabilities returns the active capabilities requested by a -// SetClusterPolicy order, if any. The values are derived by NewProposal from +// SetClusterPolicy proposal or order, if any. The values are derived by NewProposal from // the typed command and are not read from the serialized payload. func (p *Proposal) ActivePolicyCapabilities() []capabilities.CapabilityID { return append([]capabilities.CapabilityID(nil), p.activePolicyCapabilities...) diff --git a/internal/infra/node/proposal_test.go b/internal/infra/node/proposal_test.go index 7e27f763f5..9bd7f2e367 100644 --- a/internal/infra/node/proposal_test.go +++ b/internal/infra/node/proposal_test.go @@ -70,3 +70,21 @@ func TestNewProposalDerivesActivePolicyCapabilities(t *testing.T) { active[0] = 99 require.Equal(t, []capabilities.CapabilityID{7}, proposal.ActivePolicyCapabilities(), "candidate metadata must be defensive") } + +func TestNewProposalDerivesActivePolicyCapabilitiesFromNativePolicy(t *testing.T) { + command := &raftcmdpb.Proposal{ + Id: 1, + Payload: &raftcmdpb.Proposal_SetClusterPolicy{ + SetClusterPolicy: &raftcmdpb.SetClusterPolicyProposal{ + Policy: &commonpb.ClusterPolicy{CapabilityActivations: []*commonpb.CapabilityActivation{ + {Id: 7, Active: true}, + {Id: 8, Active: false}, + }}, + }, + }, + } + + proposal, err := NewProposal(1, command) + require.NoError(t, err) + require.Equal(t, []capabilities.CapabilityID{7}, proposal.ActivePolicyCapabilities()) +} From 2f8f6ce5928401ebb7031090460aebc646945c24 Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Mon, 9 Nov 2026 13:00:09 +0100 Subject: [PATCH 102/127] docs(ledger): restore raft audit evidence scenarios --- .../audits/raft-membership-leadership.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/docs/technical/audits/raft-membership-leadership.md b/docs/technical/audits/raft-membership-leadership.md index 5779f15f59..b77bc7da1e 100644 --- a/docs/technical/audits/raft-membership-leadership.md +++ b/docs/technical/audits/raft-membership-leadership.md @@ -75,6 +75,23 @@ question rather than inventing a membership guarantee. ## Transition-specific evidence +### Capability evidence and policy activation + +For bootstrap join, restart refresh, promotion, and membership changes, record +the member's `(node_id, instance_id)`, advertised capability IDs, committed +membership row, and the policy revision and active capabilities on every +replica. A restarted member must refresh its committed evidence using peers +recovered from membership even when no `--join` flag is present. Distinguish a +transient old-peer `Unimplemented` refresh response from permanent identity or +policy rejection, and prove eventual convergence through a compatible peer. +Before activating a capability, require support by the local binary and +committed evidence from every member; after activation, reject announcements +and proposals that lack the required evidence. Exercise policy activation and +membership `ConfChange` in adjacent FSM batches and compare the final policy, +membership, `ConfState`, and admission decision on every replica. This records +the foundation for future activations; the current protocol cutover uses a +coordinated stop and restart. + ### Normal membership changes Prove which `ConfChangeV2` was admitted and which configuration commits it. From d6abdfdecced9b9a2b220922b881129ef09eeb93 Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Mon, 9 Nov 2026 13:17:12 +0100 Subject: [PATCH 103/127] fix(ledger): refresh membership from recovered peers --- internal/bootstrap/join_preflight_test.go | 26 ++++++++++++++++++++ internal/bootstrap/module.go | 29 +++++++++++++++-------- 2 files changed, 45 insertions(+), 10 deletions(-) diff --git a/internal/bootstrap/join_preflight_test.go b/internal/bootstrap/join_preflight_test.go index 0555429709..f647cdd445 100644 --- a/internal/bootstrap/join_preflight_test.go +++ b/internal/bootstrap/join_preflight_test.go @@ -15,6 +15,7 @@ import ( logging "github.com/formancehq/go-libs/v5/pkg/observe/log" + raftmembership "github.com/formancehq/ledger/v3/internal/infra/membership" "github.com/formancehq/ledger/v3/internal/infra/node" "github.com/formancehq/ledger/v3/internal/storage/wal" ) @@ -68,6 +69,15 @@ func TestShouldRunJoinPreflight(t *testing.T) { }) } +func TestRecoveredRefreshPeers(t *testing.T) { + peers := recoveredRefreshPeers(1, map[uint64]raftmembership.ConfChangeContext{ + 1: {RaftAddress: "self:7777"}, + 2: {RaftAddress: "peer:7777"}, + 3: {}, + }) + require.Equal(t, []node.Peer{{ID: 2, Address: "peer:7777"}}, peers) +} + func TestShouldRunMembershipRefresh(t *testing.T) { t.Parallel() logger := logging.Testing() @@ -91,6 +101,22 @@ func TestShouldRunMembershipRefresh(t *testing.T) { }} assert.True(t, shouldRunMembershipRefresh(cfg, logger)) }) + + t.Run("joined WAL without startup flags refreshes", func(t *testing.T) { + t.Parallel() + walDir := t.TempDir() + require.NoError(t, wal.MarkClusterJoined(walDir)) + cfg := Config{RaftConfig: node.NodeConfig{WalDir: walDir}} + assert.True(t, shouldRunMembershipRefresh(cfg, logger)) + }) + + t.Run("former bootstrap node refreshes", func(t *testing.T) { + t.Parallel() + walDir := t.TempDir() + require.NoError(t, wal.MarkClusterJoined(walDir)) + cfg := Config{RaftConfig: node.NodeConfig{Bootstrap: true, WalDir: walDir}} + assert.True(t, shouldRunMembershipRefresh(cfg, logger)) + }) } // TestJoinPreflightRunsBeforeRaftTraffic proves the EN-1436 ordering invariant diff --git a/internal/bootstrap/module.go b/internal/bootstrap/module.go index 6bb6e2b3b8..2796be706d 100644 --- a/internal/bootstrap/module.go +++ b/internal/bootstrap/module.go @@ -987,7 +987,7 @@ func Module() fx.Option { } })) }, - func(lc fx.Lifecycle, node *node.Node, defaultTransport *node.DefaultTransport, cfg Config, logger logging.Logger) (*node.Node, error) { + func(lc fx.Lifecycle, node *node.Node, defaultTransport *node.DefaultTransport, membership *raftmembership.Membership, cfg Config, logger logging.Logger) (*node.Node, error) { lc.Append(nodeHook(node, logger)) // A WAL that already joined the cluster must not take the learner @@ -997,7 +997,7 @@ func Module() fx.Option { // an UpdateNode for an existing voter may need that node to contribute // to the commit quorum. if shouldRunMembershipRefresh(cfg, logger) { - lc.Append(membershipRefreshHook(cfg, logger)) + lc.Append(membershipRefreshHook(cfg, membership, logger)) } return node, nil @@ -1298,14 +1298,9 @@ func shouldRunJoinPreflight(cfg Config, logger logging.Logger) bool { } // shouldRunMembershipRefresh selects the normal-restart path. A fresh WAL is -// handled by joinPreflightHook; a bootstrap node has no remote leader to -// notify. The CLUSTER_JOINED marker is the durable boundary between those two -// startup protocols. +// handled by joinPreflightHook. Restart peers come from recovered membership, +// so neither --join nor --bootstrap is required on subsequent boots. func shouldRunMembershipRefresh(cfg Config, logger logging.Logger) bool { - if cfg.RaftConfig.Bootstrap || len(cfg.RaftConfig.Peers) == 0 { - return false - } - if !wal.IsClusterJoined(cfg.RaftConfig.WalDir) { return false } @@ -1416,11 +1411,15 @@ func newMembershipPeerDialer(cfg Config, tlsCfg TLSConfig) (func(string) (*ggrpc }, nil } -func membershipRefreshHook(cfg Config, logger logging.Logger) fx.Hook { +func membershipRefreshHook(cfg Config, membership *raftmembership.Membership, logger logging.Logger) fx.Hook { var cancel context.CancelFunc return fx.Hook{ OnStart: func(ctx context.Context) error { + cfg.RaftConfig.Peers = recoveredRefreshPeers(cfg.RaftConfig.NodeID, membership.PeerAddresses()) + if len(cfg.RaftConfig.Peers) == 0 { + return nil // A single-member cluster has no remote membership to refresh. + } logger.WithFields(map[string]any{ "nodeID": cfg.RaftConfig.NodeID, "raftAddress": cfg.RaftConfig.AdvertiseAddr, @@ -1465,6 +1464,16 @@ func membershipRefreshHook(cfg Config, logger logging.Logger) fx.Hook { } } +func recoveredRefreshPeers(selfID uint64, addresses map[uint64]raftmembership.ConfChangeContext) []node.Peer { + peers := make([]node.Peer, 0, len(addresses)) + for id, address := range addresses { + if id != selfID && address.RaftAddress != "" { + peers = append(peers, node.Peer{ID: id, Address: address.RaftAddress}) + } + } + return peers +} + const membershipRefreshStartupTimeout = 5 * time.Second // tryRefreshMembershipForStartup gives the synchronous startup attempt a From e72cc5bee4f34dd370c68c98f6b98b9231a9bffb Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Mon, 9 Nov 2026 13:17:34 +0100 Subject: [PATCH 104/127] perf(ledger): validate proposals before Raft queue --- internal/infra/capabilities/capabilities.go | 4 ++-- internal/infra/node/drain.go | 15 ++++++--------- internal/infra/node/node.go | 5 ----- internal/infra/node/proposal.go | 6 ++++++ 4 files changed, 14 insertions(+), 16 deletions(-) diff --git a/internal/infra/capabilities/capabilities.go b/internal/infra/capabilities/capabilities.go index 7c27c7c0ac..55bf44968a 100644 --- a/internal/infra/capabilities/capabilities.go +++ b/internal/infra/capabilities/capabilities.go @@ -127,8 +127,8 @@ func PrepareProposal(proposal *raftcmdpb.Proposal) (PreparedProposal, error) { // ValidateFinalProposal verifies that the declaration carried by the // serialized proposal matches both its semantic payload and the metadata -// captured by the proposal factory. It is a defensive check at the Raft -// boundary; normal admission performs the same gate before reserving an index. +// captured by the proposal factory. The factory runs this before queueing the +// proposal, keeping full decoding off the Raft orchestrator. func ValidateFinalProposal(data []byte, declared []CapabilityID) error { proposal := new(raftcmdpb.Proposal) if err := proposal.UnmarshalVT(data); err != nil { diff --git a/internal/infra/node/drain.go b/internal/infra/node/drain.go index 1987af554e..41edf53013 100644 --- a/internal/infra/node/drain.go +++ b/internal/infra/node/drain.go @@ -49,11 +49,10 @@ func (node *Node) waitForPriorMembership(ctx context.Context) error { } cmd := commands.NewCommand() cmd.CallerSnapshot = commands.SystemCallerSnapshot(commands.ComponentClusterPeer) - data, err := cmd.MarshalVT() + proposal, err := NewProposal(cmd.GetId(), cmd) if err != nil { - return fmt.Errorf("marshaling membership barrier: %w", err) + return fmt.Errorf("preparing membership barrier: %w", err) } - proposal := newProposal(cmd.GetId(), data, nil) proposal.membershipBarrier = true node.indexTracker.Lock() future, err := node.Propose(ctx, proposal) @@ -114,11 +113,10 @@ func (node *Node) BeginDrain(ctx context.Context, epoch string, members []uint64 Kind: &raftcmdpb.TechnicalUpdate_DrainMarker{DrainMarker: &raftcmdpb.DrainMarker{Epoch: epoch, Members: append([]uint64(nil), members...)}}, }) cmd.CallerSnapshot = commands.SystemCallerSnapshot(commands.ComponentClusterPeer) - data, err := cmd.MarshalVT() + proposal, err := NewProposal(cmd.GetId(), cmd) if err != nil { - return nil, fmt.Errorf("marshaling drain marker: %w", err) + return nil, fmt.Errorf("preparing drain marker: %w", err) } - proposal := newProposal(cmd.GetId(), data, nil) proposal.drainMarker = true proposal.expectedTerm = expectedTerm @@ -178,11 +176,10 @@ func (node *Node) EndDrain(ctx context.Context, epoch string, expectedTerm uint6 Kind: &raftcmdpb.TechnicalUpdate_DrainRelease{DrainRelease: &raftcmdpb.DrainRelease{Epoch: epoch}}, }) cmd.CallerSnapshot = commands.SystemCallerSnapshot(commands.ComponentClusterPeer) - data, err := cmd.MarshalVT() + proposal, err := NewProposal(cmd.GetId(), cmd) if err != nil { - return nil, fmt.Errorf("marshaling drain release: %w", err) + return nil, fmt.Errorf("preparing drain release: %w", err) } - proposal := newProposal(cmd.GetId(), data, nil) proposal.releaseMarker = true proposal.expectedTerm = expectedTerm node.indexTracker.Lock() diff --git a/internal/infra/node/node.go b/internal/infra/node/node.go index 5bcc305df7..0627d5eb81 100644 --- a/internal/infra/node/node.go +++ b/internal/infra/node/node.go @@ -2485,11 +2485,6 @@ func (node *Node) handleProposal(p *Proposal) { err = ErrLeadershipLost } } - if err == nil { - if validationErr := capabilities.ValidateFinalProposal(p.data, p.requiredCapabilities); validationErr != nil { - err = validationErr - } - } if err == nil { err = node.rawNode.Propose(p.data) } diff --git a/internal/infra/node/proposal.go b/internal/infra/node/proposal.go index e91929d28d..47b249b141 100644 --- a/internal/infra/node/proposal.go +++ b/internal/infra/node/proposal.go @@ -60,6 +60,12 @@ func NewProposal(commandID uint64, command *raftcmdpb.Proposal) (*Proposal, erro } proposal := newProposal(commandID, data, prepared.RequiredCapabilities()) + // Validate the complete serialized command before it enters the Raft queue. + // SetPredictedIndex only appends a fixed-width index field afterward; it + // cannot change the payload or its capability declaration. + if err := capabilities.ValidateFinalProposal(data, proposal.requiredCapabilities); err != nil { + return nil, err + } if policy := command.GetSetClusterPolicy().GetPolicy(); policy != nil { for _, activation := range policy.GetCapabilityActivations() { if activation != nil && activation.GetActive() { From b1ed44a6e0b9c9feeaf699834c55156d7649d5c1 Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Mon, 9 Nov 2026 15:00:01 +0100 Subject: [PATCH 105/127] style(ledger): keep membership refresh formatting clean --- internal/bootstrap/module.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/bootstrap/module.go b/internal/bootstrap/module.go index 2796be706d..748e53c89b 100644 --- a/internal/bootstrap/module.go +++ b/internal/bootstrap/module.go @@ -1471,6 +1471,7 @@ func recoveredRefreshPeers(selfID uint64, addresses map[uint64]raftmembership.Co peers = append(peers, node.Peer{ID: id, Address: address.RaftAddress}) } } + return peers } From 7ccdbdaeaec38bfb67970294d2273ac19eab2480 Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Mon, 9 Nov 2026 15:00:09 +0100 Subject: [PATCH 106/127] docs(ledger): remove retired learner promotion command --- docs/ops/cli.md | 29 ----------------------------- 1 file changed, 29 deletions(-) diff --git a/docs/ops/cli.md b/docs/ops/cli.md index 10354309d1..ccfd84f9ab 100644 --- a/docs/ops/cli.md +++ b/docs/ops/cli.md @@ -2937,35 +2937,6 @@ ledgerctl cluster transfer-leader 2 ledgerctl cluster transfer-leader 3 --timeout 5s ``` -#### cluster promote-learner - -Promote a learner (non-voting) node to a full voter in the Raft cluster. The request is forwarded to the leader. - -```bash -ledgerctl cluster promote-learner [flags] -``` - -**Flags:** - -| Flag | Default | Description | -|------|---------|-------------| -| `--timeout` | `30s` | Request timeout | - -**Behavior:** -- The request is forwarded to the current leader if sent to a follower -- The leader proposes a ConfChange to promote the learner to a voter -- Once committed, the node participates in elections and can become leader - -**Example:** - -```bash -# Promote learner node 4 to voter -ledgerctl cluster promote-learner 4 - -# Promote with custom timeout -ledgerctl cluster promote-learner 5 --timeout 60s -``` - #### cluster remove-node Remove a node (voter or learner) from the Raft cluster. The request is forwarded to the leader. Cannot remove the leader itself; transfer leadership first. From 9a0c0e9d0987579ba081b7522b9524130bc99c55 Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Mon, 9 Nov 2026 15:00:19 +0100 Subject: [PATCH 107/127] docs(ledger): mark rc4 stopped upgrade as mandatory --- docs/ops/upgrades.md | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/docs/ops/upgrades.md b/docs/ops/upgrades.md index 97b3c72b7a..370cf3637d 100644 --- a/docs/ops/upgrades.md +++ b/docs/ops/upgrades.md @@ -551,25 +551,27 @@ installation, backup/restore, rollback, or the dormant stopped-upgrade path. The pinned binary-level rc.1 test and relevant model, restore and migration suites remain separate gates. -The same harness has a separate, future `rc3-to-rc4` transition: +The same harness has a separate `rc3-to-rc4` transition for the mandatory +operator-required stopped restart: ```bash UPGRADE_TRANSITION=rc3-to-rc4 RELEASE_BRANCH=release/v3.0 just test-release-operator-upgrade ``` -Run it against a release branch whose operator lists `v3.0.0-rc.4` as a -mandatory stop boundary. The target operator and Ledger are still built from -one exact release-branch HEAD and tagged locally as rc.4; the harness publishes no tag +Run it after `v3.0.0-rc.3` has been published, against a release branch whose +operator lists `v3.0.0-rc.4` as a mandatory stop boundary. The target operator +and Ledger are built from one exact release-branch HEAD and tagged locally as +rc.4; the harness publishes no tag or image. This path requires the operator's drain barrier to advance past `Draining`, observes the StatefulSet and all old Pods absent before accepting any target Pod, and checks that the replacement StatefulSet has a new UID while PVC UIDs persist. It restarts a target voter to exercise replay, verifies the pre-upgrade account, commits another transaction, checks Raft progress and runs each voter's audit/derived-state checker after drain release. It fails -against an operator without the rc.4 boundary, so it is a release gate for the -format-change PR, not evidence that rc.3 itself needs a stop. The rc.4 release -still needs format-specific migration, snapshot, backup/restore and rollback -validation outside this scenario. +against an operator without the rc.4 boundary or if the mandatory stop is +skipped, so it is a release gate for the format-change PR, not evidence that +rc.3 itself needs a stop. The rc.4 release still needs format-specific migration, +snapshot, backup/restore and rollback validation outside this scenario. ## Coordinated Restart From bb4c83314cc70d32efa93017a748c592392c4fd7 Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Wed, 11 Nov 2026 09:27:25 +0100 Subject: [PATCH 108/127] fix(ledger): serialize capability activation with membership --- internal/application/admission/admission.go | 45 +++++++++++++++++++ .../infra/node/capability_membership_test.go | 37 +++++++++++++++ internal/infra/node/node.go | 45 +++++++++++++++++++ 3 files changed, 127 insertions(+) create mode 100644 internal/infra/node/capability_membership_test.go diff --git a/internal/application/admission/admission.go b/internal/application/admission/admission.go index b6a700000d..3400b4d282 100644 --- a/internal/application/admission/admission.go +++ b/internal/application/admission/admission.go @@ -585,7 +585,27 @@ func (a *Admission) admit(ctx context.Context, prepare func(context.Context, *co return nil, err } + // An activating policy must not overtake an admitted ConfChange, nor may a + // later ConfChange pass it before the policy is durably applied. Acquire the + // membership fence before the proposal guard locks the index tracker. + var releaseMembership func(*futures.Future[state.ApplyResult]) + if proposalActivatesCapabilities(prepared.command) { + barrier, ok := a.proposer.(interface { + AcquireCapabilityMembership(context.Context) (func(*futures.Future[state.ApplyResult]), error) + }) + if !ok { + return nil, errors.New("activating policy proposer has no membership fence") + } + releaseMembership, err = barrier.AcquireCapabilityMembership(ctx) + if err != nil { + return nil, fmt.Errorf("fencing membership for capability activation: %w", err) + } + } + fsmFuture, cleanup, err := a.proposeAdmission(ctx, prepared.command, prepared.build) + if releaseMembership != nil { + releaseMembership(fsmFuture) + } if err != nil { return nil, err } @@ -594,6 +614,31 @@ func (a *Admission) admit(ctx context.Context, prepare func(context.Context, *co return a.resolveAdmissionResult(ctx, prepared.command, fsmFuture) } +func proposalActivatesCapabilities(command *raftcmdpb.Proposal) bool { + if command == nil { + return false + } + hasActive := func(policy *commonpb.ClusterPolicy) bool { + for _, activation := range policy.GetCapabilityActivations() { + if activation != nil && activation.GetActive() { + return true + } + } + + return false + } + if hasActive(command.GetSetClusterPolicy().GetPolicy()) { + return true + } + for _, order := range command.GetOrders() { + if hasActive(order.GetSystemScoped().GetSetClusterPolicy().GetPolicy()) { + return true + } + } + + return false +} + func (a *Admission) prepareExecutionBatchAdmission(ctx context.Context, req *servicepb.ApplyRequest) (verifiedBatch, error) { batch, err := a.resolveExecutionBatch(ctx, req) if err != nil { diff --git a/internal/infra/node/capability_membership_test.go b/internal/infra/node/capability_membership_test.go new file mode 100644 index 0000000000..5e8c16146b --- /dev/null +++ b/internal/infra/node/capability_membership_test.go @@ -0,0 +1,37 @@ +package node + +import ( + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/formancehq/ledger/v3/internal/infra/state" + "github.com/formancehq/ledger/v3/internal/pkg/futures" +) + +func TestCapabilityMembershipFenceHeldUntilPolicyApply(t *testing.T) { + n := &Node{stopChannel: make(chan struct{}), terminalCh: make(chan struct{})} + n.confChangeMu.Lock() // acquired before the policy proposal is enqueued + future := futures.New[state.ApplyResult]() + n.releaseCapabilityMembershipAfter(future) + require.False(t, n.confChangeMu.TryLock(), "a learner must not pass a pending activation") + + future.Resolve(state.ApplyResult{}, nil) + require.Eventually(t, func() bool { + if !n.confChangeMu.TryLock() { + return false + } + n.confChangeMu.Unlock() + + return true + }, time.Second, time.Millisecond) +} + +func TestCapabilityMembershipFenceReleasedWithoutProposal(t *testing.T) { + n := &Node{} + n.confChangeMu.Lock() + n.releaseCapabilityMembershipAfter(nil) + require.True(t, n.confChangeMu.TryLock()) + n.confChangeMu.Unlock() +} diff --git a/internal/infra/node/node.go b/internal/infra/node/node.go index 0627d5eb81..0c86304f1e 100644 --- a/internal/infra/node/node.go +++ b/internal/infra/node/node.go @@ -3242,6 +3242,51 @@ func (node *Node) WithStableDrainMembership(ctx context.Context, fn func() error return fn() } +// AcquireCapabilityMembership fences an activating policy proposal against +// membership changes. The caller must acquire this before the proposal/index +// guard, then hand the returned release function the FSM future immediately +// after enqueue. The lock remains held through apply even if the request is +// canceled. A nil future releases it when no proposal was enqueued. +func (node *Node) AcquireCapabilityMembership(ctx context.Context) (func(*futures.Future[state.ApplyResult]), error) { + for !node.confChangeMu.TryLock() { + select { + case <-ctx.Done(): + return nil, ctx.Err() + case <-time.After(10 * time.Millisecond): + } + } + if err := node.waitForPriorMembership(ctx); err != nil { + node.confChangeMu.Unlock() + + return nil, err + } + + return node.releaseCapabilityMembershipAfter, nil +} + +func (node *Node) releaseCapabilityMembershipAfter(future *futures.Future[state.ApplyResult]) { + if future == nil { + node.confChangeMu.Unlock() + + return + } + go func() { + defer node.confChangeMu.Unlock() + waitCtx, cancel := context.WithCancel(context.Background()) + defer cancel() + go func() { + select { + case <-node.stopChannel: + cancel() + case <-node.terminalCh: + cancel() + case <-waitCtx.Done(): + } + }() + _, _ = future.Wait(waitCtx) + }() +} + // JoinAsLearner is the bootstrap admission path for a process with an empty // WAL. It deliberately keeps EN-1436's stricter stale-progress handling // separate from administrative AddLearner retries and carries the joining From 72c33d41363a8864d1937f58cde537275bad55a7 Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Wed, 11 Nov 2026 09:36:23 +0100 Subject: [PATCH 109/127] fix(ledger): reject unsupported active capabilities on recovery --- internal/bootstrap/module.go | 4 +++ internal/infra/capabilities/capabilities.go | 12 +++++++++ .../infra/capabilities/capabilities_test.go | 9 +++++++ internal/infra/membership/membership.go | 26 ++++++++++++++----- internal/infra/membership/membership_test.go | 26 ++++++++++++++++++- 5 files changed, 69 insertions(+), 8 deletions(-) diff --git a/internal/bootstrap/module.go b/internal/bootstrap/module.go index 748e53c89b..1dcd8f88cb 100644 --- a/internal/bootstrap/module.go +++ b/internal/bootstrap/module.go @@ -246,6 +246,10 @@ func Module() fx.Option { // not perform any Pebble read itself. func(machine *state.Machine, store *dal.Store, membership *raftmembership.Membership) (*state.Recovery, error) { recovery := state.NewRecovery(machine, store, state.WithStateRecovered(func(recovered *state.FSMState) error { + if err := capabilities.ValidateSupportedActive(recovered.ClusterPolicy, capabilities.SupportedIDs()); err != nil { + return err + } + return publishCapabilityGate(membership, recovered.ClusterPolicy) })) if err := recovery.RecoverState(); err != nil { diff --git a/internal/infra/capabilities/capabilities.go b/internal/infra/capabilities/capabilities.go index 55bf44968a..3e8f1064f8 100644 --- a/internal/infra/capabilities/capabilities.go +++ b/internal/infra/capabilities/capabilities.go @@ -197,6 +197,18 @@ func ActiveIDs(policy *commonpb.ClusterPolicy) []uint32 { return active } +// ValidateSupportedActive refuses a persisted policy that this binary cannot +// apply. It is used at boot and after installing a leader checkpoint. +func ValidateSupportedActive(policy *commonpb.ClusterPolicy, supported []CapabilityID) error { + for _, id := range ActiveIDs(policy) { + if !slices.Contains(supported, CapabilityID(id)) { + return fmt.Errorf("active capability %d is not supported by this binary", id) + } + } + + return nil +} + // IDsAsUint32 converts the typed capability identifiers to their protobuf wire // representation at an API boundary. func IDsAsUint32(ids []CapabilityID) []uint32 { diff --git a/internal/infra/capabilities/capabilities_test.go b/internal/infra/capabilities/capabilities_test.go index d1373d6ab3..15c581b4a7 100644 --- a/internal/infra/capabilities/capabilities_test.go +++ b/internal/infra/capabilities/capabilities_test.go @@ -13,6 +13,15 @@ func capabilityState() State { return State{Activations: map[CapabilityID]bool{}} } +func TestValidateSupportedActiveRefusesDowngrade(t *testing.T) { + policy := &commonpb.ClusterPolicy{CapabilityActivations: []*commonpb.CapabilityActivation{ + {Id: 7, Active: true}, + {Id: 8, Active: false}, + }} + require.NoError(t, ValidateSupportedActive(policy, []CapabilityID{7})) + require.ErrorContains(t, ValidateSupportedActive(policy, nil), "active capability 7") +} + func TestEvaluateIncludesVotersAndLearners(t *testing.T) { state := capabilityState() state.Activations[99] = true diff --git a/internal/infra/membership/membership.go b/internal/infra/membership/membership.go index 61160b792e..02de44f364 100644 --- a/internal/infra/membership/membership.go +++ b/internal/infra/membership/membership.go @@ -12,6 +12,7 @@ import ( logging "github.com/formancehq/go-libs/v5/pkg/observe/log" + "github.com/formancehq/ledger/v3/internal/infra/capabilities" "github.com/formancehq/ledger/v3/internal/proto/commonpb" "github.com/formancehq/ledger/v3/internal/proto/raftcmdpb" "github.com/formancehq/ledger/v3/internal/storage/dal" @@ -393,10 +394,10 @@ func (m *Membership) Remove(nodeID uint64) { // identity. Identity-less membership rows are rejected rather than persisted // as a partial member. capabilityIDs carries the peer's capability evidence. func (m *Membership) Register(nodeID uint64, raftAddr, serviceAddr string, instanceID []byte, capabilityIDs []uint32) error { - // A restart refreshes the local addresses. Preserve evidence for the same - // concrete instance when the caller has no new proof; a changed instance - // must never inherit the previous pod's capabilities. - if len(capabilityIDs) == 0 { + // A restart refreshes the local addresses. Preserve evidence for another + // peer when no new proof is available, but never preserve self evidence: + // an empty self list is the binary's real list after a downgrade. + if len(capabilityIDs) == 0 && nodeID != m.selfNodeID { if existing, ok := m.PeerAddresses()[nodeID]; ok && bytes.Equal(existing.InstanceID, instanceID) { capabilityIDs = existing.Capabilities } @@ -619,6 +620,19 @@ func (m *Membership) Rehydrate() error { // checkpoint introduced an identity-less configured member would violate the // cluster's removal/admission contract. func (m *Membership) OnSnapshotInstalled() { + // The checkpoint and spool replay may have introduced a policy that this + // binary cannot execute. Stop before accepting any further Raft entries. + m.mu.RLock() + provider := m.capabilityPolicyProvider + m.mu.RUnlock() + policy, err := provider() + if err != nil { + panic(fmt.Errorf("reading cluster policy after snapshot install: %w", err)) + } + if err := capabilities.ValidateSupportedActive(policy, capabilities.SupportedIDs()); err != nil { + panic(fmt.Errorf("snapshot requires unsupported capability: %w", err)) + } + // Force-write the locally-authoritative self row to Pebble BEFORE // the reload, so LoadAll pulls our address rather than the leader's // potentially-stale view and the next checkpoint we serve is @@ -633,9 +647,7 @@ func (m *Membership) OnSnapshotInstalled() { // Better to fall through and let Rehydrate reload from Pebble // (with the leader's possibly-stale self); the next OnSnapshot // or restart will re-attempt the self-Put. - m.mu.RLock() - selfCapabilities := append([]uint32(nil), m.addresses[m.selfNodeID].Capabilities...) - m.mu.RUnlock() + selfCapabilities := capabilities.IDsAsUint32(capabilities.SupportedIDs()) if err := m.store.PutWithEvidence(m.selfNodeID, m.selfRaftAddr, m.selfServiceAddr, m.selfInstanceID, selfCapabilities); err != nil { m.logger.WithFields(map[string]any{ "error": err, diff --git a/internal/infra/membership/membership_test.go b/internal/infra/membership/membership_test.go index cc5e7823a6..3802e4a827 100644 --- a/internal/infra/membership/membership_test.go +++ b/internal/infra/membership/membership_test.go @@ -11,6 +11,7 @@ import ( logging "github.com/formancehq/go-libs/v5/pkg/observe/log" + "github.com/formancehq/ledger/v3/internal/proto/commonpb" "github.com/formancehq/ledger/v3/internal/proto/raftcmdpb" "github.com/formancehq/ledger/v3/internal/storage/dal" ) @@ -432,10 +433,33 @@ func TestMembership_OnSnapshotInstalled(t *testing.T) { require.Equal(t, "new:1", got[1].RaftAddress) require.Equal(t, "new:3", got[3].RaftAddress) require.Equal(t, testSelfRaftAddr, got[testSelfNodeID].RaftAddress, "self must be re-upserted with local truth") - require.Equal(t, []uint32{7}, got[testSelfNodeID].Capabilities, "self capabilities must survive snapshot restore") + require.Empty(t, got[testSelfNodeID].Capabilities, "self evidence must reflect the running binary after snapshot restore") require.NotContains(t, got, uint64(7)) } +func TestMembershipRegisterSelfClearsStaleCapabilityEvidence(t *testing.T) { + ps := newTestPeerStore(t) + require.NoError(t, ps.PutWithEvidence(testSelfNodeID, testSelfRaftAddr, testSelfServiceAddr, fixedInstanceID(0x42), []uint32{7})) + m, err := NewMembership(ps, noopTransport{}, noopPool{}, testSelfNodeID, testSelfRaftAddr, testSelfServiceAddr, fixedInstanceID(0x42), logging.Testing(), NoopClusterPolicyProvider) + require.NoError(t, err) + + require.NoError(t, m.Register(testSelfNodeID, testSelfRaftAddr, testSelfServiceAddr, fixedInstanceID(0x42), nil)) + peers, err := ps.LoadAll() + require.NoError(t, err) + require.Empty(t, peers[testSelfNodeID].Capabilities) + require.Empty(t, m.PeerAddresses()[testSelfNodeID].Capabilities) +} + +func TestMembershipOnSnapshotInstalledRejectsUnsupportedActiveCapability(t *testing.T) { + ps := newTestPeerStore(t) + provider := func() (*commonpb.ClusterPolicy, error) { + return &commonpb.ClusterPolicy{CapabilityActivations: []*commonpb.CapabilityActivation{{Id: 7, Active: true}}}, nil + } + m, err := NewMembership(ps, noopTransport{}, noopPool{}, testSelfNodeID, testSelfRaftAddr, testSelfServiceAddr, fixedInstanceID(0x42), logging.Testing(), provider) + require.NoError(t, err) + require.PanicsWithError(t, "snapshot requires unsupported capability: active capability 7 is not supported by this binary", m.OnSnapshotInstalled) +} + func TestMembership_SnapshotDonorCandidates(t *testing.T) { t.Parallel() From 0d2d9d88017ae492fdb0667f9fc5b73446ae6e05 Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Wed, 11 Nov 2026 09:40:27 +0100 Subject: [PATCH 110/127] fix(ledger): retain activation fence after canceled wait --- internal/application/admission/admission.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/internal/application/admission/admission.go b/internal/application/admission/admission.go index 3400b4d282..88e798840f 100644 --- a/internal/application/admission/admission.go +++ b/internal/application/admission/admission.go @@ -907,7 +907,9 @@ func (a *Admission) proposeAdmission(ctx context.Context, cmd *raftcmdpb.Proposa guard.ReleaseLoaders() a.proposeQueueInflight.Add(-1) - return nil, nil, err + // The proposal was already enqueued. Keep its completion handle so + // capability activation retains the membership fence after cancellation. + return fsmFuture, nil, err } recordProposeDuration() From c852e720acde711f5a1dac06c305fe59d989687c Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Wed, 11 Nov 2026 11:55:51 +0100 Subject: [PATCH 111/127] fix(ledger): reserve predicted index suffix before tracker lock --- internal/infra/node/proposal.go | 6 +++++- internal/infra/node/proposal_test.go | 18 ++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/internal/infra/node/proposal.go b/internal/infra/node/proposal.go index 47b249b141..77bb0f0cf3 100644 --- a/internal/infra/node/proposal.go +++ b/internal/infra/node/proposal.go @@ -37,9 +37,13 @@ type Proposal struct { } func newProposal(commandID uint64, data []byte, required []capabilities.CapabilityID) *Proposal { + // Reserve the wire tag and fixed64 value before the index tracker is held. + // SetPredictedIndex must only append into this buffer in its critical section. + buffer := make([]byte, len(data), len(data)+protowire.SizeTag(predictedIndexField)+8) + copy(buffer, data) return &Proposal{ commandID: commandID, - data: append([]byte(nil), data...), + data: buffer, requiredCapabilities: append([]capabilities.CapabilityID(nil), required...), Future: futures.New[any](), } diff --git a/internal/infra/node/proposal_test.go b/internal/infra/node/proposal_test.go index 9bd7f2e367..73ade3b1a3 100644 --- a/internal/infra/node/proposal_test.go +++ b/internal/infra/node/proposal_test.go @@ -1,8 +1,11 @@ package node import ( + "fmt" "testing" + "google.golang.org/protobuf/encoding/protowire" + "github.com/stretchr/testify/require" "github.com/formancehq/ledger/v3/internal/infra/capabilities" @@ -47,6 +50,21 @@ func TestNewProposalAppendsPredictedIndexWithoutMutatingPrefix(t *testing.T) { require.Equal(t, before, after[:len(before)]) } +func TestNewProposalReservesPredictedIndexCapacity(t *testing.T) { + for _, size := range []int{77, 8192, 1048576} { + t.Run(fmt.Sprintf("size-%d", size), func(t *testing.T) { + command := &raftcmdpb.Proposal{Id: 1} + command.ProtoReflect().SetUnknown(protowire.AppendBytes(protowire.AppendTag(nil, 2047, protowire.BytesType), make([]byte, size))) + proposal, err := NewProposal(1, command) + require.NoError(t, err) + before := &proposal.data[0] + require.GreaterOrEqual(t, cap(proposal.data)-len(proposal.data), protowire.SizeTag(predictedIndexField)+8) + proposal.SetPredictedIndex(42) + require.Same(t, before, &proposal.data[0]) + }) + } +} + func TestNewProposalDerivesActivePolicyCapabilities(t *testing.T) { command := &raftcmdpb.Proposal{ Id: 1, From 0a9b48a63a81863ff5e2e0ca934debc02eb5c30f Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Wed, 11 Nov 2026 12:11:49 +0100 Subject: [PATCH 112/127] style(ledger): apply generated Go formatting --- internal/infra/node/proposal.go | 1 + internal/infra/node/proposal_test.go | 3 +-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/infra/node/proposal.go b/internal/infra/node/proposal.go index 77bb0f0cf3..1c47babe5a 100644 --- a/internal/infra/node/proposal.go +++ b/internal/infra/node/proposal.go @@ -41,6 +41,7 @@ func newProposal(commandID uint64, data []byte, required []capabilities.Capabili // SetPredictedIndex must only append into this buffer in its critical section. buffer := make([]byte, len(data), len(data)+protowire.SizeTag(predictedIndexField)+8) copy(buffer, data) + return &Proposal{ commandID: commandID, data: buffer, diff --git a/internal/infra/node/proposal_test.go b/internal/infra/node/proposal_test.go index 73ade3b1a3..2f93522166 100644 --- a/internal/infra/node/proposal_test.go +++ b/internal/infra/node/proposal_test.go @@ -4,9 +4,8 @@ import ( "fmt" "testing" - "google.golang.org/protobuf/encoding/protowire" - "github.com/stretchr/testify/require" + "google.golang.org/protobuf/encoding/protowire" "github.com/formancehq/ledger/v3/internal/infra/capabilities" "github.com/formancehq/ledger/v3/internal/proto/commonpb" From d7a1af99f4f19073c477f8f7767154193b37a0bd Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Wed, 11 Nov 2026 13:33:11 +0100 Subject: [PATCH 113/127] fix(ledger): treat startup refresh RPC deadlines as transient --- internal/bootstrap/module.go | 2 +- .../bootstrap/module_refresh_membership_test.go | 16 +++++++++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/internal/bootstrap/module.go b/internal/bootstrap/module.go index 1dcd8f88cb..df17f59d0f 100644 --- a/internal/bootstrap/module.go +++ b/internal/bootstrap/module.go @@ -1502,7 +1502,7 @@ func tryRefreshMembershipForStartupWithTimeout(ctx context.Context, cfg Config, // Preserve cancellation of the lifecycle itself as a hard error. Only the // timeout introduced for this bounded attempt is transient. - if ctx.Err() == nil && (errors.Is(attemptCtx.Err(), context.DeadlineExceeded) || errors.Is(err, context.DeadlineExceeded)) { + if ctx.Err() == nil && (errors.Is(attemptCtx.Err(), context.DeadlineExceeded) || errors.Is(err, context.DeadlineExceeded) || status.Code(err) == codes.DeadlineExceeded) { return fmt.Errorf("%w: synchronous refresh attempt exceeded %s: %w", errMembershipRefreshTransient, timeout, err) } diff --git a/internal/bootstrap/module_refresh_membership_test.go b/internal/bootstrap/module_refresh_membership_test.go index 7f8bf65872..9abc4978b1 100644 --- a/internal/bootstrap/module_refresh_membership_test.go +++ b/internal/bootstrap/module_refresh_membership_test.go @@ -2,6 +2,7 @@ package bootstrap import ( "context" + "errors" "net" "sync/atomic" "testing" @@ -142,5 +143,18 @@ func TestTryRefreshMembershipForStartupTreatsBoundedAttemptAsTransient(t *testin ) require.ErrorIs(t, err, errMembershipRefreshTransient) - require.ErrorIs(t, err, context.DeadlineExceeded) + require.True(t, errors.Is(err, context.DeadlineExceeded) || status.Code(err) == codes.DeadlineExceeded) +} + +func TestTryRefreshMembershipForStartupTreatsEarlyRPCDeadlineAsTransient(t *testing.T) { + t.Parallel() + + peer := &retryingRefreshServer{firstErr: status.Error(codes.DeadlineExceeded, "leader proposal timed out")} + cfg := refreshTestConfig(t, serveRefreshMembership(t, peer)) + err := tryRefreshMembershipForStartupWithTimeout( + context.Background(), cfg, cfg.TLSConfig, logging.Testing(), time.Second, + ) + + require.ErrorIs(t, err, errMembershipRefreshTransient) + require.Equal(t, int32(1), peer.attempts.Load()) } From 2f2e5468381c4a19540f2ee38ea9386b2506660f Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Wed, 11 Nov 2026 14:22:09 +0100 Subject: [PATCH 114/127] docs(ledger): clarify startup refresh deadline handling --- internal/bootstrap/module.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/bootstrap/module.go b/internal/bootstrap/module.go index df17f59d0f..d8c45436d2 100644 --- a/internal/bootstrap/module.go +++ b/internal/bootstrap/module.go @@ -1500,8 +1500,8 @@ func tryRefreshMembershipForStartupWithTimeout(ctx context.Context, cfg Config, return nil } - // Preserve cancellation of the lifecycle itself as a hard error. Only the - // timeout introduced for this bounded attempt is transient. + // Preserve lifecycle cancellation as a hard error. The bounded attempt's + // expiry and a remote RPC deadline are transient while the lifecycle is live. if ctx.Err() == nil && (errors.Is(attemptCtx.Err(), context.DeadlineExceeded) || errors.Is(err, context.DeadlineExceeded) || status.Code(err) == codes.DeadlineExceeded) { return fmt.Errorf("%w: synchronous refresh attempt exceeded %s: %w", errMembershipRefreshTransient, timeout, err) } From c27bde1e6a2a7c8a1b9a4e48a76d60ff0e30bcf2 Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Wed, 11 Nov 2026 14:27:54 +0100 Subject: [PATCH 115/127] fix(ledger): retry remote membership refresh deadlines --- internal/bootstrap/module.go | 2 +- .../bootstrap/module_refresh_membership_test.go | 16 +++++++++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/internal/bootstrap/module.go b/internal/bootstrap/module.go index d8c45436d2..0c2ffcb4dd 100644 --- a/internal/bootstrap/module.go +++ b/internal/bootstrap/module.go @@ -1775,7 +1775,7 @@ func isRetryableMembershipRefreshError(err error) bool { return false } - if st.Code() == codes.Unavailable { + if st.Code() == codes.Unavailable || st.Code() == codes.DeadlineExceeded { return true } diff --git a/internal/bootstrap/module_refresh_membership_test.go b/internal/bootstrap/module_refresh_membership_test.go index 9abc4978b1..5b596720bb 100644 --- a/internal/bootstrap/module_refresh_membership_test.go +++ b/internal/bootstrap/module_refresh_membership_test.go @@ -28,6 +28,7 @@ func TestIsRetryableMembershipRefreshError(t *testing.T) { want bool }{ {name: "unavailable", code: codes.Unavailable, want: true}, + {name: "remote deadline", code: codes.DeadlineExceeded, want: true}, {name: "old peer missing rpc", code: codes.Unimplemented, want: true}, {name: "invalid request", code: codes.InvalidArgument, want: false}, } { @@ -43,6 +44,7 @@ type retryingRefreshServer struct { attempts atomic.Int32 firstErr error + failures int32 } type blockingRefreshServer struct { @@ -56,7 +58,7 @@ func (blockingRefreshServer) RefreshMembership(ctx context.Context, _ *clusterbo } func (s *retryingRefreshServer) RefreshMembership(context.Context, *clusterbootstrappb.RefreshMembershipRequest) (*clusterbootstrappb.RefreshMembershipResponse, error) { - if s.attempts.Add(1) == 1 { + if s.attempts.Add(1) <= max(1, s.failures) { return nil, s.firstErr } @@ -103,6 +105,18 @@ func TestTryRefreshMembershipRetriesUnimplementedUntilPeerSupportsRPC(t *testing require.Equal(t, int32(2), peer.attempts.Load()) } +func TestTryRefreshMembershipRetriesRemoteDeadlinesUntilSuccess(t *testing.T) { + t.Parallel() + + peer := &retryingRefreshServer{firstErr: status.Error(codes.DeadlineExceeded, "leader proposal timed out"), failures: 2} + cfg := refreshTestConfig(t, serveRefreshMembership(t, peer)) + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + t.Cleanup(cancel) + require.NoError(t, tryRefreshMembership(ctx, cfg, cfg.TLSConfig, logging.Testing())) + require.Equal(t, int32(3), peer.attempts.Load()) +} + func TestTryRefreshMembershipStopsRetryingWhenContextIsCanceled(t *testing.T) { t.Parallel() From 252d5518f7fbbf7460b4eb3b22a3332808e37610 Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Wed, 11 Nov 2026 15:02:30 +0100 Subject: [PATCH 116/127] chore(proto): regenerate common descriptor with pinned protoc --- internal/proto/commonpb/common.pb.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/proto/commonpb/common.pb.go b/internal/proto/commonpb/common.pb.go index 2c6f21f593..d18201be0a 100644 --- a/internal/proto/commonpb/common.pb.go +++ b/internal/proto/commonpb/common.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.12 -// protoc v7.36.2 +// protoc v7.34.2 // source: common.proto package commonpb From cb1af52cfeafabc61ba909d5bd2bc118c3f014d5 Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Wed, 11 Nov 2026 15:02:31 +0100 Subject: [PATCH 117/127] refactor(ledger): inspect dedicated policy proposals for activation --- internal/application/admission/admission.go | 19 ++----------------- 1 file changed, 2 insertions(+), 17 deletions(-) diff --git a/internal/application/admission/admission.go b/internal/application/admission/admission.go index 88e798840f..1c60703612 100644 --- a/internal/application/admission/admission.go +++ b/internal/application/admission/admission.go @@ -615,23 +615,8 @@ func (a *Admission) admit(ctx context.Context, prepare func(context.Context, *co } func proposalActivatesCapabilities(command *raftcmdpb.Proposal) bool { - if command == nil { - return false - } - hasActive := func(policy *commonpb.ClusterPolicy) bool { - for _, activation := range policy.GetCapabilityActivations() { - if activation != nil && activation.GetActive() { - return true - } - } - - return false - } - if hasActive(command.GetSetClusterPolicy().GetPolicy()) { - return true - } - for _, order := range command.GetOrders() { - if hasActive(order.GetSystemScoped().GetSetClusterPolicy().GetPolicy()) { + for _, activation := range command.GetSetClusterPolicy().GetPolicy().GetCapabilityActivations() { + if activation != nil && activation.GetActive() { return true } } From 266722e8e42818bd25e44d31bb4b2b7c7c14f518 Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Wed, 11 Nov 2026 15:33:14 +0100 Subject: [PATCH 118/127] refactor(ledger): isolate capability policy admission fence --- internal/application/admission/admission.go | 53 ++++++++----------- .../admission/capability_policy_test.go | 4 ++ .../admission/proposer_generated_test.go | 39 ++++++++++++++ 3 files changed, 65 insertions(+), 31 deletions(-) diff --git a/internal/application/admission/admission.go b/internal/application/admission/admission.go index 1c60703612..917f01cce2 100644 --- a/internal/application/admission/admission.go +++ b/internal/application/admission/admission.go @@ -44,6 +44,7 @@ var tracer = otel.Tracer("admission") //go:generate mockgen -write_source_comment=false -write_package_comment=false -source admission.go -destination proposer_generated_test.go -typed -package admission . Proposer type Proposer interface { Propose(context.Context, *node.Proposal) (*futures.Future[state.ApplyResult], error) + AcquireCapabilityMembership(context.Context) (func(*futures.Future[state.ApplyResult]), error) InitialIndex() uint64 } @@ -487,7 +488,7 @@ func (a *Admission) recordPhaseOnExit(ctx context.Context, hist metric.Float64Hi func (a *Admission) Admit(ctx context.Context, req *servicepb.ApplyRequest) (*domain.ApplyResult, error) { return a.admit(ctx, func(ctx context.Context, caller *commonpb.CallerSnapshot) (*preparedAdmission, error) { return a.prepareExecutionBatch(ctx, caller, req) - }) + }, a.proposeAdmission) } // AdmitClusterPolicy admits the leader's internal cluster-policy reconciliation @@ -497,9 +498,16 @@ func (a *Admission) AdmitClusterPolicy(ctx context.Context, policy *commonpb.Clu return nil, errors.New("cluster policy must not be nil") } + propose := a.proposeAdmission + for _, activation := range policy.GetCapabilityActivations() { + if activation != nil && activation.GetActive() { + propose = a.proposeActivatingPolicy + break + } + } return a.admit(ctx, func(ctx context.Context, caller *commonpb.CallerSnapshot) (*preparedAdmission, error) { return a.prepareClusterPolicy(ctx, caller, policy) - }) + }, propose) } type preparedAdmission struct { @@ -548,7 +556,9 @@ func (a *Admission) prepareClusterPolicy(ctx context.Context, caller *commonpb.C return &preparedAdmission{command: command, build: build}, nil } -func (a *Admission) admit(ctx context.Context, prepare func(context.Context, *commonpb.CallerSnapshot) (*preparedAdmission, error)) (response *domain.ApplyResult, err error) { +type admissionPropose func(context.Context, *raftcmdpb.Proposal, *plan.BuildResult) (*futures.Future[state.ApplyResult], func(), error) + +func (a *Admission) admit(ctx context.Context, prepare func(context.Context, *commonpb.CallerSnapshot) (*preparedAdmission, error), propose admissionPropose) (response *domain.ApplyResult, err error) { caller, err := auth.ResolveCallerAttribution(ctx) if err != nil { invalid, ok := errors.AsType[*domain.ErrInvalidCallerAttribution](err) @@ -585,27 +595,7 @@ func (a *Admission) admit(ctx context.Context, prepare func(context.Context, *co return nil, err } - // An activating policy must not overtake an admitted ConfChange, nor may a - // later ConfChange pass it before the policy is durably applied. Acquire the - // membership fence before the proposal guard locks the index tracker. - var releaseMembership func(*futures.Future[state.ApplyResult]) - if proposalActivatesCapabilities(prepared.command) { - barrier, ok := a.proposer.(interface { - AcquireCapabilityMembership(context.Context) (func(*futures.Future[state.ApplyResult]), error) - }) - if !ok { - return nil, errors.New("activating policy proposer has no membership fence") - } - releaseMembership, err = barrier.AcquireCapabilityMembership(ctx) - if err != nil { - return nil, fmt.Errorf("fencing membership for capability activation: %w", err) - } - } - - fsmFuture, cleanup, err := a.proposeAdmission(ctx, prepared.command, prepared.build) - if releaseMembership != nil { - releaseMembership(fsmFuture) - } + fsmFuture, cleanup, err := propose(ctx, prepared.command, prepared.build) if err != nil { return nil, err } @@ -614,14 +604,15 @@ func (a *Admission) admit(ctx context.Context, prepare func(context.Context, *co return a.resolveAdmissionResult(ctx, prepared.command, fsmFuture) } -func proposalActivatesCapabilities(command *raftcmdpb.Proposal) bool { - for _, activation := range command.GetSetClusterPolicy().GetPolicy().GetCapabilityActivations() { - if activation != nil && activation.GetActive() { - return true - } +func (a *Admission) proposeActivatingPolicy(ctx context.Context, command *raftcmdpb.Proposal, build *plan.BuildResult) (*futures.Future[state.ApplyResult], func(), error) { + // Fence membership before the proposal guard locks the index tracker. + release, err := a.proposer.AcquireCapabilityMembership(ctx) + if err != nil { + return nil, nil, fmt.Errorf("fencing membership for capability activation: %w", err) } - - return false + future, cleanup, err := a.proposeAdmission(ctx, command, build) + release(future) + return future, cleanup, err } func (a *Admission) prepareExecutionBatchAdmission(ctx context.Context, req *servicepb.ApplyRequest) (verifiedBatch, error) { diff --git a/internal/application/admission/capability_policy_test.go b/internal/application/admission/capability_policy_test.go index 96a993502b..6e06158ee5 100644 --- a/internal/application/admission/capability_policy_test.go +++ b/internal/application/admission/capability_policy_test.go @@ -14,6 +14,10 @@ import ( type barrierTestProposer struct{} +func (barrierTestProposer) AcquireCapabilityMembership(context.Context) (func(*futures.Future[state.ApplyResult]), error) { + return func(*futures.Future[state.ApplyResult]) {}, nil +} + func (barrierTestProposer) InitialIndex() uint64 { return 1 } func (barrierTestProposer) Propose(_ context.Context, proposal *node.Proposal) (*futures.Future[state.ApplyResult], error) { diff --git a/internal/application/admission/proposer_generated_test.go b/internal/application/admission/proposer_generated_test.go index e25603d8af..d1b33f615b 100644 --- a/internal/application/admission/proposer_generated_test.go +++ b/internal/application/admission/proposer_generated_test.go @@ -41,6 +41,45 @@ func (m *MockProposer) EXPECT() *MockProposerMockRecorder { return m.recorder } +// AcquireCapabilityMembership mocks base method. +func (m *MockProposer) AcquireCapabilityMembership(arg0 context.Context) (func(*futures.Future[state.ApplyResult]), error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AcquireCapabilityMembership", arg0) + ret0, _ := ret[0].(func(*futures.Future[state.ApplyResult])) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// AcquireCapabilityMembership indicates an expected call of AcquireCapabilityMembership. +func (mr *MockProposerMockRecorder) AcquireCapabilityMembership(arg0 any) *MockProposerAcquireCapabilityMembershipCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AcquireCapabilityMembership", reflect.TypeOf((*MockProposer)(nil).AcquireCapabilityMembership), arg0) + return &MockProposerAcquireCapabilityMembershipCall{Call: call} +} + +// MockProposerAcquireCapabilityMembershipCall wrap *gomock.Call +type MockProposerAcquireCapabilityMembershipCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockProposerAcquireCapabilityMembershipCall) Return(arg0 func(*futures.Future[state.ApplyResult]), arg1 error) *MockProposerAcquireCapabilityMembershipCall { + c.Call = c.Call.Return(arg0, arg1) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockProposerAcquireCapabilityMembershipCall) Do(f func(context.Context) (func(*futures.Future[state.ApplyResult]), error)) *MockProposerAcquireCapabilityMembershipCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockProposerAcquireCapabilityMembershipCall) DoAndReturn(f func(context.Context) (func(*futures.Future[state.ApplyResult]), error)) *MockProposerAcquireCapabilityMembershipCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + // InitialIndex mocks base method. func (m *MockProposer) InitialIndex() uint64 { m.ctrl.T.Helper() From 190b172fc13ae92aa249820913f8af5cf7728736 Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Wed, 11 Nov 2026 16:29:10 +0100 Subject: [PATCH 119/127] refactor(ledger): inject capability membership fence separately --- cmd/ledgerctl/store/bootstrap_retry_test.go | 2 +- .../adapter/grpc/auth_interceptor_test.go | 1 + .../grpc/oidc_caller_attribution_test.go | 2 +- internal/application/admission/admission.go | 13 ++- .../admission/admission_phase_metrics_test.go | 1 + .../application/admission/admission_test.go | 1 + .../admission/capability_policy_test.go | 4 - .../admission/metadata_replay_test.go | 2 +- .../admission/proposer_generated_test.go | 102 +++++++++++------- .../mirror/worker_diagnostic_test.go | 4 +- internal/bootstrap/module.go | 1 + .../backup/numscript_restore_parity_test.go | 2 +- 12 files changed, 84 insertions(+), 51 deletions(-) diff --git a/cmd/ledgerctl/store/bootstrap_retry_test.go b/cmd/ledgerctl/store/bootstrap_retry_test.go index 3bb946868d..1e136af21b 100644 --- a/cmd/ledgerctl/store/bootstrap_retry_test.go +++ b/cmd/ledgerctl/store/bootstrap_retry_test.go @@ -90,7 +90,7 @@ func newBootstrapRetryAdmission(t *testing.T, store *dal.Store) *admission.Admis return future, nil }).AnyTimes() - return admission.NewAdmission(store, logger, proposer, + return admission.NewAdmission(store, logger, proposer, nil, builder, meters, gate, keys, shared, attrs, numscript.NewNumscriptCache(0), func(context.Context) error { return nil }) } diff --git a/internal/adapter/grpc/auth_interceptor_test.go b/internal/adapter/grpc/auth_interceptor_test.go index dd2eb30a2c..0a4b2faacb 100644 --- a/internal/adapter/grpc/auth_interceptor_test.go +++ b/internal/adapter/grpc/auth_interceptor_test.go @@ -532,6 +532,7 @@ func newSignatureAdmission(t *testing.T, keyID string, publicKey ed25519.PublicK store, logger, nil, + nil, plan.NewBuilder(node.NewIndexTracker(1), cache, attrs, store, nil, logger, 0), meterProvider, allowWriteGate{}, diff --git a/internal/adapter/grpc/oidc_caller_attribution_test.go b/internal/adapter/grpc/oidc_caller_attribution_test.go index 497a160952..7c98399031 100644 --- a/internal/adapter/grpc/oidc_caller_attribution_test.go +++ b/internal/adapter/grpc/oidc_caller_attribution_test.go @@ -197,7 +197,7 @@ func newCallerAttributionAdmission(t *testing.T, expectedProposals int) (*admiss return result, nil }).Times(expectedProposals) - admission := admissionapp.NewAdmission(store, logger, proposer, + admission := admissionapp.NewAdmission(store, logger, proposer, nil, plan.NewBuilder(node.NewIndexTracker(1), testCache, attrs, store, nil, logger, 0), provider, allowWriteGate{}, keystore.NewKeyStore(), state.NewSharedState(), attrs, numscript.NewNumscriptCache(0), func(context.Context) error { return nil }) diff --git a/internal/application/admission/admission.go b/internal/application/admission/admission.go index 917f01cce2..72694e52bf 100644 --- a/internal/application/admission/admission.go +++ b/internal/application/admission/admission.go @@ -44,16 +44,20 @@ var tracer = otel.Tracer("admission") //go:generate mockgen -write_source_comment=false -write_package_comment=false -source admission.go -destination proposer_generated_test.go -typed -package admission . Proposer type Proposer interface { Propose(context.Context, *node.Proposal) (*futures.Future[state.ApplyResult], error) - AcquireCapabilityMembership(context.Context) (func(*futures.Future[state.ApplyResult]), error) InitialIndex() uint64 } +type CapabilityMembershipFence interface { + AcquireCapabilityMembership(context.Context) (func(*futures.Future[state.ApplyResult]), error) +} + // Admission handles the admission of orders into the Raft cluster. // It is responsible for preloading volumes and proposing commands. type Admission struct { store *dal.Store logger logging.Logger proposer Proposer + membershipFence CapabilityMembershipFence writeGate health.WriteGate keyStore *keystore.KeyStore sharedState *state.SharedState @@ -139,6 +143,7 @@ func NewAdmission( store *dal.Store, logger logging.Logger, proposer Proposer, + membershipFence CapabilityMembershipFence, builder *plan.Builder, meterProvider metric.MeterProvider, writeGate health.WriteGate, @@ -153,6 +158,7 @@ func NewAdmission( store: store, logger: logger, proposer: proposer, + membershipFence: membershipFence, builder: builder, writeGate: writeGate, keyStore: keyStore, @@ -606,7 +612,10 @@ func (a *Admission) admit(ctx context.Context, prepare func(context.Context, *co func (a *Admission) proposeActivatingPolicy(ctx context.Context, command *raftcmdpb.Proposal, build *plan.BuildResult) (*futures.Future[state.ApplyResult], func(), error) { // Fence membership before the proposal guard locks the index tracker. - release, err := a.proposer.AcquireCapabilityMembership(ctx) + if a.membershipFence == nil { + return nil, nil, errors.New("activating policy admission has no membership fence") + } + release, err := a.membershipFence.AcquireCapabilityMembership(ctx) if err != nil { return nil, nil, fmt.Errorf("fencing membership for capability activation: %w", err) } diff --git a/internal/application/admission/admission_phase_metrics_test.go b/internal/application/admission/admission_phase_metrics_test.go index 6f2c1c1287..dd2573c11b 100644 --- a/internal/application/admission/admission_phase_metrics_test.go +++ b/internal/application/admission/admission_phase_metrics_test.go @@ -59,6 +59,7 @@ func createTestAdmissionWithReader(t *testing.T, store *dal.Store, proposer Prop store, logger, proposer, + nil, testPreloader, provider, writeGate, diff --git a/internal/application/admission/admission_test.go b/internal/application/admission/admission_test.go index 3f8d3f18be..b829872b99 100644 --- a/internal/application/admission/admission_test.go +++ b/internal/application/admission/admission_test.go @@ -118,6 +118,7 @@ func createTestAdmission(t *testing.T, store *dal.Store) (*Admission, *attribute store, logger, nil, // no proposer needed for unit tests + nil, // no membership fence needed for unit tests testPreloader, noop.NewMeterProvider(), nil, // no health checker needed for unit tests diff --git a/internal/application/admission/capability_policy_test.go b/internal/application/admission/capability_policy_test.go index 6e06158ee5..96a993502b 100644 --- a/internal/application/admission/capability_policy_test.go +++ b/internal/application/admission/capability_policy_test.go @@ -14,10 +14,6 @@ import ( type barrierTestProposer struct{} -func (barrierTestProposer) AcquireCapabilityMembership(context.Context) (func(*futures.Future[state.ApplyResult]), error) { - return func(*futures.Future[state.ApplyResult]) {}, nil -} - func (barrierTestProposer) InitialIndex() uint64 { return 1 } func (barrierTestProposer) Propose(_ context.Context, proposal *node.Proposal) (*futures.Future[state.ApplyResult], error) { diff --git a/internal/application/admission/metadata_replay_test.go b/internal/application/admission/metadata_replay_test.go index 9069255bb3..4e3e83d530 100644 --- a/internal/application/admission/metadata_replay_test.go +++ b/internal/application/admission/metadata_replay_test.go @@ -117,7 +117,7 @@ func newMetadataReplayHarness(t testing.TB) *metadataReplayHarness { }).AnyTimes() gate := health.NewMockWriteGate(gomock.NewController(t)) gate.EXPECT().CheckWritesAllowed().Return(nil).AnyTimes() - admission := NewAdmission(store, logger, proposer, plan.NewBuilder(tracker, c, attrs, store, nil, logger, 0), meter, gate, ks, shared, attrs, numscript.NewNumscriptCache(0), func(context.Context) error { return nil }) + admission := NewAdmission(store, logger, proposer, nil, plan.NewBuilder(tracker, c, attrs, store, nil, logger, 0), meter, gate, ks, shared, attrs, numscript.NewNumscriptCache(0), func(context.Context) error { return nil }) h.admission = admission h.apply = func(p *raftcmdpb.Proposal) state.ApplyResult { p.CallerSnapshot = commands.SystemCallerSnapshot(commands.ComponentClusterPolicy) diff --git a/internal/application/admission/proposer_generated_test.go b/internal/application/admission/proposer_generated_test.go index d1b33f615b..5f8e977653 100644 --- a/internal/application/admission/proposer_generated_test.go +++ b/internal/application/admission/proposer_generated_test.go @@ -41,45 +41,6 @@ func (m *MockProposer) EXPECT() *MockProposerMockRecorder { return m.recorder } -// AcquireCapabilityMembership mocks base method. -func (m *MockProposer) AcquireCapabilityMembership(arg0 context.Context) (func(*futures.Future[state.ApplyResult]), error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "AcquireCapabilityMembership", arg0) - ret0, _ := ret[0].(func(*futures.Future[state.ApplyResult])) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// AcquireCapabilityMembership indicates an expected call of AcquireCapabilityMembership. -func (mr *MockProposerMockRecorder) AcquireCapabilityMembership(arg0 any) *MockProposerAcquireCapabilityMembershipCall { - mr.mock.ctrl.T.Helper() - call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AcquireCapabilityMembership", reflect.TypeOf((*MockProposer)(nil).AcquireCapabilityMembership), arg0) - return &MockProposerAcquireCapabilityMembershipCall{Call: call} -} - -// MockProposerAcquireCapabilityMembershipCall wrap *gomock.Call -type MockProposerAcquireCapabilityMembershipCall struct { - *gomock.Call -} - -// Return rewrite *gomock.Call.Return -func (c *MockProposerAcquireCapabilityMembershipCall) Return(arg0 func(*futures.Future[state.ApplyResult]), arg1 error) *MockProposerAcquireCapabilityMembershipCall { - c.Call = c.Call.Return(arg0, arg1) - return c -} - -// Do rewrite *gomock.Call.Do -func (c *MockProposerAcquireCapabilityMembershipCall) Do(f func(context.Context) (func(*futures.Future[state.ApplyResult]), error)) *MockProposerAcquireCapabilityMembershipCall { - c.Call = c.Call.Do(f) - return c -} - -// DoAndReturn rewrite *gomock.Call.DoAndReturn -func (c *MockProposerAcquireCapabilityMembershipCall) DoAndReturn(f func(context.Context) (func(*futures.Future[state.ApplyResult]), error)) *MockProposerAcquireCapabilityMembershipCall { - c.Call = c.Call.DoAndReturn(f) - return c -} - // InitialIndex mocks base method. func (m *MockProposer) InitialIndex() uint64 { m.ctrl.T.Helper() @@ -156,3 +117,66 @@ func (c *MockProposerProposeCall) DoAndReturn(f func(context.Context, *node.Prop c.Call = c.Call.DoAndReturn(f) return c } + +// MockCapabilityMembershipFence is a mock of CapabilityMembershipFence interface. +type MockCapabilityMembershipFence struct { + ctrl *gomock.Controller + recorder *MockCapabilityMembershipFenceMockRecorder + isgomock struct{} +} + +// MockCapabilityMembershipFenceMockRecorder is the mock recorder for MockCapabilityMembershipFence. +type MockCapabilityMembershipFenceMockRecorder struct { + mock *MockCapabilityMembershipFence +} + +// NewMockCapabilityMembershipFence creates a new mock instance. +func NewMockCapabilityMembershipFence(ctrl *gomock.Controller) *MockCapabilityMembershipFence { + mock := &MockCapabilityMembershipFence{ctrl: ctrl} + mock.recorder = &MockCapabilityMembershipFenceMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockCapabilityMembershipFence) EXPECT() *MockCapabilityMembershipFenceMockRecorder { + return m.recorder +} + +// AcquireCapabilityMembership mocks base method. +func (m *MockCapabilityMembershipFence) AcquireCapabilityMembership(arg0 context.Context) (func(*futures.Future[state.ApplyResult]), error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AcquireCapabilityMembership", arg0) + ret0, _ := ret[0].(func(*futures.Future[state.ApplyResult])) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// AcquireCapabilityMembership indicates an expected call of AcquireCapabilityMembership. +func (mr *MockCapabilityMembershipFenceMockRecorder) AcquireCapabilityMembership(arg0 any) *MockCapabilityMembershipFenceAcquireCapabilityMembershipCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AcquireCapabilityMembership", reflect.TypeOf((*MockCapabilityMembershipFence)(nil).AcquireCapabilityMembership), arg0) + return &MockCapabilityMembershipFenceAcquireCapabilityMembershipCall{Call: call} +} + +// MockCapabilityMembershipFenceAcquireCapabilityMembershipCall wrap *gomock.Call +type MockCapabilityMembershipFenceAcquireCapabilityMembershipCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockCapabilityMembershipFenceAcquireCapabilityMembershipCall) Return(arg0 func(*futures.Future[state.ApplyResult]), arg1 error) *MockCapabilityMembershipFenceAcquireCapabilityMembershipCall { + c.Call = c.Call.Return(arg0, arg1) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockCapabilityMembershipFenceAcquireCapabilityMembershipCall) Do(f func(context.Context) (func(*futures.Future[state.ApplyResult]), error)) *MockCapabilityMembershipFenceAcquireCapabilityMembershipCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockCapabilityMembershipFenceAcquireCapabilityMembershipCall) DoAndReturn(f func(context.Context) (func(*futures.Future[state.ApplyResult]), error)) *MockCapabilityMembershipFenceAcquireCapabilityMembershipCall { + c.Call = c.Call.DoAndReturn(f) + return c +} diff --git a/internal/application/mirror/worker_diagnostic_test.go b/internal/application/mirror/worker_diagnostic_test.go index c086b3805b..c31df58bcd 100644 --- a/internal/application/mirror/worker_diagnostic_test.go +++ b/internal/application/mirror/worker_diagnostic_test.go @@ -89,7 +89,7 @@ func TestWorker_MalformedURLDoesNotDisclosePassword(t *testing.T) { proposer := &applyingMirrorProposer{t: t, machine: machine, store: store, tracker: tracker} writeGate := health.NewMockWriteGate(gomock.NewController(t)) writeGate.EXPECT().CheckWritesAllowed().Return(nil) - adm := admission.NewAdmission(store, logger, proposer, builder, meters, writeGate, keys, shared, attrs, numscript.NewNumscriptCache(0), func(context.Context) error { return nil }) + adm := admission.NewAdmission(store, logger, proposer, nil, builder, meters, writeGate, keys, shared, attrs, numscript.NewNumscriptCache(0), func(context.Context) error { return nil }) config := &commonpb.MirrorSourceConfig{ LedgerName: "source-ledger", Type: &commonpb.MirrorSourceConfig_Http{Http: &commonpb.HttpMirrorSourceConfig{ @@ -247,7 +247,7 @@ func TestWorker_TransportErrorsDoNotDiscloseCredentials(t *testing.T) { proposer := &applyingMirrorProposer{t: t, machine: machine, store: store, tracker: tracker} writeGate := health.NewMockWriteGate(gomock.NewController(t)) writeGate.EXPECT().CheckWritesAllowed().Return(nil) - adm := admission.NewAdmission(store, logger, proposer, builder, meters, writeGate, keys, shared, attrs, numscript.NewNumscriptCache(0), func(context.Context) error { return nil }) + adm := admission.NewAdmission(store, logger, proposer, nil, builder, meters, writeGate, keys, shared, attrs, numscript.NewNumscriptCache(0), func(context.Context) error { return nil }) sourceUser := tc.user if mode != "direct" { diff --git a/internal/bootstrap/module.go b/internal/bootstrap/module.go index 0c2ffcb4dd..7ff717a335 100644 --- a/internal/bootstrap/module.go +++ b/internal/bootstrap/module.go @@ -650,6 +650,7 @@ func Module() fx.Option { store, logger, node, + node, builder, meterProvider, hc, diff --git a/internal/infra/backup/numscript_restore_parity_test.go b/internal/infra/backup/numscript_restore_parity_test.go index 84be1ad591..832d8f1b93 100644 --- a/internal/infra/backup/numscript_restore_parity_test.go +++ b/internal/infra/backup/numscript_restore_parity_test.go @@ -111,7 +111,7 @@ func newScriptedNode(t *testing.T, store *dal.Store) *scriptedNode { return &scriptedNode{ t: t, admission: admission.NewAdmission(store, logger, - &applyingProposer{t: t, machine: machine, store: store, tracker: tracker}, + &applyingProposer{t: t, machine: machine, store: store, tracker: tracker}, nil, builder, meters, writeGate, keyStore, sharedState, attrs, numscript.NewNumscriptCache(0), func(context.Context) error { return nil }), } From 72808378d58ade03ecbaf233e5b13b17f6c09564 Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Wed, 11 Nov 2026 16:37:21 +0100 Subject: [PATCH 120/127] refactor(ledger): consolidate membership evidence writes --- .../bootstrap/join_transport_recovery_test.go | 5 +- internal/bootstrap/module.go | 2 +- .../membership/committed_peer_refresh_test.go | 2 +- internal/infra/membership/membership.go | 33 ++++----- internal/infra/membership/membership_test.go | 74 +++++++++---------- internal/infra/membership/peer_store.go | 8 +- .../membership/remove_node_blacklist_test.go | 6 +- .../node/conf_change_retry_durable_test.go | 5 +- .../infra/node/confchange_identity_test.go | 4 +- internal/infra/node/configured_peers_test.go | 18 ++--- internal/infra/node/learner_admission_test.go | 9 ++- internal/infra/node/node.go | 2 +- .../infra/node/promotion_identity_test.go | 5 +- 13 files changed, 90 insertions(+), 83 deletions(-) diff --git a/internal/bootstrap/join_transport_recovery_test.go b/internal/bootstrap/join_transport_recovery_test.go index dafadfaeab..e5f38f274c 100644 --- a/internal/bootstrap/join_transport_recovery_test.go +++ b/internal/bootstrap/join_transport_recovery_test.go @@ -9,6 +9,7 @@ import ( "time" "github.com/stretchr/testify/require" + "go.etcd.io/raft/v3/raftpb" "go.opentelemetry.io/otel/metric/noop" "google.golang.org/grpc" @@ -141,7 +142,9 @@ func newJoinRecoveryNode(t *testing.T, id uint64, addr string, peers []node.Peer cacheStore, err := cache.New(1000, meter) require.NoError(t, err) registry := state.NewStateRegistry(cacheStore, attributes.New()) - machine, err := state.NewMachine(logger, registry, state.NewCacheSnapshotter(logger, registry, nil), store, dal.NewSentinelFactory(store, false), meters, nil, state.NewSharedState(), signal.NewNotifications(), nil, "join-recovery", 0, member.WriteConfChange, state.NoopCapabilityPolicyCommitHandler) + machine, err := state.NewMachine(logger, registry, state.NewCacheSnapshotter(logger, registry, nil), store, dal.NewSentinelFactory(store, false), meters, nil, state.NewSharedState(), signal.NewNotifications(), nil, "join-recovery", 0, func(entry *raftpb.Entry, session *dal.WriteSession) error { + return member.WriteConfChange(entry, session) + }, state.NoopCapabilityPolicyCommitHandler) require.NoError(t, err) t.Cleanup(machine.Close) recovery := state.NewRecovery(machine, store) diff --git a/internal/bootstrap/module.go b/internal/bootstrap/module.go index 7ff717a335..ae73155384 100644 --- a/internal/bootstrap/module.go +++ b/internal/bootstrap/module.go @@ -349,7 +349,7 @@ func Module() fx.Option { policy = machine.ClusterPolicy() } - return membership.WriteConfChangeWithPolicy(entry, session, policy) + return membership.WriteConfChange(entry, session, policy) }, func(policy *commonpb.ClusterPolicy) { if err := publishCapabilityGate(membership, policy); err != nil { diff --git a/internal/infra/membership/committed_peer_refresh_test.go b/internal/infra/membership/committed_peer_refresh_test.go index b885ee7a78..5eb92654f1 100644 --- a/internal/infra/membership/committed_peer_refresh_test.go +++ b/internal/infra/membership/committed_peer_refresh_test.go @@ -89,7 +89,7 @@ func TestCommittedPeerRefreshUpdatesRaftTransport(t *testing.T) { // Node.finishReady performs this Set as soon as it observes the commit, // before submitting the entry to the asynchronous FSM applier. - require.NoError(t, m.Set(2, "new:7000", "new:8000", newInstanceID)) + require.NoError(t, m.Set(2, "new:7000", "new:8000", newInstanceID, nil)) require.Equal(t, "new:7000", m.PeerAddresses()[2].RaftAddress) require.Equal(t, newInstanceID, m.PeerAddresses()[2].InstanceID) diff --git a/internal/infra/membership/membership.go b/internal/infra/membership/membership.go index 02de44f364..c6bf15c568 100644 --- a/internal/infra/membership/membership.go +++ b/internal/infra/membership/membership.go @@ -341,15 +341,11 @@ func (m *Membership) GetInstanceID(nodeID uint64) ([]byte, bool) { // Cache mutation and transport wiring both happen inside the lock so // they stay in lockstep with a concurrent Rehydrate. See Rehydrate's // locking note. -func (m *Membership) Set(nodeID uint64, raftAddr, serviceAddr string, instanceID []byte) error { - return m.SetWithEvidence(nodeID, raftAddr, serviceAddr, instanceID, nil) -} - -// SetWithEvidence upserts a peer and persists its capability evidence in the +// Set upserts a peer and persists its capability evidence in the // in-memory registration. The FSM write path stores the same fields in the // durable PeerAddress row, while finishReady uses this method to keep the // transport cache and evidence synchronized before resolving the proposal. -func (m *Membership) SetWithEvidence(nodeID uint64, raftAddr, serviceAddr string, instanceID []byte, capabilityIDs []uint32) error { +func (m *Membership) Set(nodeID uint64, raftAddr, serviceAddr string, instanceID []byte, capabilityIDs []uint32) error { if err := ValidateInstanceID(instanceID); err != nil { return fmt.Errorf("setting peer %d: %w", nodeID, err) } @@ -402,11 +398,11 @@ func (m *Membership) Register(nodeID uint64, raftAddr, serviceAddr string, insta capabilityIDs = existing.Capabilities } } - if err := m.store.PutWithEvidence(nodeID, raftAddr, serviceAddr, instanceID, capabilityIDs); err != nil { + if err := m.store.Put(nodeID, raftAddr, serviceAddr, instanceID, capabilityIDs); err != nil { return fmt.Errorf("persisting peer %d: %w", nodeID, err) } - if err := m.SetWithEvidence(nodeID, raftAddr, serviceAddr, instanceID, capabilityIDs); err != nil { + if err := m.Set(nodeID, raftAddr, serviceAddr, instanceID, capabilityIDs); err != nil { return err } @@ -648,7 +644,7 @@ func (m *Membership) OnSnapshotInstalled() { // (with the leader's possibly-stale self); the next OnSnapshot // or restart will re-attempt the self-Put. selfCapabilities := capabilities.IDsAsUint32(capabilities.SupportedIDs()) - if err := m.store.PutWithEvidence(m.selfNodeID, m.selfRaftAddr, m.selfServiceAddr, m.selfInstanceID, selfCapabilities); err != nil { + if err := m.store.Put(m.selfNodeID, m.selfRaftAddr, m.selfServiceAddr, m.selfInstanceID, selfCapabilities); err != nil { m.logger.WithFields(map[string]any{ "error": err, }).Errorf("Refreshing self in Pebble before snapshot reload failed; next checkpoint we serve may carry a stale self address") @@ -677,18 +673,19 @@ func (m *Membership) OnSnapshotInstalled() { // // Promotions repeat the existing peer registration, making replay independent // of the node-local membership cache. -func (m *Membership) WriteConfChange(entry *raftpb.Entry, session *dal.WriteSession) error { +// WriteConfChange uses the committed policy when no effective policy is given. +// An explicit policy, including nil, is the state at this entry's position in +// the FSM batch and avoids validating against an older committed policy. +func (m *Membership) WriteConfChange(entry *raftpb.Entry, session *dal.WriteSession, effectivePolicy ...*commonpb.ClusterPolicy) error { + if len(effectivePolicy) > 1 { + return errors.New("at most one effective policy may be supplied") + } + if len(effectivePolicy) == 1 { + return m.writeConfChange(entry, session, effectivePolicy[0], true) + } return m.writeConfChange(entry, session, nil, false) } -// WriteConfChangeWithPolicy applies a ConfChange using the policy state at the -// entry's position in the current FSM batch. This avoids validating a -// membership change against the policy that was durable before an earlier -// SetClusterPolicy order in the same batch. -func (m *Membership) WriteConfChangeWithPolicy(entry *raftpb.Entry, session *dal.WriteSession, policy *commonpb.ClusterPolicy) error { - return m.writeConfChange(entry, session, policy, true) -} - func (m *Membership) writeConfChange(entry *raftpb.Entry, session *dal.WriteSession, effectivePolicy *commonpb.ClusterPolicy, hasEffectivePolicy bool) error { cc, ok, err := UnmarshalConfChangeV2(entry) if err != nil { diff --git a/internal/infra/membership/membership_test.go b/internal/infra/membership/membership_test.go index 3802e4a827..9983de2990 100644 --- a/internal/infra/membership/membership_test.go +++ b/internal/infra/membership/membership_test.go @@ -136,9 +136,9 @@ func TestPeerStore_PutLoadAll(t *testing.T) { ps := newTestPeerStore(t) - require.NoError(t, ps.Put(1, "pod-0:7777", "pod-0:8888", fixedInstanceID(1))) - require.NoError(t, ps.Put(2, "pod-1:7777", "pod-1:8888", fixedInstanceID(2))) - require.NoError(t, ps.Put(3, "pod-2:7777", "pod-2:8888", fixedInstanceID(3))) + require.NoError(t, ps.Put(1, "pod-0:7777", "pod-0:8888", fixedInstanceID(1), nil)) + require.NoError(t, ps.Put(2, "pod-1:7777", "pod-1:8888", fixedInstanceID(2), nil)) + require.NoError(t, ps.Put(3, "pod-2:7777", "pod-2:8888", fixedInstanceID(3), nil)) peers, err := ps.LoadAll() require.NoError(t, err) @@ -153,8 +153,8 @@ func TestPeerStore_RejectsInvalidInstanceID(t *testing.T) { ps := newTestPeerStore(t) - require.ErrorContains(t, ps.Put(1, "pod-0:7777", "pod-0:8888", nil), "instance_id must be 16 bytes") - require.ErrorContains(t, ps.Put(1, "pod-0:7777", "pod-0:8888", []byte("short")), "instance_id must be 16 bytes") + require.ErrorContains(t, ps.Put(1, "pod-0:7777", "pod-0:8888", nil, nil), "instance_id must be 16 bytes") + require.ErrorContains(t, ps.Put(1, "pod-0:7777", "pod-0:8888", []byte("short"), nil), "instance_id must be 16 bytes") } func TestPeerStore_LoadAllRejectsPersistedInvalidInstanceID(t *testing.T) { @@ -202,8 +202,8 @@ func TestPeerStore_PutOverwrites(t *testing.T) { ps := newTestPeerStore(t) - require.NoError(t, ps.Put(7, "old:7777", "old:8888", fixedInstanceID(7))) - require.NoError(t, ps.Put(7, "new:7777", "new:8888", fixedInstanceID(8))) + require.NoError(t, ps.Put(7, "old:7777", "old:8888", fixedInstanceID(7), nil)) + require.NoError(t, ps.Put(7, "new:7777", "new:8888", fixedInstanceID(8), nil)) peers, err := ps.LoadAll() require.NoError(t, err) @@ -218,8 +218,8 @@ func TestPeerStore_Delete(t *testing.T) { ps := newTestPeerStore(t) - require.NoError(t, ps.Put(1, "a:1", "a:2", fixedInstanceID(1))) - require.NoError(t, ps.Put(2, "b:1", "b:2", fixedInstanceID(2))) + require.NoError(t, ps.Put(1, "a:1", "a:2", fixedInstanceID(1), nil)) + require.NoError(t, ps.Put(2, "b:1", "b:2", fixedInstanceID(2), nil)) require.NoError(t, ps.Delete(1)) peers, err := ps.LoadAll() @@ -409,8 +409,8 @@ func TestMembership_OnSnapshotInstalled(t *testing.T) { ps := newTestPeerStore(t) // Pre-swap state: cluster A had peer 7. - require.NoError(t, ps.Put(7, "old:1", "old:2", fixedInstanceID(7))) - require.NoError(t, ps.PutWithEvidence(testSelfNodeID, testSelfRaftAddr, testSelfServiceAddr, fixedInstanceID(0x42), []uint32{7})) + require.NoError(t, ps.Put(7, "old:1", "old:2", fixedInstanceID(7), nil)) + require.NoError(t, ps.Put(testSelfNodeID, testSelfRaftAddr, testSelfServiceAddr, fixedInstanceID(0x42), []uint32{7})) m, err := NewMembership(ps, noopTransport{}, noopPool{}, testSelfNodeID, testSelfRaftAddr, testSelfServiceAddr, fixedInstanceID(0x42), logging.Testing(), NoopClusterPolicyProvider) require.NoError(t, err) @@ -420,8 +420,8 @@ func TestMembership_OnSnapshotInstalled(t *testing.T) { // Simulate a leader checkpoint restore: Pebble now has peers 1 + 3, // and no peer 7 (the source cluster A has been replaced by cluster B). require.NoError(t, ps.Delete(7)) - require.NoError(t, ps.Put(1, "new:1", "new:2", fixedInstanceID(1))) - require.NoError(t, ps.Put(3, "new:3", "new:4", fixedInstanceID(3))) + require.NoError(t, ps.Put(1, "new:1", "new:2", fixedInstanceID(1), nil)) + require.NoError(t, ps.Put(3, "new:3", "new:4", fixedInstanceID(3), nil)) // Hook fires: cache must catch up to the new Pebble state. m.OnSnapshotInstalled() @@ -439,7 +439,7 @@ func TestMembership_OnSnapshotInstalled(t *testing.T) { func TestMembershipRegisterSelfClearsStaleCapabilityEvidence(t *testing.T) { ps := newTestPeerStore(t) - require.NoError(t, ps.PutWithEvidence(testSelfNodeID, testSelfRaftAddr, testSelfServiceAddr, fixedInstanceID(0x42), []uint32{7})) + require.NoError(t, ps.Put(testSelfNodeID, testSelfRaftAddr, testSelfServiceAddr, fixedInstanceID(0x42), []uint32{7})) m, err := NewMembership(ps, noopTransport{}, noopPool{}, testSelfNodeID, testSelfRaftAddr, testSelfServiceAddr, fixedInstanceID(0x42), logging.Testing(), NoopClusterPolicyProvider) require.NoError(t, err) @@ -464,9 +464,9 @@ func TestMembership_SnapshotDonorCandidates(t *testing.T) { t.Parallel() ps := newTestPeerStore(t) - require.NoError(t, ps.Put(9, "peer-9:7000", "peer-9:8000", fixedInstanceID(9))) - require.NoError(t, ps.Put(2, "peer-2:7000", "peer-2:8000", fixedInstanceID(2))) - require.NoError(t, ps.Put(testSelfNodeID, testSelfRaftAddr, testSelfServiceAddr, fixedInstanceID(0x42))) + require.NoError(t, ps.Put(9, "peer-9:7000", "peer-9:8000", fixedInstanceID(9), nil)) + require.NoError(t, ps.Put(2, "peer-2:7000", "peer-2:8000", fixedInstanceID(2), nil)) + require.NoError(t, ps.Put(testSelfNodeID, testSelfRaftAddr, testSelfServiceAddr, fixedInstanceID(0x42), nil)) m, err := NewMembership(ps, noopTransport{}, noopPool{}, testSelfNodeID, testSelfRaftAddr, testSelfServiceAddr, fixedInstanceID(0x42), logging.Testing(), NoopClusterPolicyProvider) require.NoError(t, err) @@ -513,8 +513,8 @@ func TestMembership_RehydrateAfterReplay(t *testing.T) { ps := newTestPeerStore(t) - require.NoError(t, ps.Put(7, "before:1", "before:2", fixedInstanceID(7))) - require.NoError(t, ps.Put(9, "removed:1", "removed:2", fixedInstanceID(9))) + require.NoError(t, ps.Put(7, "before:1", "before:2", fixedInstanceID(7), nil)) + require.NoError(t, ps.Put(9, "removed:1", "removed:2", fixedInstanceID(9), nil)) raftTransport := &recordingTransport{peers: map[uint64]string{}} servicePool := &recordingPool{peers: map[uint64]string{}} @@ -529,10 +529,10 @@ func TestMembership_RehydrateAfterReplay(t *testing.T) { // Simulate WAL replay: WriteConfChange wrote these rows directly to // Pebble without touching the cache. - require.NoError(t, ps.Put(7, "after:7", "after:8", fixedInstanceID(7))) + require.NoError(t, ps.Put(7, "after:7", "after:8", fixedInstanceID(7), nil)) require.NoError(t, ps.Delete(9)) - require.NoError(t, ps.Put(1, "after:1", "after:2", fixedInstanceID(1))) - require.NoError(t, ps.Put(3, "after:3", "after:4", fixedInstanceID(3))) + require.NoError(t, ps.Put(1, "after:1", "after:2", fixedInstanceID(1), nil)) + require.NoError(t, ps.Put(3, "after:3", "after:4", fixedInstanceID(3), nil)) require.NoError(t, m.Rehydrate()) @@ -568,7 +568,7 @@ func TestMembership_StartGate(t *testing.T) { pool := &countingPool{} ps := newTestPeerStore(t) - require.NoError(t, ps.Put(1, "pod-0:7777", "pod-0:8888", fixedInstanceID(1))) + require.NoError(t, ps.Put(1, "pod-0:7777", "pod-0:8888", fixedInstanceID(1), nil)) m, err := NewMembership(ps, transport, pool, testSelfNodeID, testSelfRaftAddr, testSelfServiceAddr, fixedInstanceID(0x42), logging.Testing(), NoopClusterPolicyProvider) require.NoError(t, err) @@ -576,7 +576,7 @@ func TestMembership_StartGate(t *testing.T) { require.Equal(t, 0, transport.adds, "no wire before Start (cache-only construction)") require.Equal(t, 0, pool.adds, "no wire before Start (cache-only construction)") - require.NoError(t, m.Set(2, "pod-1:7777", "pod-1:8888", fixedInstanceID(2))) + require.NoError(t, m.Set(2, "pod-1:7777", "pod-1:8888", fixedInstanceID(2), nil)) require.Equal(t, 0, transport.adds, "Set pre-Start must be cache-only") require.Equal(t, 0, pool.adds, "Set pre-Start must be cache-only") require.Len(t, m.PeerAddresses(), 2, "cache must still reflect the Set") @@ -585,7 +585,7 @@ func TestMembership_StartGate(t *testing.T) { require.Equal(t, 2, transport.adds, "Start flushes the cache to the transport") require.Equal(t, 2, pool.adds, "Start flushes the cache to the service pool") - require.NoError(t, m.Set(3, "pod-2:7777", "pod-2:8888", fixedInstanceID(3))) + require.NoError(t, m.Set(3, "pod-2:7777", "pod-2:8888", fixedInstanceID(3), nil)) require.Equal(t, 3, transport.adds, "Set post-Start wires inline") require.Equal(t, 3, pool.adds, "Set post-Start wires inline") @@ -598,7 +598,7 @@ func TestMembership_SetRejectsInvalidInstanceID(t *testing.T) { t.Parallel() m := newTestMembership(t) - require.ErrorContains(t, m.Set(1, "pod-0:7777", "pod-0:8888", nil), "instance_id must be 16 bytes") + require.ErrorContains(t, m.Set(1, "pod-0:7777", "pod-0:8888", nil, nil), "instance_id must be 16 bytes") require.NotContains(t, m.PeerAddresses(), uint64(1)) } @@ -621,11 +621,11 @@ func TestMembership_SetRewiresChangedAddresses(t *testing.T) { require.NoError(t, err) m.Start() - require.NoError(t, m.Set(1, "old:7777", "old:8888", fixedInstanceID(1))) + require.NoError(t, m.Set(1, "old:7777", "old:8888", fixedInstanceID(1), nil)) require.Equal(t, 1, transport.adds) require.Equal(t, 0, transport.removes) - require.NoError(t, m.Set(1, "new:7777", "new:8888", fixedInstanceID(2))) + require.NoError(t, m.Set(1, "new:7777", "new:8888", fixedInstanceID(2), nil)) require.Equal(t, 2, transport.adds) // wireRemove is intentionally not called on a Raft-address change: DefaultTransport.AddPeer // refreshes the pooled connection address through ConnectionPool.AddPeer while preserving the @@ -652,8 +652,8 @@ func TestMembership_SetKeepsRaftPeerOnServiceAddressChange(t *testing.T) { require.NoError(t, err) m.Start() - require.NoError(t, m.Set(1, "raft:7777", "old:8888", fixedInstanceID(1))) - require.NoError(t, m.Set(1, "raft:7777", "new:8888", fixedInstanceID(1))) + require.NoError(t, m.Set(1, "raft:7777", "old:8888", fixedInstanceID(1), nil)) + require.NoError(t, m.Set(1, "raft:7777", "new:8888", fixedInstanceID(1), nil)) // Removing the existing Raft peer would require a fresh optional-TLS probe; // a failed probe could then strand a previously healthy replication channel. @@ -669,7 +669,7 @@ func TestMembership_ClonesInstanceIDs(t *testing.T) { m := newTestMembership(t) instanceID := fixedInstanceID(1) - require.NoError(t, m.Set(1, "pod-0:7777", "pod-0:8888", instanceID)) + require.NoError(t, m.Set(1, "pod-0:7777", "pod-0:8888", instanceID, nil)) instanceID[0] = 9 stored, ok := m.GetInstanceID(1) @@ -692,7 +692,7 @@ func TestPeerStore_PersistsCapabilityEvidenceWithIdentity(t *testing.T) { ps := newTestPeerStore(t) caps := []uint32{1, 2} identity := fixedInstanceID(0x7a) - require.NoError(t, ps.PutWithEvidence(9, "raft:9", "service:9", identity, caps)) + require.NoError(t, ps.Put(9, "raft:9", "service:9", identity, caps)) rows, err := ps.LoadAll() require.NoError(t, err) @@ -714,11 +714,11 @@ func TestMembership_ReconcileAgainstConfState(t *testing.T) { newSeeded := func(t *testing.T) *Membership { ps := newTestPeerStore(t) - require.NoError(t, ps.Put(7, "self:1", "self:2", fixedInstanceID(7))) - require.NoError(t, ps.Put(1, "voter1:1", "voter1:2", fixedInstanceID(1))) - require.NoError(t, ps.Put(2, "voter2:1", "voter2:2", fixedInstanceID(2))) - require.NoError(t, ps.Put(3, "learner:1", "learner:2", fixedInstanceID(3))) - require.NoError(t, ps.Put(99, "stale:1", "stale:2", fixedInstanceID(99))) + require.NoError(t, ps.Put(7, "self:1", "self:2", fixedInstanceID(7), nil)) + require.NoError(t, ps.Put(1, "voter1:1", "voter1:2", fixedInstanceID(1), nil)) + require.NoError(t, ps.Put(2, "voter2:1", "voter2:2", fixedInstanceID(2), nil)) + require.NoError(t, ps.Put(3, "learner:1", "learner:2", fixedInstanceID(3), nil)) + require.NoError(t, ps.Put(99, "stale:1", "stale:2", fixedInstanceID(99), nil)) m, err := NewMembership(ps, noopTransport{}, noopPool{}, 7, "self:1", "self:2", fixedInstanceID(7), logging.Testing(), NoopClusterPolicyProvider) require.NoError(t, err) diff --git a/internal/infra/membership/peer_store.go b/internal/infra/membership/peer_store.go index 26bb7b1706..0b9314d4d3 100644 --- a/internal/infra/membership/peer_store.go +++ b/internal/infra/membership/peer_store.go @@ -91,15 +91,11 @@ func peerKeyRange() (lower, upper []byte) { // from lifecycle paths for self and initial-peer registration. ConfChange // apply uses the caller's WriteSession directly. Every persisted peer must // carry the concrete member's 16-byte WAL/PVC identity. -func (p *PeerStore) Put(nodeID uint64, raftAddr, serviceAddr string, instanceID []byte) error { - return p.PutWithEvidence(nodeID, raftAddr, serviceAddr, instanceID, nil) -} - -// PutWithEvidence persists a peer directory row together with the capability +// Put persists a peer directory row together with the capability // evidence bound to its concrete instance. The evidence is deliberately part // of the same Pebble value as the identity so a crash cannot expose a peer // with a fresh instance_id and stale capabilities. -func (p *PeerStore) PutWithEvidence(nodeID uint64, raftAddr, serviceAddr string, instanceID []byte, capabilities []uint32) error { +func (p *PeerStore) Put(nodeID uint64, raftAddr, serviceAddr string, instanceID []byte, capabilities []uint32) error { if err := ValidateInstanceID(instanceID); err != nil { return fmt.Errorf("writing peer %d: %w", nodeID, err) } diff --git a/internal/infra/membership/remove_node_blacklist_test.go b/internal/infra/membership/remove_node_blacklist_test.go index f37f14edb5..9cd9dfe582 100644 --- a/internal/infra/membership/remove_node_blacklist_test.go +++ b/internal/infra/membership/remove_node_blacklist_test.go @@ -52,7 +52,7 @@ func TestWriteConfChange_RemoveNodeBlacklistsPeer(t *testing.T) { ps, store := storesForBlacklistTest(t) instanceID := fixedInstanceID(0xAA) - require.NoError(t, ps.Put(3, "pod-2:7777", "pod-2:8888", instanceID)) + require.NoError(t, ps.Put(3, "pod-2:7777", "pod-2:8888", instanceID, nil)) m, err := NewMembership(ps, noopTransport{}, noopPool{}, testSelfNodeID, testSelfRaftAddr, testSelfServiceAddr, fixedInstanceID(0x42), logging.Testing(), NoopClusterPolicyProvider) require.NoError(t, err) @@ -93,7 +93,7 @@ func TestWriteConfChange_RemoveNodeWithoutIdentityFailsLoudly(t *testing.T) { ps, store := storesForBlacklistTest(t) instanceID := fixedInstanceID(0xAA) - require.NoError(t, ps.Put(3, "pod-2:7777", "pod-2:8888", instanceID)) + require.NoError(t, ps.Put(3, "pod-2:7777", "pod-2:8888", instanceID, nil)) m, err := NewMembership(ps, noopTransport{}, noopPool{}, testSelfNodeID, testSelfRaftAddr, testSelfServiceAddr, fixedInstanceID(0x42), logging.Testing(), NoopClusterPolicyProvider) require.NoError(t, err) @@ -164,7 +164,7 @@ func TestUnregisterAndBlacklist_AtomicBatch(t *testing.T) { ps, _ := storesForBlacklistTest(t) instanceID := fixedInstanceID(0xBB) - require.NoError(t, ps.Put(7, "pod-6:7777", "pod-6:8888", instanceID)) + require.NoError(t, ps.Put(7, "pod-6:7777", "pod-6:8888", instanceID, nil)) m, err := NewMembership(ps, noopTransport{}, noopPool{}, testSelfNodeID, testSelfRaftAddr, testSelfServiceAddr, fixedInstanceID(0x42), logging.Testing(), NoopClusterPolicyProvider) require.NoError(t, err) diff --git a/internal/infra/node/conf_change_retry_durable_test.go b/internal/infra/node/conf_change_retry_durable_test.go index f8860b81af..1b37d844ce 100644 --- a/internal/infra/node/conf_change_retry_durable_test.go +++ b/internal/infra/node/conf_change_retry_durable_test.go @@ -11,6 +11,7 @@ import ( logging "github.com/formancehq/go-libs/v5/pkg/observe/log" + "github.com/formancehq/ledger/v3/internal/storage/dal" "github.com/formancehq/ledger/v3/internal/storage/wal" ) @@ -29,7 +30,9 @@ func testPromoteLearnerRetryDuringWALPersistence(t *testing.T, cancelDuringWAL b n := newConfiguredPeersTestNode(t) n.logger = logging.Testing() - setup := newTestApplierSetupWithConfChangeHandler(t, make(LocalResponses, 1024), n.membership.WriteConfChange) + setup := newTestApplierSetupWithConfChangeHandler(t, make(LocalResponses, 1024), func(entry *raftpb.Entry, session *dal.WriteSession) error { + return n.membership.WriteConfChange(entry, session) + }) n.fsm = setup.fsm waitCtx, cancelWait := context.WithTimeout(t.Context(), 10*time.Second) defer cancelWait() diff --git a/internal/infra/node/confchange_identity_test.go b/internal/infra/node/confchange_identity_test.go index 137df7e110..6808bd29b8 100644 --- a/internal/infra/node/confchange_identity_test.go +++ b/internal/infra/node/confchange_identity_test.go @@ -58,7 +58,7 @@ func TestPromotionContextCarriesCompleteRegisteredIdentity(t *testing.T) { n := &Node{membership: m} _, err := n.promotionContext(9, "promotion") require.ErrorContains(t, err, "learner 9 has no membership row") - require.NoError(t, m.Set(9, "peer:7777", "peer:8888", []byte("peer-instance-id"))) + require.NoError(t, m.Set(9, "peer:7777", "peer:8888", []byte("peer-instance-id"), nil)) for _, proposalID := range []string{"", "promotion"} { payload, err := n.promotionContext(9, proposalID) require.NoError(t, err) @@ -81,7 +81,7 @@ func TestPromotionContextRejectsIncompleteAddresses(t *testing.T) { t.Run(name, func(t *testing.T) { t.Parallel() m := newTestMembership(t) - require.NoError(t, m.Set(9, addresses[0], addresses[1], []byte("peer-instance-id"))) + require.NoError(t, m.Set(9, addresses[0], addresses[1], []byte("peer-instance-id"), nil)) n := &Node{membership: m} payload, err := n.promotionContext(9, "promotion") require.Nil(t, payload) diff --git a/internal/infra/node/configured_peers_test.go b/internal/infra/node/configured_peers_test.go index 58edea25fa..bd80a89d53 100644 --- a/internal/infra/node/configured_peers_test.go +++ b/internal/infra/node/configured_peers_test.go @@ -41,10 +41,10 @@ func newConfiguredPeersTestNode(t *testing.T) *Node { } require.Equal(t, raft.StateLeader, rawNode.Status().RaftState) m := newTestMembership(t) - require.NoError(t, m.Set(1, "self:7777", "self:8888", []byte("self-instance-id"))) - require.NoError(t, m.Set(2, "old:7777", "old:8888", []byte("peer-instance-id"))) + require.NoError(t, m.Set(1, "self:7777", "self:8888", []byte("self-instance-id"), nil)) + require.NoError(t, m.Set(2, "old:7777", "old:8888", []byte("peer-instance-id"), nil)) // A cached row alone is not evidence of a configured member. - require.NoError(t, m.Set(3, "extra:7777", "extra:8888", []byte("extra-instanceid"))) + require.NoError(t, m.Set(3, "extra:7777", "extra:8888", []byte("extra-instanceid"), nil)) return &Node{ rawNode: rawNode, @@ -113,7 +113,7 @@ func TestGetConfiguredPeers_SnapshotSurvivesMembershipChange(t *testing.T) { n.rawNode.ApplyConfChange(&raftpb.ConfChangeV2{Changes: []*raftpb.ConfChangeSingle{{ Type: new(raftpb.ConfChangeAddLearnerNode), NodeId: new(uint64(2)), }}}) - require.NoError(t, n.membership.Set(2, "new:7777", "new:8888", []byte("next-instance-id"))) + require.NoError(t, n.membership.Set(2, "new:7777", "new:8888", []byte("next-instance-id"), nil)) } cmd.errCh <- err got := <-results @@ -168,7 +168,7 @@ func TestGetConfiguredPeers_TopologySnapshotIsAtomicWithConcurrentRehydrate(t *t // the snapshot, never in between. setDone := make(chan struct{}) go func() { - require.NoError(t, n.membership.Set(2, "new:7777", "new:8888", []byte("next-instance-id"))) + require.NoError(t, n.membership.Set(2, "new:7777", "new:8888", []byte("next-instance-id"), nil)) close(setDone) }() err := cmd.fn() @@ -211,10 +211,10 @@ func TestGetConfiguredPeers_TopologySnapshotIsAtomicWithConcurrentRehydrate_Dete // Persist all configured members to Pebble so Rehydrate does not drop // voter 1 from the cache. Set() updates only the in-memory cache; // Rehydrate replaces the whole cache from LoadAll. - require.NoError(t, n.membership.PeerStore().Put(1, "self:7777", "self:8888", []byte("self-instance-id"))) + require.NoError(t, n.membership.PeerStore().Put(1, "self:7777", "self:8888", []byte("self-instance-id"), nil)) // Persist the new incarnation for peer 2 so Rehydrate loads it. // The in-memory Membership cache still holds the old row. - require.NoError(t, n.membership.PeerStore().Put(2, "new:7777", "new:8888", []byte("next-instance-id"))) + require.NoError(t, n.membership.PeerStore().Put(2, "new:7777", "new:8888", []byte("next-instance-id"), nil)) type result struct { peers []Peer err error @@ -276,8 +276,8 @@ func TestGetConfiguredPeers_RehydrateBeforeSnapshotIsVisibleInCapture(t *testing // Persist all configured members to Pebble before Rehydrate. // Set() updates only the in-memory cache; Rehydrate reloads from Pebble. // Both voter-1 (self) and the updated peer-2 row must be present. - require.NoError(t, n.membership.PeerStore().Put(1, "self:7777", "self:8888", []byte("self-instance-id"))) - require.NoError(t, n.membership.PeerStore().Put(2, "new:7777", "new:8888", []byte("next-instance-id"))) + require.NoError(t, n.membership.PeerStore().Put(1, "self:7777", "self:8888", []byte("self-instance-id"), nil)) + require.NoError(t, n.membership.PeerStore().Put(2, "new:7777", "new:8888", []byte("next-instance-id"), nil)) require.NoError(t, n.membership.Rehydrate()) results := make(chan []Peer, 1) diff --git a/internal/infra/node/learner_admission_test.go b/internal/infra/node/learner_admission_test.go index de73e2a598..f9ee97afc7 100644 --- a/internal/infra/node/learner_admission_test.go +++ b/internal/infra/node/learner_admission_test.go @@ -12,6 +12,7 @@ import ( logging "github.com/formancehq/go-libs/v5/pkg/observe/log" "github.com/formancehq/ledger/v3/internal/infra/membership" + "github.com/formancehq/ledger/v3/internal/storage/dal" ) func TestAddLearnerRejectsRemovedAndActiveIncarnations(t *testing.T) { @@ -81,7 +82,9 @@ func TestAddLearnerZeroProgressRefreshProposesCompleteReplacement(t *testing.T) synctest.Test(t, func(t *testing.T) { n := newConfiguredPeersTestNode(t) n.logger = logging.Testing() - setup := newTestApplierSetupWithConfChangeHandler(t, make(LocalResponses, 1024), n.membership.WriteConfChange) + setup := newTestApplierSetupWithConfChangeHandler(t, make(LocalResponses, 1024), func(entry *raftpb.Entry, session *dal.WriteSession) error { + return n.membership.WriteConfChange(entry, session) + }) n.fsm = setup.fsm require.Zero(t, n.rawNode.Status().Progress[2].Match) ctx, cancel := context.WithTimeout(t.Context(), time.Second) @@ -136,7 +139,9 @@ func TestRefreshMembershipProposesUpdateForExistingIdentity(t *testing.T) { n := newConfiguredPeersTestNode(t) n.logger = logging.Testing() - setup := newTestApplierSetupWithConfChangeHandler(t, make(LocalResponses, 1024), n.membership.WriteConfChange) + setup := newTestApplierSetupWithConfChangeHandler(t, make(LocalResponses, 1024), func(entry *raftpb.Entry, session *dal.WriteSession) error { + return n.membership.WriteConfChange(entry, session) + }) n.fsm = setup.fsm ctx, cancel := context.WithTimeout(t.Context(), time.Second) diff --git a/internal/infra/node/node.go b/internal/infra/node/node.go index 0c86304f1e..2341b6b5d6 100644 --- a/internal/infra/node/node.go +++ b/internal/infra/node/node.go @@ -1774,7 +1774,7 @@ func (node *Node) finishReady(result readyResult, stop chan struct{}) error { if ctx == nil { return fmt.Errorf("invariant: committed registration for node %d has no identity context", nodeID) } - if err := node.membership.SetWithEvidence(nodeID, ctx.RaftAddress, ctx.ServiceAddress, ctx.InstanceID, ctx.Capabilities); err != nil { + if err := node.membership.Set(nodeID, ctx.RaftAddress, ctx.ServiceAddress, ctx.InstanceID, ctx.Capabilities); err != nil { return fmt.Errorf("invariant: applying membership cache update: %w", err) } case raftpb.ConfChangeRemoveNode: diff --git a/internal/infra/node/promotion_identity_test.go b/internal/infra/node/promotion_identity_test.go index 2d8d87cff9..233cbc9a4b 100644 --- a/internal/infra/node/promotion_identity_test.go +++ b/internal/infra/node/promotion_identity_test.go @@ -14,6 +14,7 @@ import ( logging "github.com/formancehq/go-libs/v5/pkg/observe/log" "github.com/formancehq/ledger/v3/internal/infra/membership" + "github.com/formancehq/ledger/v3/internal/storage/dal" ) // requireCompletePromotion inspects the actual entry emitted by RawNode, so @@ -48,7 +49,9 @@ func TestPromoteLearner_ProposesCompleteRegisteredIdentity(t *testing.T) { t.Parallel() n := newConfiguredPeersTestNode(t) n.logger = logging.Testing() - setup := newTestApplierSetupWithConfChangeHandler(t, make(LocalResponses, 1024), n.membership.WriteConfChange) + setup := newTestApplierSetupWithConfChangeHandler(t, make(LocalResponses, 1024), func(entry *raftpb.Entry, session *dal.WriteSession) error { + return n.membership.WriteConfChange(entry, session) + }) n.fsm = setup.fsm results := make(chan error, 1) go func() { From 0e1aa37e90a262e53ecfc2b75c67eab999168e5e Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Wed, 11 Nov 2026 16:49:12 +0100 Subject: [PATCH 121/127] test(ledger): update membership fixture for evidence API --- internal/application/membership/membership_node_test.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/internal/application/membership/membership_node_test.go b/internal/application/membership/membership_node_test.go index b1a4675bff..13d29320c3 100644 --- a/internal/application/membership/membership_node_test.go +++ b/internal/application/membership/membership_node_test.go @@ -80,7 +80,8 @@ func newMembershipServiceNodeWithPool(t *testing.T) (*Service, *transport.Connec registry := state.NewStateRegistry(c, attributes.New()) fsm, err := state.NewMachine(logger, registry, state.NewCacheSnapshotter(logger, registry, nil), store, dal.NewSentinelFactory(store, false), meters, nil, state.NewSharedState(), - signal.NewNotifications(), nil, "membership-service-test", 0, m.WriteConfChange, + signal.NewNotifications(), nil, "membership-service-test", 0, + func(entry *raftpb.Entry, session *dal.WriteSession) error { return m.WriteConfChange(entry, session) }, state.NoopCapabilityPolicyCommitHandler) require.NoError(t, err) recovery := state.NewRecovery(fsm, store) @@ -185,7 +186,7 @@ func TestService_ListPeersReturnsCompleteIndependentSnapshot(t *testing.T) { defer cancel() require.NoError(t, s.AddLearner(ctx, 2, "peer:7777", "peer:8888", []byte("peer-instance-id"), nil)) // A cached row alone must not leak into the configured member response. - require.NoError(t, s.infraMembership.Set(3, "extra:7777", "extra:8888", []byte("extra-instanceid"))) + require.NoError(t, s.infraMembership.Set(3, "extra:7777", "extra:8888", []byte("extra-instanceid"), nil)) want := []Peer{ {ID: 1, RaftAddress: "public-self:7777", ServiceAddress: "public-self:8888", InstanceID: []byte("self-instance-id")}, {ID: 2, RaftAddress: "peer:7777", ServiceAddress: "peer:8888", InstanceID: []byte("peer-instance-id")}, From 6f87a0c0fe80d0678b5a134da095985fc4f37846 Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Wed, 11 Nov 2026 17:01:09 +0100 Subject: [PATCH 122/127] style(ledger): sync generated mocks and CI formatting --- ...bootstrap_retry_proposer_generated_test.go | 63 +++++++++++++++++++ .../grpc/oidc_proposer_generated_test.go | 63 +++++++++++++++++++ internal/application/admission/admission.go | 2 + internal/infra/membership/membership.go | 1 + 4 files changed, 129 insertions(+) diff --git a/cmd/ledgerctl/store/bootstrap_retry_proposer_generated_test.go b/cmd/ledgerctl/store/bootstrap_retry_proposer_generated_test.go index d4eac7d613..3e8639c1d7 100644 --- a/cmd/ledgerctl/store/bootstrap_retry_proposer_generated_test.go +++ b/cmd/ledgerctl/store/bootstrap_retry_proposer_generated_test.go @@ -119,3 +119,66 @@ func (c *MockProposerProposeCall) DoAndReturn(f func(context.Context, *node.Prop c.Call = c.Call.DoAndReturn(f) return c } + +// MockCapabilityMembershipFence is a mock of CapabilityMembershipFence interface. +type MockCapabilityMembershipFence struct { + ctrl *gomock.Controller + recorder *MockCapabilityMembershipFenceMockRecorder + isgomock struct{} +} + +// MockCapabilityMembershipFenceMockRecorder is the mock recorder for MockCapabilityMembershipFence. +type MockCapabilityMembershipFenceMockRecorder struct { + mock *MockCapabilityMembershipFence +} + +// NewMockCapabilityMembershipFence creates a new mock instance. +func NewMockCapabilityMembershipFence(ctrl *gomock.Controller) *MockCapabilityMembershipFence { + mock := &MockCapabilityMembershipFence{ctrl: ctrl} + mock.recorder = &MockCapabilityMembershipFenceMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockCapabilityMembershipFence) EXPECT() *MockCapabilityMembershipFenceMockRecorder { + return m.recorder +} + +// AcquireCapabilityMembership mocks base method. +func (m *MockCapabilityMembershipFence) AcquireCapabilityMembership(arg0 context.Context) (func(*futures.Future[state.ApplyResult]), error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AcquireCapabilityMembership", arg0) + ret0, _ := ret[0].(func(*futures.Future[state.ApplyResult])) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// AcquireCapabilityMembership indicates an expected call of AcquireCapabilityMembership. +func (mr *MockCapabilityMembershipFenceMockRecorder) AcquireCapabilityMembership(arg0 any) *MockCapabilityMembershipFenceAcquireCapabilityMembershipCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AcquireCapabilityMembership", reflect.TypeOf((*MockCapabilityMembershipFence)(nil).AcquireCapabilityMembership), arg0) + return &MockCapabilityMembershipFenceAcquireCapabilityMembershipCall{Call: call} +} + +// MockCapabilityMembershipFenceAcquireCapabilityMembershipCall wrap *gomock.Call +type MockCapabilityMembershipFenceAcquireCapabilityMembershipCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockCapabilityMembershipFenceAcquireCapabilityMembershipCall) Return(arg0 func(*futures.Future[state.ApplyResult]), arg1 error) *MockCapabilityMembershipFenceAcquireCapabilityMembershipCall { + c.Call = c.Call.Return(arg0, arg1) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockCapabilityMembershipFenceAcquireCapabilityMembershipCall) Do(f func(context.Context) (func(*futures.Future[state.ApplyResult]), error)) *MockCapabilityMembershipFenceAcquireCapabilityMembershipCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockCapabilityMembershipFenceAcquireCapabilityMembershipCall) DoAndReturn(f func(context.Context) (func(*futures.Future[state.ApplyResult]), error)) *MockCapabilityMembershipFenceAcquireCapabilityMembershipCall { + c.Call = c.Call.DoAndReturn(f) + return c +} diff --git a/internal/adapter/grpc/oidc_proposer_generated_test.go b/internal/adapter/grpc/oidc_proposer_generated_test.go index c85b0a600f..9ff5cb880a 100644 --- a/internal/adapter/grpc/oidc_proposer_generated_test.go +++ b/internal/adapter/grpc/oidc_proposer_generated_test.go @@ -117,3 +117,66 @@ func (c *MockProposerProposeCall) DoAndReturn(f func(context.Context, *node.Prop c.Call = c.Call.DoAndReturn(f) return c } + +// MockCapabilityMembershipFence is a mock of CapabilityMembershipFence interface. +type MockCapabilityMembershipFence struct { + ctrl *gomock.Controller + recorder *MockCapabilityMembershipFenceMockRecorder + isgomock struct{} +} + +// MockCapabilityMembershipFenceMockRecorder is the mock recorder for MockCapabilityMembershipFence. +type MockCapabilityMembershipFenceMockRecorder struct { + mock *MockCapabilityMembershipFence +} + +// NewMockCapabilityMembershipFence creates a new mock instance. +func NewMockCapabilityMembershipFence(ctrl *gomock.Controller) *MockCapabilityMembershipFence { + mock := &MockCapabilityMembershipFence{ctrl: ctrl} + mock.recorder = &MockCapabilityMembershipFenceMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockCapabilityMembershipFence) EXPECT() *MockCapabilityMembershipFenceMockRecorder { + return m.recorder +} + +// AcquireCapabilityMembership mocks base method. +func (m *MockCapabilityMembershipFence) AcquireCapabilityMembership(arg0 context.Context) (func(*futures.Future[state.ApplyResult]), error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AcquireCapabilityMembership", arg0) + ret0, _ := ret[0].(func(*futures.Future[state.ApplyResult])) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// AcquireCapabilityMembership indicates an expected call of AcquireCapabilityMembership. +func (mr *MockCapabilityMembershipFenceMockRecorder) AcquireCapabilityMembership(arg0 any) *MockCapabilityMembershipFenceAcquireCapabilityMembershipCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AcquireCapabilityMembership", reflect.TypeOf((*MockCapabilityMembershipFence)(nil).AcquireCapabilityMembership), arg0) + return &MockCapabilityMembershipFenceAcquireCapabilityMembershipCall{Call: call} +} + +// MockCapabilityMembershipFenceAcquireCapabilityMembershipCall wrap *gomock.Call +type MockCapabilityMembershipFenceAcquireCapabilityMembershipCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockCapabilityMembershipFenceAcquireCapabilityMembershipCall) Return(arg0 func(*futures.Future[state.ApplyResult]), arg1 error) *MockCapabilityMembershipFenceAcquireCapabilityMembershipCall { + c.Call = c.Call.Return(arg0, arg1) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockCapabilityMembershipFenceAcquireCapabilityMembershipCall) Do(f func(context.Context) (func(*futures.Future[state.ApplyResult]), error)) *MockCapabilityMembershipFenceAcquireCapabilityMembershipCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockCapabilityMembershipFenceAcquireCapabilityMembershipCall) DoAndReturn(f func(context.Context) (func(*futures.Future[state.ApplyResult]), error)) *MockCapabilityMembershipFenceAcquireCapabilityMembershipCall { + c.Call = c.Call.DoAndReturn(f) + return c +} diff --git a/internal/application/admission/admission.go b/internal/application/admission/admission.go index 72694e52bf..c3c54fa519 100644 --- a/internal/application/admission/admission.go +++ b/internal/application/admission/admission.go @@ -508,6 +508,7 @@ func (a *Admission) AdmitClusterPolicy(ctx context.Context, policy *commonpb.Clu for _, activation := range policy.GetCapabilityActivations() { if activation != nil && activation.GetActive() { propose = a.proposeActivatingPolicy + break } } @@ -621,6 +622,7 @@ func (a *Admission) proposeActivatingPolicy(ctx context.Context, command *raftcm } future, cleanup, err := a.proposeAdmission(ctx, command, build) release(future) + return future, cleanup, err } diff --git a/internal/infra/membership/membership.go b/internal/infra/membership/membership.go index c6bf15c568..4c61368ec5 100644 --- a/internal/infra/membership/membership.go +++ b/internal/infra/membership/membership.go @@ -683,6 +683,7 @@ func (m *Membership) WriteConfChange(entry *raftpb.Entry, session *dal.WriteSess if len(effectivePolicy) == 1 { return m.writeConfChange(entry, session, effectivePolicy[0], true) } + return m.writeConfChange(entry, session, nil, false) } From 55fd2b840b86562221b5a263c3a47bbc2471934b Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Wed, 11 Nov 2026 17:08:31 +0100 Subject: [PATCH 123/127] style(ledger): keep admission formatting stable --- internal/application/admission/admission.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/application/admission/admission.go b/internal/application/admission/admission.go index c3c54fa519..8d49ea5585 100644 --- a/internal/application/admission/admission.go +++ b/internal/application/admission/admission.go @@ -512,6 +512,7 @@ func (a *Admission) AdmitClusterPolicy(ctx context.Context, policy *commonpb.Clu break } } + return a.admit(ctx, func(ctx context.Context, caller *commonpb.CallerSnapshot) (*preparedAdmission, error) { return a.prepareClusterPolicy(ctx, caller, policy) }, propose) From 22403a724207ff9e667c9b92c025ec7191f5c29e Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Thu, 12 Nov 2026 10:19:56 +0100 Subject: [PATCH 124/127] refactor(ledger): coordinate membership outside raft node --- ...bootstrap_retry_proposer_generated_test.go | 63 ------------------- .../grpc/oidc_proposer_generated_test.go | 63 ------------------- internal/application/admission/admission.go | 8 +-- .../admission/proposer_generated_test.go | 63 ------------------- internal/bootstrap/module.go | 2 +- .../infra/node/capability_membership_test.go | 6 +- internal/infra/node/membership_coordinator.go | 62 ++++++++++++++++++ internal/infra/node/node.go | 52 +++------------ 8 files changed, 77 insertions(+), 242 deletions(-) create mode 100644 internal/infra/node/membership_coordinator.go diff --git a/cmd/ledgerctl/store/bootstrap_retry_proposer_generated_test.go b/cmd/ledgerctl/store/bootstrap_retry_proposer_generated_test.go index 3e8639c1d7..d4eac7d613 100644 --- a/cmd/ledgerctl/store/bootstrap_retry_proposer_generated_test.go +++ b/cmd/ledgerctl/store/bootstrap_retry_proposer_generated_test.go @@ -119,66 +119,3 @@ func (c *MockProposerProposeCall) DoAndReturn(f func(context.Context, *node.Prop c.Call = c.Call.DoAndReturn(f) return c } - -// MockCapabilityMembershipFence is a mock of CapabilityMembershipFence interface. -type MockCapabilityMembershipFence struct { - ctrl *gomock.Controller - recorder *MockCapabilityMembershipFenceMockRecorder - isgomock struct{} -} - -// MockCapabilityMembershipFenceMockRecorder is the mock recorder for MockCapabilityMembershipFence. -type MockCapabilityMembershipFenceMockRecorder struct { - mock *MockCapabilityMembershipFence -} - -// NewMockCapabilityMembershipFence creates a new mock instance. -func NewMockCapabilityMembershipFence(ctrl *gomock.Controller) *MockCapabilityMembershipFence { - mock := &MockCapabilityMembershipFence{ctrl: ctrl} - mock.recorder = &MockCapabilityMembershipFenceMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockCapabilityMembershipFence) EXPECT() *MockCapabilityMembershipFenceMockRecorder { - return m.recorder -} - -// AcquireCapabilityMembership mocks base method. -func (m *MockCapabilityMembershipFence) AcquireCapabilityMembership(arg0 context.Context) (func(*futures.Future[state.ApplyResult]), error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "AcquireCapabilityMembership", arg0) - ret0, _ := ret[0].(func(*futures.Future[state.ApplyResult])) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// AcquireCapabilityMembership indicates an expected call of AcquireCapabilityMembership. -func (mr *MockCapabilityMembershipFenceMockRecorder) AcquireCapabilityMembership(arg0 any) *MockCapabilityMembershipFenceAcquireCapabilityMembershipCall { - mr.mock.ctrl.T.Helper() - call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AcquireCapabilityMembership", reflect.TypeOf((*MockCapabilityMembershipFence)(nil).AcquireCapabilityMembership), arg0) - return &MockCapabilityMembershipFenceAcquireCapabilityMembershipCall{Call: call} -} - -// MockCapabilityMembershipFenceAcquireCapabilityMembershipCall wrap *gomock.Call -type MockCapabilityMembershipFenceAcquireCapabilityMembershipCall struct { - *gomock.Call -} - -// Return rewrite *gomock.Call.Return -func (c *MockCapabilityMembershipFenceAcquireCapabilityMembershipCall) Return(arg0 func(*futures.Future[state.ApplyResult]), arg1 error) *MockCapabilityMembershipFenceAcquireCapabilityMembershipCall { - c.Call = c.Call.Return(arg0, arg1) - return c -} - -// Do rewrite *gomock.Call.Do -func (c *MockCapabilityMembershipFenceAcquireCapabilityMembershipCall) Do(f func(context.Context) (func(*futures.Future[state.ApplyResult]), error)) *MockCapabilityMembershipFenceAcquireCapabilityMembershipCall { - c.Call = c.Call.Do(f) - return c -} - -// DoAndReturn rewrite *gomock.Call.DoAndReturn -func (c *MockCapabilityMembershipFenceAcquireCapabilityMembershipCall) DoAndReturn(f func(context.Context) (func(*futures.Future[state.ApplyResult]), error)) *MockCapabilityMembershipFenceAcquireCapabilityMembershipCall { - c.Call = c.Call.DoAndReturn(f) - return c -} diff --git a/internal/adapter/grpc/oidc_proposer_generated_test.go b/internal/adapter/grpc/oidc_proposer_generated_test.go index 9ff5cb880a..c85b0a600f 100644 --- a/internal/adapter/grpc/oidc_proposer_generated_test.go +++ b/internal/adapter/grpc/oidc_proposer_generated_test.go @@ -117,66 +117,3 @@ func (c *MockProposerProposeCall) DoAndReturn(f func(context.Context, *node.Prop c.Call = c.Call.DoAndReturn(f) return c } - -// MockCapabilityMembershipFence is a mock of CapabilityMembershipFence interface. -type MockCapabilityMembershipFence struct { - ctrl *gomock.Controller - recorder *MockCapabilityMembershipFenceMockRecorder - isgomock struct{} -} - -// MockCapabilityMembershipFenceMockRecorder is the mock recorder for MockCapabilityMembershipFence. -type MockCapabilityMembershipFenceMockRecorder struct { - mock *MockCapabilityMembershipFence -} - -// NewMockCapabilityMembershipFence creates a new mock instance. -func NewMockCapabilityMembershipFence(ctrl *gomock.Controller) *MockCapabilityMembershipFence { - mock := &MockCapabilityMembershipFence{ctrl: ctrl} - mock.recorder = &MockCapabilityMembershipFenceMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockCapabilityMembershipFence) EXPECT() *MockCapabilityMembershipFenceMockRecorder { - return m.recorder -} - -// AcquireCapabilityMembership mocks base method. -func (m *MockCapabilityMembershipFence) AcquireCapabilityMembership(arg0 context.Context) (func(*futures.Future[state.ApplyResult]), error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "AcquireCapabilityMembership", arg0) - ret0, _ := ret[0].(func(*futures.Future[state.ApplyResult])) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// AcquireCapabilityMembership indicates an expected call of AcquireCapabilityMembership. -func (mr *MockCapabilityMembershipFenceMockRecorder) AcquireCapabilityMembership(arg0 any) *MockCapabilityMembershipFenceAcquireCapabilityMembershipCall { - mr.mock.ctrl.T.Helper() - call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AcquireCapabilityMembership", reflect.TypeOf((*MockCapabilityMembershipFence)(nil).AcquireCapabilityMembership), arg0) - return &MockCapabilityMembershipFenceAcquireCapabilityMembershipCall{Call: call} -} - -// MockCapabilityMembershipFenceAcquireCapabilityMembershipCall wrap *gomock.Call -type MockCapabilityMembershipFenceAcquireCapabilityMembershipCall struct { - *gomock.Call -} - -// Return rewrite *gomock.Call.Return -func (c *MockCapabilityMembershipFenceAcquireCapabilityMembershipCall) Return(arg0 func(*futures.Future[state.ApplyResult]), arg1 error) *MockCapabilityMembershipFenceAcquireCapabilityMembershipCall { - c.Call = c.Call.Return(arg0, arg1) - return c -} - -// Do rewrite *gomock.Call.Do -func (c *MockCapabilityMembershipFenceAcquireCapabilityMembershipCall) Do(f func(context.Context) (func(*futures.Future[state.ApplyResult]), error)) *MockCapabilityMembershipFenceAcquireCapabilityMembershipCall { - c.Call = c.Call.Do(f) - return c -} - -// DoAndReturn rewrite *gomock.Call.DoAndReturn -func (c *MockCapabilityMembershipFenceAcquireCapabilityMembershipCall) DoAndReturn(f func(context.Context) (func(*futures.Future[state.ApplyResult]), error)) *MockCapabilityMembershipFenceAcquireCapabilityMembershipCall { - c.Call = c.Call.DoAndReturn(f) - return c -} diff --git a/internal/application/admission/admission.go b/internal/application/admission/admission.go index 8d49ea5585..c8f1a56d34 100644 --- a/internal/application/admission/admission.go +++ b/internal/application/admission/admission.go @@ -47,17 +47,13 @@ type Proposer interface { InitialIndex() uint64 } -type CapabilityMembershipFence interface { - AcquireCapabilityMembership(context.Context) (func(*futures.Future[state.ApplyResult]), error) -} - // Admission handles the admission of orders into the Raft cluster. // It is responsible for preloading volumes and proposing commands. type Admission struct { store *dal.Store logger logging.Logger proposer Proposer - membershipFence CapabilityMembershipFence + membershipFence *node.MembershipCoordinator writeGate health.WriteGate keyStore *keystore.KeyStore sharedState *state.SharedState @@ -143,7 +139,7 @@ func NewAdmission( store *dal.Store, logger logging.Logger, proposer Proposer, - membershipFence CapabilityMembershipFence, + membershipFence *node.MembershipCoordinator, builder *plan.Builder, meterProvider metric.MeterProvider, writeGate health.WriteGate, diff --git a/internal/application/admission/proposer_generated_test.go b/internal/application/admission/proposer_generated_test.go index 5f8e977653..e25603d8af 100644 --- a/internal/application/admission/proposer_generated_test.go +++ b/internal/application/admission/proposer_generated_test.go @@ -117,66 +117,3 @@ func (c *MockProposerProposeCall) DoAndReturn(f func(context.Context, *node.Prop c.Call = c.Call.DoAndReturn(f) return c } - -// MockCapabilityMembershipFence is a mock of CapabilityMembershipFence interface. -type MockCapabilityMembershipFence struct { - ctrl *gomock.Controller - recorder *MockCapabilityMembershipFenceMockRecorder - isgomock struct{} -} - -// MockCapabilityMembershipFenceMockRecorder is the mock recorder for MockCapabilityMembershipFence. -type MockCapabilityMembershipFenceMockRecorder struct { - mock *MockCapabilityMembershipFence -} - -// NewMockCapabilityMembershipFence creates a new mock instance. -func NewMockCapabilityMembershipFence(ctrl *gomock.Controller) *MockCapabilityMembershipFence { - mock := &MockCapabilityMembershipFence{ctrl: ctrl} - mock.recorder = &MockCapabilityMembershipFenceMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockCapabilityMembershipFence) EXPECT() *MockCapabilityMembershipFenceMockRecorder { - return m.recorder -} - -// AcquireCapabilityMembership mocks base method. -func (m *MockCapabilityMembershipFence) AcquireCapabilityMembership(arg0 context.Context) (func(*futures.Future[state.ApplyResult]), error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "AcquireCapabilityMembership", arg0) - ret0, _ := ret[0].(func(*futures.Future[state.ApplyResult])) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// AcquireCapabilityMembership indicates an expected call of AcquireCapabilityMembership. -func (mr *MockCapabilityMembershipFenceMockRecorder) AcquireCapabilityMembership(arg0 any) *MockCapabilityMembershipFenceAcquireCapabilityMembershipCall { - mr.mock.ctrl.T.Helper() - call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AcquireCapabilityMembership", reflect.TypeOf((*MockCapabilityMembershipFence)(nil).AcquireCapabilityMembership), arg0) - return &MockCapabilityMembershipFenceAcquireCapabilityMembershipCall{Call: call} -} - -// MockCapabilityMembershipFenceAcquireCapabilityMembershipCall wrap *gomock.Call -type MockCapabilityMembershipFenceAcquireCapabilityMembershipCall struct { - *gomock.Call -} - -// Return rewrite *gomock.Call.Return -func (c *MockCapabilityMembershipFenceAcquireCapabilityMembershipCall) Return(arg0 func(*futures.Future[state.ApplyResult]), arg1 error) *MockCapabilityMembershipFenceAcquireCapabilityMembershipCall { - c.Call = c.Call.Return(arg0, arg1) - return c -} - -// Do rewrite *gomock.Call.Do -func (c *MockCapabilityMembershipFenceAcquireCapabilityMembershipCall) Do(f func(context.Context) (func(*futures.Future[state.ApplyResult]), error)) *MockCapabilityMembershipFenceAcquireCapabilityMembershipCall { - c.Call = c.Call.Do(f) - return c -} - -// DoAndReturn rewrite *gomock.Call.DoAndReturn -func (c *MockCapabilityMembershipFenceAcquireCapabilityMembershipCall) DoAndReturn(f func(context.Context) (func(*futures.Future[state.ApplyResult]), error)) *MockCapabilityMembershipFenceAcquireCapabilityMembershipCall { - c.Call = c.Call.DoAndReturn(f) - return c -} diff --git a/internal/bootstrap/module.go b/internal/bootstrap/module.go index ae73155384..35042241d0 100644 --- a/internal/bootstrap/module.go +++ b/internal/bootstrap/module.go @@ -650,7 +650,7 @@ func Module() fx.Option { store, logger, node, - node, + node.MembershipCoordinator(), builder, meterProvider, hc, diff --git a/internal/infra/node/capability_membership_test.go b/internal/infra/node/capability_membership_test.go index 5e8c16146b..660f33d214 100644 --- a/internal/infra/node/capability_membership_test.go +++ b/internal/infra/node/capability_membership_test.go @@ -14,7 +14,9 @@ func TestCapabilityMembershipFenceHeldUntilPolicyApply(t *testing.T) { n := &Node{stopChannel: make(chan struct{}), terminalCh: make(chan struct{})} n.confChangeMu.Lock() // acquired before the policy proposal is enqueued future := futures.New[state.ApplyResult]() - n.releaseCapabilityMembershipAfter(future) + n.confChangeMu.stop = n.stopChannel + n.confChangeMu.terminal = n.terminalCh + n.confChangeMu.releaseAfter(future) require.False(t, n.confChangeMu.TryLock(), "a learner must not pass a pending activation") future.Resolve(state.ApplyResult{}, nil) @@ -31,7 +33,7 @@ func TestCapabilityMembershipFenceHeldUntilPolicyApply(t *testing.T) { func TestCapabilityMembershipFenceReleasedWithoutProposal(t *testing.T) { n := &Node{} n.confChangeMu.Lock() - n.releaseCapabilityMembershipAfter(nil) + n.confChangeMu.releaseAfter(nil) require.True(t, n.confChangeMu.TryLock()) n.confChangeMu.Unlock() } diff --git a/internal/infra/node/membership_coordinator.go b/internal/infra/node/membership_coordinator.go new file mode 100644 index 0000000000..b87ed7f5ac --- /dev/null +++ b/internal/infra/node/membership_coordinator.go @@ -0,0 +1,62 @@ +package node + +import ( + "context" + "sync" + "time" + + "github.com/formancehq/ledger/v3/internal/infra/state" + "github.com/formancehq/ledger/v3/internal/pkg/futures" +) + +// MembershipCoordinator serializes Raft membership changes with policy activation. +// Its lock is held until an activating policy has applied. +type MembershipCoordinator struct { + sync.Mutex + + waitForPriorMembership func(context.Context) error + stop <-chan struct{} + terminal <-chan struct{} +} + +func (c *MembershipCoordinator) AcquireCapabilityMembership(ctx context.Context) (func(*futures.Future[state.ApplyResult]), error) { + for !c.TryLock() { + select { + case <-ctx.Done(): + return nil, ctx.Err() + case <-time.After(10 * time.Millisecond): + } + } + if c.waitForPriorMembership != nil { + if err := c.waitForPriorMembership(ctx); err != nil { + c.Unlock() + + return nil, err + } + } + + return c.releaseAfter, nil +} + +func (c *MembershipCoordinator) releaseAfter(future *futures.Future[state.ApplyResult]) { + if future == nil { + c.Unlock() + + return + } + go func() { + defer c.Unlock() + waitCtx, cancel := context.WithCancel(context.Background()) + defer cancel() + go func() { + select { + case <-c.stop: + cancel() + case <-c.terminal: + cancel() + case <-waitCtx.Done(): + } + }() + _, _ = future.Wait(waitCtx) + }() +} diff --git a/internal/infra/node/node.go b/internal/infra/node/node.go index 2341b6b5d6..f9200134c8 100644 --- a/internal/infra/node/node.go +++ b/internal/infra/node/node.go @@ -424,7 +424,7 @@ type Node struct { // RemoveNode, PromoteLearner) so that only one proposal is in-flight at a // time. This avoids unnecessary retries caused by etcd/raft silently // dropping concurrent ConfChange proposals. - confChangeMu sync.Mutex + confChangeMu MembershipCoordinator // lastAutoPromote tracks the last time an auto-promotion was proposed for // each learner node. Used to rate-limit proposals and avoid spamming raft @@ -737,6 +737,9 @@ func NewNode( for _, opt := range opts { opt(node) } + node.confChangeMu.waitForPriorMembership = node.waitForPriorMembership + node.confChangeMu.stop = node.stopChannel + node.confChangeMu.terminal = node.terminalCh // Start ready: no leadership transition is in progress yet. node.leaderReady.Store(newCompletedLeaderReadyGeneration()) @@ -3242,49 +3245,10 @@ func (node *Node) WithStableDrainMembership(ctx context.Context, fn func() error return fn() } -// AcquireCapabilityMembership fences an activating policy proposal against -// membership changes. The caller must acquire this before the proposal/index -// guard, then hand the returned release function the FSM future immediately -// after enqueue. The lock remains held through apply even if the request is -// canceled. A nil future releases it when no proposal was enqueued. -func (node *Node) AcquireCapabilityMembership(ctx context.Context) (func(*futures.Future[state.ApplyResult]), error) { - for !node.confChangeMu.TryLock() { - select { - case <-ctx.Done(): - return nil, ctx.Err() - case <-time.After(10 * time.Millisecond): - } - } - if err := node.waitForPriorMembership(ctx); err != nil { - node.confChangeMu.Unlock() - - return nil, err - } - - return node.releaseCapabilityMembershipAfter, nil -} - -func (node *Node) releaseCapabilityMembershipAfter(future *futures.Future[state.ApplyResult]) { - if future == nil { - node.confChangeMu.Unlock() - - return - } - go func() { - defer node.confChangeMu.Unlock() - waitCtx, cancel := context.WithCancel(context.Background()) - defer cancel() - go func() { - select { - case <-node.stopChannel: - cancel() - case <-node.terminalCh: - cancel() - case <-waitCtx.Done(): - } - }() - _, _ = future.Wait(waitCtx) - }() +// MembershipCoordinator returns the shared guard for membership changes and +// capability policy activation. +func (node *Node) MembershipCoordinator() *MembershipCoordinator { + return &node.confChangeMu } // JoinAsLearner is the bootstrap admission path for a process with an empty From 205068cc5bc96b7b2180b2b7794f490d91498617 Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Thu, 12 Nov 2026 11:05:25 +0100 Subject: [PATCH 125/127] refactor(ledger): move membership coordinator to membership package --- internal/application/admission/admission.go | 5 +++-- .../coordinator.go} | 17 ++++++++++++----- .../infra/node/capability_membership_test.go | 14 ++++++++------ internal/infra/node/node.go | 8 +++----- 4 files changed, 26 insertions(+), 18 deletions(-) rename internal/infra/{node/membership_coordinator.go => membership/coordinator.go} (61%) diff --git a/internal/application/admission/admission.go b/internal/application/admission/admission.go index c8f1a56d34..4107d55ea6 100644 --- a/internal/application/admission/admission.go +++ b/internal/application/admission/admission.go @@ -26,6 +26,7 @@ import ( "github.com/formancehq/ledger/v3/internal/domain/processing/numscript" "github.com/formancehq/ledger/v3/internal/infra/attributes" "github.com/formancehq/ledger/v3/internal/infra/health" + "github.com/formancehq/ledger/v3/internal/infra/membership" "github.com/formancehq/ledger/v3/internal/infra/node" "github.com/formancehq/ledger/v3/internal/infra/plan" "github.com/formancehq/ledger/v3/internal/infra/state" @@ -53,7 +54,7 @@ type Admission struct { store *dal.Store logger logging.Logger proposer Proposer - membershipFence *node.MembershipCoordinator + membershipFence *membership.Coordinator writeGate health.WriteGate keyStore *keystore.KeyStore sharedState *state.SharedState @@ -139,7 +140,7 @@ func NewAdmission( store *dal.Store, logger logging.Logger, proposer Proposer, - membershipFence *node.MembershipCoordinator, + membershipFence *membership.Coordinator, builder *plan.Builder, meterProvider metric.MeterProvider, writeGate health.WriteGate, diff --git a/internal/infra/node/membership_coordinator.go b/internal/infra/membership/coordinator.go similarity index 61% rename from internal/infra/node/membership_coordinator.go rename to internal/infra/membership/coordinator.go index b87ed7f5ac..7df6a21653 100644 --- a/internal/infra/node/membership_coordinator.go +++ b/internal/infra/membership/coordinator.go @@ -1,4 +1,4 @@ -package node +package membership import ( "context" @@ -9,9 +9,9 @@ import ( "github.com/formancehq/ledger/v3/internal/pkg/futures" ) -// MembershipCoordinator serializes Raft membership changes with policy activation. +// Coordinator serializes Raft membership changes with policy activation. // Its lock is held until an activating policy has applied. -type MembershipCoordinator struct { +type Coordinator struct { sync.Mutex waitForPriorMembership func(context.Context) error @@ -19,7 +19,14 @@ type MembershipCoordinator struct { terminal <-chan struct{} } -func (c *MembershipCoordinator) AcquireCapabilityMembership(ctx context.Context) (func(*futures.Future[state.ApplyResult]), error) { +// Configure connects the Raft barrier and lifecycle signals before the coordinator is used. +func (c *Coordinator) Configure(waitForPriorMembership func(context.Context) error, stop, terminal <-chan struct{}) { + c.waitForPriorMembership = waitForPriorMembership + c.stop = stop + c.terminal = terminal +} + +func (c *Coordinator) AcquireCapabilityMembership(ctx context.Context) (func(*futures.Future[state.ApplyResult]), error) { for !c.TryLock() { select { case <-ctx.Done(): @@ -38,7 +45,7 @@ func (c *MembershipCoordinator) AcquireCapabilityMembership(ctx context.Context) return c.releaseAfter, nil } -func (c *MembershipCoordinator) releaseAfter(future *futures.Future[state.ApplyResult]) { +func (c *Coordinator) releaseAfter(future *futures.Future[state.ApplyResult]) { if future == nil { c.Unlock() diff --git a/internal/infra/node/capability_membership_test.go b/internal/infra/node/capability_membership_test.go index 660f33d214..227c35802d 100644 --- a/internal/infra/node/capability_membership_test.go +++ b/internal/infra/node/capability_membership_test.go @@ -1,6 +1,7 @@ package node import ( + "context" "testing" "time" @@ -12,11 +13,11 @@ import ( func TestCapabilityMembershipFenceHeldUntilPolicyApply(t *testing.T) { n := &Node{stopChannel: make(chan struct{}), terminalCh: make(chan struct{})} - n.confChangeMu.Lock() // acquired before the policy proposal is enqueued + n.confChangeMu.Configure(nil, n.stopChannel, n.terminalCh) + release, err := n.confChangeMu.AcquireCapabilityMembership(context.Background()) + require.NoError(t, err) future := futures.New[state.ApplyResult]() - n.confChangeMu.stop = n.stopChannel - n.confChangeMu.terminal = n.terminalCh - n.confChangeMu.releaseAfter(future) + release(future) require.False(t, n.confChangeMu.TryLock(), "a learner must not pass a pending activation") future.Resolve(state.ApplyResult{}, nil) @@ -32,8 +33,9 @@ func TestCapabilityMembershipFenceHeldUntilPolicyApply(t *testing.T) { func TestCapabilityMembershipFenceReleasedWithoutProposal(t *testing.T) { n := &Node{} - n.confChangeMu.Lock() - n.confChangeMu.releaseAfter(nil) + release, err := n.confChangeMu.AcquireCapabilityMembership(context.Background()) + require.NoError(t, err) + release(nil) require.True(t, n.confChangeMu.TryLock()) n.confChangeMu.Unlock() } diff --git a/internal/infra/node/node.go b/internal/infra/node/node.go index f9200134c8..bf6263bbc4 100644 --- a/internal/infra/node/node.go +++ b/internal/infra/node/node.go @@ -424,7 +424,7 @@ type Node struct { // RemoveNode, PromoteLearner) so that only one proposal is in-flight at a // time. This avoids unnecessary retries caused by etcd/raft silently // dropping concurrent ConfChange proposals. - confChangeMu MembershipCoordinator + confChangeMu membership.Coordinator // lastAutoPromote tracks the last time an auto-promotion was proposed for // each learner node. Used to rate-limit proposals and avoid spamming raft @@ -737,9 +737,7 @@ func NewNode( for _, opt := range opts { opt(node) } - node.confChangeMu.waitForPriorMembership = node.waitForPriorMembership - node.confChangeMu.stop = node.stopChannel - node.confChangeMu.terminal = node.terminalCh + node.confChangeMu.Configure(node.waitForPriorMembership, node.stopChannel, node.terminalCh) // Start ready: no leadership transition is in progress yet. node.leaderReady.Store(newCompletedLeaderReadyGeneration()) @@ -3247,7 +3245,7 @@ func (node *Node) WithStableDrainMembership(ctx context.Context, fn func() error // MembershipCoordinator returns the shared guard for membership changes and // capability policy activation. -func (node *Node) MembershipCoordinator() *MembershipCoordinator { +func (node *Node) MembershipCoordinator() *membership.Coordinator { return &node.confChangeMu } From 2545f461c45faf672bdec77792abcde479d7c6be Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Thu, 12 Nov 2026 11:18:11 +0100 Subject: [PATCH 126/127] refactor(ledger): fence every cluster policy update --- internal/application/admission/admission.go | 67 ++++++++----------- .../admission/admission_phase_metrics_test.go | 3 +- .../admission/cluster_policy_gate_test.go | 16 +++++ .../admission/metadata_replay_test.go | 3 +- internal/infra/membership/coordinator.go | 6 +- .../infra/node/capability_membership_test.go | 4 +- 6 files changed, 54 insertions(+), 45 deletions(-) diff --git a/internal/application/admission/admission.go b/internal/application/admission/admission.go index 4107d55ea6..07220c8813 100644 --- a/internal/application/admission/admission.go +++ b/internal/application/admission/admission.go @@ -51,17 +51,17 @@ type Proposer interface { // Admission handles the admission of orders into the Raft cluster. // It is responsible for preloading volumes and proposing commands. type Admission struct { - store *dal.Store - logger logging.Logger - proposer Proposer - membershipFence *membership.Coordinator - writeGate health.WriteGate - keyStore *keystore.KeyStore - sharedState *state.SharedState - builder *plan.Builder - attrs *attributes.Attributes - numscriptCache *numscript.NumscriptCache - waitLeaderReady func(context.Context) error + store *dal.Store + logger logging.Logger + proposer Proposer + membershipCoordinator *membership.Coordinator + writeGate health.WriteGate + keyStore *keystore.KeyStore + sharedState *state.SharedState + builder *plan.Builder + attrs *attributes.Attributes + numscriptCache *numscript.NumscriptCache + waitLeaderReady func(context.Context) error // auditProjectionState is node-local admission state. It may reject a // checkpoint proposal before Raft, but never affects deterministic apply. auditProjectionState func() (disabled, rebuilding bool) @@ -140,7 +140,7 @@ func NewAdmission( store *dal.Store, logger logging.Logger, proposer Proposer, - membershipFence *membership.Coordinator, + membershipCoordinator *membership.Coordinator, builder *plan.Builder, meterProvider metric.MeterProvider, writeGate health.WriteGate, @@ -152,17 +152,17 @@ func NewAdmission( opts ...func(*Admission), ) *Admission { a := &Admission{ - store: store, - logger: logger, - proposer: proposer, - membershipFence: membershipFence, - builder: builder, - writeGate: writeGate, - keyStore: keyStore, - sharedState: sharedState, - attrs: attrs, - numscriptCache: numscriptCache, - waitLeaderReady: waitLeaderReady, + store: store, + logger: logger, + proposer: proposer, + membershipCoordinator: membershipCoordinator, + builder: builder, + writeGate: writeGate, + keyStore: keyStore, + sharedState: sharedState, + attrs: attrs, + numscriptCache: numscriptCache, + waitLeaderReady: waitLeaderReady, } for _, opt := range opts { opt(a) @@ -501,18 +501,9 @@ func (a *Admission) AdmitClusterPolicy(ctx context.Context, policy *commonpb.Clu return nil, errors.New("cluster policy must not be nil") } - propose := a.proposeAdmission - for _, activation := range policy.GetCapabilityActivations() { - if activation != nil && activation.GetActive() { - propose = a.proposeActivatingPolicy - - break - } - } - return a.admit(ctx, func(ctx context.Context, caller *commonpb.CallerSnapshot) (*preparedAdmission, error) { return a.prepareClusterPolicy(ctx, caller, policy) - }, propose) + }, a.proposeClusterPolicy) } type preparedAdmission struct { @@ -609,14 +600,14 @@ func (a *Admission) admit(ctx context.Context, prepare func(context.Context, *co return a.resolveAdmissionResult(ctx, prepared.command, fsmFuture) } -func (a *Admission) proposeActivatingPolicy(ctx context.Context, command *raftcmdpb.Proposal, build *plan.BuildResult) (*futures.Future[state.ApplyResult], func(), error) { +func (a *Admission) proposeClusterPolicy(ctx context.Context, command *raftcmdpb.Proposal, build *plan.BuildResult) (*futures.Future[state.ApplyResult], func(), error) { // Fence membership before the proposal guard locks the index tracker. - if a.membershipFence == nil { - return nil, nil, errors.New("activating policy admission has no membership fence") + if a.membershipCoordinator == nil { + return nil, nil, errors.New("cluster policy admission has no membership coordinator") } - release, err := a.membershipFence.AcquireCapabilityMembership(ctx) + release, err := a.membershipCoordinator.AcquireClusterPolicy(ctx) if err != nil { - return nil, nil, fmt.Errorf("fencing membership for capability activation: %w", err) + return nil, nil, fmt.Errorf("fencing membership for cluster policy: %w", err) } future, cleanup, err := a.proposeAdmission(ctx, command, build) release(future) diff --git a/internal/application/admission/admission_phase_metrics_test.go b/internal/application/admission/admission_phase_metrics_test.go index dd2573c11b..a8220889e6 100644 --- a/internal/application/admission/admission_phase_metrics_test.go +++ b/internal/application/admission/admission_phase_metrics_test.go @@ -17,6 +17,7 @@ import ( "github.com/formancehq/ledger/v3/internal/infra/attributes" "github.com/formancehq/ledger/v3/internal/infra/cache" "github.com/formancehq/ledger/v3/internal/infra/health" + "github.com/formancehq/ledger/v3/internal/infra/membership" "github.com/formancehq/ledger/v3/internal/infra/node" "github.com/formancehq/ledger/v3/internal/infra/plan" "github.com/formancehq/ledger/v3/internal/infra/state" @@ -59,7 +60,7 @@ func createTestAdmissionWithReader(t *testing.T, store *dal.Store, proposer Prop store, logger, proposer, - nil, + &membership.Coordinator{}, testPreloader, provider, writeGate, diff --git a/internal/application/admission/cluster_policy_gate_test.go b/internal/application/admission/cluster_policy_gate_test.go index 03d4451beb..ebdf789c65 100644 --- a/internal/application/admission/cluster_policy_gate_test.go +++ b/internal/application/admission/cluster_policy_gate_test.go @@ -185,3 +185,19 @@ func TestAdmit_ClusterPolicyWriteReadinessGate(t *testing.T) { require.ErrorIs(t, err, ErrMaintenanceMode) }) } + +func TestAdmitClusterPolicyFencesMembershipWithoutActivations(t *testing.T) { + t.Parallel() + + store := createTestStoreWithoutPolicy(t) + proposer := NewMockProposer(gomock.NewController(t)) + a, _ := createTestAdmissionWithReader(t, store, proposer) + a.membershipCoordinator.Lock() + defer a.membershipCoordinator.Unlock() + + ctx, cancel := context.WithTimeout(attributedTestContext(context.Background()), 100*stdtime.Millisecond) + defer cancel() + + _, err := a.AdmitClusterPolicy(ctx, &commonpb.ClusterPolicy{Revision: 1, QueryCheckpointLimit: 10}) + require.ErrorIs(t, err, context.DeadlineExceeded) +} diff --git a/internal/application/admission/metadata_replay_test.go b/internal/application/admission/metadata_replay_test.go index 4e3e83d530..b0730556ad 100644 --- a/internal/application/admission/metadata_replay_test.go +++ b/internal/application/admission/metadata_replay_test.go @@ -21,6 +21,7 @@ import ( "github.com/formancehq/ledger/v3/internal/infra/attributes" "github.com/formancehq/ledger/v3/internal/infra/cache" "github.com/formancehq/ledger/v3/internal/infra/health" + "github.com/formancehq/ledger/v3/internal/infra/membership" "github.com/formancehq/ledger/v3/internal/infra/node" "github.com/formancehq/ledger/v3/internal/infra/plan" "github.com/formancehq/ledger/v3/internal/infra/state" @@ -117,7 +118,7 @@ func newMetadataReplayHarness(t testing.TB) *metadataReplayHarness { }).AnyTimes() gate := health.NewMockWriteGate(gomock.NewController(t)) gate.EXPECT().CheckWritesAllowed().Return(nil).AnyTimes() - admission := NewAdmission(store, logger, proposer, nil, plan.NewBuilder(tracker, c, attrs, store, nil, logger, 0), meter, gate, ks, shared, attrs, numscript.NewNumscriptCache(0), func(context.Context) error { return nil }) + admission := NewAdmission(store, logger, proposer, &membership.Coordinator{}, plan.NewBuilder(tracker, c, attrs, store, nil, logger, 0), meter, gate, ks, shared, attrs, numscript.NewNumscriptCache(0), func(context.Context) error { return nil }) h.admission = admission h.apply = func(p *raftcmdpb.Proposal) state.ApplyResult { p.CallerSnapshot = commands.SystemCallerSnapshot(commands.ComponentClusterPolicy) diff --git a/internal/infra/membership/coordinator.go b/internal/infra/membership/coordinator.go index 7df6a21653..c4debc68fc 100644 --- a/internal/infra/membership/coordinator.go +++ b/internal/infra/membership/coordinator.go @@ -9,8 +9,8 @@ import ( "github.com/formancehq/ledger/v3/internal/pkg/futures" ) -// Coordinator serializes Raft membership changes with policy activation. -// Its lock is held until an activating policy has applied. +// Coordinator serializes Raft membership changes with cluster policy updates. +// Its lock is held until a proposed policy has applied. type Coordinator struct { sync.Mutex @@ -26,7 +26,7 @@ func (c *Coordinator) Configure(waitForPriorMembership func(context.Context) err c.terminal = terminal } -func (c *Coordinator) AcquireCapabilityMembership(ctx context.Context) (func(*futures.Future[state.ApplyResult]), error) { +func (c *Coordinator) AcquireClusterPolicy(ctx context.Context) (func(*futures.Future[state.ApplyResult]), error) { for !c.TryLock() { select { case <-ctx.Done(): diff --git a/internal/infra/node/capability_membership_test.go b/internal/infra/node/capability_membership_test.go index 227c35802d..cccb1aa38d 100644 --- a/internal/infra/node/capability_membership_test.go +++ b/internal/infra/node/capability_membership_test.go @@ -14,7 +14,7 @@ import ( func TestCapabilityMembershipFenceHeldUntilPolicyApply(t *testing.T) { n := &Node{stopChannel: make(chan struct{}), terminalCh: make(chan struct{})} n.confChangeMu.Configure(nil, n.stopChannel, n.terminalCh) - release, err := n.confChangeMu.AcquireCapabilityMembership(context.Background()) + release, err := n.confChangeMu.AcquireClusterPolicy(context.Background()) require.NoError(t, err) future := futures.New[state.ApplyResult]() release(future) @@ -33,7 +33,7 @@ func TestCapabilityMembershipFenceHeldUntilPolicyApply(t *testing.T) { func TestCapabilityMembershipFenceReleasedWithoutProposal(t *testing.T) { n := &Node{} - release, err := n.confChangeMu.AcquireCapabilityMembership(context.Background()) + release, err := n.confChangeMu.AcquireClusterPolicy(context.Background()) require.NoError(t, err) release(nil) require.True(t, n.confChangeMu.TryLock()) From 2622a3b7075ea7425d5448d597ca1e2ac81d07ec Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Thu, 12 Nov 2026 11:32:27 +0100 Subject: [PATCH 127/127] test(ledger): supply coordinator in policy fixtures --- cmd/ledgerctl/store/bootstrap_retry_test.go | 3 ++- internal/infra/backup/numscript_restore_parity_test.go | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/cmd/ledgerctl/store/bootstrap_retry_test.go b/cmd/ledgerctl/store/bootstrap_retry_test.go index 1e136af21b..f8e18e9671 100644 --- a/cmd/ledgerctl/store/bootstrap_retry_test.go +++ b/cmd/ledgerctl/store/bootstrap_retry_test.go @@ -34,6 +34,7 @@ import ( "github.com/formancehq/ledger/v3/internal/infra/backup" "github.com/formancehq/ledger/v3/internal/infra/cache" "github.com/formancehq/ledger/v3/internal/infra/health" + "github.com/formancehq/ledger/v3/internal/infra/membership" "github.com/formancehq/ledger/v3/internal/infra/node" "github.com/formancehq/ledger/v3/internal/infra/plan" "github.com/formancehq/ledger/v3/internal/infra/state" @@ -90,7 +91,7 @@ func newBootstrapRetryAdmission(t *testing.T, store *dal.Store) *admission.Admis return future, nil }).AnyTimes() - return admission.NewAdmission(store, logger, proposer, nil, + return admission.NewAdmission(store, logger, proposer, &membership.Coordinator{}, builder, meters, gate, keys, shared, attrs, numscript.NewNumscriptCache(0), func(context.Context) error { return nil }) } diff --git a/internal/infra/backup/numscript_restore_parity_test.go b/internal/infra/backup/numscript_restore_parity_test.go index 832d8f1b93..e1f9b94d8e 100644 --- a/internal/infra/backup/numscript_restore_parity_test.go +++ b/internal/infra/backup/numscript_restore_parity_test.go @@ -20,6 +20,7 @@ import ( "github.com/formancehq/ledger/v3/internal/infra/attributes" "github.com/formancehq/ledger/v3/internal/infra/cache" "github.com/formancehq/ledger/v3/internal/infra/health" + "github.com/formancehq/ledger/v3/internal/infra/membership" "github.com/formancehq/ledger/v3/internal/infra/node" "github.com/formancehq/ledger/v3/internal/infra/plan" "github.com/formancehq/ledger/v3/internal/infra/state" @@ -111,7 +112,7 @@ func newScriptedNode(t *testing.T, store *dal.Store) *scriptedNode { return &scriptedNode{ t: t, admission: admission.NewAdmission(store, logger, - &applyingProposer{t: t, machine: machine, store: store, tracker: tracker}, nil, + &applyingProposer{t: t, machine: machine, store: store, tracker: tracker}, &membership.Coordinator{}, builder, meters, writeGate, keyStore, sharedState, attrs, numscript.NewNumscriptCache(0), func(context.Context) error { return nil }), }