Files
Joseph Doherty 5ce62a5900 Wire ApplyScaling, StorageRate; close out write-commands plan
ApplyScaling (HistorianTagDefinition.ApplyScaling):
The EnsT2 trailer's second byte controls server-side scaling — `FE 00`
mirrors MinRaw to MinEU and sets AnalogTag.Scaling=0; `FE 01` persists
distinct MinRaw/MaxRaw and sets Scaling=1. Decoded by toggling
set_ApplyScaling on the native harness and capturing the wire bytes for
both values with identical inputs. The earlier docs claimed
EnsureTagAsync needed a follow-up "UpdateTags" call; the WCF surface has
no such operation — toggling that one byte is the whole fix.

StorageRate (HistorianTagDefinition.StorageRateMs):
Serializer accepts a non-default rate, validated empirically against
the live server which only accepts quantized values
(1000/5000/10000/60000/300000 ms).

EnsureTagAsync upsert semantics:
Second call on the same tag name with different fields succeeds and
updates Description, MinEU, MaxEU, MinRaw, MaxRaw, Scaling in place
(verified by direct SQL inspection in a live test).

Plan + doc closeout:
write-commands-reverse-engineering.md rewritten as a current-state
plan with three workstreams (A doc closeout / B idempotency / C1
StorageRate) and a parallelism table; prior phase notes preserved as
appendix. handoff.md, implementation-status.md, wcf-contract-evidence.md,
README.md updated to remove "writes are out of scope" / non-existent
UpdateTags references and document the actual EnsT2 wire format
including the `FE xx` trailer.

Reverse-engineering harness gains --write-apply-scaling and a SQL
post-check that prints the persisted AnalogTag bounds so future RE
sessions can verify wire→DB causality without leaving the harness.

169/169 tests pass (was 165; +4 new tests covering ApplyScaling,
StorageRate golden bytes, StorageRate live persistence, and
EnsureTagAsync upsert semantics).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 22:04:27 -04:00

11 KiB

WCF Contract Evidence

