build: resync vendor/ so the kms-operator image can build again

ghcr.io/luxfi/kms-operator has had no new image since v1.12.3. The Build KMS
Operator workflow failed on every tag since (v1.12.4 … v1.12.7) while its
sibling Build KMS succeeded on the same tag, so the tags looked published.

Dockerfile.operator:27 builds with -mod=vendor, and vendor/ had drifted from
go.mod in both directions:
  - go.opentelemetry.io/otel/{metric,trace}: in vendor/modules.txt, not in go.mod
  - github.com/luxfi/{crypto,geth,ids,keys,zap,address,age,cache,constants,
    container,formatting,math,go-bip32,go-bip39}: in go.mod, not marked explicit

`go mod vendor` alone could not fix it — it aborted first on go.sum entries
missing for the k8s packages cmd/kms-operator imports (k8s.io/api/core/v1,
k8s.io/apimachinery/..., k8s.io/client-go/{dynamic,kubernetes,rest}). So:
go mod tidy (adds them, drops now-unused indirects) then go mod vendor.

Verified by stash control:
  operator, -mod=vendor:  rc=1 -> rc=0   ("inconsistent vendoring" 1 -> 0)
  server,   -mod=vendor:  rc=1 -> rc=1   (unchanged)

The server's vendor build was already broken and stays broken, for an unrelated
reason: blst.h has never been vendored (confirmed absent from HEAD's tree, so it
could not have worked before either) — go mod vendor does not copy cgo headers
that live outside the package dir. It is not a regression and does not affect
CI: the server's Dockerfile builds with GOFLAGS=-mod=mod after `rm -f go.sum &&
go mod download`, i.e. from the module cache, never from vendor/. The operator
is the only -mod=vendor consumer, which is exactly why it was the only failure.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
This commit is contained in:
zeekay
2026-07-25 17:39:26 -07:00
co-authored by Hanzo Dev
parent d333c66bae
commit 1c8b627a25
115 changed files with 4375 additions and 724 deletions
-7
View File
@@ -19,18 +19,14 @@ require (
require (
filippo.io/hpke v0.4.0 // indirect
github.com/bits-and-blooms/bitset v1.24.4 // indirect
github.com/btcsuite/btcd/btcutil v1.1.6 // indirect
github.com/cenkalti/backoff v2.2.1+incompatible // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/consensys/gnark-crypto v0.20.1 // indirect
github.com/crate-crypto/go-eth-kzg v1.5.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 // indirect
github.com/dgraph-io/ristretto/v2 v2.4.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/emicklei/go-restful/v3 v3.12.2 // indirect
github.com/ethereum/c-kzg-4844/v2 v2.1.7 // indirect
github.com/fxamacker/cbor/v2 v2.9.1 // indirect
github.com/go-ini/ini v1.67.0 // indirect
github.com/go-logr/logr v1.4.3 // indirect
@@ -57,7 +53,6 @@ require (
github.com/luxfi/constants v1.6.2 // indirect
github.com/luxfi/container v0.2.1 // indirect
github.com/luxfi/crypto v1.20.2 // indirect
github.com/luxfi/crypto/ipa v1.2.4 // indirect
github.com/luxfi/formatting v1.1.1 // indirect
github.com/luxfi/geth v1.20.1 // indirect
github.com/luxfi/go-bip32 v1.1.0 // indirect
@@ -66,8 +61,6 @@ require (
github.com/luxfi/mdns v0.1.1 // indirect
github.com/luxfi/metric v1.8.1 // indirect
github.com/luxfi/mock v0.1.1 // indirect
github.com/luxfi/pq v1.1.0 // indirect
github.com/luxfi/proto v1.4.2 // indirect
github.com/luxfi/protocol v0.0.2 // indirect
github.com/luxfi/sampler v1.1.0 // indirect
github.com/luxfi/tls v1.1.1 // indirect
+65 -42
View File
@@ -1,8 +1,10 @@
c2sp.org/CCTV/age v0.0.0-20251208015420-e9274a7bdbfd h1:ZLsPO6WdZ5zatV4UfVpr7oAwLGRZ+sebTUruuM4Ra3M=
c2sp.org/CCTV/age v0.0.0-20251208015420-e9274a7bdbfd/go.mod h1:SrHC2C7r5GkDk8R+NFVzYy/sdj0Ypg9htaPXQq5Cqeo=
filippo.io/hpke v0.4.0 h1:p575VVQ6ted4pL+it6M00V/f2qTZITO0zgmdKCkd5+A=
filippo.io/hpke v0.4.0/go.mod h1:EmAN849/P3qdeK+PCMkDpDm83vRHM5cDipBJ8xbQLVY=
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII=
github.com/bits-and-blooms/bitset v1.24.4 h1:95H15Og1clikBrKr/DuzMXkQzECs1M6hhoGXLwLQOZE=
github.com/bits-and-blooms/bitset v1.24.4/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ=
github.com/btcsuite/btcd v0.22.0-beta.0.20220111032746-97732e52810c/go.mod h1:tjmYdS6MLJ5/s0Fj4DbLgSbDHbEqLJrtnHecBFkdz5M=
github.com/btcsuite/btcd v0.23.5-0.20231215221805-96c9fd8078fd/go.mod h1:nm3Bko6zh6bWP60UxwoT5LzdGJsQJaPo6HjduXq9p6A=
@@ -32,29 +34,30 @@ github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UF
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=
github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
github.com/consensys/gnark-crypto v0.20.1 h1:PXDUBvk8AzhvWowHLWBEAfUQcV1/aZgWIqD6eMpXmDg=
github.com/consensys/gnark-crypto v0.20.1/go.mod h1:RBWrSgy+IDbGR69RRV313th3M/aZU1ubk2om+qHuTSc=
github.com/crate-crypto/go-eth-kzg v1.5.0 h1:FYRiJMJG2iv+2Dy3fi14SVGjcPteZ5HAAUe4YWlJygc=
github.com/crate-crypto/go-eth-kzg v1.5.0/go.mod h1:J9/u5sWfznSObptgfa92Jq8rTswn6ahQWEuiLHOjCUI=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
github.com/decred/dcrd/crypto/blake256 v1.1.0 h1:zPMNGQCm0g4QTY27fOCorQW7EryeQ/U0x++OzVrdms8=
github.com/decred/dcrd/crypto/blake256 v1.1.0/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 h1:5RVFMOWjMyRy8cARdy79nAmgYw3hK/4HUq48LQ6Wwqo=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40=
github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218=
github.com/dgraph-io/ristretto/v2 v2.4.0 h1:I/w09yLjhdcVD2QV192UJcq8dPBaAJb9pOuMyNy0XlU=
github.com/dgraph-io/ristretto/v2 v2.4.0/go.mod h1:0KsrXtXvnv0EqnzyowllbVJB8yBonswa2lTCK2gGo9E=
github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da h1:aIftn67I1fkbMa512G+w+Pxci9hJPB8oMnkcP3iZF38=
github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU=
github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
github.com/ethereum/c-kzg-4844/v2 v2.1.7 h1:aat3CuITdDbPC6pmEGRT0zJ5eOxzrZj8TJT5z7Xk//M=
github.com/ethereum/c-kzg-4844/v2 v2.1.7/go.mod h1:8HMkUZ5JRv4hpw/XUrYWSQNAUzhHMg2UDb/U+5m+XNw=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/fxamacker/cbor/v2 v2.9.1 h1:2rWm8B193Ll4VdjsJY28jxs70IdDsHRWgQYAI80+rMQ=
github.com/fxamacker/cbor/v2 v2.9.1/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
@@ -66,10 +69,16 @@ github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ4
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs=
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE=
github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k=
github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
@@ -81,11 +90,16 @@ github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/flatbuffers v25.12.19+incompatible h1:haMV2JRRJCe1998HeW/p0X9UaMTK6SDo0ffLn2+DbLs=
github.com/google/flatbuffers v25.12.19+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo=
github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20260302011040-a15ffb7f9dcc h1:VBbFa1lDYWEeV5FZKUiYKYT0VxCp9twUmmaq9eb8sXw=
github.com/google/pprof v0.0.0-20260302011040-a15ffb7f9dcc/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/rpc v1.2.1 h1:yC+LMV5esttgpVvNORL/xX4jvTTEUE30UZhZ5JF7K9k=
@@ -98,8 +112,10 @@ github.com/holiman/uint256 v1.3.2/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXei
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
@@ -112,89 +128,65 @@ github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu
github.com/klauspost/crc32 v1.3.0 h1:sSmTt3gUt81RP655XGZPElI0PelVTZ6YwCRnPSupoFM=
github.com/klauspost/crc32 v1.3.0/go.mod h1:D7kQaZhnkX/Y0tstFGf8VUzv2UofNGqCjnC3zdHB0Hw=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/luxfi/accel v1.2.4 h1:5VbIHyEvvfobn2zBiTFODxDw1CeqxCepZOLlvkuf9yQ=
github.com/luxfi/accel v1.2.4/go.mod h1:ISIwAX+ZfsL/S5nsP2JvfldXN6Nc+QzoWf6Jtaq+xsQ=
github.com/luxfi/address v1.0.1/go.mod h1:5j3Eh66v9zvv1GbNdZwt+23krV8JlSDaRzmWZU8ZRM0=
github.com/luxfi/address v1.1.1 h1:4afWzyBWzTiZN7RenBtdMC9LIvP9L4CSBzSquwKEAgI=
github.com/luxfi/address v1.1.1/go.mod h1:KG0jUBcgoJYeieKP5jboCq9UewwDBIOus8ZCqUMVlw8=
github.com/luxfi/age v1.5.0/go.mod h1:iAYAxgvrXxcy746+Ovh/eWWDuF9teJLNcCSSOX9RYW0=
github.com/luxfi/age v1.6.0 h1:KMD8gSOP4NVCb7NWSlRcgBZNV2xm2a+qQWPyPmiX6f4=
github.com/luxfi/age v1.6.0/go.mod h1:7cu9CIyikgyAvr5MlXFapEDQ15yBaHOSdKkK5lG04WE=
github.com/luxfi/cache v1.2.1 h1:kAzOS55/hmYeNKR+0HAKv4ma48Y6JjkI8UQeqdZ8bfI=
github.com/luxfi/cache v1.2.1/go.mod h1:co7JTxZZHpKT31Yh01LFp5aZOxmoUg157FhBLQdQHVU=
github.com/luxfi/cache v1.3.1 h1:grQhi/B5GKypG7avDMeY143QTgFbfEvQICKNIh1Cw6U=
github.com/luxfi/cache v1.3.1/go.mod h1:2MokdbeNUy/9O3mdREWkE6BiN7tRvePkXiKkcb+4M7g=
github.com/luxfi/constants v1.5.8/go.mod h1:Pu5jWHdnUtQRbWC43yTUjU/pbIIKMDOd2a2yroSfo48=
github.com/luxfi/constants v1.6.2 h1:pXHdKIFbfE9qX4xOjq2LxYvagNhhNvspUVEbPcIEKfA=
github.com/luxfi/constants v1.6.2/go.mod h1:r0oH8C/+r/XFYBq1AJxt6zWRKKRKgDzrEMop/CCs9rI=
github.com/luxfi/container v0.0.4 h1:BXhF82WyfqVP5mjlNcr7tP0Fcnvl0Ap1rkiu+rq5XuM=
github.com/luxfi/container v0.0.4/go.mod h1:Z3SpmMF5d4t77MM0nHYXURpn+EMVaeu1fhbd/3BGaek=
github.com/luxfi/container v0.2.1 h1:MTnfKXzS5+oxV5jKZerdOxSA6iMPaQI9/FWGufizzaw=
github.com/luxfi/container v0.2.1/go.mod h1:B+uM0wP0lGvt/SSK7QOEn/qBcsHzILVHlKikdCyzSgM=
github.com/luxfi/crypto v1.19.27 h1:K4l9nsnB0t61zHO5u8si06zVzW5WTjWW+/6+KRb/bfo=
github.com/luxfi/crypto v1.19.27/go.mod h1:0DCU62kX8+zhYU2qeM07A4pifJyPkPujnUOfgc8TOFQ=
github.com/luxfi/crypto v1.20.2 h1:L81WEsU/hs2A76F5PWBusG0yU74QqkDdUqqgexWUxh4=
github.com/luxfi/crypto v1.20.2/go.mod h1:qYHOM0lO4PRh7LEaObxFQUIMjmT1/paVm/WgZkobT1k=
github.com/luxfi/crypto/ipa v1.2.4 h1:6xfwhI9/HrcDkF3Ti5/NxsNQIWbwYDJmRSNIHRQ/xfU=
github.com/luxfi/crypto/ipa v1.2.4/go.mod h1:43J6f6rcfUMrZt4cQectMOZb6Ps+fAEj8ZTPC3Kk+gE=
github.com/luxfi/formatting v1.0.1/go.mod h1:mYzNf5DJOiqSSKUPzNj5dKy4tstFbN3pZlkI5716eKc=
github.com/luxfi/formatting v1.1.1 h1:MJhVXIPh1dbysvYEjtaEA/Z0FUTiI7n0DwOF54FS08c=
github.com/luxfi/formatting v1.1.1/go.mod h1:zhBWp6fLZduhpiAdPgVDdPVOyhw4FvwRUksF6+xKQCE=
github.com/luxfi/geth v1.17.12 h1:UP/fhpcfbGPTrkOCwX3d88Oc3jVm5gTOgfjgq+lek6s=
github.com/luxfi/geth v1.17.12/go.mod h1:3vQfQJd9JC+AVBjxNXa9PYQOqpbE2dKu8E3jqhPZ3LU=
github.com/luxfi/geth v1.20.1 h1:QUGQr4AKvADjwMi7t8a0OfoyxShgEcI9pwie1jFYfm0=
github.com/luxfi/geth v1.20.1/go.mod h1:GV5bIMEgWviRN+jPXERyVpI16H3iHqPcdIokDoZdrvU=
github.com/luxfi/go-bip32 v1.0.2/go.mod h1:bc7/LXDKAJQZ/F0Xjf5yXaTZxY9/ssLb4FC+Hxn/cDk=
github.com/luxfi/go-bip32 v1.1.0 h1:zjy19WKa1KJnGamRNyOZM3l/MPtV/sax7M4NMPwLHZQ=
github.com/luxfi/go-bip32 v1.1.0/go.mod h1:QyDXlzWL3xRiCbMUi4Z3J52Q/SMoCvdRLXXlYvSZor8=
github.com/luxfi/go-bip39 v1.1.2/go.mod h1:96de9VkR2kY/ASAnhMtvt3TSh+PZkAFAngNj0GjRGDo=
github.com/luxfi/go-bip39 v1.2.0 h1:fx3pFuSGawCG4In6pA4OLLStqbgIqD1j8EygFskoHzY=
github.com/luxfi/go-bip39 v1.2.0/go.mod h1:if+2OVbG4k4jKIuBt/Rse1KV1kgWQM5j5xFbUtwbNtc=
github.com/luxfi/ids v1.2.15 h1:omE+E4+0Poj9DzM11ejSFgteaSQ3KDHi5g54iH6jcxI=
github.com/luxfi/ids v1.2.15/go.mod h1:Fj73K5xcblvdE0SxU/ip+jE8VqNdu+80548su5KJ7xI=
github.com/luxfi/ids v1.3.2 h1:c6Rft5kZB4XqiCtWaGH47bfhaNFm3FGRfhEzI01GVeI=
github.com/luxfi/ids v1.3.2/go.mod h1:+5l8cYMbKpORJbQ2r98CYJo9TQATgUdnmzpYFZWMwwc=
github.com/luxfi/keys v1.1.0/go.mod h1:U3tZNDmv3nXkPoZwLtq9RNjwyN0XyoN29worigfT+c0=
github.com/luxfi/keys v1.4.1 h1:2Zcoovaz9OLPz7m7VGXfRrGnrlqt0GeUpJclsPBi4EU=
github.com/luxfi/keys v1.4.1/go.mod h1:P8EUP5DKrR1SUZBGZjDT3rWcp2P1miUlVh7IBRNBphU=
github.com/luxfi/log v1.4.3 h1:xkUKRWvQ4ZwvlUC2e0/RTtHYZOYSMvSQ9W9lbjwBmiI=
github.com/luxfi/log v1.4.3/go.mod h1:myIkufyiQomSQH34K981kbz6cG4WUoerRUh7F4XhlQI=
github.com/luxfi/math v1.4.1 h1:1t9bCCsEqnl9yIKrShlbs80DBKyYTWdnzkVfBqEeO7Q=
github.com/luxfi/math v1.4.1/go.mod h1:QvbRxauQyE1w4lvbcLSe6c8yeJz2Zj1Bq1rayGgs2tA=
github.com/luxfi/math v1.5.1 h1:FDOY75e4vn/Xra1ij99xOS/9XdxQGCPP6HONHRkCwfg=
github.com/luxfi/math v1.5.1/go.mod h1:3j9R24hVfPhrbvs45YSJP7jAyVNfwx/cj/+lAO8IGro=
github.com/luxfi/math/big v0.1.0 h1:Vz4c0RsZVPdIKPsHPgAJChH/R3p15WHRUz7LkLf+NIQ=
github.com/luxfi/math/big v0.1.0/go.mod h1:BuxSu22RbO93xBLk5Eam5nldFponoJ73xDFz4uJ3Huk=
github.com/luxfi/mdns v0.1.1 h1:g2eRr9AXcziPkkcd24M+Qu9ApEpoKKjfI79QSNqv0rQ=
github.com/luxfi/mdns v0.1.1/go.mod h1:dbp5f3h3aE7CGzwbaWzBM9cwdcekhmSrWhQevgYhhNA=
github.com/luxfi/metric v1.5.8 h1:axPwfq+erOlIue7IJp5g+hMcMtVhYHja9gJAaT3+KNA=
github.com/luxfi/metric v1.5.8/go.mod h1:fO2giazkg4NDtr72JM/QXJBYebplAMeWC1JoZyNDvKw=
github.com/luxfi/metric v1.8.1 h1:v58GgPFAOLPVxSa/JiNLwqJQNEFHdWbXZV28piMXX4s=
github.com/luxfi/metric v1.8.1/go.mod h1:R1OPAIeW4UBW3osK7j2r3/XPmczfNRFTXg4bnlemTuE=
github.com/luxfi/mock v0.1.1 h1:0HEtIjg1J6CWz+IUyP6rsGqNWTcmxjFnSQIhaDuARwY=
github.com/luxfi/mock v0.1.1/go.mod h1:jo35akl3Vtd8LbzDts8VJ0jmSVycrd1/eBi6g6t5hKU=
github.com/luxfi/pq v1.0.3 h1:ksw1dmfTR0dqqNMRS7BjGcprCO2Fhc+3Iiq2/NMuONw=
github.com/luxfi/pq v1.0.3/go.mod h1:8bppZcRElfrVt0n3nYCZW3iX1TvhvzNbdjNdK1irgIE=
github.com/luxfi/proto v1.3.4/go.mod h1:S80KrTzISCktptit/pWLzk6nHgagjJS86qbQL6gHutE=
github.com/luxfi/pq v1.1.0 h1:ADplfUSyirLymSxs3Ix0HeDTyl5oswCNUpXJt/5vLY8=
github.com/luxfi/pq v1.1.0/go.mod h1:KT5rG9ztpzIkT9QSnXK4WFqBBLzKCLjY7l1c/unBi8I=
github.com/luxfi/protocol v0.0.2 h1:TAuXMm1K4VDpG3B3brq1EE9Lb7XlbhQmVBObskgNhmc=
github.com/luxfi/protocol v0.0.2/go.mod h1:Yjn2VRwn5PXim0+JTalAyrVG6YbmuSnYOfeUXSbAsqA=
github.com/luxfi/sampler v1.1.0 h1:u3iRDl7V06ARh0e85h3HT+aZ1saCFo2yMMsh+dCJbqk=
github.com/luxfi/sampler v1.1.0/go.mod h1:kJa53S3tC9+VSbuV3RFu68MmbCCBlr2UM39LOClQ/Hs=
github.com/luxfi/tls v1.0.3/go.mod h1:dQqSiGE7YxXUxOwICoReUuIitBms9DYOaCeteBwmIWw=
github.com/luxfi/tls v1.1.1 h1:BSZ0gHSp7U8vzlmzx7WSSCz+b7Ky4JtD9HDDhn7vrDg=
github.com/luxfi/tls v1.1.1/go.mod h1:+5TDy8UtLL+tz124brZzpUDBRj+sKrq0JFqdmpMUHgw=
github.com/luxfi/vm v1.2.3/go.mod h1:ZXmw1sKA6GL3Ma3BfYUv5Fzvx3aWu2QMtkxF3lL/3R4=
github.com/luxfi/vm v1.3.1 h1:rLCbygaajehVkUoJ5czwhpUAJaC5J5okq3p+j2QSPSo=
github.com/luxfi/vm v1.3.1/go.mod h1:uViH3COP8hhCbj42v1MTohkPCDwYQCZanNIOb/StWqY=
github.com/luxfi/zap v0.7.2 h1:YecWTWNE5PPJXL56sLIkzS8b23bprUwZ5lPAQuLUtTE=
github.com/luxfi/zap v0.7.2/go.mod h1:1k+nwT+JW802YzuPAuf7CxMSGr/qxvbGgGwi5k6X9Ok=
github.com/luxfi/zap v1.2.6 h1:NBpbm9Gib41Oi/XAkAZKQ3hb+xCafo7JsrUjw+bKiAc=
github.com/luxfi/zap v1.2.6/go.mod h1:sTAe/AMMamoE85cVoe81+NbqHJkgvqS0LhY9ByHEmr0=
github.com/luxfi/zapdb v1.10.0 h1:1lLHEmkyC0BucnA/zjQYsMkUVxuEo2vQkEaQGjYfuuc=
github.com/luxfi/zapdb v1.10.0/go.mod h1:Qukh3hDRD0MnxA6z+a28JTnXhN85AiLLgp6TYr4QAMc=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mattn/go-colorable v0.1.15 h1:+u9SLTRGnXv73cEsnsmoZBom+dMU88B2M0aDcWy0/jY=
github.com/mattn/go-colorable v0.1.15/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
@@ -210,33 +202,47 @@ github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEp
github.com/minio/minio-go/v7 v7.0.100 h1:ShkWi8Tyj9RtU57OQB2HIXKz4bFgtVib0bbT1sbtLI8=
github.com/minio/minio-go/v7 v7.0.100/go.mod h1:EtGNKtlX20iL2yaYnxEigaIvj0G0GwSDnifnG8ClIdw=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8=
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/mr-tron/base58 v1.3.0 h1:K6Y13R2h+dku0wOqKtecgRnBUBPrZzLZy5aIj8lCcJI=
github.com/mr-tron/base58 v1.3.0/go.mod h1:2BuubE67DCSWwVfx37JWNG8emOC0sHEU4/HpcYgCLX8=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
github.com/onsi/ginkgo/v2 v2.28.1 h1:S4hj+HbZp40fNKuLUQOYLDgZLwNUVn19N3Atb98NCyI=
github.com/onsi/ginkgo/v2 v2.28.1/go.mod h1:CLtbVInNckU3/+gC8LzkGUb9oF+e8W8TdUsxPwvdOgE=
github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/onsi/gomega v1.39.1 h1:1IJLAad4zjPn2PsnhH70V4DKRFlrCzGBNrNaru+Vf28=
github.com/onsi/gomega v1.39.1/go.mod h1:hL6yVALoTOxeWudERyfppUcZXjMwIMLnuSfruD2lcfg=
github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM=
github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/quic-go/quic-go v0.59.1 h1:0Gmua0HW1Tv7ANR7hUYwRyD0MG5OJfgvYSZasGZzBic=
github.com/quic-go/quic-go v0.59.1/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4=
github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
@@ -244,28 +250,28 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/supranational/blst v0.3.16 h1:bTDadT+3fK497EvLdWRQEjiGnUtzJ7jjIUMF0jqwYhE=
github.com/supranational/blst v0.3.16/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw=
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc=
github.com/tinylib/msgp v1.6.3/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA=
github.com/tinylib/msgp v1.6.4 h1:mOwYbyYDLPj35mkA2BjjYejgJk9BuHxDdvRnb6v2ZcQ=
github.com/tinylib/msgp v1.6.4/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU=
go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc=
go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=
go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc=
go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo=
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk=
go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE=
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
@@ -294,6 +300,7 @@ golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81R
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -314,12 +321,14 @@ golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4=
golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
@@ -343,9 +352,12 @@ google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4=
gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
@@ -354,14 +366,25 @@ gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
k8s.io/api v0.34.0 h1:L+JtP2wDbEYPUeNGbeSa/5GwFtIA662EmT2YSLOkAVE=
k8s.io/api v0.34.0/go.mod h1:YzgkIzOOlhl9uwWCZNqpw6RJy9L2FK4dlJeayUoydug=
k8s.io/apimachinery v0.34.0 h1:eR1WO5fo0HyoQZt1wdISpFDffnWOvFLOOeJ7MgIv4z0=
k8s.io/apimachinery v0.34.0/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw=
k8s.io/client-go v0.34.0 h1:YoWv5r7bsBfb0Hs2jh8SOvFbKzzxyNo0nSb0zC19KZo=
k8s.io/client-go v0.34.0/go.mod h1:ozgMnEKXkRjeMvBZdV1AijMHLTh3pbACPvK7zFR+QQY=
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b h1:MloQ9/bdJyIu9lb1PzujOPolHyvO06MXG5TUIj2mNAA=
k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b/go.mod h1:UZ2yyWbFTpuhSbFhv24aGNOdoRdJZgsIObGBUaYVsts=
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 h1:hwvWFiBzdWw1FhfY1FooPn3kzWuJ8tmbZBHi4zVsl1Y=
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE=
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=
sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco=
sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE=
sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=
sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=
+26
View File
@@ -0,0 +1,26 @@
# Hanzo Address
## Overview
Go module: github.com/luxfi/address
## Tech Stack
- **Language**: Go
## Build & Run
```bash
go build ./...
go test ./...
```
## Structure
```
address/
LICENSE
address.go
converter.go
go.mod
go.sum
```
## Key Files
- `go.mod` -- Go module definition
+8
View File
@@ -0,0 +1,8 @@
# Licensing
This repository is licensed under the **BSD-3-Clause License** (see [LICENSE](LICENSE)). It belongs to the **public** tier of the Lux three-tier IP strategy: anyone may use, fork, or redistribute it, including for commercial purposes, subject to the BSD-3-Clause License terms.
For the canonical Lux IP and licensing strategy, see:
<https://github.com/luxfi/.github/blob/main/profile/README.md>
For commercial inquiries that go beyond the public tier (e.g. private moat acceleration kernels, Eco-tier patent-protected primitives outside Authorized Networks), contact `licensing@lux.network`.
+19
View File
@@ -0,0 +1,19 @@
# age
**Org:** luxfi · **Ecosystem:** lux · **Path:** `/Users/a/work/lux/luxfi/age`
**Origin:** https://github.com/luxfi/age.git
## Discovery
This file (`CLAUDE.md`) is the canonical agent-facing readme; `LLM.md` is a symlink to it. Update either name and both stay in sync.
## Where to look first
- `README.md` — human-facing overview (if present)
- `package.json` / `Cargo.toml` / `pyproject.toml` / `go.mod` — language & deps
- `.github/workflows/` — CI surface
- `docs/` — extended docs (if present)
## Sibling repos
See the org-level `LLM.md` at `/Users/a/work/lux/luxfi/LLM.md` for the full inventory of sibling repos and inter-repo dependencies.
+2 -2
View File
@@ -74,8 +74,8 @@ REPLICATE_AGE_RECIPIENTS_FALLBACK=age1pq1... # comma-separated, multi-recipi
### E. Operator CRD
```yaml
apiVersion: liquidity.io/v1
kind: LiquidReplicate
apiVersion: lux.network/v1
kind: ReplicateConfig
spec:
encryption:
kem: xwing # enum: hpke-mlkem768x25519 | xwing
+2
View File
@@ -1,3 +1,5 @@
<p align="center"><img src=".github/hero.svg" alt="age" width="880"></p>
<p align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://github.com/FiloSottile/age/blob/main/logo/logo_white.svg">
+1 -1
View File
@@ -9,9 +9,9 @@ import (
"fmt"
"strings"
"filippo.io/hpke"
"github.com/luxfi/age/internal/bech32"
"github.com/luxfi/age/internal/format"
"filippo.io/hpke"
"golang.org/x/crypto/chacha20poly1305"
)
+29
View File
@@ -0,0 +1,29 @@
BSD 3-Clause License
Copyright (c) 2024, Lux Partners Limited
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+12
View File
@@ -0,0 +1,12 @@
# Licensing
This repository is licensed under the **BSD 3-Clause License** (see
[LICENSE](LICENSE)). It belongs to the **public** tier of the Lux
three-tier IP strategy: anyone may use, fork, or redistribute it,
including for commercial purposes, subject to the BSD-3 terms.
For the canonical Lux IP and licensing strategy, see:
<https://github.com/luxfi/.github/blob/main/profile/README.md>
For commercial inquiries that go beyond BSD-3 (e.g. private moat
acceleration kernels), contact `licensing@lux.network`.
+22 -10
View File
@@ -21,7 +21,8 @@ type ChainConfig struct {
QChainID ids.ID // Quantum chain - post-quantum cryptography
AChainID ids.ID // Attestation chain - oracles, compute attestation
BChainID ids.ID // Bridge chain - cross-chain interop
TChainID ids.ID // Threshold chain - FHE, threshold crypto
MChainID ids.ID // MPC chain - threshold signing / bridge custody (consumes mpcvm library)
FChainID ids.ID // FHE chain - confidential compute / encrypted state (consumes mpcvm library)
ZChainID ids.ID // ZK chain - zero-knowledge proofs
GChainID ids.ID // Graph chain - dgraph
KChainID ids.ID // KMS chain - key management
@@ -109,8 +110,10 @@ func (r *ChainRegistry) MigrateChain(networkID uint32, chainName string, newChai
config.AChainID = newChainID
case "B", "bridge":
config.BChainID = newChainID
case "T", "threshold":
config.TChainID = newChainID
case "M", "mpc":
config.MChainID = newChainID
case "F", "fhe":
config.FChainID = newChainID
case "Z", "zk":
config.ZChainID = newChainID
case "D", "dex":
@@ -166,9 +169,14 @@ func (r *ChainRegistry) GetBChainID(networkID uint32) ids.ID {
return r.GetOrDefault(networkID).BChainID
}
// GetTChainID returns the T-chain ID for the given network.
func (r *ChainRegistry) GetTChainID(networkID uint32) ids.ID {
return r.GetOrDefault(networkID).TChainID
// GetMChainID returns the M-chain ID for the given network.
func (r *ChainRegistry) GetMChainID(networkID uint32) ids.ID {
return r.GetOrDefault(networkID).MChainID
}
// GetFChainID returns the F-chain ID for the given network.
func (r *ChainRegistry) GetFChainID(networkID uint32) ids.ID {
return r.GetOrDefault(networkID).FChainID
}
// GetZChainID returns the Z-chain ID for the given network.
@@ -192,7 +200,8 @@ func defaultMainnetConfig() *ChainConfig {
QChainID: ids.QChainID,
AChainID: ids.AChainID,
BChainID: ids.BChainID,
TChainID: ids.TChainID,
MChainID: ids.MChainID,
FChainID: ids.FChainID,
ZChainID: ids.ZChainID,
DChainID: ids.DChainID,
}
@@ -207,7 +216,8 @@ func defaultTestnetConfig() *ChainConfig {
QChainID: ids.QChainID,
AChainID: ids.AChainID,
BChainID: ids.BChainID,
TChainID: ids.TChainID,
MChainID: ids.MChainID,
FChainID: ids.FChainID,
ZChainID: ids.ZChainID,
DChainID: ids.DChainID,
}
@@ -222,7 +232,8 @@ func defaultDevnetConfig() *ChainConfig {
QChainID: ids.QChainID,
AChainID: ids.AChainID,
BChainID: ids.BChainID,
TChainID: ids.TChainID,
MChainID: ids.MChainID,
FChainID: ids.FChainID,
ZChainID: ids.ZChainID,
DChainID: ids.DChainID,
}
@@ -237,7 +248,8 @@ func defaultCustomConfig() *ChainConfig {
QChainID: ids.QChainID,
AChainID: ids.AChainID,
BChainID: ids.BChainID,
TChainID: ids.TChainID,
MChainID: ids.MChainID,
FChainID: ids.FChainID,
ZChainID: ids.ZChainID,
DChainID: ids.DChainID,
}
+2 -1
View File
@@ -73,7 +73,8 @@ var (
QChainID = ids.QChainID // Q-Chain: 11111111111111111111111111111111Q
AChainID = ids.AChainID // A-Chain: 11111111111111111111111111111111A
BChainID = ids.BChainID // B-Chain: 11111111111111111111111111111111B
TChainID = ids.TChainID // T-Chain: 11111111111111111111111111111111T
MChainID = ids.MChainID // M-Chain: 11111111111111111111111111111111M (MPC — threshold signing / bridge custody)
FChainID = ids.FChainID // F-Chain: 11111111111111111111111111111111F (FHE — confidential compute)
ZChainID = ids.ZChainID // Z-Chain: 11111111111111111111111111111111Z (Zero-knowledge)
GChainID = ids.GChainID // G-Chain: 11111111111111111111111111111111G (Graph/dgraph)
KChainID = ids.KChainID // K-Chain: 11111111111111111111111111111111K (KMS)
+8 -4
View File
@@ -18,7 +18,8 @@ const (
QuantumVMName = "quantumvm" // Q-Chain: Quantum-resistant security
AIVMName = "aivm" // A-Chain: AI Virtual Machine
BridgeVMName = "bridgevm" // B-Chain: Bridge/Cross-chain
ThresholdVMName = "thresholdvm" // T-Chain: Threshold signatures
MPCVMName = "mpcvm" // M-Chain: MPC threshold signing / bridge custody (LP-7100)
FHEVMName = "fhevm" // F-Chain: FHE confidential compute / encrypted state (LP-8200)
KeyVMName = "keyvm" // K-Chain: Key Management
ZKVMName = "zkvm" // Z-Chain: Zero-Knowledge proofs
GraphVMName = "graphvm" // G-Chain: GraphQL/DGraph unified data layer
@@ -40,7 +41,8 @@ var (
AttestationVMID = ids.ID{'a', 'i', 'v', 'm'} // A-Chain: Attestation/AI VM
AIVMID = AttestationVMID // Alias for AttestationVMID
BridgeVMID = ids.ID{'b', 'r', 'i', 'd', 'g', 'e', 'v', 'm'}
ThresholdVMID = ids.ID{'t', 'h', 'r', 'e', 's', 'h', 'o', 'l', 'd', 'v', 'm'}
MPCVMID = ids.ID{'m', 'p', 'c', 'v', 'm'} // M-Chain: MPC threshold signing / bridge custody
FHEVMID = ids.ID{'f', 'h', 'e', 'v', 'm'} // F-Chain: FHE confidential compute
KeyVMID = ids.ID{'k', 'e', 'y', 'v', 'm'} // K-Chain: Key Management
KVMID = KeyVMID // Alias for KeyVMID
ZKVMID = ids.ID{'z', 'k', 'v', 'm'}
@@ -107,8 +109,10 @@ func VMName(vmID ids.ID) string {
return AIVMName
case BridgeVMID:
return BridgeVMName
case ThresholdVMID:
return ThresholdVMName
case MPCVMID:
return MPCVMName
case FHEVMID:
return FHEVMName
case KeyVMID:
return KeyVMName
case ZKVMID:
+29
View File
@@ -0,0 +1,29 @@
BSD 3-Clause License
Copyright (c) 2024, Lux Partners Limited
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+8
View File
@@ -0,0 +1,8 @@
# Licensing
This repository is licensed under the **BSD-3-Clause License** (see [LICENSE](LICENSE)). It belongs to the **public** tier of the Lux three-tier IP strategy: anyone may use, fork, or redistribute it, including for commercial purposes, subject to the BSD-3-Clause License terms.
For the canonical Lux IP and licensing strategy, see:
<https://github.com/luxfi/.github/blob/main/profile/README.md>
For commercial inquiries that go beyond the public tier (e.g. private moat acceleration kernels, Eco-tier patent-protected primitives outside Authorized Networks), contact `licensing@lux.network`.
+36
View File
@@ -0,0 +1,36 @@
# Hanzo Container
## Overview
Go module: github.com/luxfi/container
## Tech Stack
- **Language**: Go
## Build & Run
```bash
go build ./...
go test ./...
```
## Structure
```
container/
LICENSE
bimap/
bloom/
buffer/
cache.go
container.go
go.mod
go.sum
heap/
iterator/
linked/
linkedhashmap/
maybe/
pool/
sampler/
```
## Key Files
- `go.mod` -- Go module definition
+3 -3
View File
@@ -50,7 +50,7 @@ func (c *LRUCache[K, V]) Put(key K, value V) {
elem.Value.(*cacheEntry[K, V]).value = value
return
}
if len(c.elements) >= c.capacity {
back := c.lru.Back()
if back != nil {
@@ -58,7 +58,7 @@ func (c *LRUCache[K, V]) Put(key K, value V) {
delete(c.elements, back.Value.(*cacheEntry[K, V]).key)
}
}
elem := c.lru.PushFront(&cacheEntry[K, V]{key, value})
c.elements[key] = elem
}
@@ -76,4 +76,4 @@ func (c *LRUCache[K, V]) Len() int {
func (c *LRUCache[K, V]) Evict(key K) {
c.Delete(key)
}
}
+1 -1
View File
@@ -31,4 +31,4 @@ type Cache[K comparable, V any] interface {
Delete(K)
Len() int
Evict(K)
}
}
+2
View File
@@ -1,3 +1,5 @@
<p align="center"><img src=".github/hero.svg" alt="crypto" width="880"></p>
# Lux Crypto
Cryptographic primitives for the Lux Network -- post-quantum signatures, key encapsulation, BLS aggregation, threshold signing, ring signatures, and EVM-compatible secp256k1.
+29
View File
@@ -0,0 +1,29 @@
BSD 3-Clause License
Copyright (c) 2024, Lux Partners Limited
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+8
View File
@@ -0,0 +1,8 @@
# Licensing
This repository is licensed under the **BSD-3-Clause License** (see [LICENSE](LICENSE)). It belongs to the **public** tier of the Lux three-tier IP strategy: anyone may use, fork, or redistribute it, including for commercial purposes, subject to the BSD-3-Clause License terms.
For the canonical Lux IP and licensing strategy, see:
<https://github.com/luxfi/.github/blob/main/profile/README.md>
For commercial inquiries that go beyond the public tier (e.g. private moat acceleration kernels, Eco-tier patent-protected primitives outside Authorized Networks), contact `licensing@lux.network`.
+30
View File
@@ -0,0 +1,30 @@
# Hanzo Formatting
## Overview
Go module: github.com/luxfi/formatting
## Tech Stack
- **Language**: Go
## Build & Run
```bash
go build ./...
go test ./...
```
## Structure
```
formatting/
LICENSE
encoding.go
encoding_benchmark_test.go
encoding_test.go
go.mod
go.sum
int_format.go
int_format_test.go
prefixed_stringer.go
```
## Key Files
- `go.mod` -- Go module definition
+3 -3
View File
@@ -11,7 +11,7 @@ import (
"math"
"strings"
hashing "github.com/luxfi/crypto/hash"
"github.com/luxfi/crypto/hash"
)
const (
@@ -107,7 +107,7 @@ func Encode(encoding Encoding, bytes []byte) (string, error) {
}
checked := make([]byte, bytesLen+checksumLen)
copy(checked, bytes)
copy(checked[len(bytes):], hashing.Checksum(bytes, checksumLen))
copy(checked[len(bytes):], hash.Checksum(bytes, checksumLen))
bytes = checked
}
@@ -163,7 +163,7 @@ func Decode(encoding Encoding, str string) ([]byte, error) {
// Verify the checksum
rawBytes := decodedBytes[:len(decodedBytes)-checksumLen]
checksum := decodedBytes[len(decodedBytes)-checksumLen:]
if !bytes.Equal(checksum, hashing.Checksum(rawBytes, checksumLen)) {
if !bytes.Equal(checksum, hash.Checksum(rawBytes, checksumLen)) {
return nil, errBadChecksum
}
decodedBytes = rawBytes
+29
View File
@@ -0,0 +1,29 @@
BSD 3-Clause License
Copyright (c) 2024, Lux Partners Limited
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+8
View File
@@ -0,0 +1,8 @@
# Licensing
This repository is licensed under the **BSD-3-Clause License** (see [LICENSE](LICENSE)). It belongs to the **public** tier of the Lux three-tier IP strategy: anyone may use, fork, or redistribute it, including for commercial purposes, subject to the BSD-3-Clause License terms.
For the canonical Lux IP and licensing strategy, see:
<https://github.com/luxfi/.github/blob/main/profile/README.md>
For commercial inquiries that go beyond the public tier (e.g. private moat acceleration kernels, Eco-tier patent-protected primitives outside Authorized Networks), contact `licensing@lux.network`.
+2
View File
@@ -1,3 +1,5 @@
<p align="center"><img src=".github/hero.svg" alt="go-bip32" width="880"></p>
# GO-BIP32
This repository contains a local copy of the original ``github.com/tyler-smith/go-bip32`` library.
-2
View File
@@ -263,9 +263,7 @@ func validateChildPublicKey(key []byte) error {
return nil
}
//
// Numerical
//
func uint32Bytes(i uint32) []byte {
bytes := make([]byte, 4)
binary.BigEndian.PutUint32(bytes, i)
+8
View File
@@ -135,3 +135,11 @@ fabric.properties
.idea/**/azureSettings.xml
# End of https://www.toptal.com/developers/gitignore/api/goland,go
AGENTS.md
CLAUDE.md
GEMINI.md
GROK.md
QWEN.md
+29
View File
@@ -0,0 +1,29 @@
BSD 3-Clause License
Copyright (c) 2024, Lux Partners Limited
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+8
View File
@@ -0,0 +1,8 @@
# Licensing
This repository is licensed under the **BSD-3-Clause License** (see [LICENSE](LICENSE)). It belongs to the **public** tier of the Lux three-tier IP strategy: anyone may use, fork, or redistribute it, including for commercial purposes, subject to the BSD-3-Clause License terms.
For the canonical Lux IP and licensing strategy, see:
<https://github.com/luxfi/.github/blob/main/profile/README.md>
For commercial inquiries that go beyond the public tier (e.g. private moat acceleration kernels, Eco-tier patent-protected primitives outside Authorized Networks), contact `licensing@lux.network`.
+42
View File
@@ -0,0 +1,42 @@
# AI Assistant Knowledge Base
**Last Updated**: $(date +%Y-%m-%d)
**Project**: $(basename "$REPO_PATH")
**Organization**: $(basename "$(dirname "$REPO_PATH")")
## Project Overview
This repository is part of the $(basename "$(dirname "$REPO_PATH")") organization.
## Essential Commands
### Development
```bash
# Add common commands here
```
## Architecture
## Key Technologies
## Development Workflow
## Context for All AI Assistants
This file (`LLM.md`) is symlinked as:
- `.AGENTS.md`
- `CLAUDE.md`
- `QWEN.md`
- `GEMINI.md`
All files reference the same knowledge base. Updates here propagate to all AI systems.
## Rules for AI Assistants
1. **ALWAYS** update LLM.md with significant discoveries
2. **NEVER** commit symlinked files (.AGENTS.md, CLAUDE.md, etc.) - they're in .gitignore
3. **NEVER** create random summary files - update THIS file
---
**Note**: This file serves as the single source of truth for all AI assistants working on this project.
+2
View File
@@ -1,3 +1,5 @@
<p align="center"><img src=".github/hero.svg" alt="go-bip39" width="880"></p>
# GO-BIP39
This repository contains a local copy of the original ``github.com/tyler-smith/go-bip39`` library.
+2
View File
@@ -1,3 +1,5 @@
<p align="center"><img src=".github/hero.svg" alt="ids" width="880"></p>
# Lux IDs Package
[![Go Reference](https://pkg.go.dev/badge/github.com/luxfi/ids.svg)](https://pkg.go.dev/github.com/luxfi/ids)
+29 -15
View File
@@ -13,9 +13,10 @@ package ids
// C-Chain: 11111111111111111111111111111111C (Contract/EVM)
// X-Chain: 11111111111111111111111111111111X (Exchange/DAG)
// Q-Chain: 11111111111111111111111111111111Q (Quantum)
// A-Chain: 11111111111111111111111111111111A (AI)
// A-Chain: 11111111111111111111111111111111A (AI/Attestation)
// B-Chain: 11111111111111111111111111111111B (Bridge)
// T-Chain: 11111111111111111111111111111111T (Threshold)
// M-Chain: 11111111111111111111111111111111M (MPC — threshold signing / bridge custody)
// F-Chain: 11111111111111111111111111111111F (FHE — confidential compute)
// Z-Chain: 11111111111111111111111111111111Z (Zero-knowledge)
// G-Chain: 11111111111111111111111111111111G (Graph/dgraph)
// K-Chain: 11111111111111111111111111111111K (KMS)
@@ -42,7 +43,8 @@ const (
QChainIDStr = nativeChainPrefix + "Q"
AChainIDStr = nativeChainPrefix + "A"
BChainIDStr = nativeChainPrefix + "B"
TChainIDStr = nativeChainPrefix + "T"
MChainIDStr = nativeChainPrefix + "M"
FChainIDStr = nativeChainPrefix + "F"
ZChainIDStr = nativeChainPrefix + "Z"
GChainIDStr = nativeChainPrefix + "G" // Coming soon
IChainIDStr = nativeChainPrefix + "I" // Identity - Coming soon
@@ -69,8 +71,11 @@ var (
// BChainID is the well-known B-Chain (Bridge) ID
BChainID ID
// TChainID is the well-known T-Chain (Threshold) ID
TChainID ID
// MChainID is the well-known M-Chain (MPC — threshold signing, bridge custody) ID
MChainID ID
// FChainID is the well-known F-Chain (FHE — confidential compute) ID
FChainID ID
// ZChainID is the well-known Z-Chain (Zero-knowledge) ID
ZChainID ID
@@ -96,7 +101,8 @@ func init() {
QChainID[nativeChainLetterPos] = 'Q'
AChainID[nativeChainLetterPos] = 'A'
BChainID[nativeChainLetterPos] = 'B'
TChainID[nativeChainLetterPos] = 'T'
MChainID[nativeChainLetterPos] = 'M'
FChainID[nativeChainLetterPos] = 'F'
ZChainID[nativeChainLetterPos] = 'Z'
GChainID[nativeChainLetterPos] = 'G'
IChainID[nativeChainLetterPos] = 'I'
@@ -138,8 +144,10 @@ func NativeChainString(id ID) string {
return AChainIDStr
case 'B':
return BChainIDStr
case 'T':
return TChainIDStr
case 'M':
return MChainIDStr
case 'F':
return FChainIDStr
case 'Z':
return ZChainIDStr
case 'G':
@@ -187,8 +195,10 @@ func NativeChainFromString(s string) (ID, bool) {
return AChainID, true
case 'B', 'b':
return BChainID, true
case 'T', 't':
return TChainID, true
case 'M', 'm':
return MChainID, true
case 'F', 'f':
return FChainID, true
case 'Z', 'z':
return ZChainID, true
case 'G', 'g':
@@ -227,8 +237,10 @@ func NativeChainFromString(s string) (ID, bool) {
return AChainID, true
case 'B':
return BChainID, true
case 'T':
return TChainID, true
case 'M':
return MChainID, true
case 'F':
return FChainID, true
case 'Z':
return ZChainID, true
case 'G':
@@ -256,7 +268,7 @@ func NativeChainAlias(id ID) string {
// AllNativeChainIDs returns all well-known native chain IDs.
func AllNativeChainIDs() []ID {
return []ID{PChainID, CChainID, XChainID, QChainID, AChainID, BChainID, TChainID, ZChainID, GChainID, IChainID, KChainID, DChainID}
return []ID{PChainID, CChainID, XChainID, QChainID, AChainID, BChainID, MChainID, FChainID, ZChainID, GChainID, IChainID, KChainID, DChainID}
}
// NativeChainIDFromLetter returns the chain ID for a given letter.
@@ -277,8 +289,10 @@ func NativeChainIDFromLetter(letter byte) (ID, bool) {
return AChainID, true
case 'B', 'b':
return BChainID, true
case 'T', 't':
return TChainID, true
case 'M', 'm':
return MChainID, true
case 'F', 'f':
return FChainID, true
case 'Z', 'z':
return ZChainID, true
case 'G', 'g':
+2 -2
View File
@@ -33,10 +33,10 @@ const (
// Allocation represents a P-chain genesis allocation
type Allocation struct {
// ETHAddr is the C-chain compatible address (0x...)
ETHAddr string `json:"evmAddr"`
ETHAddr string `json:"ethAddr"`
// LUXAddr is the P/X-chain address (P-lux1...)
LUXAddr string `json:"utxoAddr"`
LUXAddr string `json:"luxAddr"`
// InitialAmount is immediately available on X-chain (usually 0)
InitialAmount uint64 `json:"initialAmount"`
+5 -6
View File
@@ -24,7 +24,7 @@ import (
"github.com/luxfi/go-bip32"
"github.com/luxfi/go-bip39"
"github.com/luxfi/ids"
"github.com/luxfi/proto/p/signer"
"github.com/luxfi/protocol/p/signer"
luxtls "github.com/luxfi/tls"
"golang.org/x/crypto/sha3"
)
@@ -546,7 +546,7 @@ func deriveMnemonicKeyForPath(mnemonic string, account, index uint32) ([]byte, e
}
// m/44'/60' (coin type for LUX)
key, err = key.NewChildKey(bip32.FirstHardenedChild + CoinTypeEVM)
key, err = key.NewChildKey(bip32.FirstHardenedChild + LUXCoinType)
if err != nil {
return nil, fmt.Errorf("failed to derive coin type: %w", err)
}
@@ -572,9 +572,8 @@ func deriveMnemonicKeyForPath(mnemonic string, account, index uint32) ([]byte, e
return key.Key, nil
}
// CoinTypeEVM is the SLIP-0044 coin_type for EVM chains (60', shared with
// Ethereum). C-Chain and any non-Lux L1 EVM derive under this tree.
const CoinTypeEVM = 60
// LUXCoinType is the BIP-44 coin type for LUX (60' = standard Ethereum)
const LUXCoinType = 60
// deriveMnemonicKey derives an EC private key from mnemonic using BIP44 path m/44'/60'/0'/0/{index}
func deriveMnemonicKey(mnemonic string, accountIndex uint32) ([]byte, error) {
@@ -597,7 +596,7 @@ func deriveMnemonicKey(mnemonic string, accountIndex uint32) ([]byte, error) {
}
// m/44'/60' (coin type for LUX)
key, err = key.NewChildKey(bip32.FirstHardenedChild + CoinTypeEVM)
key, err = key.NewChildKey(bip32.FirstHardenedChild + LUXCoinType)
if err != nil {
return nil, fmt.Errorf("failed to derive coin type: %w", err)
}
-156
View File
@@ -1,156 +0,0 @@
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
// zap.go — load a BIP-39 mnemonic from Liquid KMS over native ZAP.
//
// This file is the one canonical path every Lux-derived service uses
// to resolve the bootstrap mnemonic: luxd, netrunner, lux/cli, and any
// descending L1's bootstrap (liquidity/network-bootstrap, hanzo-bootstrap,
// zoo-bootstrap, …). No file mount, no projected volume, no per-consumer
// copy of the env-vs-KMS precedence chain.
//
// Separation of concerns:
//
// - zap (the protocol) → github.com/zap-proto/zap
// - luxfi/kms (secret store) → opaque Get/Put/List over ZAP
// - luxfi/keys (THIS PACKAGE) → BIP-39, BIP44 derivation, mnemonic
// semantics. Composes luxfi/kms with
// bip39 validation. KMS knows nothing
// about mnemonics; this layer does.
//
// Precedence (env wins; KMS is the production fallback):
//
// 1. MNEMONIC env var local dev + CI test seam
// 2. KMS at (addr, env, path) native ZAP from Liquid KMS
package keys
import (
"context"
"errors"
"fmt"
"os"
"strings"
bip39 "github.com/luxfi/go-bip39"
"github.com/luxfi/kms/pkg/envelope"
"github.com/luxfi/kms/pkg/zapclient"
)
// MnemonicReader is the minimum surface LoadMnemonicFromKMS needs from
// a KMS client. The real *zapclient.Client satisfies it; tests inject
// fakes via the dialKMS seam below.
type MnemonicReader interface {
GetAt(ctx context.Context, path, name, env string) (string, error)
Close()
}
// dialKMS is the production seam. Tests override to inject a fake
// without touching the network.
//
// identity is REQUIRED for the production path (the KMS server's
// consensus-auth gate rejects envelopes without a signed identity).
// Passing nil dials anonymously — accepted only by legacy KMS peers
// that have explicitly disabled the auth gate (dev / loopback only).
var dialKMS = func(ctx context.Context, addr string, identity *ServiceIdentity) (MnemonicReader, error) {
cfg := zapclient.Config{PeerAddr: addr}
if identity != nil {
cfg.IdentityHeader = envelope.IdentityHeader{
NodeID: identity.NodeID,
FullDigest: identity.FullDigest,
ServicePath: identity.ServicePath,
PublicKey: identity.PublicKey,
}
cfg.Signer = identity
}
c, err := zapclient.DialWithConfig(ctx, cfg)
if err != nil {
return nil, err
}
return c, nil
}
// LoadMnemonic returns the BIP-39 mnemonic for the calling service.
// MNEMONIC env wins when set; otherwise dials KMS over native ZAP at
// `addr` and reads the secret at `path` under `env`.
//
// ctx cancellable context
// addr KMS host:port (e.g. "kms.internal.svc:9999")
// env KMS env scope ("mainnet" | "testnet" | "devnet")
// path KMS secret path (e.g. "/mnemonic" or "/foo/master")
// identity ServiceIdentity to sign the secret-opcode envelope. May
// be nil for legacy peers and the env-win short-circuit
// (the KMS dial is never reached); required for production
// peers whose consensus-auth gate is engaged.
//
// Returns the validated BIP-39 phrase. Caller is responsible for
// keeping it on the goroutine stack and not logging / persisting it.
func LoadMnemonic(ctx context.Context, addr, env, path string, identity *ServiceIdentity) (string, error) {
if e := strings.TrimSpace(os.Getenv("MNEMONIC")); e != "" {
if !bip39.IsMnemonicValid(e) {
return "", errors.New("MNEMONIC env is not a valid BIP-39 phrase")
}
return e, nil
}
return LoadMnemonicFromKMS(ctx, addr, env, path, identity)
}
// LoadMnemonicFromKMS is the production-only path (no MNEMONIC env
// short-circuit). Useful when a caller wants to force the KMS read
// regardless of ambient env — e.g., a regression test pinning the
// production code path.
//
// identity is the *ServiceIdentity threaded into the ZAP dial: its
// IdentityHeader becomes envelope.Identity and the same identity
// signs every secret-opcode envelope. Pass nil only when dialling a
// legacy KMS peer that has not enabled the consensus-auth gate
// (dev / loopback fakes — see zap_test.go's withDial seam).
func LoadMnemonicFromKMS(ctx context.Context, addr, env, path string, identity *ServiceIdentity) (string, error) {
if addr == "" {
return "", errors.New("keys.LoadMnemonicFromKMS: KMS addr is required")
}
if env == "" {
return "", errors.New("keys.LoadMnemonicFromKMS: KMS env is required")
}
if path == "" {
return "", errors.New("keys.LoadMnemonicFromKMS: KMS path is required")
}
c, err := dialKMS(ctx, addr, identity)
if err != nil {
return "", fmt.Errorf("dial KMS %s: %w", addr, err)
}
defer c.Close()
dir, name := SplitSecretPath(path)
v, err := c.GetAt(ctx, dir, name, env)
if err != nil {
return "", fmt.Errorf("read mnemonic (path=%q name=%q env=%q): %w",
dir, name, env, err)
}
m := strings.TrimSpace(v)
if m == "" {
return "", fmt.Errorf("mnemonic at %q is empty", path)
}
if !bip39.IsMnemonicValid(m) {
return "", errors.New("mnemonic from KMS is not a valid BIP-39 phrase")
}
return m, nil
}
// SplitSecretPath turns "/foo/bar/baz" into ("/foo/bar/", "baz"). When
// there is no '/' or only a leading one, the directory is "" and the
// whole remainder is the name (e.g., "/mnemonic" → ("", "mnemonic")).
//
// Exported so every consumer can address secrets with the same
// convention — one and only one addressing scheme across mnemonic,
// staking keys, gas-payer keys, etc.
func SplitSecretPath(full string) (dir, name string) {
full = strings.TrimSpace(full)
full = strings.TrimPrefix(full, "/")
i := strings.LastIndex(full, "/")
if i < 0 {
return "", full
}
return "/" + full[:i+1], full[i+1:]
}
+8
View File
@@ -0,0 +1,8 @@
# Licensing
This repository is licensed under the **BSD-3-Clause License** (see [LICENSE](LICENSE)). It belongs to the **public** tier of the Lux three-tier IP strategy: anyone may use, fork, or redistribute it, including for commercial purposes, subject to the BSD-3-Clause License terms.
For the canonical Lux IP and licensing strategy, see:
<https://github.com/luxfi/.github/blob/main/profile/README.md>
For commercial inquiries that go beyond the public tier (e.g. private moat acceleration kernels, Eco-tier patent-protected primitives outside Authorized Networks), contact `licensing@lux.network`.
+2
View File
@@ -1,3 +1,5 @@
<p align="center"><img src=".github/hero.svg" alt="math" width="880"></p>
# Lux Math Library
A comprehensive mathematical utilities library for the Lux ecosystem.
+1 -1
View File
@@ -73,5 +73,5 @@ GOWORK=off go test ./...
* [`lattice/v7/ring`](https://github.com/luxfi/lattice/tree/main/ring)
— canonical Lattigo-derived NTT/Montgomery body that this module
delegates to.
* [`luxcpp/crypto/ringtail`](https://github.com/luxcpp/crypto/tree/main/ringtail)
* [`luxcpp/crypto/corona`](https://github.com/luxcpp/crypto/tree/main/corona)
— native C++ Montgomery NTT that Phase 6 mirrors as `math` C++.
+2
View File
@@ -1,3 +1,5 @@
<p align="center"><img src=".github/hero.svg" alt="metric" width="880"></p>
# Lux Metrics Library
`github.com/luxfi/metric` is the native metrics library for Lux. It produces a
+127
View File
@@ -0,0 +1,127 @@
// Copyright (C) 2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package metric
// Currying support for the *Vec families. Mirrors prometheus/client_golang's
// MetricVec.MustCurryWith: it binds a subset of label values up front and
// returns a vec that only needs the remaining labels. Implemented natively —
// the returned vec delegates to the base vec with the fixed labels merged in,
// so children are still created and registered on the base registry exactly
// once per full label set.
// mergeLabels returns a new Labels with b overlaid on a.
func mergeLabels(a, b Labels) Labels {
out := make(Labels, len(a)+len(b))
for k, v := range a {
out[k] = v
}
for k, v := range b {
out[k] = v
}
return out
}
// curryRemaining returns the label names from all that are not fixed, in
// declared order — these are the names WithLabelValues maps positionally.
func curryRemaining(all []string, fixed Labels) []string {
rem := make([]string, 0, len(all))
for _, n := range all {
if _, ok := fixed[n]; !ok {
rem = append(rem, n)
}
}
return rem
}
// --- counter ---
func (v *counterVec) MustCurryWith(labels Labels) CounterVec {
return &curriedCounterVec{base: v, fixed: cloneLabels(labels), remaining: curryRemaining(v.labelNames, labels)}
}
type curriedCounterVec struct {
base *counterVec
fixed Labels
remaining []string
}
func (c *curriedCounterVec) With(labels Labels) Counter {
return c.base.With(mergeLabels(c.fixed, labels))
}
func (c *curriedCounterVec) WithLabelValues(values ...string) Counter {
return c.base.With(mergeLabels(c.fixed, labelsFromValues(c.remaining, values)))
}
func (c *curriedCounterVec) MustCurryWith(labels Labels) CounterVec {
return c.base.MustCurryWith(mergeLabels(c.fixed, labels))
}
func (c *curriedCounterVec) Reset() { c.base.Reset() }
// --- gauge ---
func (v *gaugeVec) MustCurryWith(labels Labels) GaugeVec {
return &curriedGaugeVec{base: v, fixed: cloneLabels(labels), remaining: curryRemaining(v.labelNames, labels)}
}
type curriedGaugeVec struct {
base *gaugeVec
fixed Labels
remaining []string
}
func (c *curriedGaugeVec) With(labels Labels) Gauge {
return c.base.With(mergeLabels(c.fixed, labels))
}
func (c *curriedGaugeVec) WithLabelValues(values ...string) Gauge {
return c.base.With(mergeLabels(c.fixed, labelsFromValues(c.remaining, values)))
}
func (c *curriedGaugeVec) MustCurryWith(labels Labels) GaugeVec {
return c.base.MustCurryWith(mergeLabels(c.fixed, labels))
}
func (c *curriedGaugeVec) Reset() { c.base.Reset() }
// --- histogram ---
func (v *histogramVec) MustCurryWith(labels Labels) HistogramVec {
return &curriedHistogramVec{base: v, fixed: cloneLabels(labels), remaining: curryRemaining(v.labelNames, labels)}
}
type curriedHistogramVec struct {
base *histogramVec
fixed Labels
remaining []string
}
func (c *curriedHistogramVec) With(labels Labels) Histogram {
return c.base.With(mergeLabels(c.fixed, labels))
}
func (c *curriedHistogramVec) WithLabelValues(values ...string) Histogram {
return c.base.With(mergeLabels(c.fixed, labelsFromValues(c.remaining, values)))
}
func (c *curriedHistogramVec) MustCurryWith(labels Labels) HistogramVec {
return c.base.MustCurryWith(mergeLabels(c.fixed, labels))
}
func (c *curriedHistogramVec) Reset() { c.base.Reset() }
// --- summary ---
func (v *summaryVec) MustCurryWith(labels Labels) SummaryVec {
return &curriedSummaryVec{base: v, fixed: cloneLabels(labels), remaining: curryRemaining(v.labelNames, labels)}
}
type curriedSummaryVec struct {
base *summaryVec
fixed Labels
remaining []string
}
func (c *curriedSummaryVec) With(labels Labels) Summary {
return c.base.With(mergeLabels(c.fixed, labels))
}
func (c *curriedSummaryVec) WithLabelValues(values ...string) Summary {
return c.base.With(mergeLabels(c.fixed, labelsFromValues(c.remaining, values)))
}
func (c *curriedSummaryVec) MustCurryWith(labels Labels) SummaryVec {
return c.base.MustCurryWith(mergeLabels(c.fixed, labels))
}
func (c *curriedSummaryVec) Reset() { c.base.Reset() }
+4 -1
View File
@@ -6,6 +6,9 @@
package metric
func init() {
DefaultRegistry = NewNoOpRegistry()
r := NewNoOpRegistry()
DefaultRegistry = r
DefaultRegisterer = r
DefaultGatherer = r
defaultFactory = NewNoOpFactory()
}
+21
View File
@@ -39,3 +39,24 @@ type MetricFamilies = []*MetricFamily
func NewHTTPHandler(gatherer Gatherer, opts HandlerOpts) http.Handler {
return HandlerForWithOpts(gatherer, opts)
}
// InstrumentMetricHandler wraps handler so each scrape is observed on reg.
// It registers promhttp_metric_handler_requests_total (scrape count) and
// promhttp_metric_handler_requests_in_flight (concurrent scrapes) and updates
// them around each call. Mirrors prometheus/client_golang/promhttp.InstrumentMetricHandler.
func InstrumentMetricHandler(reg Registerer, handler http.Handler) http.Handler {
requests := reg.NewCounter(
"promhttp_metric_handler_requests_total",
"Total number of scrapes served.",
)
inFlight := reg.NewGauge(
"promhttp_metric_handler_requests_in_flight",
"Current number of scrapes being served.",
)
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
inFlight.Inc()
defer inFlight.Dec()
requests.Inc()
handler.ServeHTTP(w, r)
})
}
+4 -4
View File
@@ -47,10 +47,10 @@ const MsgMetricBatch uint16 = 2
// payload field. One batch per Export call (typically one Gather()
// snapshot per scrape interval).
type MetricBatch struct {
AppName string `json:"appName,omitempty"`
Version string `json:"version,omitempty"`
Resource map[string]string `json:"resource,omitempty"`
TimestampNs int64 `json:"timestampNs"`
AppName string `json:"appName,omitempty"`
Version string `json:"version,omitempty"`
Resource map[string]string `json:"resource,omitempty"`
TimestampNs int64 `json:"timestampNs"`
Families []MetricFamilyWire `json:"families"`
}
+4
View File
@@ -32,6 +32,10 @@ type HandlerOpts struct {
ErrorHandling HandlerErrorHandling
// ErrorLog is used when ErrorHandling is Continue.
ErrorLog interface{ Println(...any) }
// DisableCompression mirrors prometheus/client_golang/promhttp.HandlerOpts.
// The native handler serves uncompressed exposition, so this is always
// effectively honored; the field exists for source compatibility.
DisableCompression bool
}
// HTTPHandlerOpts is an alias for HandlerOpts for compatibility.
+13
View File
@@ -106,6 +106,7 @@ type Factory interface {
type CounterVec interface {
With(Labels) Counter
WithLabelValues(...string) Counter
MustCurryWith(Labels) CounterVec
Reset()
}
@@ -113,6 +114,7 @@ type CounterVec interface {
type GaugeVec interface {
With(Labels) Gauge
WithLabelValues(...string) Gauge
MustCurryWith(Labels) GaugeVec
Reset()
}
@@ -120,6 +122,7 @@ type GaugeVec interface {
type HistogramVec interface {
With(Labels) Histogram
WithLabelValues(...string) Histogram
MustCurryWith(Labels) HistogramVec
Reset()
}
@@ -127,6 +130,7 @@ type HistogramVec interface {
type SummaryVec interface {
With(Labels) Summary
WithLabelValues(...string) Summary
MustCurryWith(Labels) SummaryVec
Reset()
}
@@ -152,6 +156,15 @@ type Request interface {
// DefaultRegistry is the default in-process registry used by package-level helpers.
var DefaultRegistry Registry = NewRegistry()
// DefaultRegisterer is the default Registerer, backed by DefaultRegistry.
// Mirrors prometheus.DefaultRegisterer so call sites that pass a package-level
// registerer (e.g. metric.Register(metric.DefaultRegisterer)) keep working.
var DefaultRegisterer Registerer = DefaultRegistry
// DefaultGatherer is the default Gatherer, backed by DefaultRegistry.
// Mirrors prometheus.DefaultGatherer for HTTP exposition of the default registry.
var DefaultGatherer Gatherer = DefaultRegistry
// Global factory instance.
var defaultFactory Factory = NewFactoryWithRegistry(DefaultRegistry)
+19
View File
@@ -776,6 +776,25 @@ func (hpr *registry) MustRegister(cs ...Collector) {
}
}
// Unregister drops the collector's metric family (all label permutations) and
// frees its name for re-registration. Returns true if the name was registered.
// Mirrors prometheus.Registerer.Unregister.
func (hpr *registry) Unregister(c Collector) bool {
name, _, ok := collectorIdentity(c)
if !ok {
return false
}
hpr.mu.Lock()
defer hpr.mu.Unlock()
_, had := hpr.registered[name]
delete(hpr.registered, name)
delete(hpr.counters, name)
delete(hpr.gauges, name)
delete(hpr.histograms, name)
delete(hpr.summaries, name)
return had
}
// Gather returns metric families for all registered metrics.
func (hpr *registry) Gather() ([]*MetricFamily, error) {
hpr.mu.RLock()
+52 -13
View File
@@ -3,23 +3,57 @@
package metric
// noopCounter is a counter that does nothing.
type noopCounter struct{ value float64 }
import (
"math"
"sync/atomic"
)
func (n *noopCounter) Inc() { n.value++ }
func (n *noopCounter) Add(v float64) { n.value += v }
func (n *noopCounter) Get() float64 { return n.value }
// noopCounter is a counter that does nothing. value is stored as float64 bits
// in a uint64 and mutated via CAS so concurrent Inc/Add/Get is race-free —
// matches metricCounter's representation (metrics_impl.go).
type noopCounter struct{ value uint64 } // atomic float64 bits
// noopGauge is a gauge that does nothing.
type noopGauge struct{ value float64 }
func (n *noopCounter) Inc() { n.Add(1) }
func (n *noopGauge) Set(v float64) { n.value = v }
func (n *noopCounter) Add(v float64) {
for {
oldBits := atomic.LoadUint64(&n.value)
newBits := math.Float64bits(math.Float64frombits(oldBits) + v)
if atomic.CompareAndSwapUint64(&n.value, oldBits, newBits) {
return
}
}
}
func (n *noopCounter) Get() float64 {
return math.Float64frombits(atomic.LoadUint64(&n.value))
}
// noopGauge is a gauge that does nothing. value is stored as float64 bits in
// a uint64 and mutated via CAS so concurrent Set/Inc/Dec/Add/Sub/Get is
// race-free — matches metricGauge's representation (metrics_impl.go).
type noopGauge struct{ value uint64 } // atomic float64 bits
func (n *noopGauge) Set(v float64) { atomic.StoreUint64(&n.value, math.Float64bits(v)) }
func (n *noopGauge) SetToCurrentTime() {}
func (n *noopGauge) Inc() { n.value++ }
func (n *noopGauge) Dec() { n.value-- }
func (n *noopGauge) Add(v float64) { n.value += v }
func (n *noopGauge) Sub(v float64) { n.value -= v }
func (n *noopGauge) Get() float64 { return n.value }
func (n *noopGauge) Inc() { n.Add(1) }
func (n *noopGauge) Dec() { n.Add(-1) }
func (n *noopGauge) Add(v float64) {
for {
oldBits := atomic.LoadUint64(&n.value)
newBits := math.Float64bits(math.Float64frombits(oldBits) + v)
if atomic.CompareAndSwapUint64(&n.value, oldBits, newBits) {
return
}
}
}
func (n *noopGauge) Sub(v float64) { n.Add(-v) }
func (n *noopGauge) Get() float64 {
return math.Float64frombits(atomic.LoadUint64(&n.value))
}
// noopHistogram is a histogram that does nothing.
type noopHistogram struct{}
@@ -36,6 +70,7 @@ type noopCounterVec struct{}
func (n *noopCounterVec) With(Labels) Counter { return &noopCounter{} }
func (n *noopCounterVec) WithLabelValues(...string) Counter { return &noopCounter{} }
func (n *noopCounterVec) MustCurryWith(Labels) CounterVec { return n }
func (n *noopCounterVec) Reset() {}
// noopGaugeVec is a gauge vector that does nothing.
@@ -43,6 +78,7 @@ type noopGaugeVec struct{}
func (n *noopGaugeVec) With(Labels) Gauge { return &noopGauge{} }
func (n *noopGaugeVec) WithLabelValues(...string) Gauge { return &noopGauge{} }
func (n *noopGaugeVec) MustCurryWith(Labels) GaugeVec { return n }
func (n *noopGaugeVec) Reset() {}
// noopHistogramVec is a histogram vector that does nothing.
@@ -50,6 +86,7 @@ type noopHistogramVec struct{}
func (n *noopHistogramVec) With(Labels) Histogram { return &noopHistogram{} }
func (n *noopHistogramVec) WithLabelValues(...string) Histogram { return &noopHistogram{} }
func (n *noopHistogramVec) MustCurryWith(Labels) HistogramVec { return n }
func (n *noopHistogramVec) Reset() {}
// noopSummaryVec is a summary vector that does nothing.
@@ -57,6 +94,7 @@ type noopSummaryVec struct{}
func (n *noopSummaryVec) With(Labels) Summary { return &noopSummary{} }
func (n *noopSummaryVec) WithLabelValues(...string) Summary { return &noopSummary{} }
func (n *noopSummaryVec) MustCurryWith(Labels) SummaryVec { return n }
func (n *noopSummaryVec) Reset() {}
// noopRegistry provides a registry that gathers nothing.
@@ -66,6 +104,7 @@ func newNoopRegistry() Registry { return &noopRegistry{} }
func (r *noopRegistry) Register(_ Collector) error { return nil }
func (r *noopRegistry) MustRegister(_ ...Collector) {}
func (r *noopRegistry) Unregister(_ Collector) bool { return false }
func (r *noopRegistry) Gather() ([]*MetricFamily, error) {
return nil, nil
}
+6 -6
View File
@@ -254,7 +254,7 @@
</div>
<div class="legend-item">
<div class="legend-color" style="background: #f093fb;"></div>
<span>Ringtail</span>
<span>Corona</span>
</div>
<div class="legend-item">
<div class="legend-color" style="background: #4facfe;"></div>
@@ -340,7 +340,7 @@
<td>Level 5</td>
</tr>
<tr>
<td><strong>Ringtail (Hybrid)</strong></td>
<td><strong>Corona (Hybrid)</strong></td>
<td class="good">6,780</td>
<td class="good">0.15 ms</td>
<td class="good">99.97%</td>
@@ -435,7 +435,7 @@
tension: 0.4
},
{
label: 'Ringtail',
label: 'Corona',
data: Array(20).fill(0).map(() => 6500 + Math.random() * 500),
borderColor: '#f093fb',
tension: 0.4
@@ -456,7 +456,7 @@
const verificationChart = new Chart(verificationCtx, {
type: 'bar',
data: {
labels: ['BLS', 'ML-DSA', 'SLH-DSA', 'Ringtail', 'Verkle'],
labels: ['BLS', 'ML-DSA', 'SLH-DSA', 'Corona', 'Verkle'],
datasets: [{
label: 'Average Verification Time',
data: [0.08, 0.12, 0.47, 0.15, 0.05],
@@ -488,7 +488,7 @@
const gasChart = new Chart(gasCtx, {
type: 'doughnut',
data: {
labels: ['BLS', 'ML-DSA', 'SLH-DSA', 'Ringtail', 'Verkle', 'Other'],
labels: ['BLS', 'ML-DSA', 'SLH-DSA', 'Corona', 'Verkle', 'Other'],
datasets: [{
data: [25, 30, 10, 20, 10, 5],
backgroundColor: [
@@ -556,7 +556,7 @@
dataset.data.shift();
const base = dataset.label === 'BLS' ? 12000 :
dataset.label === 'ML-DSA' ? 8000 :
dataset.label === 'Ringtail' ? 6500 : 18000;
dataset.label === 'Corona' ? 6500 : 18000;
dataset.data.push(base + Math.random() * 1000);
});
signatureChart.update('none');
+6 -1
View File
@@ -8,6 +8,10 @@ type Registerer interface {
Metrics
Register(Collector) error
MustRegister(...Collector)
// Unregister removes a previously registered collector, freeing its name
// to be registered again. Returns true if it was present. Mirrors
// prometheus.Registerer.Unregister.
Unregister(Collector) bool
}
// Registry is a registerer that can also gather metric families.
@@ -41,8 +45,9 @@ type prefixRegisterer struct {
next Registerer
}
func (p *prefixRegisterer) Register(c Collector) error { return p.next.Register(c) }
func (p *prefixRegisterer) Register(c Collector) error { return p.next.Register(c) }
func (p *prefixRegisterer) MustRegister(cs ...Collector) { p.next.MustRegister(cs...) }
func (p *prefixRegisterer) Unregister(c Collector) bool { return p.next.Unregister(c) }
func (p *prefixRegisterer) NewCounter(name, help string) Counter {
return p.next.NewCounter(p.prefix+name, help)
}
+29
View File
@@ -0,0 +1,29 @@
BSD 3-Clause License
Copyright (c) 2024, Lux Partners Limited
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+8
View File
@@ -0,0 +1,8 @@
# Licensing
This repository is licensed under the **BSD-3-Clause License** (see [LICENSE](LICENSE)). It belongs to the **public** tier of the Lux three-tier IP strategy: anyone may use, fork, or redistribute it, including for commercial purposes, subject to the BSD-3-Clause License terms.
For the canonical Lux IP and licensing strategy, see:
<https://github.com/luxfi/.github/blob/main/profile/README.md>
For commercial inquiries that go beyond the public tier (e.g. private moat acceleration kernels, Eco-tier patent-protected primitives outside Authorized Networks), contact `licensing@lux.network`.
+29
View File
@@ -0,0 +1,29 @@
# Hanzo Tls
## Overview
Go module: github.com/luxfi/tls
## Tech Stack
- **Language**: Go
## Build & Run
```bash
go build ./...
go test ./...
```
## Structure
```
tls/
LICENSE
asn1.go
certificate.go
go.mod
go.sum
parse.go
tls.go
verify.go
```
## Key Files
- `go.mod` -- Go module definition
+178 -1
View File
@@ -1,4 +1,4 @@
# LLM.md - Hanzo Zap
# Hanzo Zap
## Overview
Go module: github.com/luxfi/zap
@@ -71,3 +71,180 @@ The QUIC transport adds, beyond what TCP+TLS gives:
See `quic/README.md` for details, defaults, deployment notes, and the
threat-model discussion.
### GPU-aware `transport/` subpackage (LP-203 zero-copy)
`github.com/luxfi/zap/transport` registers four implementations behind
the same `Transport` interface:
| Name | Build tags | Wire | GPU-resident bytes |
|-------------|-------------------------------------|----------------|--------------------|
| `default` | always | in-proc / TCP | no |
| `uma` | `cgo,linux,cuda` or `cgo,darwin` | NIC → managed | yes (cudaMallocManaged on linux, MTLBuffer on darwin) |
| `gpudirect` | `cgo,linux,gpudirect,cuda` | NIC → GPU VRAM | yes (DMA-buf MR) |
| `dpdk` | `cgo,linux,dpdk` + `pkg-config libdpdk` | NIC poll loop | no (CPU hugepage) |
`transport.Pick("")` selects in order `gpudirect > dpdk > uma >
default`. An operator override via `ZAP_TRANSPORT={default,uma,gpudirect,
dpdk}` returns ErrNotAvailable on a host that can't provide it (no
silent fallback when the operator was explicit).
Transports that can hand out GPU-resident buffers also implement
`BufferAllocator`:
```go
tr, _ := transport.Pick("uma")
alloc, ok := tr.(transport.BufferAllocator)
if !ok { /* default transport — heap allocate */ }
buf, _ := alloc.AllocBuffer(1 << 20) // 1 MiB managed slab
copy(buf.Bytes(), payload) // CPU writes
gpuKernel.launch(buf.DevicePtr(), 1<<20) // GPU reads same bytes
buf.Release() // back to slab pool
```
UMA pool is slab-allocated (`slabClasses = 256B, 1KiB, 4KiB, 16KiB,
64KiB, 1MiB`). Pool budget defaults to 4 GiB via `ZAP_UMA_POOL_BYTES`.
Sizes above 1 MiB take a cold-path `cudaMallocManaged` per call.
Boot emits one `slog.Info` line listing the chosen transport + probe
errors for the others. Silence with `ZAP_TRANSPORT_QUIET=1`.
Probe gaps are reported honestly: on a host with libibverbs but no
Mellanox HCA visible, `Pick("gpudirect")` returns
`missing prereq(s): [ibverbs-device raw-packet-cap nvidia-peermem]`
— never a fake success.
### Read-buffer pool (bufpool.go)
The TCP dispatch read path sources frame buffers from a quantized
`sync.Pool` (`64B / 256B / 1 KiB / 4 KiB / 16 KiB / 64 KiB`) via
`readMessageRawPooled` + refcounted `*bufRef`. `Message` carries an
optional `refs *bufRef`; `Message.Release()` decrements the refcount
and (at zero) returns the slab. `Message.Retain()` extends lifetime
across goroutine boundaries (e.g. when a Call response is delivered
via channel).
Wire format is byte-identical — only the buffer lifecycle changed.
Frames over 64 KiB fall back to a one-off heap alloc transparent to
the caller. `Message.Release()` on a Builder-built or unpooled
message is a no-op so existing callers keep working unmodified.
Read benchmarks (Apple M1 Max, `go test -bench='BenchmarkRead'
-benchtime=2s -count=2`):
| Size | Legacy ns/op | Pooled ns/op | Speedup | Allocs (legacy → pooled) |
|-------|--------------|--------------|---------|--------------------------|
| 64B | 38 | 40 | 1.0x | 2 → 1, 68 → 4 B/op |
| 256B | 68 | 43 | 1.6x | 2 → 1, 260 → 4 B/op |
| 1KB | 284 | 54 | 5.3x | 2 → 1, 1028 → 4 B/op |
| 4KB | 716 | 125 | 5.7x | 2 → 1, 4100 → 4 B/op |
| 16KB | 2244 | 355 | 6.3x | 2 → 1, 16388 → 4 B/op |
| 64KB | 4802 | 1142 | 4.2x | 2 → 1, 65540 → 4 B/op |
### Per-Call QUIC streams (TransportStreamer)
`Node.Call` over QUIC opens a fresh per-Call bidirectional stream via
`TransportStreamer.OpenCallStream` instead of serializing on the
shared control stream's `ctrlMu`. The server-side `acceptCallStreams`
goroutine spawns one handler goroutine per accepted stream so
concurrent Calls execute in parallel up to QUIC's stream limit (1024
by default).
Wire format on each per-Call stream is one length-prefixed ZAP frame
in each direction — no correlation header needed since each stream
carries exactly one request + one response.
Per-stream is the only Call path on QUIC; there is no fallback. The
control stream carries one-way Sends only.
End-to-end Call throughput (2 ms simulated handler):
| Concurrency | Per-stream | Reference | Speedup |
|-------------|-------------|-------------|---------|
| 1 worker | 3.19 ms/op | 3.17 ms/op | 1.0x |
| 10 workers | 1.24 ms/op | 3.01 ms/op | 2.4x |
| 100 workers | 1.06 ms/op | 3.01 ms/op | 2.85x |
Reference = handler latency on a serialized control-stream path
(pre-optimization measurement, retained for context). Per-stream
overlaps handlers in flight (peak inflight = 32 observed in
`TestQUICCallConcurrent`).
## Locality-adaptive Router (P1) — `router.go`, `interface.go`
The one-Send-API router that decouples WHO (`Destination`) from HOW
(`Interface`) from the message (`Payload`), so a caller never names a port or a
transport:
```go
r := zap.NewRouter()
r.Register(zap.NewNodeInterface(node)) // network fallback, Cost 10 (conn_pq wire)
inproc := zap.NewInProcessInterface() // Cost 0
inproc.Register("hanzo.o11y.traces", handler)
r.Register(inproc)
r.Send(ctx, "hanzo.o11y.traces", zap.Payload{Value: spans, Encode: encodeFn})
```
**The routing rule is a Cost table, not a caller branch.** `Send` picks the
cheapest `Interface` whose `CanReach(dst)` is true. When the destination lives in
THIS binary, `InProcessInterface` (Cost 0) wins and the handler is called with
the LIVE `Payload.Value` — zero serialization, zero copy, zero socket. `Encode`
(the zero-copy wire form) is invoked ONLY when a network `Interface` is chosen,
and only then. "If in-process skip TCP; else use the wire" falls out for free.
Three "transport"-ish names, kept orthogonal — do not conflate:
- `zap.Transport` (int enum `TransportTCP`/`TransportQUIC`) — which wire ONE Node
speaks. `NodeInterface` adapts such a Node into the router.
- `zap.Router` — this P1 locality-adaptive router over many `Interface`s.
- `github.com/luxfi/zap/transport` — the GPU-aware buffer subpackage (LP-203).
This is Reticulum's model made post-quantum + zero-copy: `Destination` is a
capability aspect today; the announce-routed PQ-identity destination hash
(multi-hop, mDNS/K8s-SRV discovery interfaces, UDP/QUIC/radio interfaces) refines
it in later phases WITHOUT moving the `Send` call site. `Node` is unchanged;
`NodeInterface` adapts it. First consumer: hanzo/cloud o11y telemetry (cloud's own
spans → in-process → ClickHouse sink, never the wire). See `router_test.go`
(7 tests: cost-preference, zero-serialize proof, fallback, no-route, sort,
re-register, non-encodable-refused).
## DexSession orchestration (`dexsession/`)
ZAP/Cap'n-Proto orchestration layer for the Lux DEX: capability transport +
promise pipelining that makes the native C↔D atomic settlement flow FEEL
synchronous, while the value boundary stays 100% native.
THE HARD INVARIANT: a ZAP response is NEVER sufficient to move money. Money
moves ONLY when C consumes a real D→C atomic export object, or D consumes a real
C→D object. The orchestration layer holds no StateDB/AtomicState/shared-memory
and has NO compile edge to `precompile/dex` or the EVM — it cannot import the C
Verify path and Verify cannot import it. It traffics in VALUES (`[20]byte`
accounts, `[32]byte` ids, `uint64` amounts) and bytes only:
- `prepareSwapIntent` → 0x9999 CALLDATA the USER signs (no funds reserved, no
enforceable amountOut — the floor rides MinAmountOut in the signed tx).
- `notifyIntent` → tells D to scan/import the C→D object the signed tx created;
cannot make a D match valid without a committed D block; returns a watch.
- `importSettlement` → finalization CALLDATA / a tx that POINTS at a `DExportRef`;
the chain re-reads the real D→C object and binds recipient/asset/amount to it.
`DExportRef` is a POINTER `{sourceChainID, sourceTxID, outputIndex, intentID}`,
NOT a DFillReceipt. "No capability moves money" is provable by EXHAUSTION: the
surface has no creditBalance/settleFill/overrideMatch/adminWithdraw method.
Capabilities (QuoteCap/IntentCap/WatchCap/SettlementCap/AdminCap) are unforgeable
tokens scoped to a bootstrap grant; a session cannot widen its own authority.
The 60-byte atomic object wire, `DeriveIntentID`, `DeriveUTXOID`, the V4 swap
selector (0xF3CD914C), and the Phase-A/B hookData tags are REPRODUCED as pure
values (not imported) and pinned byte-identical to `precompile/dex` +
`chains/dexvm` by `parity_test.go` — the "three homes" pattern, keeping the
transport module free of the EVM/cgo dep graph.
Promise pipelining (`promise.go`/`pipeline.go`): dependent calls dispatch the
instant their inputs resolve (`thenAsync`), so `quote→prepare→notify→
onCommitted→import` overlaps network latency; `SwapFlow`/`SwapFlowWithSender`
wire the declarative end-to-end path. The watch streams D's result (poll/push)
and resolves to a pointer on commit.
Build/test (pure Go): `CGO_ENABLED=0 GOWORK=off go test ./dexsession/`. The
critical adversarial proof is `TestRED_ZAP_ResponseAloneCannotMoveMoney`.
+38
View File
@@ -5,6 +5,44 @@ The canonical KV + transport for Lux is `luxfi/zap` (this repo) backed by
**and** cold ancient stores. PebbleDB / leveldb remain only as legacy
in-place upgrade paths.
## ZAP runtime consolidation (serialization core)
`github.com/zap-proto/go` is the canonical pure-stdlib runtime for the ZAP
wire format (`Parse`/`Message`/`Object`/`List` + `Builder`/`ObjectBuilder`/
`ListBuilder` + `Schema`). This repo (`luxfi/zap`) historically duplicated
that core in its own `zap.go`/`builder.go`/`schema.go` while adding the
network layer (Node mesh, QUIC/TCP, mDNS, PQ-TLS handshake, EVM types, the
`forward` HTTP-over-ZAP contract). The duplication is being collapsed so
there is ONE serialization runtime; `zap-proto/go` is the source of truth and
this repo will ride it.
**Status (first safe step landed):** the two cores are now proven to emit a
byte-identical data segment, and `zap-proto/go` has absorbed the two deltas
that previously diverged it from this repo's hardened core, so the readers now
agree on accept/reject for every input.
- **Wire proof.** `zap_crosswire_test.go` (here) and the file of the same
name in `zap-proto/go` pin the SAME `goldenV1Hex` constant. This repo's
`NewBuilderV1` emits it byte-for-byte; `zap-proto/go`'s default `NewBuilder`
emits it too. The ONLY header difference between this repo's default
(Version2) output and the v1 golden vector is byte 4 (the version). A live
cross-encode in a throwaway `go.work` joining both modules confirmed every
field round-trips both directions.
- **Deltas reconciled in `zap-proto/go`** (additive — no honest wire changed):
(1) it now accepts `Version1` AND `Version2` on read (was v1-only → it would
reject this repo's default v2 buffers); (2) its reader now applies the same
hardening this repo already had — unsigned-forward `Bytes` with
`absPos < HeaderSize` rejection, `Object`/`List` `absOffset < HeaderSize`
rejection, and `List` `length ≤ msgsize` clamp.
**Remaining steps** (see `zap-proto/go`'s LLM.md for the canonical list):
`schema.go` is already byte-identical and is the first alias target; the
reader/builder alias follows once `cmd/zapgen` lands (the read-buffer pool —
`Message.refs`/`Release`/`Retain` — is network-local and stays here); the
`StartObject` pre-reserve discipline is a documented, test-covered delta to
unify when the builder is aliased (adopt `zap-proto/go`'s pre-reserve — it is
strictly more reserving and byte-identical for every honest layout).
This doc enumerates every storage surface in luxd and the migration path
per backend.
+2
View File
@@ -1,3 +1,5 @@
<p align="center"><img src=".github/hero.svg" alt="zap" width="880"></p>
# ZAP
**Z**ero-copy **A**pplication **P**rotocol for Lux.
+202
View File
@@ -0,0 +1,202 @@
// Copyright (C) 2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package zap
import (
"sync"
"sync/atomic"
)
// Pooled read-buffer slabs.
//
// readMessageRaw used to do `make([]byte, length)` on every frame, which
// allocates GC-tracked memory each read. The buffer's lifetime extends
// from one Read call to wherever the caller releases the *Message —
// typically the end of a handler invocation, or the moment a Call
// goroutine consumes its response from the response channel.
//
// We quantize buffer sizes into six classes (64B, 256B, 1 KiB, 4 KiB,
// 16 KiB, 64 KiB) so each `sync.Pool` holds slabs of one shape only
// — that avoids the variable-size pool defeat where a 1 KiB Get
// returns a 64 KiB slab that's never reused at its real shape.
//
// A `bufRef` is a refcount wrapper around the slab. dispatchLoop
// increments the count when a *Message escapes to a channel (so the
// awaiting Call goroutine can Release) and decrements once after the
// handler/parse path is done. When refs hits zero the slab returns
// to its pool.
//
// Anything larger than the largest class (> 64 KiB) falls through to
// a one-off heap allocation with a nil pool — Release on those is a
// no-op so the GC reclaims them naturally. Three reasons we don't pool
// >64 KiB:
// - Large frames are rare on the steady-state hot path (most
// dispatchLoop frames are control + correlated RPCs).
// - sync.Pool fairness under contention degrades with slab size:
// keeping >64 KiB slabs hot can pin tens of MiB per process.
// - The 10 MiB MaxFrameSize ceiling already caps the worst case
// to a single heap allocation per giant frame.
// bufSizes lists the quantization in ascending order. Anything between
// classes rounds UP to the next class — that's the only way to keep
// a class invariant ("a 256B slab is exactly 256 bytes of cap").
var bufSizes = [...]int{64, 256, 1024, 4 * 1024, 16 * 1024, 64 * 1024}
// bufPools[i] holds slabs of bufSizes[i] capacity.
//
// Declared as a pointer-to-array so the package-level initializer
// doesn't copy a `sync.Pool` value (which contains sync.noCopy). The
// pools themselves never move after init.
var bufPools = func() *[len(bufSizes)]sync.Pool {
pools := new([len(bufSizes)]sync.Pool)
for i, sz := range bufSizes {
size := sz // capture before closure binding
pools[i].New = func() any {
b := make([]byte, size)
return &b
}
}
return pools
}()
// pickBufClass returns the pool index for a buffer of length n,
// or -1 if n exceeds the largest class.
//
// Branchless ladder beats binary search at this scale (6 classes) —
// the predicted compare-and-jump is one pipeline-friendly cycle on
// modern superscalars. We tested both; the ladder won by 3-4 ns.
func pickBufClass(n int) int {
switch {
case n <= bufSizes[0]:
return 0
case n <= bufSizes[1]:
return 1
case n <= bufSizes[2]:
return 2
case n <= bufSizes[3]:
return 3
case n <= bufSizes[4]:
return 4
case n <= bufSizes[5]:
return 5
default:
return -1
}
}
// bufRef is a refcounted slab. The refcount is set to 1 by getBuf
// and decremented by release. When it hits zero the underlying slab
// is returned to its pool (if any).
//
// One bufRef tracks one slab; we pool the bufRef struct itself via a
// dedicated pool so the refcount header doesn't churn the heap either.
//
// We hold the slab as *[]byte (not []byte) so release can Put the
// same pointer that Get returned — sync.Pool deduplicates on pointer
// identity. Putting a fresh-stack &buf forces a new heap alloc for
// the slice header on every release, which was a 24-byte/op overhead
// on the first iteration.
type bufRef struct {
// bufp is the pooled slice header. nil for over-class one-offs.
bufp *[]byte
// fallback holds the slab when bufp is nil (over-class). Sized
// exactly to n; reclaimed by GC.
fallback []byte
// n is the frame length the caller asked for (≤ len(buf)).
n int
// class is the index into bufPools, or -1 if buf was a one-off
// heap allocation (frame > largest pool class).
class int
// refs counts live references to buf. Starts at 1, +1 on retain,
// -1 on release. When it reaches 0 the slab returns to its pool.
refs atomic.Int32
}
// refPool pools the bufRef header itself. bufRefs are tiny (40 bytes
// on 64-bit) but they appear on every frame — pooling them takes them
// off the GC's plate the same way the slab pool removes the buffer.
var refPool = sync.Pool{
New: func() any { return new(bufRef) },
}
// getBuf returns a refcounted slab of at least n bytes, with refs=1.
//
// Caller must call release() (or attach the ref to a *Message which
// will release on its own teardown). Failure to release leaks the
// slab — sync.Pool will eventually reclaim it via its own GC hook,
// but the steady-state allocation savings depend on prompt release.
func getBuf(n int) *bufRef {
r := refPool.Get().(*bufRef)
r.n = n
r.refs.Store(1)
class := pickBufClass(n)
r.class = class
if class < 0 {
// Anything larger than the largest class: one-off heap alloc.
// Release returns the bufRef header to refPool but lets GC
// reclaim the buffer itself.
r.bufp = nil
r.fallback = make([]byte, n)
return r
}
r.bufp = bufPools[class].Get().(*[]byte)
r.fallback = nil
return r
}
// rawBuf returns the underlying slab as a slice of class capacity
// (or n bytes for over-class fallbacks). io.ReadFull writes into the
// first n bytes.
func (r *bufRef) rawBuf() []byte {
if r.bufp != nil {
return (*r.bufp)[:bufSizes[r.class]]
}
return r.fallback
}
// Bytes returns the slab sub-slice the caller asked for. The capacity
// is the pool class size; the length is the requested n. Treat as
// read-only after the underlying frame has been parsed.
func (r *bufRef) Bytes() []byte {
if r.bufp != nil {
return (*r.bufp)[:r.n]
}
return r.fallback[:r.n]
}
// retain increments the refcount. Called when a *Message constructed
// over this slab escapes from the dispatch loop to a goroutine that
// will release it later (e.g. a Call response delivered via channel).
func (r *bufRef) retain() {
r.refs.Add(1)
}
// release decrements the refcount. When it hits zero the slab is
// returned to its pool (or simply dropped to GC if it was an
// over-class one-off allocation).
//
// release on a nil bufRef is a no-op — Message's release field is
// nil when the message wasn't built over a pooled buffer, and we
// don't want callers to have to branch on that.
func (r *bufRef) release() {
if r == nil {
return
}
if r.refs.Add(-1) != 0 {
return
}
if r.bufp != nil {
bufPools[r.class].Put(r.bufp)
r.bufp = nil
}
r.fallback = nil
r.n = 0
r.class = 0
refPool.Put(r)
}
+150 -66
View File
@@ -6,6 +6,7 @@ package zap
import (
"encoding/binary"
"math"
"sync"
)
// Builder constructs ZAP messages.
@@ -94,27 +95,30 @@ type ObjectBuilder struct {
b *Builder
startPos int
dataSize int
offsets []offsetEntry
}
type offsetEntry struct {
fieldOffset int
targetPos int // positive = known position, negative = deferred write
data []byte // actual bytes to write (deferred text/bytes)
}
// StartObject starts building an object with the given data size.
func (b *Builder) StartObject(dataSize int) *ObjectBuilder {
// StartObject starts building an object with the given data size, RESERVING the
// full fixed section up front (zero-filled). Reserving eagerly means a variable
// field (SetBytes/SetText) can append its tail data immediately after the fixed
// section and patch its own relative pointer on the spot — there is no deferred
// offset list to record intentions and replay in Finish. Callers already build
// child objects/lists BEFORE StartObject (the writeXxxList-then-StartObject
// pattern), so the object's own Set* calls are the only buffer writes between
// StartObject and Finish; appending tail bytes immediately is safe and produces
// byte-identical wire.
func (b *Builder) StartObject(dataSize int) ObjectBuilder {
b.align(Alignment)
return &ObjectBuilder{
ob := ObjectBuilder{
b: b,
startPos: b.pos,
dataSize: dataSize,
}
ob.ensureField(dataSize)
return ob
}
// SetBool sets a bool field.
func (ob *ObjectBuilder) SetBool(fieldOffset int, v bool) {
func (ob ObjectBuilder) SetBool(fieldOffset int, v bool) {
if v {
ob.SetUint8(fieldOffset, 1)
} else {
@@ -123,88 +127,111 @@ func (ob *ObjectBuilder) SetBool(fieldOffset int, v bool) {
}
// SetUint8 sets a uint8 field.
func (ob *ObjectBuilder) SetUint8(fieldOffset int, v uint8) {
func (ob ObjectBuilder) SetUint8(fieldOffset int, v uint8) {
ob.ensureField(fieldOffset + 1)
ob.b.buf[ob.startPos+fieldOffset] = v
}
// SetUint16 sets a uint16 field.
func (ob *ObjectBuilder) SetUint16(fieldOffset int, v uint16) {
func (ob ObjectBuilder) SetUint16(fieldOffset int, v uint16) {
ob.ensureField(fieldOffset + 2)
binary.LittleEndian.PutUint16(ob.b.buf[ob.startPos+fieldOffset:], v)
}
// SetUint32 sets a uint32 field.
func (ob *ObjectBuilder) SetUint32(fieldOffset int, v uint32) {
func (ob ObjectBuilder) SetUint32(fieldOffset int, v uint32) {
ob.ensureField(fieldOffset + 4)
binary.LittleEndian.PutUint32(ob.b.buf[ob.startPos+fieldOffset:], v)
}
// SetUint64 sets a uint64 field.
func (ob *ObjectBuilder) SetUint64(fieldOffset int, v uint64) {
func (ob ObjectBuilder) SetUint64(fieldOffset int, v uint64) {
ob.ensureField(fieldOffset + 8)
binary.LittleEndian.PutUint64(ob.b.buf[ob.startPos+fieldOffset:], v)
}
// SetInt8 sets an int8 field.
func (ob *ObjectBuilder) SetInt8(fieldOffset int, v int8) {
func (ob ObjectBuilder) SetInt8(fieldOffset int, v int8) {
ob.SetUint8(fieldOffset, uint8(v))
}
// SetInt16 sets an int16 field.
func (ob *ObjectBuilder) SetInt16(fieldOffset int, v int16) {
func (ob ObjectBuilder) SetInt16(fieldOffset int, v int16) {
ob.SetUint16(fieldOffset, uint16(v))
}
// SetInt32 sets an int32 field.
func (ob *ObjectBuilder) SetInt32(fieldOffset int, v int32) {
func (ob ObjectBuilder) SetInt32(fieldOffset int, v int32) {
ob.SetUint32(fieldOffset, uint32(v))
}
// SetInt64 sets an int64 field.
func (ob *ObjectBuilder) SetInt64(fieldOffset int, v int64) {
func (ob ObjectBuilder) SetInt64(fieldOffset int, v int64) {
ob.SetUint64(fieldOffset, uint64(v))
}
// SetFloat32 sets a float32 field.
func (ob *ObjectBuilder) SetFloat32(fieldOffset int, v float32) {
func (ob ObjectBuilder) SetFloat32(fieldOffset int, v float32) {
ob.SetUint32(fieldOffset, float32bits(v))
}
// SetFloat64 sets a float64 field.
func (ob *ObjectBuilder) SetFloat64(fieldOffset int, v float64) {
func (ob ObjectBuilder) SetFloat64(fieldOffset int, v float64) {
ob.SetUint64(fieldOffset, float64bits(v))
}
// SetBytesFixed copies len(v) bytes inline at fieldOffset within the
// object's fixed payload. This is the generic-width counterpart of
// SetHash (32 bytes) and SetAddress (20 bytes); it covers any N>0
// inline byte-array slot (e.g., LP-201 SessionID [16]byte, LP-208
// QuasarWitness [96]byte).
//
// Unlike SetBytes (which writes a variable-length tail pointer
// {relOffset uint32, length uint32}), SetBytesFixed writes the bytes
// IN PLACE in the fixed payload. Use this when the schema declares a
// fixed-width byte field; use SetBytes when the schema declares a
// variable-length tail.
//
// Panics on len(v) == 0 are avoided: a zero-length argument is a
// no-op (the slot is left as the zero value).
func (ob ObjectBuilder) SetBytesFixed(fieldOffset int, v []byte) {
if len(v) == 0 {
return
}
ob.ensureField(fieldOffset + len(v))
copy(ob.b.buf[ob.startPos+fieldOffset:], v)
}
// SetText sets a text (string) field.
func (ob *ObjectBuilder) SetText(fieldOffset int, v string) {
func (ob ObjectBuilder) SetText(fieldOffset int, v string) {
ob.SetBytes(fieldOffset, []byte(v))
}
// SetBytes sets a bytes field.
// The data is written after the object's fixed section during Finish().
func (ob *ObjectBuilder) SetBytes(fieldOffset int, v []byte) {
ob.ensureField(fieldOffset + 8) // offset + length
func (ob ObjectBuilder) SetBytes(fieldOffset int, v []byte) {
if len(v) == 0 {
// Null pointer
// Null pointer (offset 0, length 0).
binary.LittleEndian.PutUint32(ob.b.buf[ob.startPos+fieldOffset:], 0)
binary.LittleEndian.PutUint32(ob.b.buf[ob.startPos+fieldOffset+4:], 0)
return
}
// The fixed section is fully reserved by StartObject, so b.pos is at the
// tail: append the data now and patch this field's (relOffset, length) pair
// immediately — no deferred offset list, no per-object slice allocation.
dataPos := ob.b.pos
ob.b.grow(len(v))
copy(ob.b.buf[ob.b.pos:ob.b.pos+len(v)], v)
ob.b.pos += len(v)
// Store the data and length; the relative offset is patched in Finish()
ob.offsets = append(ob.offsets, offsetEntry{
fieldOffset: fieldOffset,
data: append([]byte(nil), v...), // copy the data
})
// Write the length now
binary.LittleEndian.PutUint32(ob.b.buf[ob.startPos+fieldOffset+4:], uint32(len(v)))
fieldAbsPos := ob.startPos + fieldOffset
relOffset := int32(dataPos - fieldAbsPos)
binary.LittleEndian.PutUint32(ob.b.buf[fieldAbsPos:], uint32(relOffset))
binary.LittleEndian.PutUint32(ob.b.buf[fieldAbsPos+4:], uint32(len(v)))
}
// SetObject sets a nested object field (by offset).
func (ob *ObjectBuilder) SetObject(fieldOffset int, objOffset int) {
func (ob ObjectBuilder) SetObject(fieldOffset int, objOffset int) {
ob.ensureField(fieldOffset + 4)
if objOffset == 0 {
@@ -218,7 +245,7 @@ func (ob *ObjectBuilder) SetObject(fieldOffset int, objOffset int) {
}
// SetList sets a list field.
func (ob *ObjectBuilder) SetList(fieldOffset int, listOffset int, length int) {
func (ob ObjectBuilder) SetList(fieldOffset int, listOffset int, length int) {
ob.ensureField(fieldOffset + 8)
if listOffset == 0 || length == 0 {
@@ -232,7 +259,7 @@ func (ob *ObjectBuilder) SetList(fieldOffset int, listOffset int, length int) {
binary.LittleEndian.PutUint32(ob.b.buf[ob.startPos+fieldOffset+4:], uint32(length))
}
func (ob *ObjectBuilder) ensureField(endOffset int) {
func (ob ObjectBuilder) ensureField(endOffset int) {
needed := ob.startPos + endOffset
if needed > ob.b.pos {
ob.b.grow(needed - ob.b.pos)
@@ -244,35 +271,38 @@ func (ob *ObjectBuilder) ensureField(endOffset int) {
}
}
// Finish finalizes the object and returns its offset.
// Writes deferred text/bytes data after the object's fixed section
// and patches relative offsets.
func (ob *ObjectBuilder) Finish() int {
// Ensure minimum size for fixed fields
ob.ensureField(ob.dataSize)
// Write deferred text/bytes data and patch relative offsets
for _, entry := range ob.offsets {
if entry.data == nil {
continue
}
// Write the data at current position (after fixed section)
dataPos := ob.b.pos
ob.b.grow(len(entry.data))
copy(ob.b.buf[ob.b.pos:ob.b.pos+len(entry.data)], entry.data)
ob.b.pos += len(entry.data)
// Patch the relative offset: relOffset = dataPos - fieldAbsPos
fieldAbsPos := ob.startPos + entry.fieldOffset
relOffset := int32(dataPos - fieldAbsPos)
binary.LittleEndian.PutUint32(ob.b.buf[fieldAbsPos:], uint32(relOffset))
}
// ReserveFixed extends the builder's write cursor so the object's
// entire fixed payload of dataSize bytes is materialized (zero-filled
// up to startPos+dataSize). Use this BEFORE writing any variable-
// length tail (list elements, deferred bytes) that should live AFTER
// the fixed section.
//
// Without this call, list elements or deferred-data writes interleave
// with the unreserved tail of the fixed section, producing incorrect
// wire bytes when later Set* calls patch fields that overlap with
// already-written variable data.
//
// Idempotent: a second call with the same dataSize is a no-op. A call
// with a smaller dataSize is also a no-op (the cursor never moves
// backwards).
//
// This is the exported counterpart to the internal ensureField helper.
// It is used by zapv1.WriteList to keep the parent's payload reserved
// before list elements are appended.
func (ob ObjectBuilder) ReserveFixed(dataSize int) {
ob.ensureField(dataSize)
}
// Finish finalizes the object and returns its offset. Every field (fixed and
// variable) is written eagerly by its Set* call — the fixed section is reserved
// in StartObject and SetBytes/SetText append their tail immediately — so there
// is nothing to replay here.
func (ob ObjectBuilder) Finish() int {
return ob.startPos
}
// FinishAsRoot finalizes and sets as the message root.
func (ob *ObjectBuilder) FinishAsRoot() int {
func (ob ObjectBuilder) FinishAsRoot() int {
offset := ob.Finish()
ob.b.rootOffset = offset
return offset
@@ -300,17 +330,22 @@ func (b *Builder) WriteText(s string) int {
type ListBuilder struct {
b *Builder
startPos int
elemSize int
count int
}
// StartList starts building a list.
func (b *Builder) StartList(elemSize int) *ListBuilder {
// StartList starts building a list. Returns a VALUE (not a heap pointer): the
// Add*/Finish methods take pointer receivers, and every caller binds the result
// to an addressable local (`lb := b.StartList(...)`), so Go auto-addresses the
// local and count mutations accumulate on it — no per-list heap allocation.
// Mirrors the value-typed StartObject. elemSize is unused (the stride is
// implicit in which Add* the caller invokes) and kept only as a documenting
// param.
func (b *Builder) StartList(elemSize int) ListBuilder {
_ = elemSize
b.align(Alignment)
return &ListBuilder{
return ListBuilder{
b: b,
startPos: b.pos,
elemSize: elemSize,
}
}
@@ -346,11 +381,60 @@ func (lb *ListBuilder) AddBytes(data []byte) {
lb.count += len(data)
}
// AddObjectPtr appends a 4-byte SIGNED relative pointer to an object at
// absolute position targetPos (pass 0 for a null element). This is the
// element kind for a list of out-of-line objects — the wire encoding of a
// "repeated message" field, where each fixed-stride slot points to a tailed
// object elsewhere in the buffer rather than inlining a fixed-size value.
//
// The relative offset is computed against THIS slot's position, so the
// reader resolves it the same way [Object.Object] does: absOffset = slotPos
// + int32(rel). Targets may precede the slot (negative rel) — the common
// case, since a builder writes the objects first and the pointer array
// after.
func (lb *ListBuilder) AddObjectPtr(targetPos int) {
lb.b.grow(4)
if targetPos == 0 {
binary.LittleEndian.PutUint32(lb.b.buf[lb.b.pos:], 0)
} else {
rel := int32(targetPos - lb.b.pos)
binary.LittleEndian.PutUint32(lb.b.buf[lb.b.pos:], uint32(rel))
}
lb.b.pos += 4
lb.count++
}
// Finish returns the list offset and length.
func (lb *ListBuilder) Finish() (offset int, length int) {
return lb.startPos, lb.count
}
// ---- Builder pool (write-side counterpart to the read bufpool) ----
// builderPool recycles Builders across messages. A pooled Builder keeps its
// grown backing array, so steady-state message construction allocates zero
// builder buffers. Byte-safety on reuse is unconditional: ensureField
// zero-fills every extended span and align zero-fills padding, so a reused
// (dirty) buffer emits bytes identical to a fresh one.
var builderPool = sync.Pool{
New: func() any { return NewBuilder(512) },
}
// GetBuilder returns a pooled Builder, reset and ready to write a Version2
// message. Callers MUST copy out the bytes returned by Finish (which aliases
// the Builder's buffer) before calling PutBuilder.
func GetBuilder() *Builder {
b := builderPool.Get().(*Builder)
b.Reset()
return b
}
// PutBuilder returns a Builder to the pool. The slice previously returned by
// b.Finish() must no longer be referenced (it aliases b's buffer).
func PutBuilder(b *Builder) {
builderPool.Put(b)
}
// Helper functions
func float32bits(f float32) uint32 {
+22
View File
@@ -0,0 +1,22 @@
// Copyright (C) 2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
// Package zap (v1) is deprecated for new schema authoring.
//
// New schemas MUST be authored via codegen (~/work/lux/zap/v2/codegen)
// which emits v1-equivalent fast paths from a declarative .zap source.
//
// Consumer dispatch for ad-hoc / dynamic schemas goes through the v2
// generic API (github.com/luxfi/zap/v1).
//
// The v1 hand-rolled code in this package remains for in-flight
// migration of legacy callers (luxfi/node/vms/platformvm/txs/zap_native,
// parts of luxfi/consensus/protocol/quasar, etc.); no new v1 schemas
// are accepted.
//
// The wire format is unchanged across v1 and v2 — this is a code-level
// decomplection of the authoring surface only. Existing v1 buffers
// remain parseable by both v1 and v2.
//
// See ~/work/lux/zap/v2/README.md for the canonical authoring path.
package zap
+10
View File
@@ -9,6 +9,7 @@ import (
"errors"
"fmt"
"io"
"net"
"runtime"
"time"
@@ -61,6 +62,15 @@ func (i *Initiator) Run(conn io.ReadWriter) (*Session, error) {
return nil, errors.New("zap-pq: initiator requires Local with private key")
}
// Bound the whole handshake by HandshakeTimeoutSec so a peer that
// connects and then stalls (or dribbles bytes) cannot pin this goroutine
// and FD indefinitely. Applies only when conn is a net.Conn; callers
// passing a bare io.ReadWriter remain responsible for their own timeout.
if nc, ok := conn.(net.Conn); ok {
_ = nc.SetReadDeadline(time.Now().Add(HandshakeTimeoutSec * time.Second))
defer func() { _ = nc.SetReadDeadline(time.Time{}) }()
}
suite := i.Suite
if suite == 0 {
suite = SuiteX25519MLKEM
+10
View File
@@ -9,6 +9,7 @@ import (
"errors"
"fmt"
"io"
"net"
"runtime"
"time"
@@ -62,6 +63,15 @@ func (rs *Responder) Run(conn io.ReadWriter) (*Session, error) {
return nil, errors.New("zap-pq: ReplayCache is required under StrictPQ/FIPS profile")
}
// Bound the whole handshake by HandshakeTimeoutSec so an unauthenticated
// peer that connects and then stalls cannot pin this goroutine and FD
// indefinitely (pre-auth slowloris). Applies only when conn is a net.Conn;
// bare io.ReadWriter callers own their own timeout.
if nc, ok := conn.(net.Conn); ok {
_ = nc.SetReadDeadline(time.Now().Add(HandshakeTimeoutSec * time.Second))
defer func() { _ = nc.SetReadDeadline(time.Time{}) }()
}
r := rs.Rand
if r == nil {
r = rand.Reader
+166
View File
@@ -0,0 +1,166 @@
// Copyright (C) 2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package zap
import (
"context"
"errors"
"fmt"
"sync"
)
// InProcessInterface is the cost-0 interface: it delivers to destinations that
// live in THIS address space by calling their handler directly. No serialization,
// no copy, no socket, no loopback TCP — a call is a call. It is the primitive
// behind "if the sink is in my own binary, don't touch the network."
//
// A Payload's live Value is handed straight to the LocalHandler; the payload's
// Encode (the wire form) is never invoked on this path, so an in-process delivery
// pays nothing for encoding. When sender and receiver are folded into one binary
// (the unified-cloud monolith), this is the interface that always wins the Cost
// race, and the ZAP wire never enters the picture.
type InProcessInterface struct {
mu sync.RWMutex
handlers map[Destination]LocalHandler
}
// NewInProcessInterface returns an empty in-process interface. Register the
// destinations this binary serves onto it, then Register it on a Router.
func NewInProcessInterface() *InProcessInterface {
return &InProcessInterface{handlers: make(map[Destination]LocalHandler)}
}
// Register binds a destination (capability aspect) to a local handler. Calling it
// again for the same dst replaces the handler (last writer wins) — a subsystem
// re-mounting its sink is idempotent. A nil handler unregisters the destination
// so CanReach falls through to the next (network) interface.
func (p *InProcessInterface) Register(dst Destination, h LocalHandler) {
p.mu.Lock()
defer p.mu.Unlock()
if h == nil {
delete(p.handlers, dst)
return
}
p.handlers[dst] = h
}
// Name identifies this interface.
func (p *InProcessInterface) Name() string { return "inprocess" }
// Cost is 0 — a same-address-space handoff is the cheapest possible delivery, so
// Router prefers it over every wire interface whenever the dst is local.
func (p *InProcessInterface) Cost() int { return 0 }
// CanReach reports whether dst has a handler registered in THIS process.
func (p *InProcessInterface) CanReach(dst Destination) bool {
p.mu.RLock()
_, ok := p.handlers[dst]
p.mu.RUnlock()
return ok
}
// Deliver dispatches the live payload to the local handler. The sender identity
// is left empty for a same-process call (the caller is us); a handler that needs
// provenance reads it from the Payload.Value it defines. Returns the handler's
// reply Payload (nil-Value ⇒ none).
func (p *InProcessInterface) Deliver(ctx context.Context, dst Destination, pl Payload) (Payload, error) {
p.mu.RLock()
h, ok := p.handlers[dst]
p.mu.RUnlock()
if !ok {
// CanReach guards this; a lost race (handler unregistered between the
// two calls) is reported honestly rather than silently dropped.
return Payload{}, fmt.Errorf("zap: in-process destination %q not registered", dst)
}
return h(ctx, "", pl)
}
// NodeInterface adapts the existing *Node (TCP/QUIC + conn_pq PQ session + mDNS
// discovery) to the Interface seam, WITHOUT changing Node. It is the network
// fallback: whenever a destination is not served in-process, Router routes here
// and the Node discovers/dials the peer and ships the ZAP wire Message.
//
// This is where the wire actually happens — and ONLY here. The Payload's live
// Value never crosses a socket; Deliver calls Payload.Encode() to materialize the
// zero-copy Message exactly once, at the moment a network hop is unavoidable.
//
// P1 scope: one-way delivery (Node.Send) — sufficient for telemetry and event
// fan-out, which is what folds first. Request/reply over the wire (Node.Call) and
// announce-driven multi-hop reachability are P2 refinements that slot in behind
// this same Interface without moving the Router.Send call site.
type NodeInterface struct {
node *Node
cost int
// connectedOnly restricts CanReach to peers with a live connection. Default
// (false) is optimistic: the interface is the network catch-all and lets
// Node.Send discover+dial on demand, surfacing a real error if the peer is
// genuinely unreachable rather than silently declining the route.
connectedOnly bool
}
// compile-time proof the interfaces satisfy the seam.
var (
_ Interface = (*InProcessInterface)(nil)
_ Interface = (*NodeInterface)(nil)
)
// DefaultNodeCost is the Cost of a LAN/cluster TCP hop — above in-process (0) and
// same-host IPC (1), below multi-hop routed (100+).
const DefaultNodeCost = 10
// NewNodeInterface wraps n as the network interface at DefaultNodeCost.
func NewNodeInterface(n *Node) *NodeInterface {
return &NodeInterface{node: n, cost: DefaultNodeCost}
}
// WithCost overrides the interface cost (e.g. a metered/slow link ranks higher).
func (i *NodeInterface) WithCost(c int) *NodeInterface { i.cost = c; return i }
// ConnectedOnly makes CanReach report true only for already-connected peers,
// turning the interface from an optimistic catch-all into a strict "route only
// where a session already exists" interface. Useful when a higher-cost interface
// should own first-contact.
func (i *NodeInterface) ConnectedOnly() *NodeInterface { i.connectedOnly = true; return i }
// Name identifies this interface.
func (i *NodeInterface) Name() string { return "node-tcp" }
// Cost ranks this interface (default DefaultNodeCost).
func (i *NodeInterface) Cost() int { return i.cost }
// CanReach reports whether the Node can deliver to dst. Optimistic by default (the
// network catch-all); strict when ConnectedOnly is set.
func (i *NodeInterface) CanReach(dst Destination) bool {
if i.node == nil {
return false
}
if !i.connectedOnly {
return true // catch-all: let Deliver discover+dial and error honestly
}
for _, p := range i.node.Peers() {
if p == string(dst) {
return true
}
}
return false
}
// ErrNotEncodable is returned when a network interface is asked to deliver a
// payload that carries no wire form (Encode == nil) — an in-process-only value
// that must not silently cross a socket.
var ErrNotEncodable = errors.New("zap: payload has no wire encoding (in-process only)")
// Deliver ships the payload to dst (peer ID) over the Node's ZAP wire. It
// materializes the wire Message lazily — the encode cost is paid here and nowhere
// else — then hands it to Node.Send. A nil Encode is a hard error, never a silent
// drop: an in-process-only payload reaching the wire is a routing bug.
func (i *NodeInterface) Deliver(ctx context.Context, dst Destination, p Payload) (Payload, error) {
if p.Encode == nil {
return Payload{}, ErrNotEncodable
}
if err := i.node.Send(ctx, string(dst), p.Encode()); err != nil {
return Payload{}, err
}
return Payload{}, nil // one-way delivery; no reply in P1
}
+221 -29
View File
@@ -12,6 +12,7 @@ import (
"io"
"log/slog"
"net"
"runtime/debug"
"sync"
"time"
@@ -36,13 +37,11 @@ type Node struct {
discovery *mdns.Discovery
// Network
listener net.Listener
transports map[string]TransportConn // peerID -> transport conn (QUIC path)
transClose func() error // closer for the QUIC listener
reqIDQuic uint32 // QUIC-path request-ID counter
reqIDQuicMu sync.Mutex
conns map[string]*Conn
connsMu sync.RWMutex
listener net.Listener
transports map[string]TransportConn // peerID -> transport conn (QUIC path)
transClose func() error // closer for the QUIC listener
conns map[string]*Conn
connsMu sync.RWMutex
// Handlers
handlers map[uint16]Handler
@@ -145,9 +144,18 @@ func (n *Node) Start() error {
n.discovery.OnPeer(n.handlePeerEvent)
// mDNS advertise/browse is BEST-EFFORT. It lets peers find each
// other zero-config on a LAN, but it is not required for service:
// the node is already accepting on its listener, and in-cluster
// peers reach it by address (Service DNS at the fixed port). On
// networks without multicast (most Kubernetes pod overlays,
// restricted hosts) discovery fails to start — the node MUST keep
// serving regardless. Log and continue rather than tearing down
// the listener.
if err := n.discovery.Start(); err != nil {
n.listener.Close()
return fmt.Errorf("failed to start discovery: %w", err)
n.logger.Warn("ZAP mDNS discovery unavailable; serving without it (peers reach this node by address)",
"nodeID", n.nodeID, "service", n.serviceType, "err", err)
n.discovery = nil
}
}
@@ -254,9 +262,11 @@ func (n *Node) Call(ctx context.Context, peerID string, msg *Message) (*Message,
conn.pendMu.Unlock()
}()
// Send wrapped request (one canonical encoder via WrapCorrelated).
// Send wrapped request via writeCorrelated — scatter-gather
// (header + body) so the body slice is never copied on the hot
// Call path.
conn.mu.Lock()
err = writeMessage(conn.conn, WrapCorrelated(reqID, ReqFlagReq, msg.Bytes()))
err = writeCorrelated(conn.conn, reqID, ReqFlagReq, msg.Bytes())
conn.mu.Unlock()
if err != nil {
return nil, err
@@ -409,6 +419,19 @@ func (n *Node) handleConn(netConn net.Conn) {
// Returns when the underlying conn errors (non-timeout) or ctx is
// cancelled. The caller is responsible for the per-connection
// cleanup (conns-map delete, log).
//
// Buffer lifecycle (post pool-aware read path):
// - Each iteration pulls one frame into a pooled *bufRef. The frame
// payload is sliced (no copy) into the Message returned by Parse.
// - For uncorrelated frames and Call requests (ReqFlagReq), the
// Message is consumed inside the iteration — handler runs, response
// is written, and the bufRef is released before the next read.
// - For Call responses (ReqFlagResp), the Message is handed off to a
// channel the Call goroutine awaits. We Retain the ref before the
// send and the receiver Releases when it consumes the response.
//
// The result is two allocations saved per dispatch: the body slab and
// the bufRef header (both pooled).
func (n *Node) dispatchLoop(netConn net.Conn, conn *Conn, peerID string) {
for {
select {
@@ -418,7 +441,7 @@ func (n *Node) dispatchLoop(netConn net.Conn, conn *Conn, peerID string) {
}
netConn.SetReadDeadline(time.Now().Add(1 * time.Second))
data, err := readMessageRaw(netConn)
ref, err := readMessageRawPooled(netConn)
if err != nil {
if errors.Is(err, io.EOF) {
return
@@ -429,23 +452,39 @@ func (n *Node) dispatchLoop(netConn net.Conn, conn *Conn, peerID string) {
n.logger.Debug("Read error", "peerID", peerID, "error", err)
return
}
data := ref.Bytes()
if reqID, flag, body, isCall := UnwrapCorrelated(data); isCall {
switch flag {
case ReqFlagResp:
if msg, err := Parse(body); err == nil {
// Hand the slab off to the awaiting Call goroutine.
// Retain holds the refcount across the channel; the
// receiver Releases on consumption. If nobody is
// listening we Release here to avoid leaking.
attachToMessage(msg, ref)
ref.retain() // matched by msg.Release() at Call site
conn.pendMu.Lock()
if ch, ok := conn.pending[reqID]; ok {
ch, ok := conn.pending[reqID]
if ok {
select {
case ch <- msg:
default:
ok = false // delivery failed
}
}
conn.pendMu.Unlock()
if !ok {
msg.Release() // drop the retain we just took
}
}
// Drop our local hold; the retain above keeps the slab
// alive for the channel consumer.
ref.release()
case ReqFlagReq:
msg, err := Parse(body)
if err != nil {
ref.release()
continue
}
msgType := msg.Flags() >> 8
@@ -453,22 +492,28 @@ func (n *Node) dispatchLoop(netConn net.Conn, conn *Conn, peerID string) {
handler, ok := n.handlers[msgType]
n.handlersMu.RUnlock()
if !ok {
ref.release()
continue
}
resp, herr := handler(n.ctx, peerID, msg)
resp, herr := n.safeHandle(handler, peerID, msgType, msg)
if herr != nil {
n.logger.Error("Handler error", "peerID", peerID, "msgType", msgType, "error", herr)
ref.release()
continue
}
if resp != nil {
conn.mu.Lock()
writeErr := writeMessage(netConn, WrapCorrelated(reqID, ReqFlagResp, resp.Bytes()))
writeErr := writeCorrelated(netConn, reqID, ReqFlagResp, resp.Bytes())
conn.mu.Unlock()
if writeErr != nil {
n.logger.Debug("Write error", "peerID", peerID, "error", writeErr)
ref.release()
return
}
}
ref.release()
default:
ref.release()
}
continue
}
@@ -476,6 +521,7 @@ func (n *Node) dispatchLoop(netConn net.Conn, conn *Conn, peerID string) {
// Uncorrelated message — direct handler dispatch.
msg, err := Parse(data)
if err != nil {
ref.release()
continue
}
msgType := msg.Flags() >> 8
@@ -483,11 +529,13 @@ func (n *Node) dispatchLoop(netConn net.Conn, conn *Conn, peerID string) {
handler, ok := n.handlers[msgType]
n.handlersMu.RUnlock()
if !ok {
ref.release()
continue
}
resp, herr := handler(n.ctx, peerID, msg)
resp, herr := n.safeHandle(handler, peerID, msgType, msg)
if herr != nil {
n.logger.Error("Handler error", "peerID", peerID, "msgType", msgType, "error", herr)
ref.release()
continue
}
if resp != nil {
@@ -496,9 +544,11 @@ func (n *Node) dispatchLoop(netConn net.Conn, conn *Conn, peerID string) {
conn.mu.Unlock()
if writeErr != nil {
n.logger.Debug("Write error", "peerID", peerID, "error", writeErr)
ref.release()
return
}
}
ref.release()
}
}
@@ -623,15 +673,19 @@ func (n *Node) getOrConnect(peerID string) (*Conn, error) {
default:
}
// Set read deadline so we can check for context cancellation
// Set read deadline so we can check for context cancellation.
// Buffer lifecycle mirrors dispatchLoop: response frames hand
// off the slab to the awaiting Call goroutine via Retain;
// everything else releases at end of iteration.
netConn.SetReadDeadline(time.Now().Add(1 * time.Second))
data, err := readMessageRaw(netConn)
ref, err := readMessageRawPooled(netConn)
if err != nil {
if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
continue
}
return
}
data := ref.Bytes()
// Check if this is a Call response (has 8-byte header with response flag)
if len(data) >= 8 {
@@ -641,15 +695,23 @@ func (n *Node) getOrConnect(peerID string) (*Conn, error) {
reqID := binary.LittleEndian.Uint32(data[0:4])
msg, err := Parse(data[8:])
if err == nil {
attachToMessage(msg, ref)
ref.retain() // matched by msg.Release() at Call site
conn.pendMu.Lock()
if ch, ok := conn.pending[reqID]; ok {
ch, ok := conn.pending[reqID]
if ok {
select {
case ch <- msg:
default:
ok = false
}
}
conn.pendMu.Unlock()
if !ok {
msg.Release()
}
}
ref.release()
continue
}
}
@@ -657,6 +719,7 @@ func (n *Node) getOrConnect(peerID string) (*Conn, error) {
// Regular message - use standard handler
msg, err := Parse(data)
if err != nil {
ref.release()
continue
}
@@ -666,22 +729,40 @@ func (n *Node) getOrConnect(peerID string) (*Conn, error) {
n.handlersMu.RUnlock()
if ok {
handler(n.ctx, peerID, msg)
// Guarded: a handler panic here must not kill this receive
// goroutine / the node. The error return is intentionally
// dropped — this is the fire-and-forget receive path for an
// outbound-dialed peer; safeHandle has already logged.
_, _ = n.safeHandle(handler, peerID, msgType, msg)
}
ref.release()
}
}()
return conn, nil
}
// ConnectDirect connects directly to a peer at the given address (bypasses mDNS).
// ConnectDirect connects directly to a peer at the given address
// (bypasses mDNS). Use ConnectDirectID when you need the handshake-
// learned peer NodeID back — e.g. to address subsequent Calls to a
// static peer whose advertised NodeID is only a placeholder.
func (n *Node) ConnectDirect(addr string) error {
_, err := n.ConnectDirectID(addr)
return err
}
// ConnectDirectID dials addr, performs the NodeID handshake, registers
// the connection, and returns the peer's learned NodeID. Idempotent: if
// a connection to that peer already exists it returns the existing
// peer's NodeID and drops the duplicate dial. (TCP transport; the QUIC
// path registers the peer internally and returns an empty id.)
func (n *Node) ConnectDirectID(addr string) (string, error) {
if n.transport == TransportQUIC {
return n.quicConnectDirect(n.ctx, addr)
return "", n.quicConnectDirect(n.ctx, addr)
}
netConn, err := net.DialTimeout("tcp", addr, 5*time.Second)
if err != nil {
return fmt.Errorf("failed to connect to %s: %w", addr, err)
return "", fmt.Errorf("failed to connect to %s: %w", addr, err)
}
if n.tlsCfg != nil {
netConn = tls.Client(netConn, n.tlsCfg)
@@ -690,19 +771,19 @@ func (n *Node) ConnectDirect(addr string) error {
// Send handshake.
if err := writeMessage(netConn, EncodeNodeIDHandshake(n.nodeID)); err != nil {
netConn.Close()
return err
return "", err
}
// Read handshake response.
data, err := readMessageRaw(netConn)
if err != nil {
netConn.Close()
return err
return "", err
}
peerID, ok := DecodeNodeIDHandshake(data)
if !ok {
netConn.Close()
return fmt.Errorf("invalid peer handshake")
return "", fmt.Errorf("invalid peer handshake")
}
conn := &Conn{
@@ -717,7 +798,7 @@ func (n *Node) ConnectDirect(addr string) error {
if _, ok := n.conns[peerID]; ok {
n.connsMu.Unlock()
netConn.Close()
return nil // Already connected, that's fine
return peerID, nil // Already connected, that's fine
}
n.conns[peerID] = conn
n.connsMu.Unlock()
@@ -741,7 +822,7 @@ func (n *Node) ConnectDirect(addr string) error {
n.dispatchLoop(netConn, conn, peerID)
}()
return nil
return peerID, nil
}
// Send sends a message over the connection.
@@ -758,11 +839,23 @@ func (c *Conn) Recv() (*Message, error) {
return readMessage(c.conn)
}
// Wire format: [4 bytes length][message bytes]
// Wire format: [4 bytes length][message bytes].
//
// writeMessage emits the frame using a single (*net.Buffers).WriteTo when
// w is a *net.TCPConn — that's one writev(2) syscall instead of two Write
// calls, and the body slice is referenced rather than copied. For other
// writers (TLS, pipes, tests) the path falls back to two sequential writes,
// which is correctness-equivalent.
func writeMessage(w io.Writer, data []byte) error {
var lenBuf [4]byte
binary.LittleEndian.PutUint32(lenBuf[:], uint32(len(data)))
if tc, ok := w.(*net.TCPConn); ok {
bufs := net.Buffers{lenBuf[:], data}
_, err := bufs.WriteTo(tc)
return err
}
if _, err := w.Write(lenBuf[:]); err != nil {
return err
}
@@ -770,6 +863,36 @@ func writeMessage(w io.Writer, data []byte) error {
return err
}
// writeCorrelated emits a Call request/response frame without copying
// the body buffer. Equivalent to writeMessage(w, WrapCorrelated(...)) but
// the 8-byte correlation header is sent as a separate IO vector — for
// TCPConn this is a single writev(2), for other writers it's three
// sequential writes (still no body copy). This replaces the per-Call
// allocation + copy WrapCorrelated() performs.
//
// reqID and flag are LE uint32 (matches WrapCorrelated wire layout).
func writeCorrelated(w io.Writer, reqID uint32, flag uint32, body []byte) error {
var hdr [12]byte
binary.LittleEndian.PutUint32(hdr[0:4], uint32(correlatedHeaderSize+len(body)))
binary.LittleEndian.PutUint32(hdr[4:8], reqID)
binary.LittleEndian.PutUint32(hdr[8:12], flag)
if tc, ok := w.(*net.TCPConn); ok {
bufs := net.Buffers{hdr[:], body}
_, err := bufs.WriteTo(tc)
return err
}
if _, err := w.Write(hdr[:]); err != nil {
return err
}
if len(body) == 0 {
return nil
}
_, err := w.Write(body)
return err
}
func readMessage(r io.Reader) (*Message, error) {
data, err := readMessageRaw(r)
if err != nil {
@@ -796,3 +919,72 @@ func readMessageRaw(r io.Reader) ([]byte, error) {
return data, nil
}
// readMessageRawPooled is the pool-aware variant of readMessageRaw.
//
// It reads one length-prefixed ZAP frame from r and returns a *bufRef
// whose Bytes() slice is the payload. The slab is sourced from a
// quantized sync.Pool — see bufpool.go for the size classes. Frames
// larger than the largest pool class fall back to a one-off heap
// allocation, transparent to the caller.
//
// Caller MUST eventually release() the *bufRef (directly, or by
// transferring ownership to a *Message via attachToMessage and then
// calling Message.Release()). Failure to release leaks the slab into
// sync.Pool's GC reclaim cycle — correct but defeats the pool.
//
// Wire format is byte-identical to readMessageRaw. The only difference
// is the buffer's lifecycle.
func readMessageRawPooled(r io.Reader) (*bufRef, error) {
var lenBuf [4]byte
if _, err := io.ReadFull(r, lenBuf[:]); err != nil {
return nil, err
}
length := binary.LittleEndian.Uint32(lenBuf[:])
if length > 10*1024*1024 { // 10MB max
return nil, errors.New("message too large")
}
ref := getBuf(int(length))
if _, err := io.ReadFull(r, ref.rawBuf()[:length]); err != nil {
ref.release()
return nil, err
}
return ref, nil
}
// attachToMessage stamps the *bufRef onto a *Message so that
// Message.Release() returns the slab to the pool. Used by dispatch
// loops that build a Message from a pooled buffer they're handing
// off to a response channel.
func attachToMessage(m *Message, ref *bufRef) {
if m != nil {
m.refs = ref
}
}
// safeHandle invokes a registered handler with a recover() guard. A handler
// is application code (e.g. forward.Serve's HTTP bridge) reachable directly
// from attacker-controlled bytes on the wire; a panic inside it — out-of-
// range index on a malformed envelope, a nil deref, a third-party library
// fault — must NEVER unwind into the per-connection dispatch goroutine and
// crash the whole node/process. On panic we log with the peer and message
// type and return an error so the dispatch loop drops that one connection;
// every other connection and the node itself survive.
//
// This is the single recover boundary for ALL handler dispatch in this
// node (correlated Call requests and uncorrelated messages alike). It is
// the one-and-only place a handler panic is contained.
func (n *Node) safeHandle(handler Handler, peerID string, msgType uint16, msg *Message) (resp *Message, err error) {
defer func() {
if r := recover(); r != nil {
n.logger.Error("ZAP handler panic recovered",
"peerID", peerID, "msgType", msgType, "panic", r,
"stack", string(debug.Stack()))
resp = nil
err = fmt.Errorf("handler panic (msgType=%d): %v", msgType, r)
}
}()
return handler(n.ctx, peerID, msg)
}
+16
View File
@@ -101,3 +101,19 @@ func UnwrapCorrelated(data []byte) (reqID uint32, flag uint32, body []byte, ok b
}
return reqID, flag, data[correlatedHeaderSize:], true
}
// makeCorrelatedFrame builds a single-slice correlation frame
// (header + body) for transports that take []byte (notably the QUIC
// TransportConn.Send shape). TCP callers should prefer writeCorrelated
// which avoids the body copy entirely via scatter-gather.
//
// One allocation per call. A pooled variant requires the transport
// interface to expose a "buffer consumed" callback so the slab can
// be returned safely; that's a cross-module change and is deferred.
func makeCorrelatedFrame(reqID uint32, flag uint32, body []byte) []byte {
buf := make([]byte, correlatedHeaderSize+len(body))
binary.LittleEndian.PutUint32(buf[0:4], reqID)
binary.LittleEndian.PutUint32(buf[4:8], flag)
copy(buf[correlatedHeaderSize:], body)
return buf
}
+143 -113
View File
@@ -5,12 +5,10 @@ package zap
import (
"context"
"encoding/binary"
"errors"
"fmt"
"io"
"net"
"sync"
"time"
"github.com/luxfi/mdns"
@@ -91,8 +89,22 @@ func (n *Node) onAcceptedTransportConn(peerID string, tc TransportConn) {
go n.serveTransportConn(peerID, tc)
}
// serveTransportConn is the QUIC equivalent of handleConn — it
// drives the read loop and dispatches incoming frames to handlers.
// serveTransportConn drives the read loops on a QUIC peer connection.
//
// Two parallel read loops run on each connection:
//
// 1. Control-stream loop (tc.Recv): handles one-way Sends only.
// 2. Per-Call stream accept loop (AcceptCallStream): handles every
// Call. Each accepted stream carries exactly one request + one
// response, so the dispatch goroutine reads, handles, writes,
// and closes the stream in one shot.
//
// The two loops are independent: each runs in its own goroutine, and
// both terminate when the connection closes or n.ctx is cancelled.
//
// The QUIC transport always implements TransportStreamer; if a
// connection somehow lacks it we close it — the per-stream Call path
// is the only Call path.
func (n *Node) serveTransportConn(peerID string, tc TransportConn) {
defer n.wg.Done()
defer func() {
@@ -105,6 +117,14 @@ func (n *Node) serveTransportConn(peerID string, tc TransportConn) {
n.logger.Info("Peer disconnected (QUIC)", "peerID", peerID)
}()
streamer, ok := tc.(TransportStreamer)
if !ok {
n.logger.Error("QUIC TransportConn missing TransportStreamer", "peerID", peerID)
return
}
n.wg.Add(1)
go n.acceptCallStreams(peerID, streamer)
for {
select {
case <-n.ctx.Done():
@@ -123,41 +143,81 @@ func (n *Node) serveTransportConn(peerID string, tc TransportConn) {
n.logger.Debug("QUIC Recv error", "peerID", peerID, "error", err)
return
}
n.dispatchFrame(peerID, data, tc)
// Control stream carries one-way Sends only. Calls ride
// per-Call streams.
msg, err := Parse(data)
if err != nil {
continue
}
n.invokeHandlerOneWay(peerID, msg)
}
}
// dispatchFrame routes a single ZAP frame to either the pending-Call
// channel (if it has a response correlation header) or to the
// registered handler. Mirrors the TCP path in node.go's handleConn.
func (n *Node) dispatchFrame(peerID string, data []byte, tc TransportConn) {
if len(data) >= 8 {
reqFlag := binary.LittleEndian.Uint32(data[4:8])
if reqFlag == ReqFlagResp {
reqID := binary.LittleEndian.Uint32(data[0:4])
msg, err := Parse(data[8:])
if err == nil {
n.routeQUICResponse(peerID, reqID, msg)
}
// acceptCallStreams runs in its own goroutine for each QUIC peer and
// processes inbound per-Call streams. Each stream gets its own handler
// goroutine so multiple in-flight Calls from the same peer execute
// concurrently — the headline parallelism win.
//
// Termination: AcceptCallStream returns a non-nil err when the
// connection is torn down. The loop exits without logging — the
// peer-disconnected line is already emitted by serveTransportConn's
// deferred close.
func (n *Node) acceptCallStreams(peerID string, s TransportStreamer) {
defer n.wg.Done()
for {
select {
case <-n.ctx.Done():
return
default:
}
if reqFlag == ReqFlagReq {
reqID := binary.LittleEndian.Uint32(data[0:4])
msg, err := Parse(data[8:])
if err != nil {
return
}
n.handleQUICRequest(peerID, tc, reqID, msg)
stream, err := s.AcceptCallStream(n.ctx)
if err != nil {
return
}
n.wg.Add(1)
go n.handleCallStream(peerID, stream)
}
}
// Regular one-way message.
msg, err := Parse(data)
// handleCallStream serves exactly one inbound per-Call stream:
// read one frame as the request, run the registered handler, and
// write the response back on the same stream. The stream is closed
// when this function returns.
func (n *Node) handleCallStream(peerID string, stream TransportStream) {
defer n.wg.Done()
defer stream.Close()
reqBytes, err := stream.ReadFrame()
if err != nil {
return
}
n.invokeHandlerOneWay(peerID, msg)
msg, err := Parse(reqBytes)
if err != nil {
return
}
msgType := msg.Flags() >> 8
n.handlersMu.RLock()
handler, ok := n.handlers[msgType]
n.handlersMu.RUnlock()
if !ok {
return
}
// Guarded: a handler panic on the QUIC per-Call stream must not crash
// the node any more than on the TCP path. safeHandle recovers, logs,
// and converts the panic to an error; the stream is then closed.
resp, herr := n.safeHandle(handler, peerID, msgType, msg)
if herr != nil {
n.logger.Error("Handler error (stream)", "peerID", peerID, "msgType", msgType, "error", herr)
return
}
if resp == nil {
return
}
if err := stream.WriteFrame(resp.Bytes()); err != nil {
n.logger.Debug("QUIC stream write error", "peerID", peerID, "error", err)
}
}
// invokeHandlerOneWay runs the registered handler and discards the
@@ -170,96 +230,77 @@ func (n *Node) invokeHandlerOneWay(peerID string, msg *Message) {
if !ok {
return
}
_, _ = handler(n.ctx, peerID, msg)
}
// handleQUICRequest dispatches a Call request and writes back the
// correlation-tagged response on the same transport conn.
func (n *Node) handleQUICRequest(peerID string, tc TransportConn, reqID uint32, msg *Message) {
msgType := msg.Flags() >> 8
n.handlersMu.RLock()
handler, ok := n.handlers[msgType]
n.handlersMu.RUnlock()
if !ok {
return
}
resp, err := handler(n.ctx, peerID, msg)
if err != nil {
n.logger.Error("Handler error", "peerID", peerID, "msgType", msgType, "error", err)
return
}
if resp == nil {
return
}
respBytes := resp.Bytes()
wrapped := make([]byte, len(respBytes)+8)
binary.LittleEndian.PutUint32(wrapped[0:4], reqID)
binary.LittleEndian.PutUint32(wrapped[4:8], ReqFlagResp)
copy(wrapped[8:], respBytes)
if err := tc.Send(wrapped); err != nil {
n.logger.Debug("QUIC Send error", "peerID", peerID, "error", err)
}
}
// quicPendingMu / quicPending hold the per-peer pending Call response
// channels for the QUIC transport. We keep them on the Node (not on
// each TransportConn) because TransportConn is an interface and we
// don't want to require implementors to carry the pending map.
var (
quicPendingMu sync.Mutex
quicPending = map[string]map[uint32]chan *Message{}
)
func (n *Node) routeQUICResponse(peerID string, reqID uint32, msg *Message) {
quicPendingMu.Lock()
defer quicPendingMu.Unlock()
if peerMap, ok := quicPending[n.nodeID+":"+peerID]; ok {
if ch, ok := peerMap[reqID]; ok {
select {
case ch <- msg:
default:
}
}
}
// Guarded: same recover boundary as every other dispatch path so a
// panicking one-way handler cannot kill the node.
_, _ = n.safeHandle(handler, peerID, msgType, msg)
}
// quicCall is the QUIC path for Node.Call.
//
// Every Call gets its own fresh bidirectional QUIC stream. The
// request goes out on the new stream, the response comes back on the
// same stream, and the stream is torn down. Concurrent Calls run in
// parallel up to the peer-advertised stream limit (1024 for ZAP's
// QUIC config) — no shared write mutex, no correlation header.
//
// QUIC's TransportConn always implements TransportStreamer; if it
// somehow does not, that is a programmer error and we surface it.
func (n *Node) quicCall(ctx context.Context, peerID string, msg *Message) (*Message, error) {
tc, err := n.getOrConnectQUIC(ctx, peerID)
if err != nil {
return nil, err
}
reqID := nextReqID(n)
respCh := make(chan *Message, 1)
key := n.nodeID + ":" + peerID
quicPendingMu.Lock()
if quicPending[key] == nil {
quicPending[key] = make(map[uint32]chan *Message)
streamer, ok := tc.(TransportStreamer)
if !ok {
return nil, fmt.Errorf("zap: QUIC TransportConn for %s missing TransportStreamer", peerID)
}
quicPending[key][reqID] = respCh
quicPendingMu.Unlock()
return n.quicCallStream(ctx, streamer, msg)
}
defer func() {
quicPendingMu.Lock()
delete(quicPending[key], reqID)
quicPendingMu.Unlock()
}()
// quicCallStream runs one Call on a fresh per-Call QUIC stream.
//
// The stream lifecycle is:
// 1. OpenCallStream — server peer's AcceptCallStream wakes.
// 2. WriteFrame(req) — single length-prefixed ZAP frame.
// 3. ReadFrame() — single length-prefixed ZAP frame (response).
// 4. Close — stream ID returns to the QUIC pool.
//
// No correlation header is needed on the wire: each stream carries
// exactly one request + one response. The response payload is the raw
// ZAP frame; we Parse it directly.
//
// Each stream gets cancelled if ctx fires before the response
// arrives; the QUIC layer will surface ErrStreamCanceled into
// ReadFrame so the goroutine exits cleanly.
func (n *Node) quicCallStream(ctx context.Context, s TransportStreamer, msg *Message) (*Message, error) {
stream, err := s.OpenCallStream(ctx)
if err != nil {
return nil, err
}
defer stream.Close()
orig := msg.Bytes()
wrapped := make([]byte, len(orig)+8)
binary.LittleEndian.PutUint32(wrapped[0:4], reqID)
binary.LittleEndian.PutUint32(wrapped[4:8], ReqFlagReq)
copy(wrapped[8:], orig)
if err := tc.Send(wrapped); err != nil {
if err := stream.WriteFrame(msg.Bytes()); err != nil {
return nil, err
}
// Read response on this stream. The QUIC layer respects the
// stream's own deadline; we set one from ctx so a cancelled
// context tears the read down.
type result struct {
resp []byte
err error
}
out := make(chan result, 1)
go func() {
resp, err := stream.ReadFrame()
out <- result{resp, err}
}()
select {
case resp := <-respCh:
return resp, nil
case r := <-out:
if r.err != nil {
return nil, r.err
}
return Parse(r.resp)
case <-ctx.Done():
return nil, ctx.Err()
}
@@ -343,14 +384,3 @@ func (n *Node) getOrConnectQUIC(ctx context.Context, peerID string) (TransportCo
go n.serveTransportConn(peerID, newTC)
return newTC, nil
}
// nextReqID returns the next request ID for a QUIC peer. We piggy-
// back on the existing Conn.reqID counter pattern but use a per-node
// global because the TransportConn interface is opaque about its own
// state. Atomicity is provided by the Node struct's reqIDMu.
func nextReqID(n *Node) uint32 {
n.reqIDQuicMu.Lock()
defer n.reqIDQuicMu.Unlock()
n.reqIDQuic++
return n.reqIDQuic
}
+128
View File
@@ -0,0 +1,128 @@
// Copyright (C) 2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package zap
import (
"context"
"errors"
"sort"
"sync"
)
// Router is ZAP's locality-adaptive router: ONE Send API over many interfaces.
//
// The complected thing in classic RPC is `Send(host:port, msg)` — WHO you are
// talking to, HOW the bytes travel, and WHERE the peer lives are braided into a
// single address. Router pulls them apart (Reticulum's model, made
// post-quantum and zero-copy):
//
// - Destination — WHO/WHAT (a capability aspect, later a PQ-identity hash).
// - Interface — HOW one hop travels (in-process memory, TCP, UDP, radio…).
// - Payload — the message, dual-form so each medium pays only for itself.
// - Router — routes: the cheapest Interface that can currently reach dst.
//
// "If in-process, skip TCP; else use the network" is therefore NOT a branch any
// caller writes — it is the Cost table (InProcess 0 < UDS 1 < TCP 10 < routed
// 100+). A caller only ever says:
//
// router.Send(ctx, dst, payload)
//
// and never names a port or a transport again. Router is distinct from the
// wire-level Transport enum (TransportTCP/TransportQUIC), which selects HOW one
// Node's socket speaks; a NodeInterface adapts such a Node into this router.
type Router struct {
mu sync.RWMutex
ifaces []Interface // kept sorted ascending by Cost()
}
// Destination names WHO/WHAT a message is for, decoupled from HOW it is reached.
// In P1 it is a dotted capability aspect ("hanzo.o11y.traces"); the announce-
// routed PQ-identity destination hash (multi-hop) refines this in the transport
// HIP WITHOUT changing this call site — that is the whole point of the seam.
type Destination string
// Payload is the value a Router delivers. It is deliberately dual-form so the
// interface the router picks pays only for what that medium needs:
//
// - Value is the LIVE native value. The in-process interface hands it to the
// local handler by pointer — zero serialization, zero copy, zero TCP. This
// is the "skip the wire entirely when we share an address space" fast path.
// - Encode lazily produces the zero-copy wire Message; a NETWORK interface
// calls it (and only then, so an in-process delivery never encodes). A nil
// Encode means the payload is in-process-only and cannot cross a wire.
type Payload struct {
Value any
Encode func() *Message
}
// LocalHandler consumes a Payload delivered IN-PROCESS. It receives the live
// Value (type-assert to the concrete type the aspect defines) and may return a
// reply Payload (nil Value ⇒ no reply). This is the handler an InProcessInterface
// dispatches to — distinct from Node's wire Handler, which decodes bytes first.
type LocalHandler func(ctx context.Context, from Destination, p Payload) (Payload, error)
// Interface moves a Payload one hop toward a Destination over a single medium.
// Interfaces are ranked by Cost; Router always prefers the cheapest one that
// can currently reach the destination. Implementations are the ONLY place a
// transport detail (a socket, a serial line, a memory handoff) lives.
type Interface interface {
// Name identifies the interface for logs/metrics ("inprocess", "node-tcp").
Name() string
// Cost ranks interfaces; lower is preferred. Convention: in-process 0,
// same-host IPC 1, LAN/cluster TCP 10, multi-hop routed 100+.
Cost() int
// CanReach reports whether this interface can deliver to dst RIGHT NOW
// (destination registered locally, peer connected/discovered, path known).
CanReach(dst Destination) bool
// Deliver hands the payload to dst over this medium and returns an optional
// reply (nil-Value Payload ⇒ none). It must not be called unless CanReach.
Deliver(ctx context.Context, dst Destination, p Payload) (Payload, error)
}
// ErrNoRoute is returned when no registered interface can reach the destination.
var ErrNoRoute = errors.New("zap: no interface can reach destination")
// NewRouter returns an empty Router. Register interfaces onto it; the cheapest
// reachable one wins per Send. A Router with no interfaces routes nothing (Send
// returns ErrNoRoute) — safe, never panics.
func NewRouter() *Router { return &Router{} }
// Register adds an interface, keeping the set sorted ascending by Cost so Send is
// a linear scan that stops at the first (cheapest) interface that CanReach. Ties
// keep registration order (stable), so an earlier-registered interface of equal
// cost is preferred.
func (t *Router) Register(i Interface) {
t.mu.Lock()
defer t.mu.Unlock()
t.ifaces = append(t.ifaces, i)
sort.SliceStable(t.ifaces, func(a, b int) bool {
return t.ifaces[a].Cost() < t.ifaces[b].Cost()
})
}
// Interfaces returns a snapshot of the registered interfaces, cheapest first.
func (t *Router) Interfaces() []Interface {
t.mu.RLock()
defer t.mu.RUnlock()
out := make([]Interface, len(t.ifaces))
copy(out, t.ifaces)
return out
}
// Send routes payload to dst over the cheapest interface that can reach it. It is
// the whole public contract: the caller expresses intent (dst + payload) and the
// locality decision (memory vs. wire) is the router's, made from the live Cost/
// CanReach table — never the caller's branch. Returns ErrNoRoute if nothing can
// currently reach dst.
func (t *Router) Send(ctx context.Context, dst Destination, p Payload) (Payload, error) {
t.mu.RLock()
ifaces := t.ifaces // slice header copy under lock; entries are immutable
t.mu.RUnlock()
for _, i := range ifaces {
if i.CanReach(dst) {
return i.Deliver(ctx, dst, p)
}
}
return Payload{}, ErrNoRoute
}
+37
View File
@@ -91,6 +91,43 @@ type TransportConn interface {
RemoteAddr() string
}
// TransportStreamer is an optional capability extension to
// TransportConn for transports that natively multiplex independent
// bidirectional streams (notably QUIC). When a TransportConn also
// implements TransportStreamer, Node.Call routes each request onto a
// fresh per-Call stream instead of serializing on the shared control
// stream — this lets concurrent Calls progress in parallel up to the
// peer-advertised stream limit (1024 for ZAP's QUIC config).
//
// TCP transport does NOT implement TransportStreamer; Node.Call
// transparently falls back to control-stream serialization on TCP.
type TransportStreamer interface {
// OpenCallStream opens a fresh bidirectional stream for a single
// request/response exchange. The caller writes exactly one frame
// (request) then reads exactly one frame (response) then closes.
OpenCallStream(ctx context.Context) (TransportStream, error)
// AcceptCallStream blocks until the peer opens a per-Call stream.
// Used by the server-side dispatch loop to fan out to handlers
// without blocking on a single serialized read.
AcceptCallStream(ctx context.Context) (TransportStream, error)
}
// TransportStream is one per-Call stream. Stream lifecycle:
//
// WriteFrame(req) // single request
// ReadFrame() // single response
// Close() // release stream ID back to the QUIC pool
//
// Both directions are length-prefixed ZAP frames identical to the
// control-stream wire format — so byte-for-byte interop with the
// TCP transport and with control-stream Call paths is preserved.
type TransportStream interface {
WriteFrame(frame []byte) error
ReadFrame() ([]byte, error)
Close() error
}
// transportRegistry holds the registered factories, keyed by
// Transport. Read-only after init.
var transportRegistry = map[Transport]TransportFactory{}
+176 -1
View File
@@ -96,8 +96,15 @@ var (
)
// Message is a ZAP message that can be read zero-copy.
//
// When the message's backing storage was sourced from the pooled read
// buffer (see bufpool.go), refs is non-nil and Release returns the
// slab to its pool. For messages built via Builder.Finish() / Parse()
// of caller-owned bytes, refs is nil and Release is a no-op — those
// buffers are GC-managed as before.
type Message struct {
data []byte
refs *bufRef // optional pool handle; nil = GC-managed
}
// Parse parses a ZAP message from bytes without copying.
@@ -143,11 +150,109 @@ func (m *Message) Version() uint16 {
return binary.LittleEndian.Uint16(m.data[4:6])
}
// ParseHeader validates a ZAP wire frame and returns the (validated
// data slice, root offset) WITHOUT allocating a [*Message]. Same checks
// as [Parse] — magic, version, size — but the result is two values, not
// a pointer. Intended for generic wrappers (zapv1) that build their own
// value-typed accessors and never need a *Message.
//
// Returns (data[:size], rootOff, nil) on success.
//
// The wire validation steps mirror [Parse] exactly (magic + version +
// size + bounds). The implementation is intentionally written as one
// linear sequence (no intermediate function calls) so the inliner
// folds the whole body into the caller. Combined with the value-
// typed [zapv1.View], this is what makes the v2 read path match v1's
// 2 ns hand-rolled per-Read cost — zero function calls, zero heap.
func ParseHeader(data []byte) ([]byte, int, error) {
return parseHeaderImpl(data)
}
// parseHeaderImpl is the actual validation body. Kept as a separate
// function so ParseHeader stays inlineable (delegates a single call)
// and per-schema WrapX shims can absorb the whole pipeline. The body
// itself is one cost unit over the inline budget — that's fine; it
// becomes ONE real function call per Wrap, comparable to v1's pattern
// where Parse runs inline and the only call is the implicit message
// constructor.
func parseHeaderImpl(data []byte) ([]byte, int, error) {
if len(data) < HeaderSize {
return nil, 0, ErrBufferTooSmall
}
if string(data[0:4]) != Magic {
return nil, 0, ErrInvalidMagic
}
version := binary.LittleEndian.Uint16(data[4:6])
if version != Version1 && version != Version2 {
return nil, 0, ErrInvalidVersion
}
size := binary.LittleEndian.Uint32(data[12:16])
if int(size) < HeaderSize || int(size) > len(data) {
return nil, 0, ErrBufferTooSmall
}
rootOff := int(binary.LittleEndian.Uint32(data[8:12]))
return data[:size], rootOff, nil
}
// WrapBuffer constructs a [*Message] over an already-built ZAP buffer
// WITHOUT re-running [Parse]'s validation checks (magic, version,
// size bounds). Use ONLY for buffers you just emitted via [Builder]
// — they are valid by construction, and skipping the recheck is a
// 14ns hot-path saving per Build call.
//
// External / untrusted bytes MUST go through Parse, never WrapBuffer.
func WrapBuffer(data []byte) *Message {
return &Message{data: data}
}
// RootObjectAt returns a [zap.Object] anchored at absolute offset
// `off` within this message. Used by zapv1.Build to avoid a redundant
// header-read after [Builder.FinishAsRoot] already returned the root
// position.
func (m *Message) RootObjectAt(off int) Object {
return Object{msg: m, offset: off}
}
// Bytes returns the underlying byte slice.
func (m *Message) Bytes() []byte {
return m.data
}
// Release returns this message's backing slab to the read-buffer pool
// when the message was sourced via the pooled read path. Safe to call
// on any *Message — when the buffer is GC-managed (Builder, Parse of
// caller bytes) Release is a no-op.
//
// After Release the underlying bytes MAY be overwritten by the next
// pool consumer. Callers must not use Bytes() / Root() / accessors on
// a released message.
//
// Release on a nil receiver is a no-op so callers can defer it
// without nil-checking.
func (m *Message) Release() {
if m == nil {
return
}
r := m.refs
m.refs = nil
m.data = nil
r.release()
}
// Retain increments the reference count on this message's backing
// slab, allowing the caller to hand the message off to a goroutine
// that will Release it later. No-op when refs is nil.
//
// Pair every Retain with exactly one Release. The dispatch loop calls
// Retain before pushing a response *Message onto a Call's response
// channel; the Call goroutine then Releases when it consumes the
// response.
func (m *Message) Retain() {
if m != nil && m.refs != nil {
m.refs.retain()
}
}
// Size returns the total message size.
func (m *Message) Size() int {
return len(m.data)
@@ -175,6 +280,26 @@ func (o Object) IsNull() bool {
return o.offset == 0
}
// Offset returns the object's absolute byte offset within its
// underlying message. Exposed so external generic wrappers (zapv1)
// can construct typed sub-views into the same buffer without
// re-walking the parent pointers.
//
// SAFETY: callers MUST treat the returned offset as opaque — it's
// load-bearing for unsafe-pointer arithmetic, so any out-of-range
// usage is the caller's responsibility. The matching message bytes
// are reachable via Message.Bytes() on this object's message.
func (o Object) Offset() int {
return o.offset
}
// Message returns the underlying [*Message] this Object is a view
// into. Used by zapv1 to alias the message's bytes for direct
// payload indexing.
func (o Object) Message() *Message {
return o.msg
}
// Bool reads a bool at the given field offset.
func (o Object) Bool(fieldOffset int) bool {
return o.Uint8(fieldOffset) != 0
@@ -246,6 +371,28 @@ func (o Object) Float64(fieldOffset int) float64 {
return math.Float64frombits(o.Uint64(fieldOffset))
}
// BytesFixedSlice returns a zero-copy slice of n inline bytes at
// fieldOffset within the object's fixed payload. This is the generic-
// width counterpart of HashSlice (32 bytes) and AddressSlice (20
// bytes); it covers any N>0 inline byte-array slot (e.g., LP-201
// SessionID [16]byte, LP-208 QuasarWitness [96]byte).
//
// Returns nil if the requested span falls outside the buffer. The
// returned slice aliases the underlying message data; the caller MUST
// NOT mutate it.
//
// This is the symmetric reader for ObjectBuilder.SetBytesFixed.
func (o Object) BytesFixedSlice(fieldOffset, n int) []byte {
if n <= 0 {
return nil
}
pos := o.offset + fieldOffset
if pos+n > len(o.msg.data) {
return nil
}
return o.msg.data[pos : pos+n]
}
// Text reads a string at the given field offset (zero-copy).
func (o Object) Text(fieldOffset int) string {
b := o.Bytes(fieldOffset)
@@ -490,7 +637,8 @@ func (l List) Uint64(i int) uint64 {
return binary.LittleEndian.Uint64(l.msg.data[pos:])
}
// Object returns an object list element.
// Object returns an INLINE object list element — element i is a fixed-stride
// object of elemSize bytes living at l.offset + i*elemSize.
func (l List) Object(i int, elemSize int) Object {
if i < 0 || i >= l.length {
return Object{}
@@ -498,6 +646,33 @@ func (l List) Object(i int, elemSize int) Object {
return Object{msg: l.msg, offset: l.offset + i*elemSize}
}
// ObjectPtr returns the i'th element of an OUT-OF-LINE object list — the read
// counterpart to [ListBuilder.AddObjectPtr]. Element i is a 4-byte SIGNED
// relative pointer at l.offset + i*4; it is dereferenced as absOffset =
// slotPos + int32(rel), exactly like [Object.Object]. A null (0) element or an
// out-of-range target yields the zero Object (IsNull). This is the canonical
// way to read a "repeated message" field where each element is a tailed object
// rather than a fixed-size inline value — the decomplected replacement for the
// byte-blob envelope-concat pattern.
func (l List) ObjectPtr(i int) Object {
if i < 0 || i >= l.length {
return Object{}
}
pos := l.offset + i*4
if pos+4 > len(l.msg.data) {
return Object{}
}
rel := int32(binary.LittleEndian.Uint32(l.msg.data[pos:]))
if rel == 0 {
return Object{} // null element
}
absOffset := pos + int(rel)
if absOffset < HeaderSize || absOffset >= len(l.msg.data) {
return Object{}
}
return Object{msg: l.msg, offset: absOffset}
}
// Bytes returns the raw bytes of the list (for byte lists).
func (l List) Bytes() []byte {
if l.msg == nil || l.offset+l.length > len(l.msg.data) {
+26 -2
View File
@@ -131,11 +131,35 @@ func rwMap(dst jsWriter, src *Reader) (n int, err error) {
n++
}
field, err = src.ReadMapKeyPtr()
var kt Type
kt, err = src.NextType()
if err != nil {
return
}
nn, err = rwquoted(dst, field)
switch kt {
case IntType:
var i64 int64
i64, err = src.ReadInt64()
if err != nil {
return
}
src.scratch = strconv.AppendInt(src.scratch[:0], i64, 10)
nn, err = rwquoted(dst, src.scratch)
case UintType:
var u64 uint64
u64, err = src.ReadUint64()
if err != nil {
return
}
src.scratch = strconv.AppendUint(src.scratch[:0], u64, 10)
nn, err = rwquoted(dst, src.scratch)
default:
field, err = src.ReadMapKeyPtr()
if err != nil {
return
}
nn, err = rwquoted(dst, field)
}
n += nn
if err != nil {
return
+21
View File
@@ -144,6 +144,27 @@ func rwMapBytes(w jsWriter, msg []byte, scratch []byte, depth int) ([]byte, []by
}
func rwMapKeyBytes(w jsWriter, msg []byte, scratch []byte, depth int) ([]byte, []byte, error) {
if len(msg) < 1 {
return msg, scratch, ErrShortBytes
}
switch getType(msg[0]) {
case IntType:
i, msg, err := ReadInt64Bytes(msg)
if err != nil {
return msg, scratch, err
}
scratch = strconv.AppendInt(scratch[:0], i, 10)
_, err = rwquoted(w, scratch)
return msg, scratch, err
case UintType:
u, msg, err := ReadUint64Bytes(msg)
if err != nil {
return msg, scratch, err
}
scratch = strconv.AppendUint(scratch[:0], u, 10)
_, err = rwquoted(w, scratch)
return msg, scratch, err
}
msg, scratch, err := rwStringBytes(w, msg, scratch, depth)
if err != nil {
if tperr, ok := err.(TypeError); ok && tperr.Encoded == BinType {
+4
View File
@@ -121,6 +121,10 @@ func Decode(r io.Reader, d Decodable) error {
// reader will be buffered.
func NewReader(r io.Reader) *Reader {
p := readerPool.Get().(*Reader)
p.recursionDepth = 0
p.maxElements = 0
p.maxRecursionDepth = 0
p.maxStrLen = 0
if p.R == nil {
p.R = fwd.NewReader(r)
} else {
+11 -7
View File
@@ -96,9 +96,9 @@ linters:
- "!**/exporters/zipkin/**"
deny:
- pkg: go.opentelemetry.io/otel/semconv
desc: "Use go.opentelemetry.io/otel/semconv/v1.40.0 instead. If a newer semconv version has been released, update the depguard rule."
desc: "Use go.opentelemetry.io/otel/semconv/v1.41.0 instead. If a newer semconv version has been released, update the depguard rule."
allow:
- go.opentelemetry.io/otel/semconv/v1.40.0
- go.opentelemetry.io/otel/semconv/v1.41.0
gocritic:
disabled-checks:
- appendAssign
@@ -134,13 +134,16 @@ linters:
strconcat: true
revive:
confidence: 0.01
enable-all-rules: false
enable-default-rules: true
max-open-files: 2048
rules:
- name: blank-imports
- name: bool-literal-in-expr
- name: constant-logical-expr
- name: context-as-argument
arguments:
- allowTypesBefore: '*testing.T'
- allow-types-before: '*testing.T'
disabled: true
- name: context-keys-type
- name: deep-exit
@@ -152,7 +155,7 @@ linters:
- name: duplicated-imports
- name: early-return
arguments:
- preserveScope
- preserve-scope
- name: empty-block
- name: empty-lines
- name: error-naming
@@ -161,7 +164,7 @@ linters:
- name: errorf
- name: exported
arguments:
- sayRepetitiveInsteadOfStutters
- say-repetitive-instead-of-stutters
- name: flag-parameter
- name: identical-branches
- name: if-return
@@ -169,11 +172,12 @@ linters:
- name: increment-decrement
- name: indent-error-flow
arguments:
- preserveScope
- preserve-scope
- name: package-comments
- name: range
- name: range-val-in-closure
- name: range-val-address
- name: receiver-naming
- name: redefines-builtin-id
- name: string-format
arguments:
@@ -183,7 +187,7 @@ linters:
- name: struct-tag
- name: superfluous-else
arguments:
- preserveScope
- preserve-scope
- name: time-equal
- name: unconditional-recursion
- name: unexported-return
+109
View File
@@ -0,0 +1,109 @@
# Agent Guide for opentelemetry-go
This file contains active, task-oriented instructions for autonomous and semi-autonomous coding agents working in this repository.
Before starting any task, read `.github/copilot-instructions.md`, `CONTRIBUTING.md`, and this file.
Treat `.github/copilot-instructions.md` as global passive guidance for every task, including docs-only and review-only work.
## Core expectations
- Preserve OpenTelemetry specification compliance, API stability, and idiomatic Go.
- Prefer minimal, surgical changes over broad refactors or speculative cleanup.
- Read the package you are editing and match its existing naming, option types, error handling, comments, tests, and concurrency patterns.
- Keep public APIs backward compatible unless the task explicitly requires a breaking change.
- Keep telemetry resilient and loosely coupled. Do not introduce behavior that can unexpectedly interfere with host applications.
- Inspect boundaries carefully: input validation, resource limits, cancellation, shutdown, error propagation, concurrency, and memory growth.
- Prefer fail-safe behavior and explicit invariants over implicit assumptions.
- Keep dependencies minimal and justified.
- Preserve host-application safety: telemetry should not panic, block indefinitely, or amplify attacker-controlled input.
- Be conservative on hot paths. Avoid unnecessary allocations, reflection, interface churn, blocking, global state, and high-cardinality telemetry.
- Write comments only for intent, invariants, and non-obvious constraints. Do not add comments that restate the code.
## Default workflow
For new features and behavior changes, use this order unless the task explicitly says otherwise:
1. Read the relevant package, its tests, and any package docs or `README.md`.
2. Add or update a failing unit test that captures the required behavior or regression.
3. Implement the smallest change that makes the test pass.
4. Refactor only after the behavior is locked in, and only if the refactor keeps the diff focused.
5. If the changed code is on a hot path or performance-sensitive, inspect existing benchmarks and run them. Add a benchmark if coverage is missing.
6. Update documentation artifacts as needed while the context is fresh. Follow the documentation and changelog conventions below for the specific updates required.
7. Run `make precommit` each time before considering the work complete.
For docs-only, test-only, or review-only tasks, still start with the required repository guidance above, then skip the workflow steps that do not apply while keeping the same discipline around scope, verification, and repository conventions.
## Verification
- Use `make` as the canonical repository verification command. The default target is `precommit`.
- `make precommit` is the expected final verification step for linting, generation, README checks, module checks, and tests.
- During iteration, targeted commands are fine for fast feedback, but do not stop there if the task changes code.
- If you touch performance-sensitive code, run focused benchmarks and compare the results using `benchstat` in addition to `make`.
## Documentation and changelog
- Non-internal, non-test packages should have Go doc comments, usually in `doc.go`.
- Non-internal, non-test, non-documentation packages should also have a `README.md` with at least a title and a `pkg.go.dev` badge.
- Prefer examples over long code snippets in GoDoc when practical.
- Keep docs aligned with actual behavior. Do not leave stale comments, stale examples, or stale package documentation behind.
- For user-visible changes, update `CHANGELOG.md` under the appropriate `Added`, `Changed`, `Deprecated`, `Fixed`, or `Removed` section within `## [Unreleased]`.
## Repository habits
- Prefer focused diffs. Avoid drive-by cleanup.
- Follow existing option patterns and exported API conventions instead of inventing new abstractions.
- Generated files are checked in. If your change affects generation, keep generated output up to date.
- Prefer fast local search tools such as `rg` when exploring the repository.
- When changing behavior, make the invariants explicit in tests.
## Personas
### Feature Agent
Use this persona for new behavior, new API surface, or spec-driven feature work.
- Start with a failing unit test.
- Confirm the expected behavior against the spec, existing package behavior, and public API compatibility.
- Implement the smallest viable change.
- Update GoDoc, examples, `README.md`, and `CHANGELOG.md` when the change is user-visible.
- If the feature touches a hot path, check benchmarks and add one if the coverage is missing.
### Refactoring Agent
Use this persona when improving structure without intentionally changing behavior.
- Treat behavior preservation as the default contract.
- Add or tighten tests before moving code if current behavior is not already pinned down.
- Avoid broad rewrites, clever abstractions, or package-wide cleanup unless explicitly requested.
- If a refactor touches a hot path, benchmark before and after.
- Keep API shape, semantics, concurrency guarantees, and failure modes unchanged unless the task says otherwise.
### Test Agent
Use this persona when adding missing coverage, reproducing bugs, or hardening regressions.
- Reproduce the bug or missing behavior with the smallest failing test you can.
- Prefer testing public behavior and externally visible invariants.
- Add targeted regression tests before changing production code.
- Only change production code when it is required to make the tested behavior correct or testable.
- Keep tests deterministic, readable, and aligned with package patterns.
### Performance Agent
Use this persona for hot-path work, allocation reduction, or throughput and latency improvements.
- Benchmark first to establish a baseline.
- Prefer changes that reduce allocations, copying, interface churn, and unnecessary synchronization.
- Do not trade away correctness, spec compliance, or API stability for micro-optimizations.
- Add or update benchmarks when performance-sensitive coverage is missing.
- If you materially change a hot path, capture before-and-after results, preferably with `benchstat`.
### Review Agent
Use this persona when asked to review code, patches, or pull requests.
- Lead with findings, not summaries.
- Order findings by severity and include precise file and line references when available.
- Focus on correctness, spec compliance, API compatibility, concurrency safety, resilience, performance regressions, missing tests, missing benchmarks, documentation gaps, and changelog gaps.
- Call out when a diff is broader than necessary.
- If you find no issues, say that explicitly and note any residual risks or verification gaps.
+96 -1
View File
@@ -11,6 +11,100 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
<!-- Released section -->
<!-- Don't change this section unless doing release -->
## [1.44.0/0.66.0/0.20.0/0.0.17] 2026-05-27
### Added
- Add `ByteSlice` and `ByteSliceValue` functions for new `BYTESLICE` attribute type in `go.opentelemetry.io/otel/attribute`. (#7948)
- Apply attribute value limit to the `KindBytes` attribute type in `go.opentelemetry.io/otel/sdk/log`. (#7990)
- Apply attribute value limit to the `BYTESLICE` attribute type in `go.opentelemetry.io/otel/sdk/trace`. (#7990)
- Support `BYTESLICE` attributes in `go.opentelemetry.io/otel/trace`. (#8153)
- Support `BYTESLICE` attributes in `go.opentelemetry.io/otel/exporters/otlp/otlptrace`. (#8153)
- Support `BYTESLICE` attributes in `go.opentelemetry.io/otel/exporters/otlp/otlplog`. (#8153)
- Support `BYTESLICE` attributes in `go.opentelemetry.io/otel/exporters/otlp/otlpmetric`. (#8153)
- Support `BYTESLICE` attributes in `go.opentelemetry.io/otel/exporters/zipkin`. (#8153)
- Add `String` method for `Value` type in `go.opentelemetry.io/otel/attribute`. (#8142)
- Add `Slice` and `SliceValue` functions for new `SLICE` attribute type in `go.opentelemetry.io/otel/attribute`. (#8166)
- Support `SLICE` attributes in `go.opentelemetry.io/otel/exporters/otlp/otlptrace`. (#8216)
- Support `SLICE` attributes in `go.opentelemetry.io/otel/exporters/otlp/otlplog`. (#8216)
- Support `SLICE` attributes in `go.opentelemetry.io/otel/exporters/otlp/otlpmetric`. (#8216)
- Support `SLICE` attributes in `go.opentelemetry.io/otel/exporters/zipkin`. (#8216)
- Apply `AttributeValueLengthLimit` to `attribute.SLICE` type attribute values in `go.opentelemetry.io/otel/sdk/trace`, recursively truncating contained string values. (#8217)
- Add `Error` field on `Record` type in `go.opentelemetry.io/otel/log/logtest`. (#8148)
- Add `WithMaxRequestSize` option in `go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc`. (#8157)
- Add `WithMaxRequestSize` option in `go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp`. (#8157)
- Add `WithMaxRequestSize` option in `go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc`. (#8157)
- Add `WithMaxRequestSize` option in `go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp`. (#8157)
- Add `WithMaxRequestSize` option in `go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc`. (#8157)
- Add `WithMaxRequestSize` option in `go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp`. (#8157)
- Add `Settable` to `go.opentelemetry.io/otel/metric/x` to allow reusing attribute options. (#8178)
- Add experimental support for splitting metric data across multiple batches in `go.opentelemetry.io/otel/sdk/metric`.
Set `OTEL_GO_X_METRIC_EXPORT_BATCH_SIZE=<max_size>` to enable for all periodic readers.
See `go.opentelemetry.io/otel/sdk/metric/internal/x` for feature documentation. (#8071)
- Add experimental self-observability metrics in `go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc`.
Enable with `OTEL_GO_X_SELF_OBSERVABILITY=true` environment variable.
See `go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc/internal/x` for feature documentation. (#8192)
- Add experimental self-observability metrics in `go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp`.
Enable with `OTEL_GO_X_SELF_OBSERVABILITY=true` environment variable.
See `go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp/internal/x` for feature documentation. (#8194)
- Add experimental self-observability metrics in `go.opentelemetry.io/otel/exporters/stdout/stdoutlog`.
Enable with `OTEL_GO_X_SELF_OBSERVABILITY=true` environment variable.
See `go.opentelemetry.io/otel/stdout/stdoutlog/internal/x` for feature documentation. (#8263)
- Add `WithDefaultAttributes` to `go.opentelemetry.io/otel/metric/x` to support setting default attributes on instruments. (#8135)
- Add `go.opentelemetry.io/otel/semconv/v1.41.0` package.
The package contains semantic conventions from the `v1.41.0` version of the OpenTelemetry Semantic Conventions.
See the [migration documentation](./semconv/v1.41.0/MIGRATION.md) for information on how to upgrade from `go.opentelemetry.io/otel/semconv/v1.40.0`. (#8324)
- Add Observable variants of instruments to `go.opentelemetry.io/otel/semconv/v1.41.0` package. (#8350)
- Generate explicit histogram bucket boundaries from weaver configuration for HTTP and RPC duration instruments in `go.opentelemetry.io/otel/semconv/v1.41.0`. (#8002)
### Changed
- ⚠️ **Breaking Change:** `go.opentelemetry.io/otel/sdk/metric` now applies a default cardinality limit of 2000 to comply with the Metrics SDK specification recommendation.
New attribute sets are dropped when the cardinality limit is reached. The measurement of these sets are aggregated into a special attribute set containing `attribute.Bool("otel.metric.overflow", true)`.
This can break users who relied on the previous unlimited default.
Set `WithCardinalityLimit(0)` or the deprecated `OTEL_GO_X_CARDINALITY_LIMIT=0` environment variable to preserve unlimited cardinality.
Note that support for `OTEL_GO_X_CARDINALITY_LIMIT` may be removed in a future release. (#8247)
- `ErrorType` in `go.opentelemetry.io/otel/semconv` now unwraps errors created with `fmt.Errorf` when deriving the `error.type` attribute. (#8133)
- `go.opentelemetry.io/otel/sdk/log` now unwraps error chains created with `fmt.Errorf` when deriving the `error.type` attribute from errors on log records. (#8133)
- `Set.MarshalLog` method in `go.opentelemetry.io/otel/attribute` now uses `Value.String` formatting following the [OpenTelemetry AnyValue representation for non-OTLP protocols](https://opentelemetry.io/docs/specs/otel/common/#anyvalue). (#8169)
- Optimize `go.opentelemetry.io/otel/sdk/metric` to return a drop reservoir and short-circuit `Offer` calls to the exemplar reservoir when `exemplar.AlwaysOffFilter` is configured. (#8211) (#8267)
- Optimize `go.opentelemetry.io/otel/sdk/metric` to return a drop reservoir for asynchronous instruments when `exemplar.TraceBasedFilter` is configured. (#8286)
### Deprecated
- Deprecate `Value.Emit` method in `go.opentelemetry.io/otel/attribute`.
Use `Value.String` instead. (#8176)
### Fixed
- Limit OTLP request size to 64 MiB by default in `go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc`.
The limit applies before compression, oversized requests are treated as non-retryable errors, and the limit can be configured with the new `WithMaxRequestSize` option. (#8157, #8365)
- Limit OTLP request size to 64 MiB by default in `go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp`.
The limit applies before compression, oversized requests are treated as non-retryable errors, and the limit can be configured with the new `WithMaxRequestSize` option. (#8157, #8365)
- Limit OTLP request size to 64 MiB by default in `go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc`.
The limit applies before compression, oversized requests are treated as non-retryable errors, and the limit can be configured with the new `WithMaxRequestSize` option. (#8157, #8365)
- Limit OTLP request size to 64 MiB by default in `go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp`.
The limit applies before compression, oversized requests are treated as non-retryable errors, and the limit can be configured with the new `WithMaxRequestSize` option. (#8157, #8365)
- Limit OTLP request size to 64 MiB by default in `go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc`.
The limit applies before compression, oversized requests are treated as non-retryable errors, and the limit can be configured with the new `WithMaxRequestSize` option. (#8157, #8365)
- Limit OTLP request size to 64 MiB by default in `go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp`.
The limit applies before compression, oversized requests are treated as non-retryable errors, and the limit can be configured with the new `WithMaxRequestSize` option. (#8157, #8365)
- Fix gzipped request body replay on redirect in `go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp`. (#8135)
- Fix gzipped request body replay on redirect in `go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp`. (#8152)
- `go.opentelemetry.io/otel/exporters/prometheus` now uses `Value.String` formatting for label values following the [OpenTelemetry AnyValue representation for non-OTLP protocols](https://opentelemetry.io/docs/specs/otel/common/#anyvalue). (#8170)
- Propagate errors from the exporter when calling `Shutdown` on `BatchSpanProcessor` in `go.opentelemetry.io/otel/sdk/trace`. (#8197)
- Fix stale status code reporting on self-observability metrics in `go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp` and `go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp`. (#8226)
- Fix a concurrent `Collect` data race and potential panic in `go.opentelemetry.io/otel/exporters/prometheus` when `WithResourceAsConstantLabels` option is used. (#8227)
- Fix race condition in `FixedSizeReservoir` in `go.opentelemetry.io/otel/sdk/metric/exemplar` by reverting #7447. (#8249)
- Fix `FixedSizeReservoir` in `go.opentelemetry.io/otel/sdk/metric/exemplar` to safely handle zero size.
A capacity check in the constructor initializes the reservoir safely and skips initialization for zero-cap; early returns in `Offer()` and `Collect()` ensure no-op behavior. (#8295)
- Fix counting of spans and logs in self-observability metrics in `go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc`, `go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp`, `go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc`, and `go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp`. (#8254)
- Drop conflicting scope attributes named `name`, `version`, or `schema_url` from metric labels in `go.opentelemetry.io/otel/exporters/prometheus`, preserving the dedicated `otel_scope_name`, `otel_scope_version`, and `otel_scope_schema_url` labels. (#8264)
- Close schema files opened by `ParseFile` in `go.opentelemetry.io/otel/schema/v1.0` and `go.opentelemetry.io/otel/schema/v1.1`. ([GHSA-995v-fvrw-c78m](https://github.com/open-telemetry/opentelemetry-go/security/advisories/GHSA-995v-fvrw-c78m))
- Enforce the 8192-byte baggage size limit during extraction/parsing, changing behavior when the limit is exceeded in `go.opentelemetry.io/otel/baggage` and `go.opentelemetry.io/otel/propagation`. (#8222)
- Fix `go.opentelemetry.io/otel/semconv/v1.41.0` to include `Attr*` helper methods for required attributes on observable instruments. (#8361)
- Limit baggage extraction error reporting in `go.opentelemetry.io/otel/propagation` to prevent malformed or oversized baggage headers from flooding logs. ([GHSA-5wrp-cwcj-q835](https://github.com/open-telemetry/opentelemetry-go/security/advisories/GHSA-5wrp-cwcj-q835))
## [1.43.0/0.65.0/0.19.0] 2026-04-02
### Added
@@ -3619,7 +3713,8 @@ It contains api and sdk for trace and meter.
- CircleCI build CI manifest files.
- CODEOWNERS file to track owners of this project.
[Unreleased]: https://github.com/open-telemetry/opentelemetry-go/compare/v1.43.0...HEAD
[Unreleased]: https://github.com/open-telemetry/opentelemetry-go/compare/v1.44.0...HEAD
[1.44.0/0.66.0/0.20.0/0.0.17]: https://github.com/open-telemetry/opentelemetry-go/releases/tag/v1.44.0
[1.43.0/0.65.0/0.19.0]: https://github.com/open-telemetry/opentelemetry-go/releases/tag/v1.43.0
[1.42.0/0.64.0/0.18.0/0.0.16]: https://github.com/open-telemetry/opentelemetry-go/releases/tag/v1.42.0
[1.41.0/0.63.0/0.17.0/0.0.15]: https://github.com/open-telemetry/opentelemetry-go/releases/tag/v1.41.0
+3
View File
@@ -0,0 +1,3 @@
# Instructions for Claude Code
@AGENTS.md
+89 -12
View File
@@ -11,6 +11,12 @@ for a summary description of past meetings. To request edit access,
join the meeting or get in touch on
[Slack](https://cloud-native.slack.com/archives/C01NPAXACKT).
The meeting is open for all to join. We invite everyone to join our
meeting, regardless of your experience level. Whether you're a
seasoned OpenTelemetry developer, just starting your journey, or
simply curious about the work we do, you're more than welcome to
participate!
## Development
You can view and edit the source code by cloning this repository:
@@ -746,8 +752,8 @@ Encapsulate setup in constructor functions, ensuring clear ownership and scope:
import (
"errors"
semconv "go.opentelemetry.io/otel/semconv/v1.40.0"
"go.opentelemetry.io/otel/semconv/v1.40.0/otelconv"
semconv "go.opentelemetry.io/otel/semconv/v1.41.0"
"go.opentelemetry.io/otel/semconv/v1.41.0/otelconv"
)
type SDKComponent struct {
@@ -808,11 +814,11 @@ func (c *Component) initObservability() {
#### Performance
When observability is disabled there should be little to no overhead.
When observability is disabled or the instrument is not `Enabled`, there should be little to no overhead.
```go
func (e *Exporter) ExportSpans(ctx context.Context, spans []trace.ReadOnlySpan) error {
if e.inst != nil {
if e.inst != nil && e.inst.Enabled(ctx) {
attrs := expensiveOperation()
e.inst.recordSpanInflight(ctx, int64(len(spans)), attrs...)
}
@@ -829,7 +835,7 @@ func (e *Exporter) ExportSpans(ctx context.Context, spans []trace.ReadOnlySpan)
}
func (i *instrumentation) recordSpanInflight(ctx context.Context, count int64, attrs ...attribute.KeyValue) {
if i == nil || i.inflight == nil {
if i == nil || i.inflight == nil || !i.inflight.Enabled(ctx) {
return
}
i.inflight.Add(ctx, count, metric.WithAttributes(attrs...))
@@ -865,8 +871,12 @@ var (
)
func (i *instrumentation) record(ctx context.Context, value int64, baseAttrs ...attribute.KeyValue) {
if !i.counter.Enabled(ctx) {
return
}
attrs := attrPool.Get().(*[]attribute.KeyValue)
defer func() {
clear(*attrs) // Clear references to strings/etc to let GC collect them.
*attrs = (*attrs)[:0] // Reset.
attrPool.Put(attrs)
}()
@@ -877,6 +887,7 @@ func (i *instrumentation) record(ctx context.Context, value int64, baseAttrs ...
addOpt := addOptPool.Get().(*[]metric.AddOption)
defer func() {
clear(*addOpt)
*addOpt = (*addOpt)[:0]
addOptPool.Put(addOpt)
}()
@@ -1007,16 +1018,20 @@ Ensure observability measurements receive the correct context, especially for tr
```go
func (e *Exporter) ExportSpans(ctx context.Context, spans []trace.ReadOnlySpan) error {
// Use the provided context for observability measurements
e.inst.recordSpanExportStarted(ctx, len(spans))
if e.inst.Enabled(ctx) {
e.inst.recordSpanExportStarted(ctx, len(spans))
}
err := e.doExport(ctx, spans)
if err != nil {
e.inst.recordSpanExportFailed(ctx, len(spans), err)
} else {
e.inst.recordSpanExportSucceeded(ctx, len(spans))
if e.inst.Enabled(ctx) {
if err != nil {
e.inst.recordSpanExportFailed(ctx, len(spans), err)
} else {
e.inst.recordSpanExportSucceeded(ctx, len(spans))
}
}
return err
}
```
@@ -1039,7 +1054,7 @@ func (e *Exporter) ExportSpans(ctx context.Context, spans []trace.ReadOnlySpan)
All observability metrics should follow the [OpenTelemetry Semantic Conventions for SDK metrics](https://github.com/open-telemetry/semantic-conventions/blob/1cf2476ae5e518225a766990a28a6d5602bd5a30/docs/otel/sdk-metrics.md).
Use the metric semantic conventions convenience package [otelconv](./semconv/v1.40.0/otelconv/metric.go).
Use the metric semantic conventions convenience package [otelconv](./semconv/v1.41.0/otelconv/metric.go).
##### Component Identification
@@ -1109,6 +1124,68 @@ func TestObservability(t *testing.T) {
Test order should not affect results.
Ensure that any global state (e.g. component ID counters) is reset between tests.
### Experimental Features
To support the development of new features in the specification, we use the following patterns to implement in-development features without adding new public artifacts in stable modules.
#### Experimental behavior with no API artifacts
Features that change behavior without changing the API (e.g., exemplar collection, auto-generation of identifiers) are implemented behind a feature gate.
The implementation resides in an `/internal/x` package and is activated through environment variables with the `OTEL_GO_X_` prefix (e.g., `OTEL_GO_X_OBSERVABILITY`).
The feature must be documented in a `README.md` file in the `/internal/x` package.
#### Experimental methods on SDK-only interfaces
Features that require new methods on SDK interfaces are defined as a new interface in an experimental module (e.g., `go.opentelemetry.io/otel/sdk/x`).
The SDK uses type assertions (without importing the unstable package) to check if passing types implement these experimental interfaces.
The SDK must not depend on the experimental module.
#### Experimental structs, functions, or interfaces
Features that don't need any changes to the existing stable package are implemented in an experimental module (e.g., `go.opentelemetry.io/otel/sdk/x`).
#### Experimental signals and components
New telemetry signals (e.g., Logs before stabilization) and components (e.g. bridges) are hosted in new, unstable modules (e.g., `go.opentelemetry.io/otel/log` before 1.0.0).
The package should have the final name it will use once stabilized (i.e. not `/x`), and is released at a v0.x.y version to indicate it is not stable.
Most new components are hosted in [opentelemetry-go-contrib](https://github.com/open-telemetry/opentelemetry-go-contrib).
#### Experimental options for API or SDK functions
Experimental Options functions are implemented in an experimental module (e.g., `go.opentelemetry.io/otel/sdk/x`).
The return type of the Option function must embed the option's type (e.g. `metric.InstrumentOption`), and have an `Experimental()` method to prevent the API from panicking when the option is used.
The SDK uses type assertions (without importing the unstable package) to check if passing types implement these experimental interfaces.
The SDK must not depend on the experimental module.
For example:
```go
type myOption struct {
// Embed the stable option type.
metric.InstrumentOption
value string
}
// Experimental prevents the API from panicking when the option is used.
func (o myOption) Experimental() {}
// The SDK can use type assertions to use this function.
func (o myOption) Value() string { return o.value }
func WithMyOption(value string) metric.InstrumentOption {
return myOption{value: value}
}
```
#### Not Supported
The following kinds of experimental features are **not currently supported** on stable interfaces:
- Experimental methods on API interfaces
- Experimental fields for API or SDK exported structs
In some cases forks or long-lived branches may be used for prototyping these features.
## Approvers and Maintainers
### Maintainers
+9 -1
View File
@@ -191,8 +191,16 @@ benchmark: $(OTEL_GO_MOD_DIRS:%=benchmark/%)
benchmark/%:
cd $* && $(GO) test -run='^$$' -bench=. $(ARGS) ./...
# sdk/metric is split into two shards to work around CodSpeed limitations.
# See https://github.com/CodSpeedHQ/codspeed-go/issues/56
BENCHMARK_SHARDS := $(filter-out ./sdk/metric,$(OTEL_GO_MOD_DIRS)) ./sdk/metric/root ./sdk/metric/internal
benchmark/./sdk/metric/root:
cd ./sdk/metric && $(GO) test -run='^$$' -bench=. $(ARGS) . ./exemplar/...
benchmark/./sdk/metric/internal:
cd ./sdk/metric && $(GO) test -run='^$$' -bench=. $(ARGS) ./internal/...
print-sharded-benchmarks:
@echo $(OTEL_GO_MOD_DIRS) | jq -cR 'split(" ")'
@echo $(BENCHMARK_SHARDS) | jq -cR 'split(" ")'
.PHONY: golangci-lint golangci-lint-fix
golangci-lint-fix: ARGS=--fix
+3 -1
View File
@@ -105,7 +105,9 @@ func (d *defaultAttrEncoder) Encode(iter Iterator) string {
if keyValue.Value.Type() == STRING {
copyAndEscape(buf, keyValue.Value.AsString())
} else {
_, _ = buf.WriteString(keyValue.Value.Emit())
_, _ = buf.WriteString(
keyValue.Value.Emit(),
) //nolint:staticcheck // Preserve the existing default encoder output.
}
}
return buf.String()
+47 -11
View File
@@ -27,6 +27,8 @@ const (
int64SliceID uint64 = 3762322556277578591 // "_[]int64" (little endian)
float64SliceID uint64 = 7308324551835016539 // "[]double" (little endian)
stringSliceID uint64 = 7453010373645655387 // "[]string" (little endian)
byteSliceID uint64 = 6874028470941080415 // "_[]byte_" (little endian)
sliceID uint64 = 7883494272577650031 // "__slice_" (little endian)
emptyID uint64 = 7305809155345288421 // "__empty_" (little endian)
)
@@ -42,53 +44,87 @@ func hashKVs(kvs []KeyValue) uint64 {
// hashKV returns the xxHash64 hash of kv with h as the base.
func hashKV(h xxhash.Hash, kv KeyValue) xxhash.Hash {
h = h.String(string(kv.Key))
return hashValue(h, kv.Value)
}
switch kv.Value.Type() {
func hashValue(h xxhash.Hash, v Value) xxhash.Hash {
switch v.Type() {
case BOOL:
h = h.Uint64(boolID)
h = h.Uint64(kv.Value.numeric)
h = h.Uint64(v.numeric)
case INT64:
h = h.Uint64(int64ID)
h = h.Uint64(kv.Value.numeric)
h = h.Uint64(v.numeric)
case FLOAT64:
h = h.Uint64(float64ID)
// Assumes numeric stored with math.Float64bits.
h = h.Uint64(kv.Value.numeric)
h = h.Uint64(v.numeric)
case STRING:
h = h.Uint64(stringID)
h = h.String(kv.Value.stringly)
h = h.String(v.stringly)
case BOOLSLICE:
h = h.Uint64(boolSliceID)
rv := reflect.ValueOf(kv.Value.slice)
rv := reflect.ValueOf(v.slice)
for i := 0; i < rv.Len(); i++ {
h = h.Bool(rv.Index(i).Bool())
}
case INT64SLICE:
h = h.Uint64(int64SliceID)
rv := reflect.ValueOf(kv.Value.slice)
rv := reflect.ValueOf(v.slice)
for i := 0; i < rv.Len(); i++ {
h = h.Int64(rv.Index(i).Int())
}
case FLOAT64SLICE:
h = h.Uint64(float64SliceID)
rv := reflect.ValueOf(kv.Value.slice)
rv := reflect.ValueOf(v.slice)
for i := 0; i < rv.Len(); i++ {
h = h.Float64(rv.Index(i).Float())
}
case STRINGSLICE:
h = h.Uint64(stringSliceID)
rv := reflect.ValueOf(kv.Value.slice)
rv := reflect.ValueOf(v.slice)
for i := 0; i < rv.Len(); i++ {
h = h.String(rv.Index(i).String())
}
case BYTESLICE:
h = h.Uint64(byteSliceID)
h = h.String(v.stringly)
case SLICE:
h = h.Uint64(sliceID)
switch vals := v.slice.(type) {
case [0]Value:
// No values to hash, but the type identifier is still hashed above.
case [1]Value:
h = hashValueSlice(h, vals[:])
case [2]Value:
h = hashValueSlice(h, vals[:])
case [3]Value:
h = hashValueSlice(h, vals[:])
case [4]Value:
h = hashValueSlice(h, vals[:])
case [5]Value:
h = hashValueSlice(h, vals[:])
default:
rv := reflect.ValueOf(v.slice)
for i := 0; i < rv.Len(); i++ {
h = hashValue(h, rv.Index(i).Interface().(Value))
}
}
case EMPTY:
h = h.Uint64(emptyID)
default:
// Logging is an alternative, but using the internal logger here
// causes an import cycle so it is not done.
v := kv.Value.AsInterface()
msg := fmt.Sprintf("unknown value type: %[1]v (%[1]T)", v)
val := v.AsInterface()
msg := fmt.Sprintf("unknown value type: %[1]v (%[1]T)", val)
panic(msg)
}
return h
}
func hashValueSlice(h xxhash.Hash, vals []Value) xxhash.Hash {
for _, v := range vals {
h = hashValue(h, v)
}
return h
}
+22
View File
@@ -117,6 +117,28 @@ func (k Key) StringSlice(v []string) KeyValue {
}
}
// ByteSlice creates a KeyValue instance with a BYTESLICE Value.
//
// If creating both a key and value at the same time, use the provided
// convenience function instead -- ByteSlice(name, value).
func (k Key) ByteSlice(v []byte) KeyValue {
return KeyValue{
Key: k,
Value: ByteSliceValue(v),
}
}
// Slice creates a KeyValue instance with a SLICE Value.
//
// If creating both a key and value at the same time, use the provided
// convenience function instead -- Slice(name, values...).
func (k Key) Slice(v ...Value) KeyValue {
return KeyValue{
Key: k,
Value: SliceValue(v...),
}
}
// Defined reports whether the key is not empty.
func (k Key) Defined() bool {
return len(k) != 0
+10
View File
@@ -68,6 +68,16 @@ func StringSlice(k string, v []string) KeyValue {
return Key(k).StringSlice(v)
}
// ByteSlice creates a KeyValue with a BYTESLICE Value type.
func ByteSlice(k string, v []byte) KeyValue {
return Key(k).ByteSlice(v)
}
// Slice creates a KeyValue with a SLICE Value type.
func Slice(k string, v ...Value) KeyValue {
return Key(k).Slice(v...)
}
// Stringer creates a new key-value pair with a passed name and a string
// value generated by the passed Stringer interface.
func Stringer(k string, v fmt.Stringer) KeyValue {
+2 -2
View File
@@ -401,7 +401,7 @@ func computeDataFixed(kvs []KeyValue) any {
func computeDataReflect(kvs []KeyValue) any {
at := reflect.New(reflect.ArrayOf(len(kvs), keyValueType)).Elem()
for i, keyValue := range kvs {
*(at.Index(i).Addr().Interface().(*KeyValue)) = keyValue
*at.Index(i).Addr().Interface().(*KeyValue) = keyValue
}
return at.Interface()
}
@@ -415,7 +415,7 @@ func (l *Set) MarshalJSON() ([]byte, error) {
func (l Set) MarshalLog() any {
kvs := make(map[string]string)
for _, kv := range l.ToSlice() {
kvs[string(kv.Key)] = kv.Value.Emit()
kvs[string(kv.Key)] = kv.Value.String()
}
return kvs
}
+4 -2
View File
@@ -17,11 +17,13 @@ func _() {
_ = x[INT64SLICE-6]
_ = x[FLOAT64SLICE-7]
_ = x[STRINGSLICE-8]
_ = x[BYTESLICE-9]
_ = x[SLICE-10]
}
const _Type_name = "EMPTYBOOLINT64FLOAT64STRINGBOOLSLICEINT64SLICEFLOAT64SLICESTRINGSLICE"
const _Type_name = "EMPTYBOOLINT64FLOAT64STRINGBOOLSLICEINT64SLICEFLOAT64SLICESTRINGSLICEBYTESLICESLICE"
var _Type_index = [...]uint8{0, 5, 9, 14, 21, 27, 36, 46, 58, 69}
var _Type_index = [...]uint8{0, 5, 9, 14, 21, 27, 36, 46, 58, 69, 78, 83}
func (i Type) String() string {
idx := int(i) - 0
+742
View File
@@ -4,9 +4,14 @@
package attribute // import "go.opentelemetry.io/otel/attribute"
import (
"encoding/base64"
"encoding/json"
"fmt"
"math"
"reflect"
"strconv"
"strings"
"unicode/utf8"
attribute "go.opentelemetry.io/otel/attribute/internal"
)
@@ -45,6 +50,10 @@ const (
FLOAT64SLICE
// STRINGSLICE is a slice of strings Type Value.
STRINGSLICE
// BYTESLICE is a slice of bytes Type Value.
BYTESLICE
// SLICE is a slice of Value Type values.
SLICE
// INVALID is used for a Value with no value set.
//
// Deprecated: Use EMPTY instead as an empty value is a valid value.
@@ -134,6 +143,19 @@ func StringSliceValue(v []string) Value {
return Value{vtype: STRINGSLICE, slice: attribute.SliceValue(v)}
}
// ByteSliceValue creates a BYTESLICE Value.
func ByteSliceValue(v []byte) Value {
return Value{
vtype: BYTESLICE,
stringly: string(v),
}
}
// SliceValue creates a SLICE Value.
func SliceValue(v ...Value) Value {
return Value{vtype: SLICE, slice: sliceValue(v)}
}
// Type returns a type of the Value.
func (v Value) Type() Type {
return v.vtype
@@ -215,6 +237,59 @@ func (v Value) asStringSlice() []string {
return attribute.AsSlice[string](v.slice)
}
// AsSlice returns the []Value value. Make sure that the Value's type is
// SLICE.
func (v Value) AsSlice() []Value {
if v.vtype != SLICE {
return nil
}
return v.asSlice()
}
func (v Value) asSlice() []Value {
switch vals := v.slice.(type) {
case [0]Value:
return []Value{}
case [1]Value:
return []Value{vals[0]}
case [2]Value:
return []Value{vals[0], vals[1]}
case [3]Value:
return []Value{vals[0], vals[1], vals[2]}
case [4]Value:
return []Value{vals[0], vals[1], vals[2], vals[3]}
case [5]Value:
return []Value{vals[0], vals[1], vals[2], vals[3], vals[4]}
default:
return asValueSliceReflect(v.slice)
}
}
func asValueSliceReflect(v any) []Value {
rv := reflect.ValueOf(v)
if !rv.IsValid() || rv.Kind() != reflect.Array || rv.Type().Elem() != reflect.TypeFor[Value]() {
return nil
}
cpy := make([]Value, rv.Len())
if len(cpy) > 0 {
_ = reflect.Copy(reflect.ValueOf(cpy), rv)
}
return cpy
}
// AsByteSlice returns the bytes value. Make sure that the Value's type
// is BYTESLICE.
func (v Value) AsByteSlice() []byte {
if v.vtype != BYTESLICE {
return nil
}
return v.asByteSlice()
}
func (v Value) asByteSlice() []byte {
return []byte(v.stringly)
}
type unknownValueType struct{}
// AsInterface returns Value's data as any.
@@ -236,13 +311,60 @@ func (v Value) AsInterface() any {
return v.stringly
case STRINGSLICE:
return v.asStringSlice()
case BYTESLICE:
return v.asByteSlice()
case SLICE:
return v.asSlice()
case EMPTY:
return nil
}
return unknownValueType{}
}
// String returns a string representation of Value using the
// [OpenTelemetry AnyValue representation for non-OTLP protocols] rules.
//
// Strings are returned as-is without JSON quoting, booleans and integers use
// JSON literals, floating-point values use JSON numbers except that NaN and
// ±Inf are rendered as NaN, Infinity, and -Infinity, byte slices are
// base64-encoded, empty values are the empty string, and slices are encoded as
// JSON arrays. String, byte, and special floating-point values inside arrays
// are encoded as JSON strings, and empty values inside arrays are encoded as
// null.
//
// [OpenTelemetry AnyValue representation for non-OTLP protocols]: https://opentelemetry.io/docs/specs/otel/common/#anyvalue-representation-for-non-otlp-protocols
func (v Value) String() string {
switch v.Type() {
case BOOL:
return strconv.FormatBool(v.AsBool())
case BOOLSLICE:
return formatBoolSliceValue(v.slice)
case INT64:
return strconv.FormatInt(v.AsInt64(), 10)
case INT64SLICE:
return formatInt64SliceValue(v.slice)
case FLOAT64:
return formatFloat64(v.AsFloat64())
case FLOAT64SLICE:
return formatFloat64SliceValue(v.slice)
case STRING:
return v.stringly
case STRINGSLICE:
return formatStringSliceValue(v.slice)
case BYTESLICE:
return formatByteSlice(v.stringly)
case SLICE:
return formatValueSliceValue(v.slice)
case EMPTY:
return ""
default:
return "unknown"
}
}
// Emit returns a string representation of Value's data.
//
// Deprecated: Use [Value.String] instead.
func (v Value) Emit() string {
switch v.Type() {
case BOOLSLICE:
@@ -273,6 +395,10 @@ func (v Value) Emit() string {
return string(j)
case STRING:
return v.stringly
case BYTESLICE:
return formatByteSlice(v.stringly)
case SLICE:
return formatValueSliceValue(v.slice)
case EMPTY:
return ""
default:
@@ -280,6 +406,622 @@ func (v Value) Emit() string {
}
}
const (
jsonArrayBracketsLen = len("[]")
boolArrayElemMaxLen = len("false")
int64ArrayElemMaxLen = len("-9223372036854775808")
float64ArrayElemMaxLen = len("-1.7976931348623157e+308")
commaLen = len(",")
)
func sliceValue(v []Value) any {
switch len(v) {
case 0:
return [0]Value{}
case 1:
return [1]Value{v[0]}
case 2:
return [2]Value{v[0], v[1]}
case 3:
return [3]Value{v[0], v[1], v[2]}
case 4:
return [4]Value{v[0], v[1], v[2], v[3]}
case 5:
return [5]Value{v[0], v[1], v[2], v[3], v[4]}
default:
return sliceValueReflect(v)
}
}
func sliceValueReflect(v []Value) any {
cp := reflect.New(reflect.ArrayOf(len(v), reflect.TypeFor[Value]())).Elem()
reflect.Copy(cp, reflect.ValueOf(v))
return cp.Interface()
}
func formatBoolSliceValue(v any) string {
switch vals := v.(type) {
case [0]bool:
return "[]"
case [1]bool:
return formatBoolSlice(vals[:])
case [2]bool:
return formatBoolSlice(vals[:])
case [3]bool:
return formatBoolSlice(vals[:])
default:
return formatBoolSliceReflect(v)
}
}
func formatBoolSlice(vals []bool) string {
var b strings.Builder
appendBoolSlice(&b, vals)
return b.String()
}
func formatBoolSliceReflect(v any) string {
var b strings.Builder
appendBoolSliceReflect(&b, reflect.ValueOf(v))
return b.String()
}
func appendBoolSliceValue(dst *strings.Builder, v any) {
switch vals := v.(type) {
case [0]bool:
_, _ = dst.WriteString("[]")
case [1]bool:
appendBoolSlice(dst, vals[:])
case [2]bool:
appendBoolSlice(dst, vals[:])
case [3]bool:
appendBoolSlice(dst, vals[:])
default:
appendBoolSliceReflect(dst, reflect.ValueOf(v))
}
}
func appendBoolSlice(dst *strings.Builder, vals []bool) {
dst.Grow(jsonArrayBracketsLen + len(vals)*(boolArrayElemMaxLen+commaLen))
_ = dst.WriteByte('[')
for i, val := range vals {
if i > 0 {
_ = dst.WriteByte(',')
}
if val {
_, _ = dst.WriteString("true")
} else {
_, _ = dst.WriteString("false")
}
}
_ = dst.WriteByte(']')
}
func appendBoolSliceReflect(dst *strings.Builder, rv reflect.Value) {
dst.Grow(jsonArrayBracketsLen + rv.Len()*(boolArrayElemMaxLen+commaLen))
_ = dst.WriteByte('[')
for i := 0; i < rv.Len(); i++ {
if i > 0 {
_ = dst.WriteByte(',')
}
if rv.Index(i).Bool() {
_, _ = dst.WriteString("true")
} else {
_, _ = dst.WriteString("false")
}
}
_ = dst.WriteByte(']')
}
func formatInt64SliceValue(v any) string {
switch vals := v.(type) {
case [0]int64:
return "[]"
case [1]int64:
return formatInt64Slice(vals[:])
case [2]int64:
return formatInt64Slice(vals[:])
case [3]int64:
return formatInt64Slice(vals[:])
default:
return formatInt64SliceReflect(v)
}
}
func formatInt64Slice(vals []int64) string {
var b strings.Builder
appendInt64Slice(&b, vals)
return b.String()
}
func formatInt64SliceReflect(v any) string {
var b strings.Builder
appendInt64SliceReflect(&b, reflect.ValueOf(v))
return b.String()
}
func appendInt64SliceValue(dst *strings.Builder, v any) {
switch vals := v.(type) {
case [0]int64:
_, _ = dst.WriteString("[]")
case [1]int64:
appendInt64Slice(dst, vals[:])
case [2]int64:
appendInt64Slice(dst, vals[:])
case [3]int64:
appendInt64Slice(dst, vals[:])
default:
appendInt64SliceReflect(dst, reflect.ValueOf(v))
}
}
func appendInt64Slice(dst *strings.Builder, vals []int64) {
dst.Grow(jsonArrayBracketsLen + len(vals)*(int64ArrayElemMaxLen+commaLen))
_ = dst.WriteByte('[')
var buf [int64ArrayElemMaxLen]byte
for i, val := range vals {
if i > 0 {
_ = dst.WriteByte(',')
}
out := strconv.AppendInt(buf[:0], val, 10)
_, _ = dst.Write(out)
}
_ = dst.WriteByte(']')
}
func appendInt64SliceReflect(dst *strings.Builder, rv reflect.Value) {
dst.Grow(jsonArrayBracketsLen + rv.Len()*(int64ArrayElemMaxLen+commaLen))
_ = dst.WriteByte('[')
var scratch [int64ArrayElemMaxLen]byte
for i := 0; i < rv.Len(); i++ {
if i > 0 {
_ = dst.WriteByte(',')
}
out := strconv.AppendInt(scratch[:0], rv.Index(i).Int(), 10)
_, _ = dst.Write(out)
}
_ = dst.WriteByte(']')
}
func formatFloat64(v float64) string {
switch {
case math.IsNaN(v):
return "NaN"
case math.IsInf(v, 1):
return "Infinity"
case math.IsInf(v, -1):
return "-Infinity"
default:
return strconv.FormatFloat(v, 'g', -1, 64)
}
}
func formatFloat64SliceValue(v any) string {
switch vals := v.(type) {
case [0]float64:
return "[]"
case [1]float64:
return formatFloat64Slice(vals[:])
case [2]float64:
return formatFloat64Slice(vals[:])
case [3]float64:
return formatFloat64Slice(vals[:])
default:
return formatFloat64SliceReflect(v)
}
}
func formatFloat64Slice(vals []float64) string {
var b strings.Builder
appendFloat64Slice(&b, vals)
return b.String()
}
func formatFloat64SliceReflect(v any) string {
var b strings.Builder
appendFloat64SliceReflect(&b, reflect.ValueOf(v))
return b.String()
}
func appendFloat64SliceValue(dst *strings.Builder, v any) {
switch vals := v.(type) {
case [0]float64:
_, _ = dst.WriteString("[]")
case [1]float64:
appendFloat64Slice(dst, vals[:])
case [2]float64:
appendFloat64Slice(dst, vals[:])
case [3]float64:
appendFloat64Slice(dst, vals[:])
default:
appendFloat64SliceReflect(dst, reflect.ValueOf(v))
}
}
func appendFloat64Slice(dst *strings.Builder, vals []float64) {
dst.Grow(jsonArrayBracketsLen + len(vals)*(float64ArrayElemMaxLen+commaLen))
_ = dst.WriteByte('[')
var buf [float64ArrayElemMaxLen]byte
for i, val := range vals {
if i > 0 {
_ = dst.WriteByte(',')
}
switch {
case math.IsNaN(val):
_, _ = dst.WriteString(`"NaN"`)
case math.IsInf(val, 1):
_, _ = dst.WriteString(`"Infinity"`)
case math.IsInf(val, -1):
_, _ = dst.WriteString(`"-Infinity"`)
default:
out := strconv.AppendFloat(buf[:0], val, 'g', -1, 64)
_, _ = dst.Write(out)
}
}
_ = dst.WriteByte(']')
}
func appendFloat64SliceReflect(dst *strings.Builder, rv reflect.Value) {
dst.Grow(jsonArrayBracketsLen + rv.Len()*(float64ArrayElemMaxLen+commaLen))
_ = dst.WriteByte('[')
var scratch [float64ArrayElemMaxLen]byte
for i := 0; i < rv.Len(); i++ {
if i > 0 {
_ = dst.WriteByte(',')
}
val := rv.Index(i).Float()
switch {
case math.IsNaN(val):
_, _ = dst.WriteString(`"NaN"`)
case math.IsInf(val, 1):
_, _ = dst.WriteString(`"Infinity"`)
case math.IsInf(val, -1):
_, _ = dst.WriteString(`"-Infinity"`)
default:
out := strconv.AppendFloat(scratch[:0], val, 'g', -1, 64)
_, _ = dst.Write(out)
}
}
_ = dst.WriteByte(']')
}
func formatStringSliceValue(v any) string {
switch vals := v.(type) {
case [0]string:
return "[]"
case [1]string:
return formatStringSlice(vals[:])
case [2]string:
return formatStringSlice(vals[:])
case [3]string:
return formatStringSlice(vals[:])
default:
return formatStringSliceReflect(v)
}
}
func formatStringSlice(vals []string) string {
var b strings.Builder
appendStringSlice(&b, vals)
return b.String()
}
func formatStringSliceReflect(v any) string {
var b strings.Builder
appendStringSliceReflect(&b, reflect.ValueOf(v))
return b.String()
}
func appendStringSliceValue(dst *strings.Builder, v any) {
switch vals := v.(type) {
case [0]string:
_, _ = dst.WriteString("[]")
case [1]string:
appendStringSlice(dst, vals[:])
case [2]string:
appendStringSlice(dst, vals[:])
case [3]string:
appendStringSlice(dst, vals[:])
default:
appendStringSliceReflect(dst, reflect.ValueOf(v))
}
}
func appendStringSlice(dst *strings.Builder, vals []string) {
size := jsonArrayBracketsLen
for _, val := range vals {
size += len(val) + commaLen + 2 // Account for JSON string quotes and comma.
}
dst.Grow(size)
_ = dst.WriteByte('[')
for i, val := range vals {
if i > 0 {
_ = dst.WriteByte(',')
}
appendJSONString(dst, val)
}
_ = dst.WriteByte(']')
}
func appendStringSliceReflect(dst *strings.Builder, rv reflect.Value) {
size := jsonArrayBracketsLen
for i := 0; i < rv.Len(); i++ {
size += len(rv.Index(i).String()) + commaLen + 2 // Account for JSON string quotes and comma.
}
dst.Grow(size)
_ = dst.WriteByte('[')
for i := 0; i < rv.Len(); i++ {
if i > 0 {
_ = dst.WriteByte(',')
}
appendJSONString(dst, rv.Index(i).String())
}
_ = dst.WriteByte(']')
}
func formatByteSlice(v string) string {
var b strings.Builder
appendBase64(&b, v)
return b.String()
}
func formatValueSliceValue(v any) string {
switch vals := v.(type) {
case [0]Value:
return "[]"
case [1]Value:
return formatValueSlice(vals[:])
case [2]Value:
return formatValueSlice(vals[:])
case [3]Value:
return formatValueSlice(vals[:])
case [4]Value:
return formatValueSlice(vals[:])
case [5]Value:
return formatValueSlice(vals[:])
default:
return formatValueSliceReflect(v)
}
}
func formatValueSlice(vals []Value) string {
var b strings.Builder
appendValueSlice(&b, vals)
return b.String()
}
func formatValueSliceReflect(v any) string {
var b strings.Builder
appendValueSliceReflect(&b, reflect.ValueOf(v))
return b.String()
}
func appendValueSliceValue(dst *strings.Builder, v any) {
switch vals := v.(type) {
case [0]Value:
_, _ = dst.WriteString("[]")
case [1]Value:
appendValueSlice(dst, vals[:])
case [2]Value:
appendValueSlice(dst, vals[:])
case [3]Value:
appendValueSlice(dst, vals[:])
case [4]Value:
appendValueSlice(dst, vals[:])
case [5]Value:
appendValueSlice(dst, vals[:])
default:
appendValueSliceReflect(dst, reflect.ValueOf(v))
}
}
func appendValueSlice(dst *strings.Builder, vals []Value) {
// Estimate 10 bytes per value for small values and commas.
dst.Grow(jsonArrayBracketsLen + len(vals)*commaLen + len(vals)*10)
_ = dst.WriteByte('[')
for i, val := range vals {
if i > 0 {
_ = dst.WriteByte(',')
}
appendJSONValue(dst, val)
}
_ = dst.WriteByte(']')
}
func appendValueSliceReflect(dst *strings.Builder, rv reflect.Value) {
// Estimate 10 bytes per value for small values and commas.
dst.Grow(jsonArrayBracketsLen + rv.Len()*commaLen + rv.Len()*10)
_ = dst.WriteByte('[')
for i := 0; i < rv.Len(); i++ {
if i > 0 {
_ = dst.WriteByte(',')
}
appendJSONValue(dst, rv.Index(i).Interface().(Value))
}
_ = dst.WriteByte(']')
}
func appendJSONValue(dst *strings.Builder, v Value) {
switch v.Type() {
case BOOL:
if v.AsBool() {
_, _ = dst.WriteString("true")
} else {
_, _ = dst.WriteString("false")
}
case BOOLSLICE:
appendBoolSliceValue(dst, v.slice)
case INT64:
var buf [int64ArrayElemMaxLen]byte
out := strconv.AppendInt(buf[:0], v.AsInt64(), 10)
_, _ = dst.Write(out)
case INT64SLICE:
appendInt64SliceValue(dst, v.slice)
case FLOAT64:
val := v.AsFloat64()
switch {
case math.IsNaN(val):
appendJSONString(dst, "NaN")
case math.IsInf(val, 1):
appendJSONString(dst, "Infinity")
case math.IsInf(val, -1):
appendJSONString(dst, "-Infinity")
default:
var buf [float64ArrayElemMaxLen]byte
out := strconv.AppendFloat(buf[:0], val, 'g', -1, 64)
_, _ = dst.Write(out)
}
case FLOAT64SLICE:
appendFloat64SliceValue(dst, v.slice)
case STRING:
appendJSONString(dst, v.stringly)
case STRINGSLICE:
appendStringSliceValue(dst, v.slice)
case BYTESLICE:
_ = dst.WriteByte('"')
appendBase64(dst, v.stringly)
_ = dst.WriteByte('"')
case SLICE:
appendValueSliceValue(dst, v.slice)
case EMPTY:
_, _ = dst.WriteString("null")
default:
appendJSONString(dst, "unknown")
}
}
// appendJSONString appends s to dst as a JSON string literal.
//
// This is adapted from the Go standard library's encoding/json
// [appendString implementation]. It keeps the same escaping behavior we need
// here, but writes directly into a strings.Builder and intentionally does not
// apply HTML escaping because the OpenTelemetry non-OTLP AnyValue representation
// only requires JSON array string encoding. We inline this instead of using
// encoding/json so slice formatting avoids allocations and reflection.
//
// [appendString implementation]: https://github.com/golang/go/blob/3b5954c6349d31465dca409b45ab6597e0942d9f/src/encoding/json/encode.go#L998-L1064
func appendJSONString(dst *strings.Builder, s string) {
const hex = "0123456789abcdef" // For escaping bytes to hex.
_ = dst.WriteByte('"')
start := 0
for i := 0; i < len(s); {
if c := s[i]; c < utf8.RuneSelf {
if c >= 0x20 && c != '\\' && c != '"' {
i++
continue
}
if start < i {
_, _ = dst.WriteString(s[start:i])
}
switch c {
case '\\', '"':
_ = dst.WriteByte('\\')
_ = dst.WriteByte(c)
case '\b':
_, _ = dst.WriteString(`\b`)
case '\f':
_, _ = dst.WriteString(`\f`)
case '\n':
_, _ = dst.WriteString(`\n`)
case '\r':
_, _ = dst.WriteString(`\r`)
case '\t':
_, _ = dst.WriteString(`\t`)
default:
_, _ = dst.WriteString(`\u00`)
_ = dst.WriteByte(hex[c>>4])
_ = dst.WriteByte(hex[c&0x0f])
}
i++
start = i
continue
}
r, size := utf8.DecodeRuneInString(s[i:])
if r == utf8.RuneError && size == 1 {
if start < i {
_, _ = dst.WriteString(s[start:i])
}
// Match encoding/json by replacing invalid UTF-8 with U+FFFD.
_, _ = dst.WriteString(`\ufffd`)
i++
start = i
continue
}
if r == '\u2028' || r == '\u2029' {
if start < i {
_, _ = dst.WriteString(s[start:i])
}
// Escape JSONP-sensitive separators unconditionally, like encoding/json.
_, _ = dst.WriteString(`\u202`)
_ = dst.WriteByte(hex[r&0x0f])
i += size
start = i
continue
}
i += size
}
if start < len(s) {
_, _ = dst.WriteString(s[start:])
}
_ = dst.WriteByte('"')
}
// This is adapted from the Go standard library's encoding/base64
// [Encoding.Encode implementation]. It keeps the same encoding behavior we need
// here, but writes directly into a strings.Builder. We inline this instead of using
// encoding/base64 to avoid allocations.
//
// [Encoding.Encode implementation]: https://github.com/golang/go/blob/3b5954c6349d31465dca409b45ab6597e0942d9f/src/encoding/base64/base64.go#L139-L189
func appendBase64(dst *strings.Builder, s string) {
const encode = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
dst.Grow(base64.StdEncoding.EncodedLen(len(s)))
i := 0
for ; i+2 < len(s); i += 3 {
n := uint32(s[i])<<16 | uint32(s[i+1])<<8 | uint32(s[i+2])
_ = dst.WriteByte(encode[n>>18&0x3f])
_ = dst.WriteByte(encode[n>>12&0x3f])
_ = dst.WriteByte(encode[n>>6&0x3f])
_ = dst.WriteByte(encode[n&0x3f])
}
switch len(s) - i {
case 1:
n := uint32(s[i]) << 16
_ = dst.WriteByte(encode[n>>18&0x3f])
_ = dst.WriteByte(encode[n>>12&0x3f])
_ = dst.WriteByte('=')
_ = dst.WriteByte('=')
case 2:
n := uint32(s[i])<<16 | uint32(s[i+1])<<8
_ = dst.WriteByte(encode[n>>18&0x3f])
_ = dst.WriteByte(encode[n>>12&0x3f])
_ = dst.WriteByte(encode[n>>6&0x3f])
_ = dst.WriteByte('=')
}
}
// MarshalJSON returns the JSON encoding of the Value.
func (v Value) MarshalJSON() ([]byte, error) {
var jsonVal struct {
+26 -4
View File
@@ -14,6 +14,10 @@ import (
)
const (
maxParseErrors = 5
// W3C Baggage specification limits.
// https://www.w3.org/TR/baggage/#limits
maxMembers = 64
maxBytesPerBaggageString = 8192
@@ -493,9 +497,15 @@ func New(members ...Member) (Baggage, error) {
// from the W3C Baggage specification which allows duplicate list-members, but
// conforms to the OpenTelemetry Baggage specification.
//
// If the baggage-string exceeds the maximum allowed members (64) or bytes
// (8192), members are dropped until the limits are satisfied and an error is
// returned along with the partial result.
// If the raw baggage-string exceeds the maximum allowed bytes (8192), an
// empty Baggage and an error are returned.
//
// Otherwise, members are parsed left-to-right and accumulated until one of
// the following conditions is reached, at which point parsing stops and an
// error is returned alongside the partial result:
// - accepting the next member would cause the encoded baggage to exceed
// 8192 bytes, or
// - the baggage already contains 64 distinct keys.
//
// Invalid members are skipped and the error is returned along with the
// partial result containing the valid members.
@@ -504,9 +514,14 @@ func Parse(bStr string) (Baggage, error) {
return Baggage{}, nil
}
if n := len(bStr); n > maxBytesPerBaggageString {
return Baggage{}, fmt.Errorf("%w: %d", errBaggageBytes, n)
}
b := make(baggage.List)
sizes := make(map[string]int) // Track per-key byte sizes
var totalBytes int
var parseErrors int
var truncateErr error
for memberStr := range strings.SplitSeq(bStr, listDelimiter) {
// Check member count limit.
@@ -517,7 +532,10 @@ func Parse(bStr string) (Baggage, error) {
m, err := parseMember(memberStr)
if err != nil {
truncateErr = errors.Join(truncateErr, err)
parseErrors++
if parseErrors <= maxParseErrors {
truncateErr = errors.Join(truncateErr, err)
}
continue // skip invalid member, keep processing
}
@@ -553,6 +571,10 @@ func Parse(bStr string) (Baggage, error) {
totalBytes = newTotalBytes
}
if dropped := parseErrors - maxParseErrors; dropped > 0 {
truncateErr = errors.Join(truncateErr, fmt.Errorf("and %d more invalid member(s)", dropped))
}
if len(b) == 0 {
return Baggage{}, truncateErr
}
+1 -1
View File
@@ -1,4 +1,4 @@
# This is a renovate-friendly source of Docker images.
FROM python:3.13.6-slim-bullseye@sha256:e98b521460ee75bca92175c16247bdf7275637a8faaeb2bcfa19d879ae5c4b9a AS python
FROM otel/weaver:v0.22.1@sha256:33ae522ae4b71c1c562563c1d81f46aa0f79f088a0873199143a1f11ac30e5c9 AS weaver
FROM otel/weaver:v0.23.0@sha256:7984ecb55b859eb3034ae9d836c4eeda137e2bdd0873b7ba2bb6c3d24d6ff457 AS weaver
FROM avtodev/markdown-lint:v1@sha256:6aeedc2f49138ce7a1cd0adffc1b1c0321b841dc2102408967d9301c031949ee AS markdown
+9
View File
@@ -51,6 +51,9 @@ type Float64ObservableCounterConfig struct {
func NewFloat64ObservableCounterConfig(opts ...Float64ObservableCounterOption) Float64ObservableCounterConfig {
var config Float64ObservableCounterConfig
for _, o := range opts {
if _, ok := o.(experimentalOption); ok {
continue
}
config = o.applyFloat64ObservableCounter(config)
}
return config
@@ -111,6 +114,9 @@ func NewFloat64ObservableUpDownCounterConfig(
) Float64ObservableUpDownCounterConfig {
var config Float64ObservableUpDownCounterConfig
for _, o := range opts {
if _, ok := o.(experimentalOption); ok {
continue
}
config = o.applyFloat64ObservableUpDownCounter(config)
}
return config
@@ -168,6 +174,9 @@ type Float64ObservableGaugeConfig struct {
func NewFloat64ObservableGaugeConfig(opts ...Float64ObservableGaugeOption) Float64ObservableGaugeConfig {
var config Float64ObservableGaugeConfig
for _, o := range opts {
if _, ok := o.(experimentalOption); ok {
continue
}
config = o.applyFloat64ObservableGauge(config)
}
return config
+9
View File
@@ -50,6 +50,9 @@ type Int64ObservableCounterConfig struct {
func NewInt64ObservableCounterConfig(opts ...Int64ObservableCounterOption) Int64ObservableCounterConfig {
var config Int64ObservableCounterConfig
for _, o := range opts {
if _, ok := o.(experimentalOption); ok {
continue
}
config = o.applyInt64ObservableCounter(config)
}
return config
@@ -110,6 +113,9 @@ func NewInt64ObservableUpDownCounterConfig(
) Int64ObservableUpDownCounterConfig {
var config Int64ObservableUpDownCounterConfig
for _, o := range opts {
if _, ok := o.(experimentalOption); ok {
continue
}
config = o.applyInt64ObservableUpDownCounter(config)
}
return config
@@ -167,6 +173,9 @@ type Int64ObservableGaugeConfig struct {
func NewInt64ObservableGaugeConfig(opts ...Int64ObservableGaugeOption) Int64ObservableGaugeConfig {
var config Int64ObservableGaugeConfig
for _, o := range opts {
if _, ok := o.(experimentalOption); ok {
continue
}
config = o.applyInt64ObservableGauge(config)
}
return config
+7
View File
@@ -42,11 +42,18 @@ type MeterOption interface {
applyMeter(MeterConfig) MeterConfig
}
type experimentalOption interface {
Experimental()
}
// NewMeterConfig creates a new MeterConfig and applies
// all the given options.
func NewMeterConfig(opts ...MeterOption) MeterConfig {
var config MeterConfig
for _, o := range opts {
if _, ok := o.(experimentalOption); ok {
continue
}
config = o.applyMeter(config)
}
return config
+39 -12
View File
@@ -24,10 +24,10 @@ all instruments fall into two overlapping logical categories: asynchronous or
synchronous, and int64 or float64.
All synchronous instruments ([Int64Counter], [Int64UpDownCounter],
[Int64Histogram], [Float64Counter], [Float64UpDownCounter], and
[Float64Histogram]) are used to measure the operation and performance of source
code during the source code execution. These instruments only make measurements
when the source code they instrument is run.
[Int64Histogram], [Int64Gauge], [Float64Counter], [Float64UpDownCounter],
[Float64Histogram], and [Float64Gauge]) are used to measure the operation and
performance of source code during the source code execution. These instruments
only make measurements when the source code they instrument is run.
All asynchronous instruments ([Int64ObservableCounter],
[Int64ObservableUpDownCounter], [Int64ObservableGauge],
@@ -50,9 +50,11 @@ incrementally increase in value. UpDownCounters ([Int64UpDownCounter],
values that can increase and decrease. When more information needs to be
conveyed about all the synchronous measurements made during a collection cycle,
a Histogram ([Int64Histogram] and [Float64Histogram]) should be used. Finally,
when just the most recent measurement needs to be conveyed about an
asynchronous measurement, a Gauge ([Int64ObservableGauge] and
[Float64ObservableGauge]) should be used.
when just the most recent measurement needs to be conveyed, a Gauge
([Int64Gauge], [Float64Gauge], [Int64ObservableGauge], and
[Float64ObservableGauge]) should be used: the synchronous variants record an
instantaneous value at a specific point in code, while the observable variants
sample the value via a callback once per collection cycle.
See the [OpenTelemetry documentation] for more information about instruments
and their intended use.
@@ -80,11 +82,11 @@ Measurements are made by recording values and information about the values with
an instrument. How these measurements are recorded depends on the instrument.
Measurements for synchronous instruments ([Int64Counter], [Int64UpDownCounter],
[Int64Histogram], [Float64Counter], [Float64UpDownCounter], and
[Float64Histogram]) are recorded using the instrument methods directly. All
counter instruments have an Add method that is used to measure an increment
value, and all histogram instruments have a Record method to measure a data
point.
[Int64Histogram], [Int64Gauge], [Float64Counter], [Float64UpDownCounter],
[Float64Histogram], and [Float64Gauge]) are recorded using the instrument
methods directly. All counter instruments have an Add method that is used to
measure an increment value, and all histogram and synchronous gauge
instruments have a Record method to measure a data point.
Asynchronous instruments ([Int64ObservableCounter],
[Int64ObservableUpDownCounter], [Int64ObservableGauge],
@@ -107,6 +109,31 @@ respectively):
If the criteria are not met, use the RegisterCallback method of the [Meter] that
created the instrument to register a [Callback].
# Avoiding Expensive Computations
All synchronous instruments provide an Enabled method that reports whether the
instrument will process measurements for the given context. When no SDK is
registered or the instrument is otherwise disabled, Enabled returns false. This
can be used to avoid expensive measurement work when a measurement will not be
recorded:
if counter.Enabled(ctx) {
counter.Add(ctx, 1, metric.WithAttributes(expensiveAttributes()...))
}
This is especially valuable when computing attributes is expensive.
[WithAttributes] performs non-trivial work on every call to build an
[attribute.Set] from the provided attributes, and that work is wasted if the
measurement is not recorded.
For performance sensitive code where the same attribute set is used repeatedly,
prefer [WithAttributeSet]. It accepts a pre-built [attribute.Set], letting you
pay the construction cost once and reuse it across many measurements:
attrs := attribute.NewSet(attribute.String("key", "val"))
// ... later, on each call:
counter.Add(ctx, 1, metric.WithAttributeSet(attrs))
# API Implementations
This package does not conform to the standard Go versioning policy, all of its
+33 -6
View File
@@ -3,7 +3,9 @@
package metric // import "go.opentelemetry.io/otel/metric"
import "go.opentelemetry.io/otel/attribute"
import (
"go.opentelemetry.io/otel/attribute"
)
// Observable is used as a grouping mechanism for all instruments that are
// updated within a Callback.
@@ -228,6 +230,9 @@ type AddConfig struct {
func NewAddConfig(opts []AddOption) AddConfig {
config := AddConfig{attrs: *attribute.EmptySet()}
for _, o := range opts {
if _, ok := o.(experimentalOption); ok {
continue
}
config = o.applyAdd(config)
}
return config
@@ -253,6 +258,9 @@ type RecordConfig struct {
func NewRecordConfig(opts []RecordOption) RecordConfig {
config := RecordConfig{attrs: *attribute.EmptySet()}
for _, o := range opts {
if _, ok := o.(experimentalOption); ok {
continue
}
config = o.applyRecord(config)
}
return config
@@ -278,6 +286,9 @@ type ObserveConfig struct {
func NewObserveConfig(opts []ObserveOption) ObserveConfig {
config := ObserveConfig{attrs: *attribute.EmptySet()}
for _, o := range opts {
if _, ok := o.(experimentalOption); ok {
continue
}
config = o.applyObserve(config)
}
return config
@@ -299,6 +310,10 @@ type attrOpt struct {
set attribute.Set
}
func (o *attrOpt) Set(set attribute.Set) {
o.set = set
}
// mergeSets returns the union of keys between a and b. Any duplicate keys will
// use the value associated with b.
func mergeSets(a, b attribute.Set) attribute.Set {
@@ -311,7 +326,7 @@ func mergeSets(a, b attribute.Set) attribute.Set {
return attribute.NewSet(merged...)
}
func (o attrOpt) applyAdd(c AddConfig) AddConfig {
func (o *attrOpt) applyAdd(c AddConfig) AddConfig {
switch {
case o.set.Len() == 0:
case c.attrs.Len() == 0:
@@ -322,7 +337,7 @@ func (o attrOpt) applyAdd(c AddConfig) AddConfig {
return c
}
func (o attrOpt) applyRecord(c RecordConfig) RecordConfig {
func (o *attrOpt) applyRecord(c RecordConfig) RecordConfig {
switch {
case o.set.Len() == 0:
case c.attrs.Len() == 0:
@@ -333,7 +348,7 @@ func (o attrOpt) applyRecord(c RecordConfig) RecordConfig {
return c
}
func (o attrOpt) applyObserve(c ObserveConfig) ObserveConfig {
func (o *attrOpt) applyObserve(c ObserveConfig) ObserveConfig {
switch {
case o.set.Len() == 0:
case c.attrs.Len() == 0:
@@ -350,8 +365,14 @@ func (o attrOpt) applyObserve(c ObserveConfig) ObserveConfig {
// If multiple WithAttributeSet or WithAttributes options are passed the
// attributes will be merged together in the order they are passed. Attributes
// with duplicate keys will use the last value passed.
//
// Experimental: The returned option may implement
// [go.opentelemetry.io/otel/metric/x.Settable][attribute.Set], which can be
// used to replace the option's attribute set and reuse the option without
// additional allocations. This behavior is experimental and may be changed or
// removed in a future release without notice.
func WithAttributeSet(attributes attribute.Set) MeasurementOption {
return attrOpt{set: attributes}
return &attrOpt{set: attributes}
}
// WithAttributes converts attributes into an attribute Set and sets the Set to
@@ -369,8 +390,14 @@ func WithAttributeSet(attributes attribute.Set) MeasurementOption {
//
// See [WithAttributeSet] for information about how multiple WithAttributes are
// merged.
//
// Experimental: The returned option may implement
// [go.opentelemetry.io/otel/metric/x.Settable][[]attribute.KeyValue], which can be
// used to replace the option's attributes and reuse the option without
// additional allocations. This behavior is experimental and may be changed or
// removed in a future release without notice.
func WithAttributes(attributes ...attribute.KeyValue) MeasurementOption {
cp := make([]attribute.KeyValue, len(attributes))
copy(cp, attributes)
return attrOpt{set: attribute.NewSet(cp...)}
return &attrOpt{set: attribute.NewSet(cp...)}
}
+12
View File
@@ -51,6 +51,9 @@ type Float64CounterConfig struct {
func NewFloat64CounterConfig(opts ...Float64CounterOption) Float64CounterConfig {
var config Float64CounterConfig
for _, o := range opts {
if _, ok := o.(experimentalOption); ok {
continue
}
config = o.applyFloat64Counter(config)
}
return config
@@ -116,6 +119,9 @@ type Float64UpDownCounterConfig struct {
func NewFloat64UpDownCounterConfig(opts ...Float64UpDownCounterOption) Float64UpDownCounterConfig {
var config Float64UpDownCounterConfig
for _, o := range opts {
if _, ok := o.(experimentalOption); ok {
continue
}
config = o.applyFloat64UpDownCounter(config)
}
return config
@@ -182,6 +188,9 @@ type Float64HistogramConfig struct {
func NewFloat64HistogramConfig(opts ...Float64HistogramOption) Float64HistogramConfig {
var config Float64HistogramConfig
for _, o := range opts {
if _, ok := o.(experimentalOption); ok {
continue
}
config = o.applyFloat64Histogram(config)
}
return config
@@ -251,6 +260,9 @@ type Float64GaugeConfig struct {
func NewFloat64GaugeConfig(opts ...Float64GaugeOption) Float64GaugeConfig {
var config Float64GaugeConfig
for _, o := range opts {
if _, ok := o.(experimentalOption); ok {
continue
}
config = o.applyFloat64Gauge(config)
}
return config
+12
View File
@@ -51,6 +51,9 @@ type Int64CounterConfig struct {
func NewInt64CounterConfig(opts ...Int64CounterOption) Int64CounterConfig {
var config Int64CounterConfig
for _, o := range opts {
if _, ok := o.(experimentalOption); ok {
continue
}
config = o.applyInt64Counter(config)
}
return config
@@ -116,6 +119,9 @@ type Int64UpDownCounterConfig struct {
func NewInt64UpDownCounterConfig(opts ...Int64UpDownCounterOption) Int64UpDownCounterConfig {
var config Int64UpDownCounterConfig
for _, o := range opts {
if _, ok := o.(experimentalOption); ok {
continue
}
config = o.applyInt64UpDownCounter(config)
}
return config
@@ -182,6 +188,9 @@ type Int64HistogramConfig struct {
func NewInt64HistogramConfig(opts ...Int64HistogramOption) Int64HistogramConfig {
var config Int64HistogramConfig
for _, o := range opts {
if _, ok := o.(experimentalOption); ok {
continue
}
config = o.applyInt64Histogram(config)
}
return config
@@ -251,6 +260,9 @@ type Int64GaugeConfig struct {
func NewInt64GaugeConfig(opts ...Int64GaugeOption) Int64GaugeConfig {
var config Int64GaugeConfig
for _, o := range opts {
if _, ok := o.(experimentalOption); ok {
continue
}
config = o.applyInt64Gauge(config)
}
return config
+60 -12
View File
@@ -5,6 +5,9 @@ package propagation // import "go.opentelemetry.io/otel/propagation"
import (
"context"
"errors"
"fmt"
"sync"
"go.opentelemetry.io/otel/baggage"
"go.opentelemetry.io/otel/internal/errorhandler"
@@ -13,11 +16,18 @@ import (
const (
baggageHeader = "baggage"
maxParseErrors = 5
// W3C Baggage specification limits.
// https://www.w3.org/TR/baggage/#limits
maxMembers = 64
maxMembers = 64
maxBytesPerBaggageString = 8192
)
// handleExtractErrOnce limits error reporting for attacker-controlled baggage headers
// to one process-wide emission, preventing repeated extraction from flooding logs.
var handleExtractErrOnce sync.Once
// Baggage is a propagator that supports the W3C Baggage format.
//
// This propagates user-defined baggage associated with a trace. The complete
@@ -57,7 +67,9 @@ func extractSingleBaggage(parent context.Context, carrier TextMapCarrier) contex
bag, err := baggage.Parse(bStr)
if err != nil {
errorhandler.GetErrorHandler().Handle(err)
handleExtractErrOnce.Do(func() {
errorhandler.GetErrorHandler().Handle(err)
})
}
if bag.Len() == 0 {
return parent
@@ -72,24 +84,60 @@ func extractMultiBaggage(parent context.Context, carrier ValuesGetter) context.C
}
var members []baggage.Member
for _, bStr := range bVals {
currBag, err := baggage.Parse(bStr)
if err != nil {
errorhandler.GetErrorHandler().Handle(err)
var totalBytes int
var parseErrors int
var truncateErr error
for i, bStr := range bVals {
if i > 0 {
totalBytes++ // comma separator between combined header values
}
if currBag.Len() == 0 {
continue
totalBytes += len(bStr)
if totalBytes > maxBytesPerBaggageString {
// Per the W3C Baggage spec, the byte limit applies to the
// combination of all baggage headers, not each header
// individually. Mirror the single-header behavior of
// reporting the error and returning the parent context
// with no baggage attached.
handleExtractErrOnce.Do(func() {
errorhandler.GetErrorHandler().Handle(fmt.Errorf(
"baggage: aggregate header size %d exceeds %d byte limit",
totalBytes,
maxBytesPerBaggageString,
))
})
return parent
}
members = append(members, currBag.Members()...)
if len(members) >= maxMembers {
break
// If members exceed the limit, stop parsing baggage.
if len(members) <= maxMembers {
currBag, err := baggage.Parse(bStr)
if err != nil {
parseErrors++
if parseErrors <= maxParseErrors {
truncateErr = errors.Join(truncateErr, err)
}
}
if currBag.Len() == 0 {
continue
}
members = append(members, currBag.Members()...)
}
}
if dropped := parseErrors - maxParseErrors; dropped > 0 {
truncateErr = errors.Join(truncateErr, fmt.Errorf("and %d more error(s)", dropped))
}
b, err := baggage.New(members...)
if err != nil {
errorhandler.GetErrorHandler().Handle(err)
truncateErr = errors.Join(truncateErr, err)
}
if truncateErr != nil {
handleExtractErrOnce.Do(func() {
errorhandler.GetErrorHandler().Handle(truncateErr)
})
}
if b.Len() == 0 {
return parent
}

Some files were not shown because too many files have changed in this diff Show More