Files
histsdk/docs/reverse-engineering/wcf-contract-evidence.md
T
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

192 lines
11 KiB
Markdown

# 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.