owpengram-server/internal/mtprotoedge/inbound_layer_rpc.go

1277 lines
48 KiB
Go

package mtprotoedge
import (
"context"
"errors"
"fmt"
"github.com/iamxvbaba/td/bin"
"github.com/iamxvbaba/td/mt"
"github.com/iamxvbaba/td/tg"
"github.com/iamxvbaba/td/tgerr"
"github.com/iamxvbaba/td/tlprofile"
"go.uber.org/zap"
)
var inboundLayerDecodeLimits = tlprofile.Limits{
MaxWireBytes: maxInflightRPCBytes,
// contacts.editCloseFriends and contacts.setBlocked deliberately allow
// 5,000 entries. Keep the coarse generated allocation ceiling above every
// registered semantic field cap; field callbacks reject 5,001 before the
// typed decoder allocates the vector.
MaxVectorElements: 8 << 10,
MaxAggregateElements: 128 << 10,
MaxDepth: 32,
}
var errDefaultLayerAdmission = errors.New("selected layer profile rejected naked RPC")
var errLayerRPCGZIPCapacity = errors.New("exact layer gzip admission capacity exhausted")
var errLayerRPCAdmissionCapability = errors.New("exact layer RPC admission capability unavailable")
const (
maxLayerRPCDependencyIDs = 128
// Exact admission materializes canonical request structs, interface-backed
// nested objects, vectors, strings, and byte slices before a scheduler task
// exists. Current generated request-reachable TL structs are at most 376
// bytes (guarded by TestLayerRPCAdmissionMaterializationConstants), so 512
// bytes is the forward-drift ceiling. One ceiling per maximum decode depth is
// reserved as fixed graph slack; the 60x wire factor covers repeated nested
// values, slice/interface headers, payload copies, and allocator rounding.
// The largest legal upload part (512 KiB) charges less than the ordinary
// 32-MiB per-connection budget and therefore remains admissible by default.
layerRPCAdmissionStaticObjectBytes = 512
layerRPCAdmissionWireFactor = 60
layerRPCAdmissionGraphSlack = layerRPCAdmissionStaticObjectBytes * 32
)
type layerRPCDependencySet struct {
waiters []*rpcResultWaiter
failed bool
}
type layerRPCProfileEvidence struct {
profile tlprofile.Profile
admissionSeq uint64
present bool
fresh bool
commit bool
publish bool
}
// layerRPCAdmissionCursor is the wire-ordered, side-effect-free view used while
// decoding one MTProto container. evidenceMsgID is the last explicit
// invokeWithLayer proof, not merely the profile used by an arbitrary cached
// request. It therefore advances only after generated admission reports
// ProfileEvidence.
type layerRPCAdmissionCursor struct {
state LayerProfileSnapshot
rawLayer int
evidenceMsgID int64
}
func (c *layerRPCAdmissionCursor) observe(profile tlprofile.Profile, msgID int64) error {
return c.observeRaw(int(profile), msgID)
}
func (c *layerRPCAdmissionCursor) observeRaw(layer int, msgID int64) error {
if c == nil || msgID <= 0 {
return fmt.Errorf("invalid provisional Layer evidence msg_id %d", msgID)
}
if layer <= 0 {
return fmt.Errorf("invalid provisional Layer evidence %d", layer)
}
if c.evidenceMsgID != 0 {
switch {
case msgID < c.evidenceMsgID:
return nil
case msgID == c.evidenceMsgID:
if c.rawLayer != layer {
return fmt.Errorf("%w: msg_id %d selected Layer %d after Layer %d", ErrLayerProfileConflict, msgID, layer, c.rawLayer)
}
return nil
}
}
c.state = LayerProfileSnapshot{}
if profile, supported := tlprofile.ResolveProfile(layer); supported {
c.state = LayerProfileSnapshot{Profile: profile, Origin: LayerProfileExplicit}
}
c.rawLayer = layer
c.evidenceMsgID = msgID
return nil
}
func (s *Server) initialLayerRPCAdmissionCursor(ctx context.Context, c *Conn) (layerRPCAdmissionCursor, error) {
if c == nil {
return layerRPCAdmissionCursor{}, fmt.Errorf("nil Conn for provisional Layer admission")
}
state, rawLayer, msgID := c.layerProfileRawEvidenceState()
if rawLayer == 0 && state.Origin != LayerProfileUnknown {
rawLayer = int(state.Profile)
}
cursor := layerRPCAdmissionCursor{state: state, rawLayer: rawLayer, evidenceMsgID: msgID}
var (
layer int
registryMsgID int64
found bool
)
if _, durable := s.layerRPC.(LayerRPCDurableSessionProfileResolver); durable {
// Production resolves durable exact-session evidence once while creating
// this physical Conn (seedInitialLayerProfile). The Conn snapshot is then
// the admission authority until its own wire carries invokeWithLayer.
// Re-reading PostgreSQL for every RPC/container batch puts a database RTT on
// the protocol hot path and would also let another process silently rewrite
// an already-live physical connection. A stale grammar self-corrects through
// CONNECTION_LAYER_INVALID -> invokeWithLayer; a replacement Conn performs
// a fresh durable seed before admitting its first request.
return cursor, nil
} else if resolver, ok := s.layerRPC.(LayerRPCOrderedSessionProfileResolver); ok {
layer, registryMsgID, found = resolver.NegotiatedSessionLayerEvidence(c.authKeyID, c.sessionID)
} else {
return cursor, nil
}
if !found {
return cursor, nil
}
if registryMsgID < 0 || layer <= 0 {
return layerRPCAdmissionCursor{}, fmt.Errorf("invalid exact session Layer evidence layer=%d msg_id=%d", layer, registryMsgID)
}
if registryMsgID == 0 {
if cursor.evidenceMsgID == 0 {
cursor.state = LayerProfileSnapshot{}
if profile, supported := tlprofile.ResolveProfile(layer); supported {
cursor.state = LayerProfileSnapshot{Profile: profile, Origin: LayerProfileExplicit}
}
cursor.rawLayer = layer
}
return cursor, nil
}
if err := cursor.observeRaw(layer, registryMsgID); err != nil {
return layerRPCAdmissionCursor{}, fmt.Errorf("merge exact session Layer evidence: %w", err)
}
// Restore the same raw watermark onto this physical generation. Unsupported
// future Layers intentionally leave its usable profile unknown.
if registryMsgID > 0 {
if err := c.seedRawLayerEvidence(layer, registryMsgID); err != nil {
return layerRPCAdmissionCursor{}, fmt.Errorf("restore exact session raw Layer evidence: %w", err)
}
cursor.state, cursor.rawLayer, cursor.evidenceMsgID = c.layerProfileRawEvidenceState()
}
return cursor, nil
}
// layerRPCAdmissionReservationSize returns a saturating conservative charge.
// Saturation deliberately turns hostile integer-sized inputs into ordinary
// capacity rejection; it must never wrap into a small accepted reservation.
func layerRPCAdmissionReservationSize(wireBytes int) int {
if wireBytes < 0 {
wireBytes = 0
}
maxInt := int(^uint(0) >> 1)
if wireBytes > (maxInt-layerRPCAdmissionGraphSlack)/layerRPCAdmissionWireFactor {
return maxInt
}
return wireBytes*layerRPCAdmissionWireFactor + layerRPCAdmissionGraphSlack
}
// layerRPCGZIPExpansionBudget bridges transient process-wide expansion memory
// to the durable scheduler charge of one exact typed request. sourceBytes is
// conservative: it retains the original compressed wire plus every successful
// expansion seen across nested envelopes or an authoritative-profile re-decode.
type layerRPCGZIPExpansionBudget struct {
server *Server
plan *inboundPlan
reservation *inboundRPCBatchReservation
entry int
baseSourceBytes int
attemptExpandedBytes int
chargedSourceBytes int
}
func (b *layerRPCGZIPExpansionBudget) beginAttempt() {
if b != nil {
b.attemptExpandedBytes = 0
}
}
func (b *layerRPCGZIPExpansionBudget) expand(wire []byte, admissionLimit int) ([]byte, func(), error) {
noop := func() {}
if b == nil || b.server == nil || b.plan == nil || b.reservation == nil {
return nil, noop, errors.New("invalid exact layer gzip expansion budget")
}
frameRemaining := maxDispatchExpandedBytes - b.plan.gzipExpandedBytes
limit := min(admissionLimit, frameRemaining)
if limit <= 0 {
return nil, noop, errors.Join(
errLayerRPCGZIPCapacity,
fmt.Errorf("cumulative gzip expansion reached %d bytes", maxDispatchExpandedBytes),
)
}
data, release, err := b.server.decodeGZIPWithGlobalBudgetLimit(&bin.Buffer{Buf: wire}, limit)
if err != nil {
if work := gzipExpansionWork(err); work > 0 {
b.plan.gzipExpandedBytes += work
}
if errors.Is(err, ErrInboundFrameBudgetExceeded) {
return nil, release, errors.Join(errLayerRPCGZIPCapacity, err)
}
if limit < admissionLimit && errors.Is(err, errGZIPExpansionLimit) {
return nil, release, errors.Join(errLayerRPCGZIPCapacity, err)
}
return nil, release, err
}
b.plan.gzipExpandedBytes += len(data)
b.attemptExpandedBytes += len(data)
targetSourceBytes := b.baseSourceBytes + b.attemptExpandedBytes
targetCharge := layerRPCAdmissionReservationSize(targetSourceBytes)
if targetSourceBytes > b.chargedSourceBytes {
if err := b.reservation.growEntry(b.entry, targetCharge); err != nil {
if errors.Is(err, ErrInboundRPCQueueFull) {
return nil, release, errors.Join(errLayerRPCGZIPCapacity, err)
}
return nil, release, err
}
b.chargedSourceBytes = targetSourceBytes
}
return data, release, nil
}
// prepareInboundLayerRPCBatch is the production API path. The whole container
// reserves conservative task/materialization capacity before the first exact
// decoder callback. Admission then classifies every request; fresh owners keep
// their original tickets, while replay/error/join entries are released without
// a release/reacquire gap for the retained tasks.
func (s *Server) prepareInboundLayerRPCBatch(ctx context.Context, c *Conn, plan *inboundPlan) error {
var candidateItems []int
var provisionalSpecs []inboundRPCSpec
for index := range plan.items {
item := &plan.items[index]
if item.kind == inboundItemRPC {
candidateItems = append(candidateItems, index)
provisionalSpecs = append(provisionalSpecs, inboundRPCSpec{
method: s.typeName(item.typeID),
size: layerRPCAdmissionReservationSize(len(item.body)),
})
}
}
if len(candidateItems) == 0 {
return nil
}
reservation, err := c.reserveInboundRPCBatch(ctx, provisionalSpecs)
if err != nil {
if errors.Is(err, ErrInboundRPCQueueFull) {
for _, index := range candidateItems {
plan.items[index].kind = inboundItemCapacityError
}
return nil
}
return err
}
// Install ownership immediately: every return below is covered by plan.close.
plan.rpcReservation = reservation
// Decode the container in wire order with a provisional cursor. Explicit
// evidence may select the grammar for a following naked item even when the
// evidence request is already pending/completed. The ordered msg_id watermark
// makes an old replay inert and rejects a same-msg_id conflicting selector.
// Nothing mutates Conn/registry until exact flight identity accepts the item.
admissionCursor, err := s.initialLayerRPCAdmissionCursor(ctx, c)
if err != nil {
return err
}
evidence := make([]layerRPCProfileEvidence, len(plan.items))
decodeOptions := make([]tlprofile.AdmissionOptions, len(candidateItems))
expansionBudgets := make([]*layerRPCGZIPExpansionBudget, len(candidateItems))
materializationCapacity := false
for reservationIndex, index := range candidateItems {
item := &plan.items[index]
itemState := admissionCursor.state
existingProfile, existing := s.rpcResults.ExactAdmissionProfile(c.authKeyID, c.sessionID, item.msgID)
if existing {
itemState = LayerProfileSnapshot{Profile: existingProfile, Origin: LayerProfileExplicit}
}
expansionBudget := &layerRPCGZIPExpansionBudget{
server: s, plan: plan, reservation: reservation,
entry: reservationIndex, baseSourceBytes: len(item.body), chargedSourceBytes: len(item.body),
}
expansionBudgets[reservationIndex] = expansionBudget
options := tlprofile.AdmissionOptions{
Limits: inboundLayerDecodeLimits,
ExpandGZIP: expansionBudget.expand,
}
decodeOptions[reservationIndex] = options
admitted, method, err := s.decodeInboundLayerRPCWithOptions(itemState, item.body, options)
if err != nil {
if errors.Is(err, errLayerRPCGZIPCapacity) {
materializationCapacity = true
break
}
if errors.Is(err, ErrConnClosed) || errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
return err
}
if errors.Is(err, errLayerRPCAdmissionCapability) {
return err
}
if terminal, recognized := wrappedDestroyAuthKeyTerminal(err); recognized {
if terminal.WireSize != bin.Word || !validWrappedDestroyAuthKeyChain(terminal) {
s.log.Debug("Wrapped destroy_auth_key terminal rejected",
zap.Int("profile", int(terminal.Profile)),
zap.Int("wire_size", terminal.WireSize),
zap.Int("wrapper_count", terminal.WrapperCount()),
zap.Int64("msg_id", item.msgID),
)
item.kind = inboundItemRPCAdmissionError
item.method = "destroy_auth_key"
item.payload = &mt.RPCError{ErrorCode: 400, ErrorMessage: "INPUT_REQUEST_INVALID"}
c.metrics.InboundRPCDropped(item.method, "layer_admission")
continue
}
if len(plan.items) != 1 {
return errDestroyAuthKeyMustBeExclusive
}
item.kind = inboundItemDestroyAuthKey
item.method = "destroy_auth_key"
item.payload = destroyAuthKeyRequest{}
continue
}
if errors.Is(err, ErrLayerProfileConflict) {
return err
}
s.log.Debug("RPC exact admission rejected",
zap.String("method", method),
zap.String("auth_key_id", c.authKeyHex),
zap.Int64("session_id", c.sessionID),
zap.Int64("msg_id", item.msgID),
zap.Error(err),
)
item.kind = inboundItemRPCAdmissionError
item.method = method
item.payload = layerRPCAdmissionError(err)
c.metrics.InboundRPCDropped(method, "layer_admission")
continue
}
item.admitted = admitted
item.method = method
if profile, hasEvidence := admitted.ProfileEvidence(); hasEvidence {
if existing && profile != existingProfile {
return fmt.Errorf("%w: cached msg_id %d used Layer %d but replay selected Layer %d", ErrLayerProfileConflict, item.msgID, existingProfile, profile)
}
evidence[index] = layerRPCProfileEvidence{profile: profile, present: true, fresh: item.profileEvidenceFresh()}
if evidence[index].fresh {
if err := admissionCursor.observe(profile, item.msgID); err != nil {
return err
}
}
}
}
if materializationCapacity {
for _, index := range candidateItems {
item := &plan.items[index]
item.kind = inboundItemCapacityError
item.admitted = tlprofile.Admission{}
c.metrics.InboundRPCDropped(s.typeName(item.typeID), "materialization_capacity")
}
if err := reservation.retain(nil, nil); err != nil {
return err
}
plan.rpcReservation = nil
return nil
}
var indices []int
var reservationIndices []int
var specs []inboundRPCSpec
var ownersInPlan map[int64]*rpcResultOwnerLease
flightCapacity := false
clearedPostInitCandidates := false
provisionalRewrapInitialized := c.rpcRewrapInitialized.Load()
acceptedInitState := false
for reservationIndex, index := range candidateItems {
item := &plan.items[index]
if item.kind != inboundItemRPC {
continue
}
method := item.method
_, isInitRewrap := admittedRPCRewrapInit(item.admitted)
if isInitRewrap {
if item.profileEvidenceFresh() {
provisionalRewrapInitialized = true
acceptedInitState = true
}
if candidate := s.rpcRewrap.claimSemantic(
c,
item.admitted.Prepared().SemanticIdentity(),
item.admitted.Call().Identity(),
); candidate != nil {
claim, err := s.acquireAdmittedLayerRPC(
c, item, &evidence[index], decodeOptions[reservationIndex], expansionBudgets[reservationIndex],
)
if errors.Is(err, errLayerRPCGZIPCapacity) {
s.rpcRewrap.release(candidate)
c.metrics.InboundRPCDropped(candidate.method, "materialization_capacity")
flightCapacity = true
item.kind = inboundItemCapacityError
continue
}
if errors.Is(err, ErrRPCResultFlightCapacity) {
s.rpcRewrap.release(candidate)
c.metrics.InboundRPCDropped(candidate.method, "flight_capacity")
flightCapacity = true
item.kind = inboundItemCapacityError
continue
}
if err != nil {
s.rpcRewrap.release(candidate)
return err
}
method = item.method
item.admissionSeq = claim.admissionSeq
if evidence[index].present && evidence[index].fresh {
evidence[index].commit = true
evidence[index].admissionSeq = claim.admissionSeq
evidence[index].publish = claim.state == rpcResultAcquireOwner
}
switch claim.state {
case rpcResultAcquireCompleted:
// Transfer the materialization ticket to the plan before any
// profile-restore step can fail; plan.close is the universal abort
// path and sendCached releases it after outbound-budget handoff.
item.payload = claim.encoded
after, prepareErr := s.prepareAdmittedLayerRPCReplay(ctx, c, item.msgID, claim.admissionSeq, item.profileEvidenceFresh(), item.admitted)
if prepareErr != nil {
s.rpcRewrap.release(candidate)
return prepareErr
}
s.rpcRewrap.commit(candidate)
item.kind = inboundItemReplayRPC
if claim.executionKnown && claim.executionOK {
item.replayAfterSuccessfulDelivery = after
}
case rpcResultAcquireAcknowledged:
// Explicit ACK is terminal proof for this request msg_id. Keep
// exact admission metadata, retire the old rewrap candidate and
// make the duplicate ACK-only without replay side effects.
s.rpcRewrap.commit(candidate)
item.kind = inboundItemDuplicate
item.payload = nil
case rpcResultAcquirePending:
after, prepareErr := s.prepareAdmittedLayerRPCReplay(ctx, c, item.msgID, claim.admissionSeq, item.profileEvidenceFresh(), item.admitted)
if prepareErr != nil {
s.rpcRewrap.release(candidate)
return prepareErr
}
s.rpcRewrap.commit(candidate)
item.kind = inboundItemRewrappedRPC
item.payload = claim.waiter
plan.rewrapAliases = append(plan.rewrapAliases, &rpcRewrapAlias{
conn: c, itemIndex: index, newReqID: item.msgID, method: candidate.method,
oldWaiter: claim.waiter,
afterSuccessfulDelivery: after,
})
case rpcResultAcquireOwner:
after, prepareErr := s.prepareAdmittedLayerRPCReplay(ctx, c, item.msgID, claim.admissionSeq, item.profileEvidenceFresh(), item.admitted)
if prepareErr != nil {
s.rpcRewrap.release(candidate)
claim.owner.Abort()
return prepareErr
}
if ownersInPlan == nil {
ownersInPlan = make(map[int64]*rpcResultOwnerLease)
}
ownersInPlan[item.msgID] = claim.owner
item.kind = inboundItemRewrappedRPC
item.payload = claim.owner
plan.rewrapAliases = append(plan.rewrapAliases, &rpcRewrapAlias{
conn: c, itemIndex: index, newReqID: item.msgID, method: candidate.method,
oldWaiter: candidate.waiter, newOwner: claim.owner,
sourceConn: candidate.source, sourceOwner: candidate.owner,
afterSuccessfulDelivery: after,
candidate: candidate, registry: s.rpcRewrap,
})
default:
s.rpcRewrap.release(candidate)
return ErrRPCResultFlightInvalid
}
s.log.Info("RPC init rewrap matched",
zap.String("method", candidate.method),
zap.Int64("old_req_msg_id", candidate.reqMsgID),
zap.Int64("new_req_msg_id", item.msgID),
zap.Bool("same_connection", candidate.source == c),
zap.String("auth_key_id", c.authKeyHex), zap.Int64("session_id", c.sessionID))
continue
}
} else if provisionalRewrapInitialized && !clearedPostInitCandidates {
s.rpcRewrap.clearSession(c)
clearedPostInitCandidates = true
}
claim, err := s.acquireAdmittedLayerRPC(
c, item, &evidence[index], decodeOptions[reservationIndex], expansionBudgets[reservationIndex],
)
if errors.Is(err, errLayerRPCGZIPCapacity) {
c.metrics.InboundRPCDropped(method, "materialization_capacity")
flightCapacity = true
item.kind = inboundItemCapacityError
continue
}
if errors.Is(err, ErrRPCResultFlightCapacity) {
c.metrics.InboundRPCDropped(method, "flight_capacity")
flightCapacity = true
item.kind = inboundItemCapacityError
continue
}
if err != nil {
return err
}
method = item.method
item.admissionSeq = claim.admissionSeq
if evidence[index].present && evidence[index].fresh {
evidence[index].commit = true
evidence[index].publish = claim.state == rpcResultAcquireOwner
evidence[index].admissionSeq = claim.admissionSeq
}
switch claim.state {
case rpcResultAcquireCompleted:
item.payload = claim.encoded
after, prepareErr := s.prepareAdmittedLayerRPCReplay(ctx, c, item.msgID, claim.admissionSeq, item.profileEvidenceFresh(), item.admitted)
if prepareErr != nil {
return prepareErr
}
item.kind = inboundItemReplayRPC
if claim.executionKnown && claim.executionOK {
item.replayAfterSuccessfulDelivery = after
}
case rpcResultAcquireAcknowledged:
item.kind = inboundItemDuplicate
item.payload = nil
case rpcResultAcquirePending:
if ownersInPlan[item.msgID] != nil {
item.kind = inboundItemDuplicate
item.payload = nil
} else {
after, prepareErr := s.prepareAdmittedLayerRPCReplay(ctx, c, item.msgID, claim.admissionSeq, item.profileEvidenceFresh(), item.admitted)
if prepareErr != nil {
return prepareErr
}
item.kind = inboundItemRewrappedRPC
item.payload = claim.waiter
plan.rewrapAliases = append(plan.rewrapAliases, &rpcRewrapAlias{
conn: c, itemIndex: index, newReqID: item.msgID, method: method, oldWaiter: claim.waiter,
afterSuccessfulDelivery: after,
})
}
case rpcResultAcquireOwner:
if ownersInPlan == nil {
ownersInPlan = make(map[int64]*rpcResultOwnerLease)
indices = make([]int, 0, len(plan.items))
specs = make([]inboundRPCSpec, 0, len(plan.items))
}
ownersInPlan[item.msgID] = claim.owner
plan.rpcOwners = append(plan.rpcOwners, claim.owner)
item.payload = claim.owner
indices = append(indices, index)
reservationIndices = append(reservationIndices, reservationIndex)
specs = append(specs, inboundRPCSpec{
method: method,
size: layerRPCAdmissionReservationSize(item.admitted.Prepared().WireSize()),
})
if !provisionalRewrapInitialized && !isInitRewrap {
s.rpcRewrap.registerSemantic(
c,
item.admitted.Prepared().SemanticIdentity(),
item.admitted.Call().Identity(),
item.msgID,
method,
claim.owner,
)
}
default:
return ErrRPCResultFlightInvalid
}
}
if flightCapacity {
for _, index := range indices {
plan.items[index].kind = inboundItemCapacityError
}
plan.rejectNewRPCOwners(indices)
for _, index := range candidateItems {
plan.items[index].admitted = tlprofile.Admission{}
}
if err := reservation.retain(nil, nil); err != nil {
return err
}
plan.rpcReservation = nil
return nil
}
var profileCapacityItems map[int]struct{}
for _, index := range candidateItems {
proof := evidence[index]
if !proof.present || !proof.commit {
continue
}
current, err := s.commitLayerProfileEvidence(ctx, c, proof.profile, plan.items[index].msgID)
if err != nil {
if isLayerEvidenceDurabilityUnavailable(err) {
if _, localErr := c.freezeLayerProfileAt(proof.profile, plan.items[index].msgID); localErr != nil {
return fmt.Errorf("apply connection-local Layer evidence fallback: %w", localErr)
}
// The durable failure limits propagation, not validity of the
// selector on this physical connection. Keep fresh=true so an
// initConnection can initialize this Conn, enable updates and retain
// its device metadata. Only exact/auth-key/session publication is
// suppressed; a replacement connection must negotiate again.
evidence[index].publish = false
s.log.Error("Durable exact session Layer unavailable; using connection-local request profile",
zap.String("auth_key_id", c.authKeyHex), zap.Int64("session_id", c.sessionID),
zap.Int64("msg_id", plan.items[index].msgID), zap.Int("layer", int(proof.profile)), zap.Error(err))
continue
}
if isExactSessionProfileCapacityError(err) {
if profileCapacityItems == nil {
profileCapacityItems = make(map[int]struct{})
}
profileCapacityItems[index] = struct{}{}
continue
}
return err
}
evidence[index].publish = evidence[index].publish && current
}
if len(profileCapacityItems) != 0 {
// Match the existing whole-container flight-capacity policy: no newly
// acquired owner from this admission unit may execute or publish shared
// evidence. Completed/pending joins remain replayable unless their own
// fresh selector was the capacity failure.
for index := range profileCapacityItems {
item := &plan.items[index]
item.kind = inboundItemCapacityError
if _, ownsFlight := item.payload.(*rpcResultOwnerLease); !ownsFlight {
item.payload = nil
}
}
plan.rejectNewRPCOwners(indices)
keptAliases := plan.rewrapAliases[:0]
for _, alias := range plan.rewrapAliases {
if alias == nil {
continue
}
if _, rejected := profileCapacityItems[alias.itemIndex]; rejected {
alias.releaseCandidate()
continue
}
keptAliases = append(keptAliases, alias)
}
plan.rewrapAliases = keptAliases
for _, index := range candidateItems {
plan.items[index].admitted = tlprofile.Admission{}
}
if err := reservation.retain(nil, nil); err != nil {
return err
}
plan.rpcReservation = nil
return nil
}
if acceptedInitState {
// Every explicit profile commit, including durable local-only fallback, has
// now classified the item. Only a still-fresh init wrapper may advance the
// connection-initialized marker.
for _, index := range candidateItems {
if plan.items[index].profileEvidenceFresh() {
if _, isInit := admittedRPCRewrapInit(plan.items[index].admitted); isInit {
c.rpcRewrapInitialized.Store(true)
break
}
}
}
}
for _, index := range candidateItems {
proof := evidence[index]
if !proof.present || !proof.commit {
continue
}
if evidence[index].publish {
publisher, ok := s.layerRPC.(LayerRPCAdmissionProfilePublisher)
if ok {
if proof.admissionSeq == 0 {
return ErrRPCAdmissionSeqExhausted
}
safeFloor := s.rpcResults.stableAdmissionSafeFloor()
if safeFloor == 0 || safeFloor > proof.admissionSeq {
return fmt.Errorf("invalid active admission safe floor %d for sequence %d", safeFloor, proof.admissionSeq)
}
if err := publisher.PublishAdmittedLayerProfileEvidence(
ctx, c.authKeyID, c.sessionID, plan.items[index].msgID,
proof.admissionSeq, safeFloor, int(proof.profile),
); err != nil {
return fmt.Errorf("publish admitted Layer profile evidence: %w", err)
}
}
}
}
if len(specs) == 0 {
for _, index := range candidateItems {
plan.items[index].admitted = tlprofile.Admission{}
}
if err := reservation.retain(nil, nil); err != nil {
return err
}
plan.rpcReservation = nil
return nil
}
dependencies := make([]layerRPCDependencySet, len(indices))
for taskIndex, itemIndex := range indices {
item := &plan.items[itemIndex]
dependencies[taskIndex] = s.layerRPCDependencies(c, item.msgID, item.admitted)
}
plan.rpcTasks = make([]inboundRPC, len(indices))
for taskIndex, itemIndex := range indices {
item := &plan.items[itemIndex]
owner, _ := item.payload.(*rpcResultOwnerLease)
plan.rpcTasks[taskIndex] = s.newInboundLayerRPCTask(c, item.msgID, item.admissionSeq, item.method, item.profileEvidenceFresh(), item.admitted, dependencies[taskIndex], owner)
}
// Tasks now own the admitted request leases. Drop the plan's value copies
// before non-fresh reservations become reusable.
for _, index := range candidateItems {
plan.items[index].admitted = tlprofile.Admission{}
}
if err := reservation.retain(reservationIndices, specs); err != nil {
return err
}
return nil
}
// acquireAdmittedLayerRPC resolves the only legal admission TOCTOU: two
// replacement physical connections may both miss the pre-decode hint and use
// different inherited defaults for the same naked body. The winner's stored
// profile is then authoritative for this msg_id. Re-decode once under that
// profile and retry the full identity claim; a changed body still mismatches.
// The mismatch carries that profile from inside the shard lock, so an immediate
// winner abort/eviction cannot make us fall back to an ambiguous loser default.
func (s *Server) acquireAdmittedLayerRPC(
c *Conn,
item *inboundItem,
evidence *layerRPCProfileEvidence,
options tlprofile.AdmissionOptions,
expansionBudget *layerRPCGZIPExpansionBudget,
) (rpcResultAcquire, error) {
if s == nil || c == nil || item == nil {
return rpcResultAcquire{}, ErrRPCResultFlightInvalid
}
acquire := func() (rpcResultAcquire, error) {
profile := tlprofile.Profile(0)
if effective, known := item.admitted.EffectiveProfile(); known {
profile = effective
}
return s.rpcResults.AcquireLayerIdentified(
c.authKeyID, c.sessionID, item.msgID,
profile, item.admitted.Prepared().Identity(),
)
}
claim, err := acquire()
if !errors.Is(err, ErrRPCResultIdentityMismatch) {
return claim, err
}
var mismatch *rpcResultIdentityMismatchError
if !errors.As(err, &mismatch) || !mismatch.hasProfile {
return rpcResultAcquire{}, err
}
winnerProfile := mismatch.profile
if winnerProfile == item.admitted.Call().Profile() {
// Same profile plus a different full identity is a same-msg_id body
// mutation, not an inherited-default race.
return rpcResultAcquire{}, err
}
// Drop the losing typed graph before materializing its authoritative-profile
// replacement. The grown reservation therefore needs to cover the larger
// graph, not two simultaneous copies.
item.admitted = tlprofile.Admission{}
if expansionBudget != nil {
expansionBudget.beginAttempt()
}
redecoded, method, decodeErr := s.decodeInboundLayerRPCWithOptions(
LayerProfileSnapshot{Profile: winnerProfile, Origin: LayerProfileExplicit},
item.body,
options,
)
if decodeErr != nil {
return rpcResultAcquire{}, decodeErr
}
item.admitted = redecoded
item.method = method
if evidence != nil {
fresh := evidence.fresh
*evidence = layerRPCProfileEvidence{fresh: fresh}
if profile, present := redecoded.ProfileEvidence(); present {
evidence.profile = profile
evidence.present = true
}
}
return acquire()
}
func (s *Server) prepareAdmittedLayerRPCReplay(ctx context.Context, c *Conn, msgID int64, admissionSeq uint64, profileEvidenceFresh bool, request tlprofile.Admission) (func() error, error) {
preparer, ok := s.layerRPC.(LayerRPCReplayPreparer)
if !ok || c == nil {
return nil, nil
}
ctx = s.withLayerRPCProfileEvidenceFresh(ctx, profileEvidenceFresh)
after, err := preparer.PrepareAdmittedReplay(ctx, c.authKeyID, c.sessionID, msgID, admissionSeq, request)
if err != nil {
s.log.Warn("Prepare exact RPC replay side effects failed",
zap.String("auth_key_id", c.authKeyHex),
zap.Int64("session_id", c.sessionID),
zap.Error(err))
return nil, fmt.Errorf("prepare exact RPC replay effects: %w", err)
}
return after, nil
}
func (s *Server) withLayerRPCProfileEvidenceFresh(ctx context.Context, fresh bool) context.Context {
if ctx == nil {
ctx = context.Background()
}
if decorator, ok := s.layerRPC.(LayerRPCProfileEvidenceContext); ok {
if decorated := decorator.WithLayerRPCProfileEvidenceFresh(ctx, fresh); decorated != nil {
return decorated
}
}
return ctx
}
// admitInboundLayerRPC is the force-style compatibility entry point used by
// focused tests and old embedders. Production must call admitInboundLayerRPCAt
// with the real inner MTProto client msg_id.
func (s *Server) admitInboundLayerRPC(c *Conn, body []byte) (tlprofile.Admission, string, error) {
return s.admitInboundLayerRPCAt(c, 0, body)
}
func (s *Server) admitInboundLayerRPCAt(c *Conn, msgID int64, body []byte) (tlprofile.Admission, string, error) {
if s == nil || s.layerRPC == nil || c == nil || len(body) < bin.Word {
return tlprofile.Admission{}, "unknown", fmt.Errorf("invalid exact RPC admission input")
}
request, method, err := s.decodeInboundLayerRPC(c.LayerProfileState(), body)
if err != nil {
return tlprofile.Admission{}, method, err
}
if profile, hasEvidence := request.ProfileEvidence(); hasEvidence {
if _, err := s.commitLayerProfileEvidence(context.Background(), c, profile, msgID); err != nil {
if !isLayerEvidenceDurabilityUnavailable(err) {
return tlprofile.Admission{}, method, err
}
if msgID > 0 {
if _, localErr := c.freezeLayerProfileAt(profile, msgID); localErr != nil {
return tlprofile.Admission{}, method, localErr
}
}
}
}
return request, method, nil
}
// layerRPCAdmissionHasExplicitSelector distinguishes a failed naked request
// under a stale default from a failed explicit correction. Generated
// unsupported/conflicting invokeWithLayer failures carry the wrapper semantic
// in LayerCodecError. A truncated selector can fail before that typed error is
// constructed, so the bounded fallback walks only transparent wrapper prefixes
// whose query offset is fixed and allocation-free.
func layerRPCAdmissionHasExplicitSelector(body []byte, admissionErr error) bool {
var codecErr *tlprofile.LayerCodecError
if errors.As(admissionErr, &codecErr) && codecErr.Semantic == tlprofile.SemanticMethodInvokeWithLayer {
return true
}
probe := &bin.Buffer{Buf: body}
for depth := 0; depth < inboundLayerDecodeLimits.MaxDepth; depth++ {
id, err := probe.PeekID()
if err != nil {
return false
}
switch id {
case tg.InvokeWithLayerRequestTypeID:
// Seeing the constructor is enough: a missing/malformed layer field is
// itself an explicit correction attempt and must retain its real error.
return true
case tg.InvokeWithoutUpdatesRequestTypeID:
if err := probe.ConsumeID(id); err != nil {
return false
}
case tg.InvokeAfterMsgRequestTypeID:
if err := probe.ConsumeID(id); err != nil {
return false
}
if _, err := probe.Long(); err != nil {
return false
}
case tg.InvokeAfterMsgsRequestTypeID:
if err := probe.ConsumeID(id); err != nil {
return false
}
count, err := probe.VectorHeader()
if err != nil || count > probe.Len()/8 {
return false
}
probe.Buf = probe.Buf[count*8:]
default:
return false
}
}
return false
}
// decodeInboundLayerRPC is side-effect free. Production batch admission uses
// it with a wire-ordered provisional profile cursor, then publishes explicit
// evidence only after the full request identity has acquired an owner (or a
// genuine new-msg_id rewrap alias).
func (s *Server) decodeInboundLayerRPC(state LayerProfileSnapshot, body []byte) (tlprofile.Admission, string, error) {
return s.decodeInboundLayerRPCWithLimits(state, body, inboundLayerDecodeLimits)
}
func (s *Server) decodeInboundLayerRPCWithLimits(state LayerProfileSnapshot, body []byte, limits tlprofile.Limits) (tlprofile.Admission, string, error) {
return s.decodeInboundLayerRPCWithOptions(state, body, tlprofile.AdmissionOptions{Limits: limits})
}
func (s *Server) decodeInboundLayerRPCWithOptions(state LayerProfileSnapshot, body []byte, options tlprofile.AdmissionOptions) (tlprofile.Admission, string, error) {
if s == nil || s.layerRPC == nil || len(body) < bin.Word {
return tlprofile.Admission{}, "unknown", fmt.Errorf("invalid exact RPC admission input")
}
b := &bin.Buffer{Buf: body}
var (
request tlprofile.Admission
err error
)
if state.Origin != LayerProfileUnknown {
if admitter, ok := s.layerRPC.(LayerRPCDefaultProfileOptionsAdmitter); ok {
request, err = admitter.AdmitDefaultLayerWithOptions(state.Profile, b, options)
} else if admitter, ok := s.layerRPC.(LayerRPCDefaultProfileAdmitter); ok {
// Preserve the stable pre-options capability first: unlike strict
// exact admission, default admission lets an explicit invokeWithLayer
// correct inherited or restored profile evidence.
request, err = admitter.AdmitDefaultLayer(state.Profile, b, options.Limits)
} else if admitter, ok := s.layerRPC.(LayerRPCOptionsAdmitter); ok {
request, err = admitter.AdmitLayerWithOptions(state.Profile, b, options)
} else {
request, err = s.layerRPC.AdmitLayer(state.Profile, b, options.Limits)
}
} else if admitter, ok := s.layerRPC.(LayerRPCOptionsAdmitter); ok {
request, err = admitter.AdmitUnprofiledWithOptions(b, options)
} else {
request, err = s.layerRPC.AdmitUnprofiled(b, options.Limits)
}
if err != nil && options.ExpandGZIP != nil && errors.Is(err, tlprofile.ErrGZIPExpanderMissing) {
// A handler compiled against the stable Limits-only boundary remains
// valid for plain requests. Encountering gzip_packed without the optional
// capability is instead a server wiring/programming error: returning the
// generated error as INPUT_REQUEST_INVALID would silently blame a valid
// client envelope and make recovery impossible.
err = fmt.Errorf("%w: handler cannot use caller-owned bounded gzip expansion: %w", errLayerRPCAdmissionCapability, err)
}
method := "unknown"
if err == nil {
_, method, _ = tlprofile.SemanticName(request.Call().Method())
if b.Len() != 0 {
return tlprofile.Admission{}, method, fmt.Errorf("exact RPC admission left %d bytes", b.Len())
}
if effective, known := request.EffectiveProfile(); known && effective != request.Call().Profile() {
return tlprofile.Admission{}, method, fmt.Errorf("%w: effective profile %d differs from call profile %d", ErrLayerProfileConflict, effective, request.Call().Profile())
}
// A generated invariant terminal may use canonical decoding internally
// before the client declares a layer. Only explicit invokeWithLayer (or the
// strict compatibility fallback above) publishes new profile evidence.
if profile, hasEvidence := request.ProfileEvidence(); hasEvidence {
if profile != request.Call().Profile() {
return tlprofile.Admission{}, method, fmt.Errorf("%w: generated profile evidence %d differs from call profile %d", ErrLayerProfileConflict, profile, request.Call().Profile())
}
}
return request, method, nil
}
if state.Origin != LayerProfileUnknown && !layerRPCAdmissionHasExplicitSelector(body, err) {
// Both inherited metadata and a restored same-session snapshot may be stale
// after a client upgrade. One naked-RPC failure asks the official client to
// clear connectionInited and resend explicit invokeWithLayer. Explicit
// retries keep their real unsupported/conflict/malformed error even when the
// selector sits below invokeAfter* or invokeWithoutUpdates.
err = fmt.Errorf("%w: %w", errDefaultLayerAdmission, err)
}
if id, peekErr := (&bin.Buffer{Buf: body}).PeekID(); peekErr == nil {
method = s.typeName(id)
}
if codecErr := new(tlprofile.LayerCodecError); errors.As(err, &codecErr) {
if codecErr.Semantic != 0 {
if _, semanticMethod, ok := tlprofile.SemanticName(codecErr.Semantic); ok && semanticMethod != "" {
method = semanticMethod
}
} else if codecErr.WireID != 0 {
// Nested unknown terminals have no semantic id. Prefer the generated
// failing constructor over the top-level transparent wrapper so the
// compatibility trace names the actual missing RPC.
method = s.typeName(codecErr.WireID)
}
}
var unknownTerminal *tlprofile.UnknownTerminalError
if errors.As(err, &unknownTerminal) && unknownTerminal.WireID != 0 {
method = s.typeName(unknownTerminal.WireID)
}
if errors.Is(err, tlprofile.ErrUnknownRPCMethod) && s.log != nil {
if terminal, recognized := wrappedDestroyAuthKeyTerminal(err); recognized {
method = "destroy_auth_key"
s.log.Debug("Generated wrapper admission exposed MTProto service terminal",
zap.String("method", method),
zap.Int("profile", int(terminal.Profile)),
zap.Uint32("wire_id", terminal.WireID),
zap.Int("wire_size", terminal.WireSize),
)
} else {
s.log.Warn("Unhandled RPC admission (compatibility trace)",
zap.String("method", method), zap.Error(err))
}
}
return tlprofile.Admission{}, method, err
}
// commitLayerProfileEvidence publishes one generated invokeWithLayer proof.
// The exact-session registry is the cross-physical-connection linearization
// point; the Conn cursor then prevents a concurrent older admission from
// overwriting its local wire epoch. Older cached duplicates remain decodable
// and request-bound, but cannot mutate session/profile state.
func (s *Server) commitLayerProfileEvidence(ctx context.Context, c *Conn, profile tlprofile.Profile, msgID int64) (bool, error) {
if s == nil || c == nil {
return false, fmt.Errorf("invalid layer profile evidence target")
}
if msgID > 0 {
if registry, ok := s.layerRPC.(LayerRPCDurableSessionProfileAdvancer); ok {
layer, authoritativeMsgID, publishShared, err := registry.AdvanceNegotiatedSessionLayerEvidence(
ctx, c.authKeyID, c.sessionID, int(profile), msgID,
)
if err != nil {
return false, fmt.Errorf("advance durable exact session Layer evidence: %w", err)
}
if layer <= 0 || authoritativeMsgID <= 0 {
return false, fmt.Errorf("%w: durable exact session evidence returned layer=%d msg_id=%d", ErrLayerProfileConflict, layer, authoritativeMsgID)
}
if s.conns != nil {
if _, err := s.conns.ApplyOrderedRawLayerForSession(c, c.authKeyID, c.sessionID, layer, authoritativeMsgID); err != nil {
return false, err
}
} else if _, err := c.freezeRawLayerProfileAt(layer, authoritativeMsgID); err != nil {
return false, err
}
authoritative, supported := tlprofile.ResolveProfile(layer)
return supported && authoritative == profile && authoritativeMsgID == msgID && publishShared, nil
}
if registry, ok := s.layerRPC.(LayerRPCOrderedSessionProfileRegistry); ok {
_, err := registry.FreezeNegotiatedSessionLayerAt(c.authKeyID, c.sessionID, int(profile), msgID)
if err != nil {
if isExactSessionProfileCapacityError(err) {
return false, fmt.Errorf("freeze ordered exact session registry capacity: %w", err)
}
return false, fmt.Errorf("%w: freeze ordered exact session registry: %w", ErrLayerProfileConflict, err)
}
// Always re-read and broadcast the authoritative registry value, even
// when this proof was stale/identical. Two physical generations may
// interleave registry commit and local publication; broadcasting the
// max cursor to every active/claim Conn makes their final state converge.
layer, authoritativeMsgID, found := registry.NegotiatedSessionLayerEvidence(c.authKeyID, c.sessionID)
if !found || authoritativeMsgID <= 0 {
return false, fmt.Errorf("%w: ordered exact session evidence disappeared after commit", ErrLayerProfileConflict)
}
authoritative, supported := tlprofile.ResolveProfile(layer)
if s.conns != nil {
if _, err := s.conns.ApplyOrderedRawLayerForSession(c, c.authKeyID, c.sessionID, layer, authoritativeMsgID); err != nil {
return false, err
}
} else if _, err := c.freezeRawLayerProfileAt(layer, authoritativeMsgID); err != nil {
return false, err
}
if !supported {
// A future durable Layer is an ordering watermark, not a codec this
// binary can use. Keep every Conn unknown and let a greater supported
// invokeWithLayer self-heal it.
return false, nil
}
return authoritative == profile && authoritativeMsgID == msgID, nil
}
// Older handlers without a persistent ordered registry still receive
// per-Conn replay protection. Production Router implements the interface.
applied, err := c.freezeLayerProfileAt(profile, msgID)
if err != nil || !applied {
return applied, err
}
if registry, ok := s.layerRPC.(LayerRPCSessionProfileRegistry); ok {
if err := registry.FreezeNegotiatedSessionLayer(c.authKeyID, c.sessionID, int(profile)); err != nil {
return false, fmt.Errorf("%w: freeze exact session registry: %v", ErrLayerProfileConflict, err)
}
}
if s.conns != nil {
s.conns.SeedInheritedLayerForRawAuthKey(c.authKeyID, int(profile))
}
return true, nil
}
// msgID==0 intentionally preserves the previous force semantics for direct
// admission tests. It is never used by the production batch path.
if registry, ok := s.layerRPC.(LayerRPCSessionProfileRegistry); ok {
if err := registry.FreezeNegotiatedSessionLayer(c.authKeyID, c.sessionID, int(profile)); err != nil {
return false, fmt.Errorf("%w: freeze exact session registry: %v", ErrLayerProfileConflict, err)
}
}
if err := c.FreezeLayerProfile(profile); err != nil {
return false, err
}
if s.conns != nil {
s.conns.SeedInheritedLayerForRawAuthKey(c.authKeyID, int(profile))
}
return true, nil
}
type exactSessionProfileCapacityMarker interface {
ExactSessionProfileCapacity()
}
type layerEvidenceDurabilityUnavailableMarker interface {
LayerEvidenceDurabilityUnavailable()
}
func isExactSessionProfileCapacityError(err error) bool {
var marker exactSessionProfileCapacityMarker
return errors.As(err, &marker)
}
func isLayerEvidenceDurabilityUnavailable(err error) bool {
var marker layerEvidenceDurabilityUnavailableMarker
return errors.As(err, &marker)
}
func layerRPCAdmissionError(err error) *mt.RPCError {
if errors.Is(err, errDefaultLayerAdmission) {
return &mt.RPCError{ErrorCode: 400, ErrorMessage: "CONNECTION_LAYER_INVALID"}
}
if errors.Is(err, tlprofile.ErrProfileRequired) {
return &mt.RPCError{ErrorCode: 400, ErrorMessage: "CONNECTION_NOT_INITED"}
}
var rpcErr *tgerr.Error
if errors.As(err, &rpcErr) {
return &mt.RPCError{ErrorCode: rpcErr.Code, ErrorMessage: rpcErr.Message}
}
if errors.Is(err, tlprofile.ErrUnknownRPCMethod) {
return &mt.RPCError{ErrorCode: 501, ErrorMessage: "NOT_IMPLEMENTED"}
}
return &mt.RPCError{ErrorCode: 400, ErrorMessage: "INPUT_REQUEST_INVALID"}
}
func (s *Server) layerRPCDependencies(c *Conn, msgID int64, request tlprofile.Admission) layerRPCDependencySet {
result := layerRPCDependencySet{}
seen := make(map[int64]struct{})
for index := 0; index < request.WrapperCount(); index++ {
wrapper, _ := request.Wrapper(index)
var ids []int64
switch wrapper.Semantic() {
case tlprofile.SemanticMethodInvokeAfterMsg:
id, err := layerRPCWrapperRequired[int64](wrapper, "msg_id")
if err != nil {
result.failed = true
continue
}
ids = []int64{id}
case tlprofile.SemanticMethodInvokeAfterMsgs:
var err error
ids, err = layerRPCWrapperRequired[[]int64](wrapper, "msg_ids")
if err != nil || len(ids) > maxLayerRPCDependencyIDs {
result.failed = true
continue
}
default:
continue
}
for _, dependencyID := range ids {
if dependencyID <= 0 || dependencyID >= msgID {
result.failed = true
continue
}
if _, duplicate := seen[dependencyID]; duplicate {
continue
}
seen[dependencyID] = struct{}{}
dependency, ok := s.rpcResults.ObserveDependency(c.authKeyID, c.sessionID, dependencyID)
if !ok {
result.failed = true
continue
}
if dependency.completed {
if !dependency.success {
result.failed = true
}
continue
}
if dependency.waiter != nil {
result.waiters = append(result.waiters, dependency.waiter)
}
}
}
return result
}
func admittedRPCRewrapInit(request tlprofile.Admission) (rpcRewrapInit, bool) {
if request.WrapperCount() != 2 {
return rpcRewrapInit{}, false
}
layerWrapper, ok := request.Wrapper(0)
if !ok || layerWrapper.Semantic() != tlprofile.SemanticMethodInvokeWithLayer {
return rpcRewrapInit{}, false
}
initWrapper, ok := request.Wrapper(1)
if !ok || initWrapper.Semantic() != tlprofile.SemanticMethodInitConnection {
return rpcRewrapInit{}, false
}
layer, err := layerRPCWrapperRequired[int](layerWrapper, "layer")
if err != nil || layer != int(request.Call().Profile()) {
return rpcRewrapInit{}, false
}
apiID, err := layerRPCWrapperRequired[int](initWrapper, "api_id")
if err != nil {
return rpcRewrapInit{}, false
}
deviceModel, err := layerRPCWrapperRequired[string](initWrapper, "device_model")
if err != nil {
return rpcRewrapInit{}, false
}
system, err := layerRPCWrapperRequired[string](initWrapper, "system_version")
if err != nil {
return rpcRewrapInit{}, false
}
appVersion, err := layerRPCWrapperRequired[string](initWrapper, "app_version")
if err != nil {
return rpcRewrapInit{}, false
}
systemLang, err := layerRPCWrapperRequired[string](initWrapper, "system_lang_code")
if err != nil {
return rpcRewrapInit{}, false
}
langPack, err := layerRPCWrapperRequired[string](initWrapper, "lang_pack")
if err != nil {
return rpcRewrapInit{}, false
}
langCode, err := layerRPCWrapperRequired[string](initWrapper, "lang_code")
if err != nil {
return rpcRewrapInit{}, false
}
return rpcRewrapInit{
layer: layer, apiID: apiID, deviceModel: deviceModel, system: system,
appVersion: appVersion, systemLang: systemLang, langPack: langPack, langCode: langCode,
}, true
}
func layerRPCWrapperRequired[T any](wrapper tlprofile.Wrapper, name string) (T, error) {
var zero T
value, present, ok, err := wrapper.Value(name)
if err != nil || !ok || !present {
return zero, fmt.Errorf("invalid RPC wrapper field %q", name)
}
typed, ok := value.(T)
if !ok {
return zero, fmt.Errorf("RPC wrapper field %q has type %T", name, value)
}
return typed, nil
}