Local run evidence

  • current\aahClient.dll export inventory ran successfully. SHA256: 77a778988e2d8f2d0e88113f8c8b0788a0ef34fa5134938a353976778144dc83.
  • ArchestrA.HistorianAccess.OpenConnection succeeded against localhost:32568 using HistorianConnectionArgs with ConnectionType=Process and ReadOnly=true.
  • Holding that native connection open produced established TCP sessions from the native PowerShell process to 127.0.0.1:32568; the server-side listener was owned by SMSvcHost.exe, consistent with WCF Net.TCP port sharing.
  • The managed harness command dotnet run --no-build --project tools\AVEVA.Historian.ReverseEngineering -- wcf-probe localhost 32568 successfully called GetV through fully managed WCF/MDAS:
    • net.tcp://localhost:32568/Hist returned version 11
    • net.tcp://localhost:32568/Retr returned version 4
    • net.tcp://localhost:32568/Stat returned version 0
    • net.tcp://localhost:32568/Trx returned version 2
    • net.tcp://localhost:32568/Storage did not listen on this local install
    • net.tcp://localhost:32568/HistCert and /Hist-Integrated reset when called with the plain managed GetV contract, while prefixed variants such as /HCAP/HistCert returned EndpointNotFound Sanitized output is stored in docs\reverse-engineering\wcf-probe-localhost.json.
  • netsh trace and pktmon produced ETL files under %TEMP%\histsdk-captures, but their converted PCAPNG files contained zero packets. Built-in Windows packet capture is not sufficient for loopback evidence on this machine.
  • A dedicated managed certificate-binding probe now reaches HistCert.GetV through MDAS over WCF Net.TCP transport security:
    • net.tcp://localhost:32568/HistCert returned version 11
    • net.tcp://10.100.0.48:32568/HistCert initially failed endpoint identity validation because the server certificate presented DNS identity localhost
    • the same remote endpoint returned version 11 when the client supplied endpoint DNS identity localhost Sanitized outputs are stored in docs\reverse-engineering\wcf-cert-probe-localhost-latest.json, docs\reverse-engineering\wcf-cert-probe-remote-latest.json, and docs\reverse-engineering\wcf-cert-probe-remote-localhost-identity-latest.json.
  • The same remote server also accepts the plain managed WCF/MDAS probe on the expected non-security-specific service paths:
    • net.tcp://10.100.0.48:32568/Hist returned version 11
    • net.tcp://10.100.0.48:32568/Retr returned version 4
    • net.tcp://10.100.0.48:32568/Stat returned version 0
    • net.tcp://10.100.0.48:32568/Trx returned version 2 Sanitized output is stored in docs\reverse-engineering\wcf-probe-remote-latest.json.
  • Managed remote Open2 evidence matches localhost: integrated Windows auth succeeds on net.tcp://10.100.0.48:32568/Hist-Integrated, while the same Windows transport binding fails on plain /Hist before dispatch. The successful returned handle is accepted by Retr.IsOriginalAllowed. Session output bytes and transient handle values are redacted in docs\reverse-engineering\wcf-open2-remote-latest.json.
  • Managed remote StartQuery2 evidence is still negative but sharper: all 22 reconstructed DataQueryRequest variants successfully open the integrated session and pass Retr.IsOriginalAllowed, then StartQuery2 returns false with zero response and error sizes. The legacy StartQuery call returns code 238 for each request and also returns zero response size/no response buffer. Sanitized request hashes are stored in docs\reverse-engineering\wcf-start-query-remote-latest.json.
  • A later bounded managed replay of the first byte-matched full-history candidate used the same integrated open and Retr.IsOriginalAllowed path; StartQuery2 still returned false with zero response/error sizes, while legacy StartQuery faulted with a server null-reference. This keeps Open2 as useful connection evidence, but not as a viable replacement for the native OpenConnection3 session state required by query reads.
  • Managed wildcard tag browse remains positive evidence for an Open2-backed operation: Retr.StartLikeTagNameSearch returned 0, and one GetLikeTagnames batch returned the deterministic 66-byte single-tag buffer with SHA-256 2d450a55f392aed0026e9a957fefa3b116aab6ec81912c5d824c6b9a1ff5a4a1.
  • Managed remote scalar tag calls also accept the integrated session handle: Retr.GetTagTypeFromName returns code 0 and tag type 1 for OtOpcUaParityTest_001.Counter; Retr.IsManualTag returns code 0 and false; legacy Retr.GetTagInfoFromName returns 238 with zero metadata bytes. Five GetTgByNm tag-name buffer variants and five GetTg tag-id buffer variants all return 238, sequence 0, and zero output bytes. This suggests the calls are reaching server logic but the metadata-returning contract shape or request buffer is still incomplete. Sanitized output is stored in docs\reverse-engineering\wcf-tag-info-remote-latest.json.

Decompiled service contracts

current\aahClientManaged.dll contains WCF contracts with namespace aa:

  • HistoryServiceContract.IHistoryServiceContract
    • [ServiceContract(Name = "Hist", Namespace = "aa")]
    • GetInterfaceVersion as operation GetV
    • OpenConnection as operation Open
    • CloseConnection as operation Close
    • ValidateClient as operation VldC
    • UpdateClientStatus as operation UpdC
    • AddTags as AddT, RegisterTags as RTag
    • AddStreamValues as AddS, SetClientTimeOut as SetT
  • HistoryServiceContract.IHistoryServiceContract2
    • [ServiceContract(Name = "Hist", Namespace = "aa")]
    • byte-buffer session open uses OpenConnection2 as operation Open2
    • extended client status uses UpdC2 / UpdC3
    • extended write and maintenance calls include EnsT, DelT, AddS2, ExKey, ValCl, and GetI
  • RetrievalServiceContract.IRetrievalServiceContract
    • [ServiceContract(Name = "Retr", Namespace = "aa")]
    • StartQuery, GetNextQueryResultBuffer, EndQuery use default operation names
    • tag type/name helpers and tag info calls use default operation names
  • RetrievalServiceContract.IRetrievalServiceContract2/3/4
    • extended bool/error-buffer variants
    • SQL/recordset byte stream calls
    • tag query calls QTB, QTG, QTE
    • event query calls use default operation names
    • extended property calls include GetTgByNm2 and GetTepByNm
  • StorageServiceContract.IStorageServiceContract
    • [ServiceContract(Name = "Storage", Namespace = "aa")]
    • storage/session, metadata, streamed-value, block, snapshot, and delete-tag calls
  • StatusServiceContract.IStatusServiceContract
    • [ServiceContract(Name = "Stat", Namespace = "aa")]
    • GetInterfaceVersion as GetV
    • server time, timezone, DB case sensitivity, and logging use default operation names
  • StatusServiceContract.IStatusServiceContract2
    • extended status operations include GetSystemParameter, GetTimeZoneNames, license checks, historian info, and process/ping helpers
    • ping and historian-info helpers use aliases PNGS, PNGP, and GETHI
  • TransactionServiceContract.ITransactionServiceContract
    • [ServiceContract(Name = "Trx", Namespace = "aa")]
    • snapshot forwarding and non-streamed value transactions

aahMDASEncoder.ClientMessageEncoder wraps an inner WCF encoder and exposes media/content type application/x-mdas. This means the first managed driver transport target should be WCF Net.TCP plus the MDAS content-type encoder, not the earlier speculative raw-frame layer.

Current unknowns

  • Endpoint URI paths net.tcp://{host}:32568/Hist, /Retr, /Stat, and /Trx are confirmed for GetV calls on the local 2020 install.
  • Relay and local WCF probe evidence also identify security-specific history endpoints /HistCert and /Hist-Integrated. /HistCert is confirmed as a Hist contract endpoint when called with MDAS over TLS transport security; /Hist-Integrated remains the Windows negotiate endpoint for integrated session open.
  • Managed Open2 evidence confirms /Hist-Integrated is the correct endpoint for integrated Windows auth. The plain /Hist endpoint rejects the Windows transport-security upgrade before dispatching the operation.
  • The storage contract is confirmed statically, but /Storage was not a listening endpoint in the local probe; storage may be routed through session-specific storage/shard endpoints.
  • Hist.OpenConnection reaches server logic, but the native password/session packet encoding is not decoded yet. See wcf-open-localhost.md.
  • Hist.Open2 is confirmed reachable with a managed version-1 byte buffer. Empty credentials return custom native error 171 (AuthenticationFailed), and the harness decodes observed five-byte error buffers as type plus little-endian error code. This confirms framing has progressed past packet-version rejection. See wcf-open2-localhost.md.
  • Stat is reachable, but CStatusConnectionWCF.GetServerTime is a no-op stub in the decompiled native WCF path and handle-dependent status calls fail with handle 0. See wcf-status-localhost.md.
  • Query request and response byte-buffer layouts are still proprietary payloads inside WCF operations such as StartQuery and GetNextQueryResultBuffer.
  • Write payload layouts decoded for the two supported ops:
    • Hist.EnsT2(analog) 144-byte CTagMetadata InBuff payload — leading 0x4E marker, fixed 10-byte signature, 1-byte CDataType discriminator (0x01 Float / 0x21 Double / 0x09 UInt2 / 0x11 UInt4 / 0x29 Int2 / 0x31 Int4), 16 zero placeholder bytes, compact-ASCII tag name, 16 bytes of 0xFF, compact-ASCII description, compact-ASCII MDAS, 7-byte flag block, uint32 storage rate, int64 FILETIME, scaling block (compact 1A 03 for default 0/100/0/100 ranges OR 1F 00 + 4 doubles MinEU/MaxEU/MinRaw/MaxRaw for explicit), compact-ASCII engineering unit, uint32 0x2710 constant, double 1.0 (IntegralDivisor), 2-byte trailer FE xx where xx is the ApplyScaling flag (0x00 false / 0x01 true). Live-verified: with 0x01 the server persists distinct MinRaw/MaxRaw and sets AnalogTag.Scaling=1; with 0x00 it mirrors MinRaw to MinEU. Captured fixtures live at artifacts/reverse-engineering/instrumented-wcf-writemessage-writes/ (default ranges) and artifacts/reverse-engineering/apply-scaling-experiment/ (both ApplyScaling values for the same input ranges). Connection mode is 0x401 (Process | Write | IntegratedSecurity) — the read-mode 0x402 makes the server return err 132 silently.
    • Hist.DelT tagNames byte buffer — ushort 0x6751, ushort 1, uint32 tagCount, then per tag uint32 charCount + UTF-16-LE chars. Decoded via wire capture against the sandbox tag.
    • Hist.AddS2 (write samples) is architecturally blocked — server runtime cache requires IOServer / Application Server pipeline registration, not just a Tag row in Runtime.dbo. Three reproduction attempts (real wwTagKey, fresh session, 8s settle wait) confirmed 129 "Tag not found in cache" is the gate. No AddS2 wire bytes leave the client.