threshold: make 3-of-5 unambiguous, and prove it drives a real Safe

The stack had two different numbers called "threshold". cmp.Keygen and
frost.Keygen take t, the polynomial degree, and need t+1 signers. Operators,
dashboards and runbooks say k-of-n. Provisioning a "3-of-5" wallet by passing
threshold=3 produces a 4-of-5, silently — nothing errors, and the mistake only
surfaces when three custodians hold a ceremony and cannot sign a key that
already holds value.

pkg/quorum is now the single place those two numbers meet. Policy{K,N} states
what operators mean; Degree() == K-1 is the only k->t conversion in the tree.
Callers pass policy.Degree() and never compute the off-by-one themselves.

Proofs, all against the real protocols rather than mocks:

  protocols/quorum_3of5_test.go — five-party CGGMP21 DKG at the policy degree,
  three parties sign, and the signature both verifies under the group key and
  ECRECOVERS to the group's EVM address. Recovery is the part that matters for
  custody: a key can verify correctly and still resolve to an address no one
  controls. Four different 3-subsets all sign for one address. Negatives: every
  2-of-5 subset is refused, and a forged third share cannot produce a signature
  that verifies. FROST covered at the same policy.

  protocols/safe_3of5_test.go — the custody claim end to end. Real SafeL2 v1.5.0
  bytecode behind the real SafeProxyFactory on the luxfi/geth EVM, owned solely
  by the 3-of-5 group address. The digest comes from the deployed Safe's own
  getTransactionHash, so the test cannot sign something the contract would not
  check. execTransaction succeeds, value moves, the nonce advances, a replay is
  rejected, and a valid signature from a different 3-of-5 key is rejected.

Fixes found along the way:

  protocols/lss/adapters/evm.go — GenerateEVMAddress hashed the COMPRESSED
  point, i.e. 32 bytes of X, where the EVM requires Keccak over the 64-byte
  uncompressed X||Y. Every address it produced was well-formed and belonged to
  no key; value sent to one is unspendable. It had no callers, so this was a
  loaded gun rather than a live wound. Now decompresses, and reports failure
  instead of returning a plausible wrong answer.

  internal/test/harness.go — WithTimeout moved the harness deadline but not the
  handler's own ProtocolTimeout, which was hard-coded to five minutes, so a
  raised timeout silently did nothing and a CGGMP21 DKG under -race died at
  five minutes blaming the context. The harness now owns one deadline. Also
  removes a duplicate RunProtocolWithTimeout.

  protocols/cmp/cmp_threshold_test.go — cases labelled "3-of-5" passed degree 3
  and signed with four parties. They were 4-of-5. Labels now derive from the
  policy, so the name and the behaviour are one fact.

  go.sum — was missing entries for luxfi/metric and mattn/go-isatty; the FROST
  packages did not compile from a clean checkout.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
This commit is contained in:
zeekay
2026-07-25 11:40:29 -07:00
co-authored by Hanzo Dev
parent 74672ad61e
commit 247e5e4740
12 changed files with 1767 additions and 99 deletions
+115 -6
View File
@@ -30,50 +30,159 @@ require (
)
require (
filippo.io/hpke v0.4.0 // indirect
github.com/ALTree/bigfloat v0.2.0 // indirect
github.com/Masterminds/semver/v3 v3.4.0 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/aws/aws-sdk-go-v2 v1.41.5 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.5 // indirect
github.com/aws/aws-sdk-go-v2/config v1.32.13 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.19.13 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.21 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.18 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.10 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.18 // indirect
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.2 // indirect
github.com/aws/aws-sdk-go-v2/service/signin v1.0.9 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.30.14 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.18 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.41.10 // indirect
github.com/aws/smithy-go v1.24.2 // indirect
github.com/bits-and-blooms/bitset v1.24.4 // 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/cosmos/go-bip39 v1.0.0 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.7 // 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/dchest/siphash v1.2.3 // indirect
github.com/deckarep/golang-set/v2 v2.8.0 // indirect
github.com/dgraph-io/ristretto/v2 v2.4.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/emicklei/dot v1.11.0 // indirect
github.com/ethereum/c-kzg-4844/v2 v2.1.7 // indirect
github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab // indirect
github.com/ferranbt/fastssz v1.0.0 // indirect
github.com/go-ini/ini v1.67.0 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
github.com/gofrs/flock v0.13.0 // indirect
github.com/golang-jwt/jwt/v4 v4.5.2 // indirect
github.com/golang/snappy v1.0.0 // indirect
github.com/google/flatbuffers v25.12.19+incompatible // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/pprof v0.0.0-20260302011040-a15ffb7f9dcc // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/rpc v1.2.1 // indirect
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect
github.com/grandcat/zeroconf v1.0.0 // indirect
github.com/hanzoai/vfs v0.4.3 // indirect
github.com/hanzos3/go-sdk v1.0.2 // indirect
github.com/hashicorp/go-bexpr v0.1.16 // indirect
github.com/holiman/billy v0.0.0-20250707135307-f2f9b9aae7db // indirect
github.com/holiman/bloomfilter/v2 v2.0.3 // indirect
github.com/huin/goupnp v1.3.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jackpal/go-nat-pmp v1.0.2 // indirect
github.com/klauspost/compress v1.18.6 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/luxfi/accel v1.2.0 // indirect
github.com/klauspost/crc32 v1.3.0 // indirect
github.com/luxfi/accel v1.2.4 // indirect
github.com/luxfi/age v1.6.0 // indirect
github.com/luxfi/atomic v1.0.0 // indirect
github.com/luxfi/cache v1.3.1 // indirect
github.com/luxfi/compress v0.1.1 // indirect
github.com/luxfi/concurrent v0.1.1 // indirect
github.com/luxfi/consensus v1.36.2 // indirect
github.com/luxfi/constants v1.6.2 // indirect
github.com/luxfi/container v0.2.1 // indirect
github.com/luxfi/crypto/ipa v1.2.4 // indirect
github.com/luxfi/database v1.21.1 // indirect
github.com/luxfi/dkg v0.3.5 // indirect
github.com/luxfi/ids v1.3.2 // indirect
github.com/luxfi/math v1.5.1 // indirect
github.com/luxfi/math/big v0.1.0 // indirect
github.com/luxfi/mdns v0.1.1 // indirect
github.com/luxfi/mlwe v0.2.1 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.21 // indirect
github.com/luxfi/mlwe v0.3.0 // indirect
github.com/luxfi/mock v0.1.1 // indirect
github.com/luxfi/p2p v1.22.1 // indirect
github.com/luxfi/pq v1.1.0 // indirect
github.com/luxfi/precompile v0.19.3 // indirect
github.com/luxfi/runtime v1.3.1 // indirect
github.com/luxfi/sampler v1.1.0 // indirect
github.com/luxfi/utils v1.3.1 // indirect
github.com/luxfi/validators v1.3.1 // indirect
github.com/luxfi/version v1.0.1 // indirect
github.com/luxfi/vm v1.3.1 // indirect
github.com/luxfi/warp v1.24.1 // indirect
github.com/luxfi/zapdb v1.10.1 // indirect
github.com/mattn/go-colorable v0.1.15 // indirect
github.com/mattn/go-isatty v0.0.22 // indirect
github.com/miekg/dns v1.1.72 // indirect
github.com/mimoo/StrobeGo v0.0.0-20220103164710-9a04d6ca976b // indirect
github.com/minio/crc64nvme v1.1.1 // indirect
github.com/minio/md5-simd v1.1.2 // indirect
github.com/minio/minio-go/v7 v7.0.100 // indirect
github.com/minio/sha256-simd v1.0.1 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/mitchellh/pointerstructure v1.2.1 // indirect
github.com/montanaflynn/stats v0.9.0 // indirect
github.com/mr-tron/base58 v1.3.0 // indirect
github.com/philhofer/fwd v1.2.0 // indirect
github.com/pion/dtls/v2 v2.2.12 // indirect
github.com/pion/logging v0.2.4 // indirect
github.com/pion/stun/v2 v2.0.0 // indirect
github.com/pion/transport/v2 v2.2.10 // indirect
github.com/pion/transport/v3 v3.1.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/rs/cors v1.11.1 // indirect
github.com/rs/xid v1.6.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/shirou/gopsutil v3.21.11+incompatible // indirect
github.com/spf13/pflag v1.0.10 // indirect
github.com/supranational/blst v0.3.16 // indirect
github.com/tinylib/msgp v1.6.4 // indirect
github.com/tklauser/go-sysconf v0.4.0 // indirect
github.com/tklauser/numcpus v0.12.0 // indirect
github.com/urfave/cli/v2 v2.27.7 // indirect
github.com/wlynxg/anet v0.0.5 // indirect
github.com/x448/float16 v0.8.4 // indirect
github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/otel v1.44.0 // indirect
go.opentelemetry.io/otel/metric v1.44.0 // indirect
go.opentelemetry.io/otel/trace v1.44.0 // indirect
go.uber.org/mock v0.6.0 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90 // indirect
golang.org/x/exp v0.0.0-20260529124908-c761662dc8c9 // indirect
golang.org/x/mod v0.36.0 // indirect
golang.org/x/net v0.55.0 // indirect
golang.org/x/sys v0.45.0 // indirect
golang.org/x/text v0.37.0 // indirect
golang.org/x/time v0.15.0 // indirect
golang.org/x/tools v0.45.0 // indirect
gonum.org/v1/gonum v0.17.0 // indirect
google.golang.org/protobuf v1.36.11 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
require (
github.com/holiman/uint256 v1.3.2
github.com/luxfi/corona v0.10.4
github.com/luxfi/geth v1.20.1
github.com/luxfi/lens v0.2.1
github.com/luxfi/magnetar v1.2.0
github.com/luxfi/magnetar v1.2.3
github.com/luxfi/metric v1.8.1
github.com/luxfi/pulsar v1.9.2
github.com/luxfi/zap v1.2.6
+360 -30
View File
@@ -1,103 +1,288 @@
filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
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/ALTree/bigfloat v0.2.0 h1:AwNzawrpFuw55/YDVlcPw0F0cmmXrmngBHhVrvdXPvM=
github.com/ALTree/bigfloat v0.2.0/go.mod h1:+NaH2gLeY6RPBPPQf4aRotPPStg+eXc8f9ZaE4vRfD4=
github.com/ChainSafe/go-schnorrkel v1.1.0 h1:rZ6EU+CZFCjB4sHUE1jIu8VDoB/wRKZxoe1tkcO71Wk=
github.com/ChainSafe/go-schnorrkel v1.1.0/go.mod h1:ABkENxiP+cvjFiByMIZ9LYbRoNNLeBLiakC1XeTFxfE=
github.com/DataDog/zstd v1.5.7 h1:ybO8RBeh29qrxIhCA9E8gKY6xfONU9T6G6aP9DTKfLE=
github.com/DataDog/zstd v1.5.7/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw=
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/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/aws/aws-sdk-go-v2 v1.41.5 h1:dj5kopbwUsVUVFgO4Fi5BIT3t4WyqIDjGKCangnV/yY=
github.com/aws/aws-sdk-go-v2 v1.41.5/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.5 h1:zWFmPmgw4sveAYi1mRqG+E/g0461cJ5M4bJ8/nc6d3Q=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.5/go.mod h1:nVUlMLVV8ycXSb7mSkcNu9e3v/1TJq2RTlrPwhYWr5c=
github.com/aws/aws-sdk-go-v2/config v1.32.13 h1:5KgbxMaS2coSWRrx9TX/QtWbqzgQkOdEa3sZPhBhCSg=
github.com/aws/aws-sdk-go-v2/config v1.32.13/go.mod h1:8zz7wedqtCbw5e9Mi2doEwDyEgHcEE9YOJp6a8jdSMY=
github.com/aws/aws-sdk-go-v2/credentials v1.19.13 h1:mA59E3fokBvyEGHKFdnpNNrvaR351cqiHgRg+JzOSRI=
github.com/aws/aws-sdk-go-v2/credentials v1.19.13/go.mod h1:yoTXOQKea18nrM69wGF9jBdG4WocSZA1h38A+t/MAsk=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.21 h1:NUS3K4BTDArQqNu2ih7yeDLaS3bmHD0YndtA6UP884g=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.21/go.mod h1:YWNWJQNjKigKY1RHVJCuupeWDrrHjRqHm0N9rdrWzYI=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21 h1:Rgg6wvjjtX8bNHcvi9OnXWwcE0a2vGpbwmtICOsvcf4=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21/go.mod h1:A/kJFst/nm//cyqonihbdpQZwiUhhzpqTsdbhDdRF9c=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21 h1:PEgGVtPoB6NTpPrBgqSE5hE/o47Ij9qk/SEZFbUOe9A=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21/go.mod h1:p+hz+PRAYlY3zcpJhPwXlLC4C+kqn70WIHwnzAfs6ps=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 h1:qYQ4pzQ2Oz6WpQ8T3HvGHnZydA72MnLuFK9tJwmrbHw=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6/go.mod h1:O3h0IK87yXci+kg6flUKzJnWeziQUKciKrLjcatSNcY=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.18 h1:eZioDaZGJ0tMM4gzmkNIO2aAoQd+je7Ug7TkvAzlmkU=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.18/go.mod h1:CCXwUKAJdoWr6/NcxZ+zsiPr6oH/Q5aTooRGYieAyj4=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 h1:5EniKhLZe4xzL7a+fU3C2tfUN4nWIqlLesfrjkuPFTY=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7/go.mod h1:x0nZssQ3qZSnIcePWLvcoFisRXJzcTVvYpAAdYX8+GI=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.10 h1:fJvQ5mIBVfKtiyx0AHY6HeWcRX5LGANLpq8SVR+Uazs=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.10/go.mod h1:Kzm5e6OmNH8VMkgK9t+ry5jEih4Y8whqs+1hrkxim1I=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21 h1:c31//R3xgIJMSC8S6hEVq+38DcvUlgFY0FM6mSI5oto=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21/go.mod h1:r6+pf23ouCB718FUxaqzZdbpYFyDtehyZcmP5KL9FkA=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.18 h1:/A/xDuZAVD2BpsS2fftFRo/NoEKQJ8YTnJDEHBy2Gtg=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.18/go.mod h1:hWe9b4f+djUQGmyiGEeOnZv69dtMSgpDRIvNMvuvzvY=
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.2 h1:M1A9AjcFwlxTLuf0Faj88L8Iqw0n/AJHjpZTQzMMsSc=
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.2/go.mod h1:KsdTV6Q9WKUZm2mNJnUFmIoXfZux91M3sr/a4REX8e0=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.9 h1:QKZH0S178gCmFEgst8hN0mCX1KxLgHBKKY/CLqwP8lg=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.9/go.mod h1:7yuQJoT+OoH8aqIxw9vwF+8KpvLZ8AWmvmUWHsGQZvI=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.14 h1:GcLE9ba5ehAQma6wlopUesYg/hbcOhFNWTjELkiWkh4=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.14/go.mod h1:WSvS1NLr7JaPunCXqpJnWk1Bjo7IxzZXrZi1QQCkuqM=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.18 h1:mP49nTpfKtpXLt5SLn8Uv8z6W+03jYVoOSAl/c02nog=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.18/go.mod h1:YO8TrYtFdl5w/4vmjL8zaBSsiNp3w0L1FfKVKenZT7w=
github.com/aws/aws-sdk-go-v2/service/sts v1.41.10 h1:p8ogvvLugcR/zLBXTXrTkj0RYBUdErbMnAFFp12Lm/U=
github.com/aws/aws-sdk-go-v2/service/sts v1.41.10/go.mod h1:60dv0eZJfeVXfbT1tFJinbHrDfSJ2GZl4Q//OSSNAVw=
github.com/aws/smithy-go v1.24.2 h1:FzA3bu/nt/vDvmnkg+R8Xl46gmzEDam6mZ1hzmwXFng=
github.com/aws/smithy-go v1.24.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
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/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4=
github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
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/cockroachdb/errors v1.12.0 h1:d7oCs6vuIMUQRVbi6jWWWEJZahLCfJpnJSVobd1/sUo=
github.com/cockroachdb/errors v1.12.0/go.mod h1:SvzfYNNBshAVbZ8wzNc/UPK3w1vf0dKDUP41ucAIf7g=
github.com/cockroachdb/fifo v0.0.0-20240816210425-c5d0cb0b6fc0 h1:pU88SPhIFid6/k0egdR5V6eALQYq2qbSmukrkgIh/0A=
github.com/cockroachdb/fifo v0.0.0-20240816210425-c5d0cb0b6fc0/go.mod h1:9/y3cnZ5GKakj/H4y9r9GTjCvAFta7KLgSHPJJYc52M=
github.com/cockroachdb/logtags v0.0.0-20241215232642-bb51bb14a506 h1:ASDL+UJcILMqgNeV5jiqR4j+sTuvQNHdf2chuKj1M5k=
github.com/cockroachdb/logtags v0.0.0-20241215232642-bb51bb14a506/go.mod h1:Mw7HqKr2kdtu6aYGn3tPmAftiP3QPX63LdK/zcariIo=
github.com/cockroachdb/pebble v1.1.5 h1:5AAWCBWbat0uE0blr8qzufZP5tBjkRyy/jWe1QWLnvw=
github.com/cockroachdb/pebble v1.1.5/go.mod h1:17wO9el1YEigxkP/YtV8NtCivQDgoCyBg5c4VR/eOWo=
github.com/cockroachdb/redact v1.1.8 h1:8eVLLj6juKxiKrAEw2b8cJvNqWq++U8WOfQFuL7KTaA=
github.com/cockroachdb/redact v1.1.8/go.mod h1:GceHHpJ0rMDpYARL5In88Alq/xMBUtVlz7Qxix6ZVkw=
github.com/cockroachdb/tokenbucket v0.0.0-20250429170803-42689b6311bb h1:3bCgBvB8PbJVMX1ouCcSIxvsqKPYM7gs72o0zC76n9g=
github.com/cockroachdb/tokenbucket v0.0.0-20250429170803-42689b6311bb/go.mod h1:7nc4anLGjupUW/PeY5qiNYsdNXj7zopG+eqsS7To5IQ=
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/cosmos/go-bip39 v1.0.0 h1:pcomnQdrdH22njcAatO0yWojsUnCO3y2tNoV1cb6hHY=
github.com/cosmos/go-bip39 v1.0.0/go.mod h1:RNJv0H/pOIVgxw6KS7QeX2a0Uo0aKUlfhZ4xuwvCdJw=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=
github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
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/cronokirby/saferith v0.33.0 h1:TgoQlfsD4LIwx71+ChfRcIpjkw+RPOapDEVxa+LhwLo=
github.com/cronokirby/saferith v0.33.0/go.mod h1:QKJhjoqUtBsXCAVEjw38mFqoi7DebT7kthcD7UzbnoA=
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/dchest/siphash v1.2.3 h1:QXwFc8cFOR2dSa/gE6o/HokBMWtLUaNDVd+22aKHeEA=
github.com/dchest/siphash v1.2.3/go.mod h1:0NvQU092bT0ipiFN++/rXm69QG9tVxLAlQHIXMPAkHc=
github.com/deckarep/golang-set/v2 v2.8.0 h1:swm0rlPCmdWn9mESxKOjWk8hXSqoxOp+ZlfuyaAdFlQ=
github.com/deckarep/golang-set/v2 v2.8.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4=
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.4.1 h1:5RVFMOWjMyRy8cARdy79nAmgYw3hK/4HUq48LQ6Wwqo=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40=
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/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/dot v1.11.0 h1:zsrhCuFHAJge/aZIC4N4LdHy5tqYu4tWEaUzIwdYj4Y=
github.com/emicklei/dot v1.11.0/go.mod h1:DeV7GvQtIw4h2u73RKBkkFdvVAz0D9fzeJrgPW6gy/s=
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/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab h1:rvv6MJhy07IMfEKuARQ9TKojGqLVNxQajaXEp/BoqSk=
github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab/go.mod h1:IuLm4IsPipXKF7CW5Lzf68PIbZ5yl7FFd74l/E0o9A8=
github.com/ferranbt/fastssz v1.0.0 h1:9EXXYsracSqQRBQiHeaVsG/KQeYblPf40hsQPb9Dzk8=
github.com/ferranbt/fastssz v1.0.0/go.mod h1:Ea3+oeoRGGLGm5shYAeDgu6PGUlcvQhE2fILyD9+tGg=
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/getsentry/sentry-go v0.44.1 h1:/cPtrA5qB7uMRrhgSn9TYtcEF36auGP3Y6+ThvD/yaI=
github.com/getsentry/sentry-go v0.44.1/go.mod h1:XDotiNZbgf5U8bPDUAfvcFmOnMQQceESxyKaObSssW0=
github.com/gkampitakis/ciinfo v0.3.2 h1:JcuOPk8ZU7nZQjdUhctuhQofk7BGHuIy0c9Ez8BNhXs=
github.com/gkampitakis/ciinfo v0.3.2/go.mod h1:1NIwaOcFChN4fa/B0hEBdAb6npDlFL8Bwx4dfRLRqAo=
github.com/gkampitakis/go-diff v1.3.2 h1:Qyn0J9XJSDTgnsgHRdz9Zp24RaJeKMUHg2+PDZZdC4M=
github.com/gkampitakis/go-diff v1.3.2/go.mod h1:LLgOrpqleQe26cte8s36HTWcTmMEur6OPYerdAAS9tk=
github.com/gkampitakis/go-snaps v0.5.15 h1:amyJrvM1D33cPHwVrjo9jQxX8g/7E2wYdZ+01KS3zGE=
github.com/gkampitakis/go-snaps v0.5.15/go.mod h1:HNpx/9GoKisdhw9AFOBT1N7DBs9DiHo/hGheFGBZ+mc=
github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
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-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
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/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw=
github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs=
github.com/golang/snappy v1.0.0/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/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
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=
github.com/gorilla/rpc v1.2.1/go.mod h1:uNpOihAlF5xRFLuTYhfR0yfCTm0WTQSQttkMSptRfGk=
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo=
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA=
github.com/grandcat/zeroconf v1.0.0 h1:uHhahLBKqwWBV6WZUDAT71044vwOTL+McW0mBJvo6kE=
github.com/grandcat/zeroconf v1.0.0/go.mod h1:lTKmG1zh86XyCoUeIHSA4FJMBwCJiQmGfcP2PdzytEs=
github.com/gtank/merlin v0.1.1 h1:eQ90iG7K9pOhtereWsmyRJ6RAwcP4tHTDBHXNg+u5is=
github.com/gtank/merlin v0.1.1/go.mod h1:T86dnYJhcGOh5BjZFCJWTDeTK7XW8uE+E21Cy/bIQ+s=
github.com/gtank/ristretto255 v0.2.0 h1:LeOuWr6giplWkkMizx2emfG03SRPJqKt1nfIHLVHQ/0=
github.com/gtank/ristretto255 v0.2.0/go.mod h1:OJ1ox/dWcp7sJ5grYDcZ+kkHYuj5nelW5aaL7ESVXBw=
github.com/hanzoai/vfs v0.4.1 h1:erXat8LUZRI+fuw+8XNpslWdAaIrkGPXNGbP3PGQUBU=
github.com/hanzoai/vfs v0.4.1/go.mod h1:8AzyntfZgjK6iiB4A+xPestHgSAGUEHMthTkhACRIjM=
github.com/hanzoai/vfs v0.4.3 h1:QN9SemEQBq9x1l/toi51/TZWctbt3i3mgUJfz5RPALY=
github.com/hanzoai/vfs v0.4.3/go.mod h1:wTHfTpJ/165yz0qfPBNFcYRg+tGw8YDwPu+xgps88zU=
github.com/hanzos3/go-sdk v1.0.2 h1:EOJQGVnwclkzIyRJyWqtqmA2muyaSsF4y+7KYC4Vhdw=
github.com/hanzos3/go-sdk v1.0.2/go.mod h1:oOp/rYVDpZIv2Mn3ZacAUiIahRNOi+V/vlGukvIcuI0=
github.com/hashicorp/go-bexpr v0.1.16 h1:D+fKoGyUzXVS0FdjOX1ws3vIck8DVtBqQ0tsusmYDR8=
github.com/hashicorp/go-bexpr v0.1.16/go.mod h1:HGKbAByHn2aJWUV47gL7+IjLK79iU3EZIbOwCXJZLoE=
github.com/holiman/billy v0.0.0-20250707135307-f2f9b9aae7db h1:IZUYC/xb3giYwBLMnr8d0TGTzPKFGNTCGgGLoyeX330=
github.com/holiman/billy v0.0.0-20250707135307-f2f9b9aae7db/go.mod h1:xTEYN9KCHxuYHs+NmrmzFcnvHMzLLNiGFafCb1n3Mfg=
github.com/holiman/bloomfilter/v2 v2.0.3 h1:73e0e/V0tCydx14a0SCYS/EWCxgwLZ18CZcZKVu0fao=
github.com/holiman/bloomfilter/v2 v2.0.3/go.mod h1:zpoh+gs7qcpqrHr3dB55AMiJwo0iURXE7ZOP9L9hSkA=
github.com/holiman/uint256 v1.3.2 h1:a9EgMPSC1AAaj1SZL5zIQD3WbwTuHrMGOerLjGmM/TA=
github.com/holiman/uint256 v1.3.2/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E=
github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc=
github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus=
github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc=
github.com/joshdk/go-junit v1.0.0 h1:S86cUKIdwBHWwA6xCmFlf3RTLfVXYQfvanM5Uh+K6GE=
github.com/joshdk/go-junit v1.0.0/go.mod h1:TiiV0PqkaNfFXjEiyjWM3XXrhVyCa1K4Zfga6W52ung=
github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao=
github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
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.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
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.0 h1:mZcR55GUYseKxeEFCY5EHO51tAhkPDry4LBEn4nnllY=
github.com/luxfi/accel v1.2.0/go.mod h1:K00BcnLzEYMPHwCFq8Tf/450ApmTs9xBVvYOnofJCkc=
github.com/luxfi/corona v0.8.0 h1:NwDafcJ6ySXdKmBbfch0hL83uTwu5NBedirOPnyhCMk=
github.com/luxfi/corona v0.8.0/go.mod h1:SfS7xo/k4uoteEYwYy+QCMPzTU8EIEbLnbWKx5ENVCw=
github.com/luxfi/corona v0.10.2 h1:pIS5uJq2lHnk6NFjoaBN8CvjPp9wwFHek91rV0u3jHk=
github.com/luxfi/corona v0.10.2/go.mod h1:gx0nrRq4TB/scQm1kNU10lyRDQhKLMAYK1fIrfCrZ9s=
github.com/luxfi/crypto v1.19.17 h1:l2LLu7UFyICtJVfraLDLRi+lFGiDXKHSL18M9/m1gsQ=
github.com/luxfi/crypto v1.19.17/go.mod h1:INjdZtke85k8hX/QAmTMAY8bbZ4gzGZQLqURg3xf6Gk=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/leanovate/gopter v0.2.11 h1:vRjThO1EKPb/1NsDXuDrzldR28RLkBflWYcU9CvzWu4=
github.com/leanovate/gopter v0.2.11/go.mod h1:aK3tzZP/C+p1m3SPRE4SYZFGP7jjkuSI4f7Xvpt0S9c=
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/age v1.6.0 h1:KMD8gSOP4NVCb7NWSlRcgBZNV2xm2a+qQWPyPmiX6f4=
github.com/luxfi/age v1.6.0/go.mod h1:7cu9CIyikgyAvr5MlXFapEDQ15yBaHOSdKkK5lG04WE=
github.com/luxfi/atomic v1.0.0 h1:xUV60MuzRvXngaQ1sM0yVC2v4TRoLlUGkkH7M9PS4yw=
github.com/luxfi/atomic v1.0.0/go.mod h1:0G2mTlQ6TXWHICUHrUUPu1/qAiIyR4gSZ2tva9ci/bI=
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/compress v0.1.1 h1:cQjRYQFRrw8HinjW1T8FmKgqdRwrFYDCdecc1t0i+uQ=
github.com/luxfi/compress v0.1.1/go.mod h1:d4CkRRmwPzafIp57Jxra3RmAmNwNwU8Oc0QBBRlTqGo=
github.com/luxfi/concurrent v0.1.1 h1:LkAmNXybOsAm97nFILqMDBp/YLgleuLmyGomFT7YNxU=
github.com/luxfi/concurrent v0.1.1/go.mod h1:GlkfiJtPBpatNxvROf7hkPi4gsnmdHGmqhDnVWFrkZE=
github.com/luxfi/consensus v1.36.2 h1:IbeWQF1wEcA/MNxe4fKniSICPndAGEysV/A3DZGMXFM=
github.com/luxfi/consensus v1.36.2/go.mod h1:m4aMFbKYxwM2X4oPYRjq4LmDSShfCgAHnhmxIGKDyFU=
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.2.1 h1:MTnfKXzS5+oxV5jKZerdOxSA6iMPaQI9/FWGufizzaw=
github.com/luxfi/container v0.2.1/go.mod h1:B+uM0wP0lGvt/SSK7QOEn/qBcsHzILVHlKikdCyzSgM=
github.com/luxfi/corona v0.10.4 h1:/+Uy5iOWBMkr+XACnRRiyrbb5ebsZLsUXjHfJW3sFyw=
github.com/luxfi/corona v0.10.4/go.mod h1:44Tjnm2uRG22kmmLfCzR8QEO8OXZ3jR/OnUKLsnjVJ8=
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/database v1.21.1 h1:GNnoWVa82l+n2dK7x2aG8LR2NEToq6ZCRX0sQjmK0OM=
github.com/luxfi/database v1.21.1/go.mod h1:Gc7Z2OPrrcYLnAL8B1trOnguXauOlSDV5tkviLN6Xec=
github.com/luxfi/dkg v0.3.5 h1:s2L2mMQaz+n9m0b0ghvoV5VZNxiwb2z4WrGugvK0udY=
github.com/luxfi/dkg v0.3.5/go.mod h1:M+WH7GFRN+YUD851Rlnumdp0Md98kplNN8pVx65U8I8=
github.com/luxfi/geth v1.20.1 h1:QUGQr4AKvADjwMi7t8a0OfoyxShgEcI9pwie1jFYfm0=
github.com/luxfi/geth v1.20.1/go.mod h1:GV5bIMEgWviRN+jPXERyVpI16H3iHqPcdIokDoZdrvU=
github.com/luxfi/ids v1.3.2 h1:c6Rft5kZB4XqiCtWaGH47bfhaNFm3FGRfhEzI01GVeI=
github.com/luxfi/ids v1.3.2/go.mod h1:+5l8cYMbKpORJbQ2r98CYJo9TQATgUdnmzpYFZWMwwc=
github.com/luxfi/lattice/v7 v7.1.4 h1:hQR02M6cHTAV5+joOPi9gb9Gm+z/hKJnhJF4IlciIJs=
github.com/luxfi/lattice/v7 v7.1.4/go.mod h1:DmIQFi3mJiehVsR235l1NKYEU0JhU649OX5p7gMEW2c=
github.com/luxfi/lens v0.1.4 h1:goGjGDXx2BNdjzXDunL5QT8elK2ZyCcc0z8TAbtWYrg=
github.com/luxfi/lens v0.1.4/go.mod h1:mL+G8IK+9L41d78/2FYRgfhEzAjcr5+VEXB8SGuHbus=
github.com/luxfi/lens v0.2.1 h1:5Qd0GdjbM+XUVgwDbZ452tKkR7yeE8QnBTHHaH8fJNY=
github.com/luxfi/lens v0.2.1/go.mod h1:6FIhC8weEE5RbNMF3SaE+XPSB9cr6FmjypYBoHkz4JQ=
github.com/luxfi/log v1.4.3 h1:xkUKRWvQ4ZwvlUC2e0/RTtHYZOYSMvSQ9W9lbjwBmiI=
github.com/luxfi/log v1.4.3/go.mod h1:myIkufyiQomSQH34K981kbz6cG4WUoerRUh7F4XhlQI=
github.com/luxfi/magnetar v1.2.0 h1:bsxHmBnJiswc/A6ElQ0pWz5g6ogqewIEKKqR26VgizA=
github.com/luxfi/magnetar v1.2.0/go.mod h1:7J9YP9jByWbwCjssMFJNUkTU8tcPlSUoVSSiYShtvFs=
github.com/luxfi/math v1.4.1 h1:1t9bCCsEqnl9yIKrShlbs80DBKyYTWdnzkVfBqEeO7Q=
github.com/luxfi/math v1.4.1/go.mod h1:QvbRxauQyE1w4lvbcLSe6c8yeJz2Zj1Bq1rayGgs2tA=
github.com/luxfi/magnetar v1.2.3/go.mod h1:z9PLkqzzYiaFGT/qFBQSnNoHmZrg8y7JlYGiNnHAAdk=
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.7 h1:LoSPEUpak2SLcynF+LT2cXjl9ECp4nY+Lia9zudmDv4=
github.com/luxfi/metric v1.5.7/go.mod h1:CMguEhyuLi4YUWyXimJ+UHply99BDFrL0pxedB7rBqM=
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/mlwe v0.2.1 h1:pRwTjNUUtzUxRIlMbUPpeh9DE2/NdqfS17hfdogazp4=
github.com/luxfi/mlwe v0.2.1/go.mod h1:DD9EHTeiyh/y0KGGeqL+q9S4n8raeGiGdaG/BQPAvT0=
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/pulsar v1.2.0 h1:cdJOoaRsCz/lLPyKQSLBFfXbJpqx2CQU/wXxqQeAXwA=
github.com/luxfi/pulsar v1.2.0/go.mod h1:GPm+Q9ZdX604GE687vkBQhWNA1FOBvRbhYwhbbfdi2w=
github.com/luxfi/pulsar v1.7.1 h1:q/TVn0TWeMImm67EJZ3CMmBAGQCW+bhIX7YQGL3aYK0=
github.com/luxfi/pulsar v1.7.1/go.mod h1:wW+V2mUVOqppN68+n5u9Qtlfcscd0vOz4acvubcSOKI=
github.com/luxfi/pulsar v1.9.0 h1:c0JnatYF79aN87aof9VlYjIoCzmixxrgNPeUUuh8ScU=
github.com/luxfi/pulsar v1.9.0/go.mod h1:1+/atAiiiOm9RnXM3c66eHF3garjAa3C+sn4rAU7JUU=
github.com/luxfi/zap v0.8.1 h1:IzYWf4T35QjsUJhoNbuhew8MDDRYmlZUVx4gKBNLjMA=
github.com/luxfi/zap v0.8.1/go.mod h1:gUoQ/V9nTLLIb4/n0bY/L2zQDYK6pDBG121rK4t8bpg=
github.com/luxfi/mlwe v0.3.0 h1:5mtXLbO2RxaE45r75sj43c6UdpjDKQ5nTQcOGuoRQT8=
github.com/luxfi/mlwe v0.3.0/go.mod h1:DD9EHTeiyh/y0KGGeqL+q9S4n8raeGiGdaG/BQPAvT0=
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/p2p v1.22.1 h1:M59Iy+FIJta99aFfTpG6EE70jm9uqIX+iHrGBYPHDhg=
github.com/luxfi/p2p v1.22.1/go.mod h1:FHOSavVcq8JS1ZQtfddd9Jj1gaPstz1cjvNgczAQRyg=
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/precompile v0.19.3 h1:ljJholS+9mlR8EjbfM96BLYvIni7xCt7KkX06YzAIJU=
github.com/luxfi/precompile v0.19.3/go.mod h1:c+dh+FWfpKr3FNfI4LhIdnn0naQmmS5Yz0KKTt4LknY=
github.com/luxfi/pulsar v1.9.2 h1:pFLoAfBlCwFZfchqHn78J6yMe29AhaoKGRa/KTUP8CE=
github.com/luxfi/pulsar v1.9.2/go.mod h1:uTOtribcUvTTwAOy0Ztg0S2AUiNAsVqFfopTrKW+zjM=
github.com/luxfi/runtime v1.3.1 h1:vsQZ3sl6XMeyHuNGCMM86d0whrE/lhMre4CCJaClPdE=
github.com/luxfi/runtime v1.3.1/go.mod h1:Tct998uUcmCQbUC1WLEeGLDH2IaVgMb+SkKcHKZpHV0=
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/utils v1.3.1 h1:Us02ag60kGu94B41XIelExa7c+K6zPKwDJyq+eB+hc0=
github.com/luxfi/utils v1.3.1/go.mod h1:ROZrzpt6Kx8ttS1mo12oqsOzRB088GQ1h9jXEoDDpNA=
github.com/luxfi/validators v1.3.1 h1:+/7j0CTXlMKyaSLFM+gd6Fq64/edORJfrD/xGnf7Xcg=
github.com/luxfi/validators v1.3.1/go.mod h1:sIQyUZOvXoJ/9/RCOhHSzjumZIoxiqacNOn5mQWVozU=
github.com/luxfi/version v1.0.1 h1:T/1KYWEMmsrNQk7pN7PFPAwh/7XbeX7cFAKLBqI37Sk=
github.com/luxfi/version v1.0.1/go.mod h1:Y5fPkQ2DB0XRBCxgSPXp4ISzL1/jptKnmFknShRJCyg=
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/warp v1.24.1 h1:9F+z6fy4sxmXp+3LlR9m3TiZ8Dfthh+s5KOeewSJL30=
github.com/luxfi/warp v1.24.1/go.mod h1:kRGQDt6EB3oRLYo71aXqnmJuCBVfND3oQ5/2miaLoyg=
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/luxfi/zapdb v1.10.1 h1:XV3k4UTTKKxUMgbfC7woPXgUEIJd3P5nj2lGTQ88xeE=
github.com/luxfi/zapdb v1.10.1/go.mod h1:3Y0hH2A9kvjR+Bp9N2yEbtHnhXGHhqCQOLvBRkHrrM0=
github.com/maruel/natural v1.1.1 h1:Hja7XhhmvEFhcByqDoHz9QZbkWey+COd9xWfCfn1ioo=
github.com/maruel/natural v1.1.1/go.mod h1:v+Rfd79xlw1AgVBjbO0BEQmptqb5HvL/k9GRHB7ZKEg=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.21 h1:xYae+lCNBP7QuW4PUnNG61ffM4hVIfm+zUzDuSzYLGs=
github.com/mattn/go-isatty v0.0.21/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
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=
github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4=
github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
github.com/mfridman/tparse v0.18.0 h1:wh6dzOKaIwkUGyKgOntDW4liXSo37qg5AXbIhkMV3vE=
github.com/mfridman/tparse v0.18.0/go.mod h1:gEvqZTuCgEhPbYk/2lS3Kcxg1GmTxxU7kTC8DvP0i/A=
github.com/miekg/dns v1.1.27/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
@@ -106,33 +291,90 @@ github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDETo
github.com/mimoo/StrobeGo v0.0.0-20181016162300-f8f6d4d2b643/go.mod h1:43+3pMjjKimDBf5Kr4ZFNGbLql1zKkbImw+fZbw3geM=
github.com/mimoo/StrobeGo v0.0.0-20220103164710-9a04d6ca976b h1:QrHweqAtyJ9EwCaGHBu1fghwxIPiopAHV06JlXrMHjk=
github.com/mimoo/StrobeGo v0.0.0-20220103164710-9a04d6ca976b/go.mod h1:xxLb2ip6sSUts3g1irPVHyk/DGslwQsNOo9I7smJfNU=
github.com/minio/crc64nvme v1.1.1 h1:8dwx/Pz49suywbO+auHCBpCtlW1OfpcLN7wYgVR6wAI=
github.com/minio/crc64nvme v1.1.1/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg=
github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
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/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM=
github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8=
github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/pointerstructure v1.2.1 h1:ZhBBeX8tSlRpu/FFhXH4RC4OJzFlqsQhoHZAz4x7TIw=
github.com/mitchellh/pointerstructure v1.2.1/go.mod h1:BRAsLI5zgXmw97Lf6s25bs8ohIXc3tViBH44KcwB2g4=
github.com/montanaflynn/stats v0.9.0 h1:tsBJ0RXwph9BmAuFoCmqGv6e8xa0MENQ8m0ptKq29mQ=
github.com/montanaflynn/stats v0.9.0/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
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/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.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/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s=
github.com/pion/dtls/v2 v2.2.12 h1:KP7H5/c1EiVAAKUmXyCzPiQe5+bCJrpOeKg/L05dunk=
github.com/pion/dtls/v2 v2.2.12/go.mod h1:d9SYc9fch0CqK90mRk1dC7AkzzpwJj6u2GU3u+9pqFE=
github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8=
github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so=
github.com/pion/stun/v2 v2.0.0 h1:A5+wXKLAypxQri59+tmQKVs7+l6mMM+3d+eER9ifRU0=
github.com/pion/stun/v2 v2.0.0/go.mod h1:22qRSh08fSEttYUmJZGlriq9+03jtVmXNODgLccj8GQ=
github.com/pion/transport/v2 v2.2.1/go.mod h1:cXXWavvCnFF6McHTft3DWS9iic2Mftcz1Aq29pGcU5g=
github.com/pion/transport/v2 v2.2.4/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0=
github.com/pion/transport/v2 v2.2.10 h1:ucLBLE8nuxiHfvkFKnkDQRYWYfp8ejf4YBOPfaQpw6Q=
github.com/pion/transport/v2 v2.2.10/go.mod h1:sq1kSLWs+cHW9E+2fJP95QudkzbK7wscs8yYgQToO5E=
github.com/pion/transport/v3 v3.0.1/go.mod h1:UY7kiITrlMv7/IKgd5eTUcaahZx5oUN3l9SzK5f5xE0=
github.com/pion/transport/v3 v3.1.1 h1:Tr684+fnnKlhPceU+ICdrw6KKkTms+5qHMgw6bIkYOM=
github.com/pion/transport/v3 v3.1.1/go.mod h1:+c2eewC5WJQHiAA46fkMMzoYZSuGzA/7E2FPrOYHctQ=
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/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=
github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc=
github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo=
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/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA=
github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI=
github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
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/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
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.20220721030215-126854af5e6d h1:vfofYNRScrDdvS342BElfbETmL1Aiz3i2t0zfRj16Hs=
github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d/go.mod h1:RRCYJbIwD5jmqPI9XoAFR0OcDxqUctll6zUj/+B4S48=
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
@@ -141,47 +383,133 @@ github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
github.com/tinylib/msgp v1.6.4 h1:mOwYbyYDLPj35mkA2BjjYejgJk9BuHxDdvRnb6v2ZcQ=
github.com/tinylib/msgp v1.6.4/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA=
github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA=
github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI=
github.com/tklauser/go-sysconf v0.4.0 h1:7H0uAN+7RkwWRaxhYXDLqa5V3LPrJeV8wmD9dRUgPQU=
github.com/tklauser/go-sysconf v0.4.0/go.mod h1:8mTNWyog7H+MpKijp4VmKJAd2bbYQ2zuUwkYRbUArPI=
github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw=
github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ=
github.com/tklauser/numcpus v0.12.0/go.mod h1:ABHeXzJnr/qqwguhClkZKT1/8VABcYrsyUiUGobwWJg=
github.com/urfave/cli/v2 v2.27.7 h1:bH59vdhbjLv3LAvIu6gd0usJHgoTTPhCFib8qqOwXYU=
github.com/urfave/cli/v2 v2.27.7/go.mod h1:CyNAG/xg+iAOg0N4MPGZqVmv2rCoP267496AOXUZjA4=
github.com/wlynxg/anet v0.0.3/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU=
github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
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/xrash/smetrics v0.0.0-20250705151800-55b8f293f342 h1:FnBeRrxr7OU4VvAzt5X7s6266i6cSVkkFPS0TuXWbIg=
github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ=
github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
github.com/zeebo/blake3 v0.2.4 h1:KYQPkhpRtcqh0ssGYcKLG1JYvddkEA8QwCM/yBqhaZI=
github.com/zeebo/blake3 v0.2.4/go.mod h1:7eeQ6d2iXWRGF6npfaxl2CU+xy2Fjo2gxeyZGCRUjcE=
github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo=
github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4=
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 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
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 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=
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 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
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=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988=
golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc=
golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90 h1:jiDhWWeC7jfWqR9c/uplMOqJ0sbNlNWv0UkzE0vX1MA=
golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90/go.mod h1:xE1HEv6b+1SCZ5/uscMRjUBKtIxworgEcEi+/n9NQDQ=
golang.org/x/exp v0.0.0-20260529124908-c761662dc8c9 h1:4d4PbuBNwaxMXkXI8yiIYjydtMU+04RHeuSxJdgKftM=
golang.org/x/exp v0.0.0-20260529124908-c761662dc8c9/go.mod h1:d2fgXJLVs4dYDHUk5lwMIfzRzSrWCfGZb0ZqeLa/Vcw=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4=
golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
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/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
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.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
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=
golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8=
golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
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=
@@ -189,6 +517,8 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntN
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
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=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
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=
+25 -28
View File
@@ -23,18 +23,29 @@ type Harness struct {
logger log.Logger
registry metric.Registry
// timeout bounds both the harness context and each handler's own
// ProtocolTimeout. Keeping one value for both is what makes WithTimeout
// mean anything: the handler config used to hard-code 5 minutes, so
// raising the harness timeout moved the deadline that fired but not the
// one inside the handler, and a slow protocol (CGGMP21 DKG under -race)
// still died at five minutes with a misleading error.
timeout time.Duration
mu sync.RWMutex
handlers map[party.ID]*protocol.Handler
results map[party.ID]interface{}
errors map[party.ID]error
}
// DefaultTimeout bounds a harness run unless WithTimeout raises it.
const DefaultTimeout = 5 * time.Minute
// NewHarness creates a new test harness with proper context management.
// Default 5-minute timeout matches the per-handler ProtocolTimeout below;
// `go test ./...` runs many packages in parallel and harness wall-clock can
// stretch under CPU contention even when the protocol itself takes <1s.
func NewHarness(t testing.TB, partyIDs []party.ID) *Harness {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
ctx, cancel := context.WithTimeout(context.Background(), DefaultTimeout)
h := &Harness{
t: t,
@@ -43,6 +54,7 @@ func NewHarness(t testing.TB, partyIDs []party.ID) *Harness {
network: NewNetwork(partyIDs),
logger: log.NewTestLogger(log.InfoLevel),
registry: metric.NewRegistry(),
timeout: DefaultTimeout,
handlers: make(map[party.ID]*protocol.Handler),
results: make(map[party.ID]interface{}),
errors: make(map[party.ID]error),
@@ -57,10 +69,13 @@ func NewHarness(t testing.TB, partyIDs []party.ID) *Harness {
return h
}
// WithTimeout sets a custom timeout for the harness context
// WithTimeout sets the deadline for the harness context and for every handler
// created afterwards. Call it before CreateHandler; handlers already created
// keep the timeout they were built with.
func (h *Harness) WithTimeout(timeout time.Duration) *Harness {
h.cancel() // Cancel old context
h.ctx, h.cancel = context.WithTimeout(context.Background(), timeout)
h.timeout = timeout
return h
}
@@ -86,7 +101,7 @@ func (h *Harness) CreateHandler(id party.ID, startFunc protocol.StartFunc, sessi
PriorityBuffer: 1000,
MessageTimeout: 30 * time.Second,
RoundTimeout: 60 * time.Second,
ProtocolTimeout: 5 * time.Minute, // Don't let handler create its own timeout
ProtocolTimeout: h.timeout, // the harness owns the deadline; see WithTimeout
}
// Use a context without timeout - the harness manages timeouts
@@ -262,34 +277,16 @@ func (h *Harness) Cleanup() {
h.mu.Unlock()
}
// RunProtocol is a convenience function to run a protocol with all parties
// RunProtocol is a convenience function to run a protocol with all parties,
// bounded by DefaultTimeout.
func RunProtocol(t testing.TB, partyIDs []party.ID, sessionID []byte, createStart func(party.ID) protocol.StartFunc) (map[party.ID]interface{}, error) {
harness := NewHarness(t, partyIDs)
// If no session ID provided, generate a unique one
if sessionID == nil {
sessionID = []byte(fmt.Sprintf("session-%d-%d", time.Now().UnixNano(), rand.Int63()))
}
// Create handlers for all parties
for _, id := range partyIDs {
startFunc := createStart(id)
_, err := harness.CreateHandler(id, startFunc, sessionID)
if err != nil {
return nil, fmt.Errorf("failed to create handler for party %s: %w", id, err)
}
}
// Run the protocol
if err := harness.Run(); err != nil {
return nil, err
}
// Return all results
return harness.Results(), nil
return RunProtocolWithTimeout(t, partyIDs, sessionID, DefaultTimeout, createStart)
}
// RunProtocolWithTimeout runs a protocol with a custom timeout
// RunProtocolWithTimeout is RunProtocol with an explicit deadline, for
// protocols that legitimately need longer than DefaultTimeout — a full
// CGGMP21 key generation builds Paillier moduli and their ring-mod proofs for
// every party, which under the race detector runs well past five minutes.
func RunProtocolWithTimeout(t testing.TB, partyIDs []party.ID, sessionID []byte, timeout time.Duration, createStart func(party.ID) protocol.StartFunc) (map[party.ID]interface{}, error) {
harness := NewHarness(t, partyIDs).WithTimeout(timeout)
+123
View File
@@ -0,0 +1,123 @@
// Package quorum expresses a threshold-signing policy as the value operators
// actually reason about: k signers out of n parties — "3-of-5".
//
// # Why this package exists
//
// The threshold protocols in this module (CGGMP21 in protocols/cmp, FROST in
// protocols/frost) are both parameterised by t, the number of corruptions
// tolerated. Both require t+1 shares to produce a signature. Operators,
// dashboards, runbooks and Safe policies instead speak in k-of-n, where k is
// the number of signers that must cooperate.
//
// Those are two different numbers for the same policy, and the off-by-one
// between them is silent: provisioning a "3-of-5" wallet by passing
// threshold=3, parties=5 to a protocol that reads threshold as t produces a
// 4-of-5 wallet. Nothing errors. The mistake only surfaces when three
// custodians hold a signing ceremony and cannot produce a signature — which,
// for a key that already controls funds, is an outage with no recovery path
// short of a resharing ceremony with a party that may no longer exist.
//
// Policy is therefore the single place k and t are allowed to meet. Callers
// state the policy they mean (k-of-n) and hand [Policy.Degree] to the
// protocol. Nothing else in the stack should compute k-1 or t+1 by hand.
package quorum
import (
"fmt"
"strconv"
"strings"
)
// Policy is a signing policy: K signers, drawn from N parties, are required
// and sufficient to produce one signature.
//
// The zero Policy is not valid; construct with [New] or [Parse].
type Policy struct {
// K is the number of parties that must cooperate to sign.
K int
// N is the total number of parties holding a share.
N int
}
// New returns the k-of-n policy, or an error explaining why it is not a
// policy anyone should deploy.
//
// k == 1 is rejected: a 1-of-n key is a single key with n copies of the
// secret-recovery capability, which is strictly worse than one key, not
// better. Callers that genuinely want single-party signing should use a
// single-party signer rather than a degenerate threshold configuration.
func New(k, n int) (Policy, error) {
switch {
case n < 2:
return Policy{}, fmt.Errorf("quorum: n = %d, want at least 2 parties", n)
case k < 2:
return Policy{}, fmt.Errorf("quorum: k = %d, want at least 2 signers (k=1 is not a threshold policy)", k)
case k > n:
return Policy{}, fmt.Errorf("quorum: k = %d exceeds n = %d; the policy could never be satisfied", k, n)
}
return Policy{K: k, N: n}, nil
}
// MustNew is [New] for policies fixed at compile time. It panics on an
// invalid policy.
func MustNew(k, n int) Policy {
p, err := New(k, n)
if err != nil {
panic(err)
}
return p
}
// Degree returns t, the polynomial degree and corruption bound the underlying
// protocols are parameterised by. It is the value to pass as the `threshold`
// argument of cmp.Keygen and frost.Keygen, and the value stored in
// cmp/config.Config.Threshold and frost/keygen.Config.Threshold.
//
// t = K-1: any K shares reconstruct, any t = K-1 do not.
func (p Policy) Degree() int { return p.K - 1 }
// Tolerance returns how many of the N parties may be unavailable while the
// policy can still be satisfied: N-K.
//
// Note that this is availability, not security. The security bound — how many
// parties may be actively corrupted without forging a signature — is
// [Policy.Degree]. For 3-of-5 those coincide at 2; for 4-of-5 they do not
// (tolerance 1, corruption bound 3).
func (p Policy) Tolerance() int { return p.N - p.K }
// Valid reports whether p is a deployable policy.
func (p Policy) Valid() bool {
_, err := New(p.K, p.N)
return err == nil
}
// String renders the policy the way operators write it: "3-of-5".
func (p Policy) String() string { return strconv.Itoa(p.K) + "-of-" + strconv.Itoa(p.N) }
// FromDegree reconstructs the policy of a key already generated with
// polynomial degree t shared among n parties — the inverse of [Policy.Degree],
// for reading back an existing cmp or frost config.
func FromDegree(t, n int) (Policy, error) {
return New(t+1, n)
}
// Parse reads the operator form, "3-of-5". It accepts surrounding space and
// an "of" separator with or without hyphens ("3 of 5"), because those are the
// forms that show up in runbooks and dashboards.
func Parse(s string) (Policy, error) {
norm := strings.ToLower(strings.TrimSpace(s))
norm = strings.ReplaceAll(norm, "-", " ")
fields := strings.Fields(norm)
if len(fields) != 3 || fields[1] != "of" {
return Policy{}, fmt.Errorf("quorum: cannot parse %q, want the form \"3-of-5\"", s)
}
k, err := strconv.Atoi(fields[0])
if err != nil {
return Policy{}, fmt.Errorf("quorum: cannot parse %q: bad signer count %q", s, fields[0])
}
n, err := strconv.Atoi(fields[2])
if err != nil {
return Policy{}, fmt.Errorf("quorum: cannot parse %q: bad party count %q", s, fields[2])
}
return New(k, n)
}
+137
View File
@@ -0,0 +1,137 @@
package quorum_test
import (
"testing"
"github.com/luxfi/threshold/pkg/quorum"
)
// The whole point of this package is that "3-of-5" means three signers, and
// the protocol degree for it is 2. If this test ever fails, every k-of-n
// wallet provisioned through the stack is off by one.
func TestPolicyDegreeIsKMinusOne(t *testing.T) {
for _, tc := range []struct {
k, n int
wantDegree int
wantTolerance int
wantString string
}{
{2, 3, 1, 1, "2-of-3"},
{3, 5, 2, 2, "3-of-5"},
{4, 5, 3, 1, "4-of-5"},
{4, 7, 3, 3, "4-of-7"},
{5, 5, 4, 0, "5-of-5"},
} {
p := quorum.MustNew(tc.k, tc.n)
if got := p.Degree(); got != tc.wantDegree {
t.Errorf("%s: Degree() = %d, want %d", p, got, tc.wantDegree)
}
if got := p.Tolerance(); got != tc.wantTolerance {
t.Errorf("%s: Tolerance() = %d, want %d", p, got, tc.wantTolerance)
}
if got := p.String(); got != tc.wantString {
t.Errorf("String() = %q, want %q", got, tc.wantString)
}
}
}
// The 3-of-5 case specifically, spelled out, because it is the policy the
// treasury migration is designed around.
func TestPolicy3of5(t *testing.T) {
p := quorum.MustNew(3, 5)
if p.Degree() != 2 {
t.Fatalf("3-of-5 degree = %d, want 2 (a degree-3 key would need 4 signers)", p.Degree())
}
if p.Tolerance() != 2 {
t.Fatalf("3-of-5 tolerance = %d, want 2", p.Tolerance())
}
}
func TestNewRejectsUndeployablePolicies(t *testing.T) {
for _, tc := range []struct{ name string; k, n int }{
{"k exceeds n", 6, 5},
{"k=1 is not a threshold policy", 1, 5},
{"k=0", 0, 5},
{"negative k", -1, 5},
{"n too small", 2, 1},
{"zero policy", 0, 0},
} {
t.Run(tc.name, func(t *testing.T) {
if _, err := quorum.New(tc.k, tc.n); err == nil {
t.Fatalf("New(%d, %d) succeeded, want error", tc.k, tc.n)
}
if (quorum.Policy{K: tc.k, N: tc.n}).Valid() {
t.Fatalf("Policy{%d,%d}.Valid() = true, want false", tc.k, tc.n)
}
})
}
}
// FromDegree is how an already-generated cmp/frost config reports its policy.
// It must round-trip with Degree so a stored key reads back as the policy it
// was created with.
func TestFromDegreeRoundTrips(t *testing.T) {
for n := 2; n <= 9; n++ {
for k := 2; k <= n; k++ {
p := quorum.MustNew(k, n)
back, err := quorum.FromDegree(p.Degree(), p.N)
if err != nil {
t.Fatalf("FromDegree(%d, %d): %v", p.Degree(), p.N, err)
}
if back != p {
t.Fatalf("round-trip: got %s, want %s", back, p)
}
}
}
}
func TestParse(t *testing.T) {
for _, tc := range []struct {
in string
want quorum.Policy
wantErr bool
}{
{in: "3-of-5", want: quorum.MustNew(3, 5)},
{in: " 3-of-5 ", want: quorum.MustNew(3, 5)},
{in: "3 of 5", want: quorum.MustNew(3, 5)},
{in: "3-OF-5", want: quorum.MustNew(3, 5)},
{in: "2-of-3", want: quorum.MustNew(2, 3)},
{in: "6-of-5", wantErr: true},
{in: "1-of-5", wantErr: true},
{in: "3/5", wantErr: true},
{in: "three-of-five", wantErr: true},
{in: "", wantErr: true},
} {
got, err := quorum.Parse(tc.in)
if tc.wantErr {
if err == nil {
t.Errorf("Parse(%q) = %s, want error", tc.in, got)
}
continue
}
if err != nil {
t.Errorf("Parse(%q): %v", tc.in, err)
continue
}
if got != tc.want {
t.Errorf("Parse(%q) = %s, want %s", tc.in, got, tc.want)
}
}
}
// Parse and String are inverses, so a policy can survive a round trip through
// a config file or a dashboard string.
func TestParseStringRoundTrip(t *testing.T) {
for n := 2; n <= 9; n++ {
for k := 2; k <= n; k++ {
p := quorum.MustNew(k, n)
back, err := quorum.Parse(p.String())
if err != nil {
t.Fatalf("Parse(%q): %v", p.String(), err)
}
if back != p {
t.Fatalf("round-trip: %s -> %q -> %s", p, p.String(), back)
}
}
}
}
+33 -25
View File
@@ -12,35 +12,40 @@ import (
"github.com/luxfi/threshold/pkg/math/curve"
"github.com/luxfi/threshold/pkg/party"
"github.com/luxfi/threshold/pkg/pool"
"github.com/luxfi/threshold/pkg/quorum"
"github.com/luxfi/threshold/protocols/cmp"
"github.com/stretchr/testify/require"
)
// TestCMPThresholdPerformance tests CMP with exactly T+1 parties (minimum required)
func TestCMPThresholdPerformance(t *testing.T) {
// Stated as quorum policies, not bare degrees. The names used to be
// written out by hand alongside a raw `threshold` field that cmp.Keygen
// reads as the polynomial DEGREE, so the case labelled "3-of-5" passed
// degree 3 and signed with 4 parties — it was a 4-of-5. Deriving both the
// degree and the signer count from one quorum.Policy makes the label and
// the behaviour the same fact.
testCases := []struct {
name string
n int
threshold int
timeout time.Duration
policy quorum.Policy
timeout time.Duration
}{
{"2-of-3", 3, 2, 5 * time.Second},
{"3-of-5", 5, 3, 8 * time.Second},
{"4-of-7", 7, 4, 10 * time.Second},
{quorum.MustNew(2, 3), 5 * time.Second},
{quorum.MustNew(3, 5), 8 * time.Second},
{quorum.MustNew(4, 7), 10 * time.Second},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// Use exactly T+1 parties for signing (minimum required)
allParties := test.PartyIDs(tc.n)
signingParties := allParties[:tc.threshold+1]
t.Run(tc.policy.String(), func(t *testing.T) {
// Exactly K parties sign — the minimum the policy allows.
allParties := test.PartyIDs(tc.policy.N)
signingParties := allParties[:tc.policy.K]
pl := pool.NewPool(0)
defer pl.TearDown()
// Quick keygen initialization test
for _, id := range signingParties {
startFunc := cmp.Keygen(curve.Secp256k1{}, id, allParties, tc.threshold, pl)
startFunc := cmp.Keygen(curve.Secp256k1{}, id, allParties, tc.policy.Degree(), pl)
require.NotNil(t, startFunc, "Keygen should initialize for party %s", id)
// Test that start function creates a valid round
@@ -49,7 +54,8 @@ func TestCMPThresholdPerformance(t *testing.T) {
require.NotNil(t, round)
}
t.Logf("%s: CMP threshold performance test passed with %d signers", tc.name, len(signingParties))
t.Logf("%s: CMP threshold performance test passed with %d signers at degree %d",
tc.policy, len(signingParties), tc.policy.Degree())
})
}
}
@@ -167,19 +173,21 @@ func TestCMPSubsetSigners(t *testing.T) {
// TestCMPPerformanceScaling tests performance with increasing party counts
func TestCMPPerformanceScaling(t *testing.T) {
// As above: the policy is the stated value, the degree is derived from it.
// Printing a raw degree as if it were the signer count is how "3-of-5"
// ends up naming a 4-of-5 configuration.
testCases := []struct {
n int
threshold int
maxTime time.Duration
policy quorum.Policy
maxTime time.Duration
}{
{3, 2, 1 * time.Second},
{5, 3, 2 * time.Second},
{7, 5, 3 * time.Second},
{quorum.MustNew(2, 3), 1 * time.Second},
{quorum.MustNew(3, 5), 2 * time.Second},
{quorum.MustNew(5, 7), 3 * time.Second},
}
for _, tc := range testCases {
t.Run(fmt.Sprintf("%d-of-%d", tc.threshold, tc.n), func(t *testing.T) {
partyIDs := test.PartyIDs(tc.n)
t.Run(tc.policy.String(), func(t *testing.T) {
partyIDs := test.PartyIDs(tc.policy.N)
pl := pool.NewPool(0)
defer pl.TearDown()
@@ -188,7 +196,7 @@ func TestCMPPerformanceScaling(t *testing.T) {
// Initialize keygen for all parties
var initTime time.Duration
for _, id := range partyIDs {
startFunc := cmp.Keygen(curve.Secp256k1{}, id, partyIDs, tc.threshold, pl)
startFunc := cmp.Keygen(curve.Secp256k1{}, id, partyIDs, tc.policy.Degree(), pl)
require.NotNil(t, startFunc)
round, err := startFunc(nil)
@@ -198,10 +206,10 @@ func TestCMPPerformanceScaling(t *testing.T) {
initTime = time.Since(start)
require.Less(t, initTime, tc.maxTime,
"Initialization for %d parties should complete within %v", tc.n, tc.maxTime)
"Initialization for %d parties should complete within %v", tc.policy.N, tc.maxTime)
t.Logf("%d-of-%d: Initialization completed in %v (limit: %v)",
tc.threshold, tc.n, initTime, tc.maxTime)
t.Logf("%s: Initialization completed in %v (limit: %v)",
tc.policy, initTime, tc.maxTime)
})
}
}
+25 -10
View File
@@ -7,6 +7,7 @@ import (
"fmt"
"math/big"
"github.com/decred/dcrd/dcrec/secp256k1/v4"
"github.com/luxfi/threshold/pkg/math/curve"
"golang.org/x/crypto/sha3"
)
@@ -482,18 +483,32 @@ func (e *EVMAdapter) encodeBlobTx(tx *EVMTransaction) []byte {
return encoded
}
// GenerateEVMAddress generates an EVM address from public key
func (e *EVMAdapter) GenerateEVMAddress(publicKey curve.Point) [20]byte {
// Keccak256(pubkey)[12:]
pubBytes, _ := publicKey.MarshalBinary()
// GenerateEVMAddress derives the 20-byte EVM account address of a public key:
// the low 20 bytes of Keccak256 over the 64-byte UNCOMPRESSED encoding, X‖Y,
// with no format prefix.
//
// The point must be decompressed first. curve.Point.MarshalBinary returns the
// 33-byte compressed form (0x02/0x03 ‖ X), so hashing it — or its 32-byte X
// tail — produces a well-formed address that belongs to no key anyone holds.
// Nothing about such an address looks wrong, and value sent to it is
// unspendable, so this function returns ok=false rather than a plausible
// wrong answer when the point cannot be decompressed.
func (e *EVMAdapter) GenerateEVMAddress(publicKey curve.Point) ([20]byte, bool) {
var addr [20]byte
compressed, err := publicKey.MarshalBinary()
if err != nil || len(compressed) != 33 {
return addr, false
}
pub, err := secp256k1.ParsePubKey(compressed)
if err != nil {
return addr, false
}
h := sha3.NewLegacyKeccak256()
h.Write(pubBytes[1:]) // Skip format byte
hash := h.Sum(nil)
var addr [20]byte
copy(addr[:], hash[12:])
return addr
h.Write(pub.SerializeUncompressed()[1:]) // X‖Y, dropping the 0x04 prefix
copy(addr[:], h.Sum(nil)[12:])
return addr, true
}
// EstimateGas estimates gas for transaction
+119
View File
@@ -0,0 +1,119 @@
// SPDX-License-Identifier: BSD-3-Clause
package adapters
import (
"crypto/rand"
"testing"
"github.com/decred/dcrd/dcrec/secp256k1/v4"
"golang.org/x/crypto/sha3"
"github.com/luxfi/threshold/pkg/math/curve"
"github.com/luxfi/threshold/pkg/math/sample"
)
// GenerateEVMAddress must agree with what an EVM node computes when it
// ecrecovers a signature: Keccak256 over the 64-byte uncompressed public key,
// low 20 bytes.
//
// The regression this pins: curve.Point.MarshalBinary returns the 33-byte
// COMPRESSED encoding, so deriving from pubBytes[1:] hashes the 32-byte X
// coordinate alone. That yields a syntactically valid address for every key,
// belonging to no key at all — an unspendable destination that no unit test
// comparing "an address" to "an address" would catch. Only comparing against
// an independent derivation from the uncompressed key finds it.
func TestGenerateEVMAddressMatchesUncompressedKeccak(t *testing.T) {
e := NewEVMAdapter(Ethereum)
group := curve.Secp256k1{}
for i := 0; i < 256; i++ {
secret := sample.Scalar(rand.Reader, group)
point := secret.ActOnBase()
got, ok := e.GenerateEVMAddress(point)
if !ok {
t.Fatalf("iteration %d: GenerateEVMAddress reported failure on a valid point", i)
}
// Independent reference derivation straight from the secret scalar,
// via decred's secp256k1 rather than the adapter's own code path.
compressed, err := point.MarshalBinary()
if err != nil {
t.Fatalf("iteration %d: marshal: %v", i, err)
}
pub, err := secp256k1.ParsePubKey(compressed)
if err != nil {
t.Fatalf("iteration %d: parse: %v", i, err)
}
uncompressed := pub.SerializeUncompressed()
if len(uncompressed) != 65 || uncompressed[0] != 0x04 {
t.Fatalf("iteration %d: bad uncompressed encoding", i)
}
h := sha3.NewLegacyKeccak256()
h.Write(uncompressed[1:])
var want [20]byte
copy(want[:], h.Sum(nil)[12:])
if got != want {
t.Fatalf("iteration %d: address = 0x%x, want 0x%x", i, got, want)
}
// The specific wrong answer the old code produced, spelled out, so
// that reintroducing it fails loudly rather than silently.
hWrong := sha3.NewLegacyKeccak256()
hWrong.Write(compressed[1:]) // 32-byte X only
var compressedDerived [20]byte
copy(compressedDerived[:], hWrong.Sum(nil)[12:])
if got == compressedDerived {
t.Fatalf("iteration %d: address was derived from the COMPRESSED point; "+
"EVM requires Keccak over uncompressed X‖Y", i)
}
}
}
// Both Y-parities must derive correctly. An implementation that drops parity
// (or assumes even-Y) is right for about half of all keys, which is exactly
// the sort of bug that survives a small test sample.
func TestGenerateEVMAddressHandlesBothYParities(t *testing.T) {
e := NewEVMAdapter(Ethereum)
group := curve.Secp256k1{}
seenEven, seenOdd := false, false
for i := 0; i < 512 && !(seenEven && seenOdd); i++ {
point := sample.Scalar(rand.Reader, group).ActOnBase()
compressed, err := point.MarshalBinary()
if err != nil {
t.Fatalf("marshal: %v", err)
}
addr, ok := e.GenerateEVMAddress(point)
if !ok {
t.Fatalf("GenerateEVMAddress failed on a valid point")
}
if addr == ([20]byte{}) {
t.Fatalf("derived the zero address for parity byte 0x%02x", compressed[0])
}
switch compressed[0] {
case 0x02:
seenEven = true
case 0x03:
seenOdd = true
default:
t.Fatalf("unexpected compressed prefix 0x%02x", compressed[0])
}
}
if !seenEven || !seenOdd {
t.Fatalf("did not exercise both Y parities (even=%v odd=%v)", seenEven, seenOdd)
}
}
// The identity point has no address. Returning ok=false is the contract;
// returning a plausible-looking [20]byte would be the dangerous behaviour.
func TestGenerateEVMAddressRejectsIdentity(t *testing.T) {
e := NewEVMAdapter(Ethereum)
if _, ok := e.GenerateEVMAddress(curve.Secp256k1{}.NewPoint()); ok {
t.Fatal("GenerateEVMAddress accepted the identity point; it has no EVM address")
}
}
+436
View File
@@ -0,0 +1,436 @@
// SPDX-License-Identifier: BSD-3-Clause
package protocols_test
import (
"bytes"
"crypto/rand"
"testing"
"time"
decredsecp "github.com/decred/dcrd/dcrec/secp256k1/v4"
decredecdsa "github.com/decred/dcrd/dcrec/secp256k1/v4/ecdsa"
"github.com/stretchr/testify/require"
"golang.org/x/crypto/sha3"
"github.com/luxfi/threshold/internal/test"
"github.com/luxfi/threshold/pkg/ecdsa"
"github.com/luxfi/threshold/pkg/math/curve"
"github.com/luxfi/threshold/pkg/math/sample"
"github.com/luxfi/threshold/pkg/party"
"github.com/luxfi/threshold/pkg/pool"
"github.com/luxfi/threshold/pkg/protocol"
"github.com/luxfi/threshold/pkg/quorum"
"github.com/luxfi/threshold/protocols/cmp"
"github.com/luxfi/threshold/protocols/frost"
)
// policy is the custody policy under test: three of five shareholders must
// cooperate to sign, and any two must not be able to.
//
// It is stated once, as a quorum.Policy, and every protocol call below takes
// its degree from policy.Degree(). Writing the degree as a bare literal is
// exactly the mistake this file exists to catch: a "3-of-5" key created at
// degree 3 is a 4-of-5 key, and nothing about it looks wrong until three
// custodians try to sign and cannot.
var policy = quorum.MustNew(3, 5)
// cggmpTimeout bounds the CGGMP21 sessions below. A five-party key
// generation builds a Paillier modulus and its ring-mod proof per party; with
// the race detector on, that runs well past the harness default of five
// minutes. The deadline exists to stop a hung test, not to assert
// performance, so it is set generously.
const cggmpTimeout = 45 * time.Minute
// evmAddress derives the 20-byte EVM account address of a threshold group
// key, the way the EVM itself does: Keccak256 over the 64-byte uncompressed
// public key (X‖Y, no format prefix), low 20 bytes.
//
// curve.Point.MarshalBinary returns the 33-byte COMPRESSED encoding, so the
// point must be decompressed first. Hashing the compressed bytes — or the
// 32-byte X alone — yields a well-formed but entirely wrong address, and
// value sent to it is unspendable. That failure is silent at derivation time,
// which is why the tests below never trust a derived address on its own: each
// one is confirmed by ecrecover on a real signature.
func evmAddress(t testing.TB, pub curve.Point) [20]byte {
t.Helper()
compressed, err := pub.MarshalBinary()
require.NoError(t, err, "marshal group public key")
require.Len(t, compressed, 33, "curve.Point.MarshalBinary must be the compressed encoding")
parsed, err := decredsecp.ParsePubKey(compressed)
require.NoError(t, err, "group public key must be a valid secp256k1 point")
uncompressed := parsed.SerializeUncompressed() // 0x04 ‖ X(32) ‖ Y(32)
require.Len(t, uncompressed, 65)
h := sha3.NewLegacyKeccak256()
h.Write(uncompressed[1:]) // drop the 0x04 prefix: hash X‖Y
sum := h.Sum(nil)
var addr [20]byte
copy(addr[:], sum[12:])
return addr
}
// recoverAddress performs the operation an EVM node performs on a signature:
// ecrecover to a public key, then Keccak to an address. A signature that
// recovers to the group's address is a signature the Safe contract's
// checkNSignatures will accept for an owner at that address.
func recoverAddress(t testing.TB, digest []byte, sig *ecdsa.Signature) [20]byte {
t.Helper()
eth, err := sig.SigEthereum()
require.NoError(t, err, "SigEthereum")
require.Len(t, eth, 65, "EVM signature must be R‖S‖V")
require.LessOrEqual(t, eth[64], byte(1), "recovery id must be 0 or 1")
compact := make([]byte, 65)
compact[0] = 27 + eth[64] // decred compact header, uncompressed key
copy(compact[1:], eth[:64])
pub, _, err := decredecdsa.RecoverCompact(compact, digest)
require.NoError(t, err, "ecrecover must succeed on a canonical signature")
h := sha3.NewLegacyKeccak256()
h.Write(pub.SerializeUncompressed()[1:])
sum := h.Sum(nil)
var addr [20]byte
copy(addr[:], sum[12:])
return addr
}
// keygen3of5 runs a real CGGMP21 distributed key generation across all five
// parties at the policy's degree and returns each party's private config plus
// the group public key they all agree on. There is no dealer: no party, and
// no point in this function, ever holds the full secret key.
func keygen3of5(t testing.TB, ids party.IDSlice, pools map[party.ID]*pool.Pool, sessionID string) (map[party.ID]*cmp.Config, curve.Point) {
t.Helper()
results, err := test.RunProtocolWithTimeout(t, ids, []byte(sessionID), cggmpTimeout,
func(id party.ID) protocol.StartFunc {
return cmp.Keygen(curve.Secp256k1{}, id, ids, policy.Degree(), pools[id])
})
require.NoError(t, err, "CGGMP21 DKG at degree %d must complete", policy.Degree())
require.Len(t, results, policy.N, "every one of the %d parties must receive a share", policy.N)
configs := make(map[party.ID]*cmp.Config, policy.N)
var groupKey curve.Point
for id, r := range results {
cfg, ok := r.(*cmp.Config)
require.True(t, ok, "party %s: keygen result is %T, want *cmp.Config", id, r)
// The stored degree is what a later signing session reads to decide
// how many signers it needs. If it is not policy.Degree(), the key on
// disk is not the policy that was requested.
require.Equal(t, policy.Degree(), cfg.Threshold,
"party %s: stored degree must be %d for a %s key", id, policy.Degree(), policy)
readBack, err := quorum.FromDegree(cfg.Threshold, policy.N)
require.NoError(t, err)
require.Equal(t, policy, readBack, "party %s: stored key reads back as %s, want %s", id, readBack, policy)
configs[id] = cfg
if groupKey == nil {
groupKey = cfg.PublicPoint()
} else {
require.True(t, groupKey.Equal(cfg.PublicPoint()),
"party %s disagrees on the group public key — the shares are not for one key", id)
}
}
require.NotNil(t, groupKey)
require.False(t, groupKey.IsIdentity(), "group public key must not be the identity point")
return configs, groupKey
}
func newPools(t testing.TB, ids party.IDSlice) map[party.ID]*pool.Pool {
t.Helper()
pools := make(map[party.ID]*pool.Pool, len(ids))
for _, id := range ids {
pools[id] = pool.NewPool(0)
}
t.Cleanup(func() {
for _, pl := range pools {
pl.TearDown()
}
})
return pools
}
// TestQuorum3of5_CGGMP21_RecoversToGroupAddress is the end-to-end custody
// proof: five parties run a real distributed key generation, three of them
// sign, and the resulting signature ecrecovers to the group's EVM address.
//
// The ecrecover step is the one that matters for a Safe. Verifying the
// signature against the group public key proves the threshold maths is
// right; recovering it to the group ADDRESS proves the EVM will attribute
// that signature to the account the Safe has as an owner. A key can pass the
// first check and fail the second — that is precisely what a mis-derived
// address (compressed point, dropped Y parity, unpadded coordinate) looks
// like, and it is unrecoverable once the Safe is live.
func TestQuorum3of5_CGGMP21_RecoversToGroupAddress(t *testing.T) {
ids := test.PartyIDs(policy.N)
pools := newPools(t, ids)
configs, groupKey := keygen3of5(t, ids, pools, "quorum/3of5/cggmp21/keygen")
groupAddr := evmAddress(t, groupKey)
t.Logf("%s CGGMP21 group address: 0x%x", policy, groupAddr)
digest := make([]byte, 32)
copy(digest, "lux 3-of-5 cggmp21 custody proof")
signers := ids[:policy.K]
sigResults, err := test.RunProtocolWithTimeout(t, signers, []byte("quorum/3of5/cggmp21/sign"), cggmpTimeout,
func(id party.ID) protocol.StartFunc {
return cmp.Sign(configs[id], signers, digest, pools[id])
})
require.NoError(t, err, "%d signers must be able to sign a %s key", len(signers), policy)
require.Len(t, sigResults, policy.K)
// Every signer must output the same signature, and it must both verify
// under the group key and recover to the group address.
var canonical []byte
for id, r := range sigResults {
sig, ok := r.(*ecdsa.Signature)
require.True(t, ok, "party %s: sign result is %T, want *ecdsa.Signature", id, r)
require.True(t, sig.Verify(groupKey, digest),
"party %s: signature must verify under the group public key", id)
gotAddr := recoverAddress(t, digest, sig)
require.Equal(t, groupAddr, gotAddr,
"party %s: signature recovers to 0x%x, want the group address 0x%x", id, gotAddr, groupAddr)
eth, err := sig.SigEthereum()
require.NoError(t, err)
if canonical == nil {
canonical = eth
} else {
require.True(t, bytes.Equal(canonical, eth),
"party %s produced a different signature — signers must agree byte for byte", id)
}
}
}
// TestQuorum3of5_CGGMP21_EverySubsetSignsForOneAddress proves the key is a
// single threshold key rather than a collection of per-subset keys: several
// different 3-party subsets each produce a signature, and all of them recover
// to the same address.
//
// This is the property a Safe owner rotation depends on. The Safe stores one
// owner address; whichever three custodians happen to be available must all
// produce signatures that resolve to that one address.
func TestQuorum3of5_CGGMP21_EverySubsetSignsForOneAddress(t *testing.T) {
if testing.Short() {
t.Skip("multiple CGGMP21 signing sessions; skipped under -short")
}
ids := test.PartyIDs(policy.N)
pools := newPools(t, ids)
configs, groupKey := keygen3of5(t, ids, pools, "quorum/3of5/cggmp21/subsets/keygen")
groupAddr := evmAddress(t, groupKey)
digest := make([]byte, 32)
copy(digest, "one address, any three signers--")
// Disjoint-as-possible subsets, so no single party is in all of them:
// every share gets exercised and no party is load-bearing.
subsets := [][]party.ID{
{ids[0], ids[1], ids[2]},
{ids[2], ids[3], ids[4]},
{ids[0], ids[2], ids[4]},
{ids[1], ids[3], ids[4]},
}
for i, signers := range subsets {
signers := signers
name := ""
for _, id := range signers {
name += string(id)
}
t.Run(name, func(t *testing.T) {
results, err := test.RunProtocolWithTimeout(t, signers,
[]byte("quorum/3of5/cggmp21/subsets/sign/"+name), cggmpTimeout,
func(id party.ID) protocol.StartFunc {
return cmp.Sign(configs[id], signers, digest, pools[id])
})
require.NoError(t, err, "subset %d %v must be able to sign", i, signers)
for _, r := range results {
sig := r.(*ecdsa.Signature)
require.True(t, sig.Verify(groupKey, digest), "subset %v: signature must verify", signers)
require.Equal(t, groupAddr, recoverAddress(t, digest, sig),
"subset %v: every 3-of-5 subset must sign for the same address", signers)
}
})
}
}
// TestQuorum3of5_CGGMP21_TwoSignersCannotSign is the negative that gives the
// policy meaning. Two of five must not be able to produce a signature.
//
// The refusal happens before any round runs: cmp.Sign -> sign.StartSign ->
// config.CanSign -> ValidThreshold(t=2, n=2) is false. No partial signature
// is ever emitted, so there is nothing for an attacker holding two shares to
// aggregate offline.
func TestQuorum3of5_CGGMP21_TwoSignersCannotSign(t *testing.T) {
ids := test.PartyIDs(policy.N)
pools := newPools(t, ids)
configs, groupKey := keygen3of5(t, ids, pools, "quorum/3of5/cggmp21/insufficient/keygen")
digest := make([]byte, 32)
copy(digest, "two shares must not be enough---")
// Every 2-party subset, not just the first: a policy that holds for
// {a,b} but not {d,e} is not a policy.
for i := 0; i < policy.N; i++ {
for j := i + 1; j < policy.N; j++ {
signers := []party.ID{ids[i], ids[j]}
name := string(ids[i]) + string(ids[j])
t.Run(name, func(t *testing.T) {
results, err := test.RunProtocolWithTimeout(t, signers,
[]byte("quorum/3of5/cggmp21/insufficient/sign/"+name), cggmpTimeout,
func(id party.ID) protocol.StartFunc {
return cmp.Sign(configs[id], signers, digest, pools[id])
})
require.Error(t, err,
"%d signers MUST be refused for a %s key", len(signers), policy)
// Belt and braces: even if some result leaked through, it must
// not be a signature that verifies.
for _, r := range results {
if sig, ok := r.(*ecdsa.Signature); ok && sig != nil {
require.False(t, sig.Verify(groupKey, digest),
"subset %v produced a VERIFYING signature below the quorum", signers)
}
}
})
}
}
}
// TestQuorum3of5_CGGMP21_ForgedShareCannotSign covers the other way a quorum
// can be faked: the right NUMBER of signers, but one of them holding a share
// it did not receive from the DKG.
//
// Three parties attempt to sign, but one substitutes a random secret share
// for its real one. The session must not yield a signature that verifies
// under the group key. This is what stops an attacker who has compromised two
// genuine shares from making up a third.
func TestQuorum3of5_CGGMP21_ForgedShareCannotSign(t *testing.T) {
ids := test.PartyIDs(policy.N)
pools := newPools(t, ids)
configs, groupKey := keygen3of5(t, ids, pools, "quorum/3of5/cggmp21/forged/keygen")
digest := make([]byte, 32)
copy(digest, "a forged third share must fail--")
signers := ids[:policy.K]
// Deep-copy the victim's config through its own serialisation, then
// replace the secret share. Copying is essential: cmp.Config holds
// pointer-backed curve values, so mutating in place would corrupt the
// config the honest tests share.
forger := signers[len(signers)-1]
raw, err := configs[forger].MarshalBinary()
require.NoError(t, err)
tampered := cmp.EmptyConfig(curve.Secp256k1{})
require.NoError(t, tampered.UnmarshalBinary(raw))
tampered.ECDSA = sample.Scalar(rand.Reader, curve.Secp256k1{})
signConfigs := make(map[party.ID]*cmp.Config, len(signers))
for _, id := range signers {
signConfigs[id] = configs[id]
}
signConfigs[forger] = tampered
results, err := test.RunProtocolWithTimeout(t, signers, []byte("quorum/3of5/cggmp21/forged/sign"), cggmpTimeout,
func(id party.ID) protocol.StartFunc {
return cmp.Sign(signConfigs[id], signers, digest, pools[id])
})
// CGGMP21 is an identifiable-abort protocol: the expected outcome is that
// the session errors out. What is NOT acceptable under any circumstance is
// a signature that verifies under the real group key.
for id, r := range results {
sig, ok := r.(*ecdsa.Signature)
if !ok || sig == nil {
continue
}
require.False(t, sig.Verify(groupKey, digest),
"party %s accepted a signature built on a forged share — threshold security is broken", id)
}
require.Error(t, err, "a signing session containing a forged share must abort")
}
// TestQuorum3of5_FROST_VerifiesUnderGroupKey is the Schnorr/EdDSA half of the
// same claim: a real FROST distributed key generation at the same policy,
// signed by three of five, verifying under the group key — and refused at two.
//
// FROST uses the same degree convention as CGGMP21 (threshold = tolerated
// corruptions, threshold+1 signers required), so policy.Degree() is correct
// for both and there is one place in the codebase that knows why.
func TestQuorum3of5_FROST_VerifiesUnderGroupKey(t *testing.T) {
ids := test.PartyIDs(policy.N)
kgResults, err := test.RunProtocol(t, ids, []byte("quorum/3of5/frost/keygen"),
func(id party.ID) protocol.StartFunc {
return frost.Keygen(curve.Secp256k1{}, id, ids, policy.Degree())
})
require.NoError(t, err, "FROST DKG at degree %d must complete", policy.Degree())
require.Len(t, kgResults, policy.N)
configs := make(map[party.ID]*frost.Config, policy.N)
var groupKey curve.Point
for id, r := range kgResults {
cfg, ok := r.(*frost.Config)
require.True(t, ok, "party %s: keygen result is %T, want *frost.Config", id, r)
require.Equal(t, policy.Degree(), cfg.Threshold,
"party %s: stored degree must be %d for a %s key", id, policy.Degree(), policy)
configs[id] = cfg
if groupKey == nil {
groupKey = cfg.PublicKey
} else {
require.True(t, groupKey.Equal(cfg.PublicKey),
"party %s disagrees on the group public key", id)
}
}
require.NotNil(t, groupKey)
message := []byte("lux 3-of-5 frost custody proof")
// POSITIVE: three signers produce a signature that verifies.
signers := ids[:policy.K]
sigResults, err := test.RunProtocol(t, signers, []byte("quorum/3of5/frost/sign"),
func(id party.ID) protocol.StartFunc {
return frost.Sign(configs[id], signers, message)
})
require.NoError(t, err, "%d signers must be able to sign a %s FROST key", len(signers), policy)
for id, r := range sigResults {
// frost.Sign yields the signature by value, unlike cmp.Sign.
sig, ok := r.(frost.Signature)
require.True(t, ok, "party %s: sign result is %T, want frost.Signature", id, r)
require.True(t, sig.Verify(groupKey, message),
"party %s: FROST signature must verify under the group public key", id)
}
// NEGATIVE: two signers must not.
twoSigners := ids[:policy.K-1]
negResults, negErr := test.RunProtocol(t, twoSigners, []byte("quorum/3of5/frost/sign-2"),
func(id party.ID) protocol.StartFunc {
return frost.Sign(configs[id], twoSigners, message)
})
for _, r := range negResults {
if sig, ok := r.(frost.Signature); ok {
require.False(t, sig.Verify(groupKey, message),
"a %d-signer FROST session produced a VERIFYING signature below the quorum", len(twoSigners))
}
}
require.Error(t, negErr, "%d signers MUST be refused for a %s FROST key", len(twoSigners), policy)
}
+392
View File
@@ -0,0 +1,392 @@
// SPDX-License-Identifier: BSD-3-Clause
package protocols_test
import (
"encoding/hex"
"math/big"
"os"
"strings"
"testing"
"github.com/holiman/uint256"
"github.com/stretchr/testify/require"
gethabi "github.com/luxfi/geth/accounts/abi"
gethcommon "github.com/luxfi/geth/common"
"github.com/luxfi/geth/core/state"
"github.com/luxfi/geth/core/tracing"
gethtypes "github.com/luxfi/geth/core/types"
"github.com/luxfi/geth/core/vm/runtime"
"github.com/luxfi/threshold/internal/test"
tecdsa "github.com/luxfi/threshold/pkg/ecdsa"
"github.com/luxfi/threshold/pkg/party"
"github.com/luxfi/threshold/pkg/protocol"
"github.com/luxfi/threshold/protocols/cmp"
)
// safeABI is the slice of the Safe / SafeProxyFactory surface this proof
// drives. getTransactionHash is deliberately included: the digest a Safe
// expects its owners to sign is EIP-712 over a domain that binds the Safe's
// own address and chain id, and asking the deployed contract for it is the
// only way to be sure the test signs what the contract will check. A
// hand-rolled EIP-712 encoder that disagrees with the contract would make
// this test prove nothing.
const safeABI = `[
{"type":"function","name":"setup","stateMutability":"nonpayable","inputs":[
{"name":"_owners","type":"address[]"},{"name":"_threshold","type":"uint256"},
{"name":"to","type":"address"},{"name":"data","type":"bytes"},
{"name":"fallbackHandler","type":"address"},{"name":"paymentToken","type":"address"},
{"name":"payment","type":"uint256"},{"name":"paymentReceiver","type":"address"}],"outputs":[]},
{"type":"function","name":"getTransactionHash","stateMutability":"view","inputs":[
{"name":"to","type":"address"},{"name":"value","type":"uint256"},{"name":"data","type":"bytes"},
{"name":"operation","type":"uint8"},{"name":"safeTxGas","type":"uint256"},
{"name":"baseGas","type":"uint256"},{"name":"gasPrice","type":"uint256"},
{"name":"gasToken","type":"address"},{"name":"refundReceiver","type":"address"},
{"name":"_nonce","type":"uint256"}],"outputs":[{"name":"","type":"bytes32"}]},
{"type":"function","name":"execTransaction","stateMutability":"payable","inputs":[
{"name":"to","type":"address"},{"name":"value","type":"uint256"},{"name":"data","type":"bytes"},
{"name":"operation","type":"uint8"},{"name":"safeTxGas","type":"uint256"},
{"name":"baseGas","type":"uint256"},{"name":"gasPrice","type":"uint256"},
{"name":"gasToken","type":"address"},{"name":"refundReceiver","type":"address"},
{"name":"signatures","type":"bytes"}],"outputs":[{"name":"","type":"bool"}]},
{"type":"function","name":"nonce","stateMutability":"view","inputs":[],"outputs":[{"name":"","type":"uint256"}]},
{"type":"function","name":"getOwners","stateMutability":"view","inputs":[],"outputs":[{"name":"","type":"address[]"}]},
{"type":"function","name":"getThreshold","stateMutability":"view","inputs":[],"outputs":[{"name":"","type":"uint256"}]},
{"type":"function","name":"isOwner","stateMutability":"view","inputs":[{"name":"owner","type":"address"}],"outputs":[{"name":"","type":"bool"}]},
{"type":"function","name":"VERSION","stateMutability":"view","inputs":[],"outputs":[{"name":"","type":"string"}]},
{"type":"function","name":"createProxyWithNonce","stateMutability":"nonpayable","inputs":[
{"name":"_singleton","type":"address"},{"name":"initializer","type":"bytes"},
{"name":"saltNonce","type":"uint256"}],"outputs":[{"name":"proxy","type":"address"}]}
]`
// evmHarness runs the real luxfi/geth EVM interpreter over one persistent
// StateDB. Contract code is the actual deployed bytecode and every call is a
// real state transition, so a signature the Safe rejects fails this test.
//
// This deliberately drives core/vm rather than ethclient/simulated: in
// luxfi/geth v1.20.1 the simulated backend cannot be constructed at all.
// node.New calls conf.Logger.IsZero() on a nil interface, and past that,
// eth/backend.go:524 calls dnsdisc.NewClient(dnsdisc.Config{}) unconditionally
// — before its own len(EthDiscoveryURLs) > 0 guard — which panics on a nil
// logger inside withDefaults. Both are nil-interface method calls on zero
// configs.
//
// The tradeoff is honest and worth stating: there is no chain here, so there
// are no blocks and no transaction hashes. What is proved is exactly the
// contract-level claim — that the real SafeL2 bytecode accepts a 3-of-5
// threshold signature and moves value on it.
type evmHarness struct {
t *testing.T
abi gethabi.ABI
cfg *runtime.Config
}
func newEVMHarness(t *testing.T) *evmHarness {
t.Helper()
sdb, err := state.New(gethtypes.EmptyRootHash, state.NewDatabaseForTesting())
require.NoError(t, err)
deployer := gethcommon.HexToAddress("0x00000000000000000000000000000000deb10relay"[:42])
sdb.SetBalance(deployer, uint256.MustFromDecimal("1000000000000000000000000"), tracing.BalanceChangeUnspecified)
parsed, err := gethabi.JSON(strings.NewReader(safeABI))
require.NoError(t, err)
return &evmHarness{
t: t,
abi: parsed,
cfg: &runtime.Config{
State: sdb,
Origin: deployer,
GasLimit: 30_000_000,
BlockNumber: big.NewInt(1),
Time: 1_700_000_000,
Value: big.NewInt(0),
},
}
}
// deploy publishes contract bytecode and returns its address.
func (h *evmHarness) deploy(name string) gethcommon.Address {
h.t.Helper()
raw, err := os.ReadFile("testdata/" + name + ".bin")
require.NoError(h.t, err, "missing %s bytecode", name)
code, err := hex.DecodeString(strings.TrimSpace(string(raw)))
require.NoError(h.t, err)
h.cfg.Value = big.NewInt(0)
_, addr, _, err := runtime.Create(code, h.cfg)
require.NoError(h.t, err, "deploying %s reverted", name)
require.NotEqual(h.t, gethcommon.Address{}, addr)
return addr
}
// exec calls a contract and requires it to succeed, returning the return data.
func (h *evmHarness) exec(to gethcommon.Address, data []byte) []byte {
h.t.Helper()
out, err := h.tryExec(to, data)
require.NoError(h.t, err, "call reverted")
return out
}
// tryExec calls a contract and surfaces a revert as an error, for the
// negative cases where a revert is the expected outcome.
func (h *evmHarness) tryExec(to gethcommon.Address, data []byte) ([]byte, error) {
h.t.Helper()
h.cfg.Value = big.NewInt(0)
out, _, err := runtime.Call(to, data, h.cfg)
return out, err
}
// call is a read-only invocation. State changes are rolled back via a
// snapshot so a view call can never mutate the world.
func (h *evmHarness) call(to gethcommon.Address, data []byte) []byte {
h.t.Helper()
snap := h.cfg.State.Snapshot()
defer h.cfg.State.RevertToSnapshot(snap)
return h.exec(to, data)
}
// fund credits an account directly. Funding is not the claim under test, so
// it is done as a state edit rather than a transfer transaction.
func (h *evmHarness) fund(addr gethcommon.Address, wei *big.Int) {
h.t.Helper()
h.cfg.State.SetBalance(addr, uint256.MustFromBig(wei), tracing.BalanceChangeUnspecified)
}
func (h *evmHarness) balance(addr gethcommon.Address) *big.Int {
return h.cfg.State.GetBalance(addr).ToBig()
}
// TestQuorum3of5_CGGMP21_OperatesGnosisSafe is the custody proof that matters:
// a Safe whose only owner is a 3-of-5 CGGMP21 group key, executing a real
// value transfer authorised by three of the five shareholders.
//
// Nothing here is stubbed. The Safe is the real SafeL2 singleton bytecode
// behind the real SafeProxyFactory, running on a real EVM. The digest is the
// one the deployed Safe computes for itself. The signature comes from an
// actual distributed signing session — no party ever holds the whole key, and
// the two non-participating shareholders contribute nothing.
//
// If the group key were mis-derived, if the recovery id were wrong, if S were
// left high, or if the quorum were off by one, execTransaction would revert
// inside checkNSignatures and this test would fail. That is the whole reason
// to run it against the contract rather than against our own verifier.
func TestQuorum3of5_CGGMP21_OperatesGnosisSafe(t *testing.T) {
if testing.Short() {
t.Skip("full CGGMP21 DKG plus EVM deployment; skipped under -short")
}
// --- 1. Real distributed key generation. No dealer, no full key anywhere.
ids := test.PartyIDs(policy.N)
pools := newPools(t, ids)
configs, groupKey := keygen3of5(t, ids, pools, "safe/3of5/keygen")
mpcAddr := gethcommon.BytesToAddress(func() []byte { a := evmAddress(t, groupKey); return a[:] }())
t.Logf("3-of-5 CGGMP21 group address (the Safe's only owner): %s", mpcAddr.Hex())
// --- 2. Real EVM, real Safe contracts.
h := newEVMHarness(t)
singleton := h.deploy("SafeL2")
factory := h.deploy("SafeProxyFactory")
t.Logf("SafeL2 singleton: %s SafeProxyFactory: %s", singleton.Hex(), factory.Hex())
version := new(string)
require.NoError(t, h.abi.UnpackIntoInterface(version, "VERSION",
h.call(singleton, mustPack(t, h.abi, "VERSION"))))
t.Logf("Safe version: %s", *version)
// --- 3. A brand-new Safe owned solely by the MPC group address.
//
// Threshold 1 here is the SAFE's owner count, not the MPC quorum: the Safe
// has exactly one owner, and that owner is itself a 3-of-5 threshold key.
// The 3-of-5 is enforced cryptographically inside the signing ceremony, so
// the Safe cannot observe or weaken it.
initializer := mustPack(t, h.abi, "setup",
[]gethcommon.Address{mpcAddr}, // _owners
big.NewInt(1), // _threshold
gethcommon.Address{}, // to
[]byte{}, // data
gethcommon.Address{}, // fallbackHandler
gethcommon.Address{}, // paymentToken
big.NewInt(0), // payment
gethcommon.Address{}, // paymentReceiver
)
createCall := mustPack(t, h.abi, "createProxyWithNonce", singleton, initializer, big.NewInt(0))
// Read the proxy address off an eth_call first, then actually create it.
var safeAddr gethcommon.Address
require.NoError(t, h.abi.UnpackIntoInterface(&safeAddr, "createProxyWithNonce", h.call(factory, createCall)))
h.exec(factory, createCall)
t.Logf("throwaway Safe deployed: %s", safeAddr.Hex())
// The Safe must really be owned by the MPC address and nothing else.
var owners []gethcommon.Address
require.NoError(t, h.abi.UnpackIntoInterface(&owners, "getOwners", h.call(safeAddr, mustPack(t, h.abi, "getOwners"))))
require.Equal(t, []gethcommon.Address{mpcAddr}, owners, "the Safe's owner set must be exactly the MPC group address")
isOwner := new(bool)
require.NoError(t, h.abi.UnpackIntoInterface(isOwner, "isOwner", h.call(safeAddr, mustPack(t, h.abi, "isOwner", mpcAddr))))
require.True(t, *isOwner)
// --- 4. Fund the Safe, then move value out of it under MPC authority.
fundAmount := new(big.Int).Mul(big.NewInt(5), big.NewInt(1e18))
h.fund(safeAddr, fundAmount)
require.Equal(t, 0, h.balance(safeAddr).Cmp(fundAmount), "Safe should hold the funded amount")
recipient := gethcommon.HexToAddress("0x000000000000000000000000000000000000d00d")
transfer := new(big.Int).Mul(big.NewInt(3), big.NewInt(1e18))
// --- 5. Ask the Safe itself what digest its owner must sign.
safeTxHashRaw := h.call(safeAddr, mustPack(t, h.abi, "getTransactionHash",
recipient, transfer, []byte{}, uint8(0),
big.NewInt(0), big.NewInt(0), big.NewInt(0),
gethcommon.Address{}, gethcommon.Address{}, big.NewInt(0)))
var safeTxHash [32]byte
require.NoError(t, h.abi.UnpackIntoInterface(&safeTxHash, "getTransactionHash", safeTxHashRaw))
t.Logf("safeTxHash to be threshold-signed: 0x%x", safeTxHash)
// --- 6. Three of the five shareholders sign it. Two sit the round out.
signers := ids[:policy.K]
t.Logf("signing parties: %v (of %v)", signers, ids)
sigResults, err := test.RunProtocolWithTimeout(t, signers, []byte("safe/3of5/sign"), cggmpTimeout,
func(id party.ID) protocol.StartFunc {
return cmp.Sign(configs[id], signers, safeTxHash[:], pools[id])
})
require.NoError(t, err, "the 3-of-5 signing ceremony must complete")
sig := sigResults[signers[0]].(*tecdsa.Signature)
require.True(t, sig.Verify(groupKey, safeTxHash[:]), "signature must verify under the group key")
require.Equal(t, mpcAddr, gethcommon.BytesToAddress(func() []byte {
a := recoverAddress(t, safeTxHash[:], sig)
return a[:]
}()), "signature must recover to the Safe's owner")
eth, err := sig.SigEthereum()
require.NoError(t, err)
// Safe's checkNSignatures reads v in {27,28} for a plain ECDSA owner
// signature; SigEthereum emits the raw recovery id 0/1.
safeSig := make([]byte, 65)
copy(safeSig, eth)
safeSig[64] = eth[64] + 27
// --- 7. Execute. The Safe verifies the signature itself.
execCall := mustPack(t, h.abi, "execTransaction",
recipient, transfer, []byte{}, uint8(0),
big.NewInt(0), big.NewInt(0), big.NewInt(0),
gethcommon.Address{}, gethcommon.Address{}, safeSig)
execOut, err := h.tryExec(safeAddr, execCall)
require.NoError(t, err, "execTransaction reverted — the Safe rejected the 3-of-5 signature")
success := new(bool)
require.NoError(t, h.abi.UnpackIntoInterface(success, "execTransaction", execOut))
require.True(t, *success, "execTransaction returned false")
t.Log("execTransaction SUCCEEDED — the Safe accepted a signature produced by 3 of 5 parties")
// --- 8. The money actually moved, and the Safe advanced its nonce.
require.Equal(t, 0, h.balance(recipient).Cmp(transfer),
"recipient must have received the transfer authorised by 3 of 5 parties")
require.Equal(t, 0, h.balance(safeAddr).Cmp(new(big.Int).Sub(fundAmount, transfer)), "Safe balance must be debited")
safeNonce := new(big.Int)
require.NoError(t, h.abi.UnpackIntoInterface(&safeNonce, "nonce", h.call(safeAddr, mustPack(t, h.abi, "nonce"))))
require.Equal(t, int64(1), safeNonce.Int64(), "Safe nonce must advance after a successful execTransaction")
// --- 9. NEGATIVE: replaying the same signature must fail. The nonce is
// bound into safeTxHash, so the old signature no longer authorises
// anything — this is what stops a captured signature being reused.
_, err = h.tryExec(safeAddr, execCall)
require.Error(t, err, "replaying a spent Safe signature must be rejected")
t.Logf("replay correctly rejected: %v", err)
}
// TestQuorum3of5_CGGMP21_SafeRejectsSubQuorumSignature is the negative that
// closes the loop at the contract boundary: a signature produced by a
// DIFFERENT 3-of-5 group (standing in for any key that is not the owner) is
// refused by the Safe.
//
// The two-signers-cannot-sign case is proved cryptographically in
// TestQuorum3of5_CGGMP21_TwoSignersCannotSign — the protocol refuses to emit a
// signature at all, so there is nothing to hand to a Safe. What remains to
// prove here is that the Safe checks WHICH key signed, not merely that some
// well-formed signature was supplied.
func TestQuorum3of5_CGGMP21_SafeRejectsForeignSignature(t *testing.T) {
if testing.Short() {
t.Skip("two full CGGMP21 DKGs plus EVM deployment; skipped under -short")
}
ids := test.PartyIDs(policy.N)
pools := newPools(t, ids)
// The real owner key, and an unrelated key generated the same way.
ownerConfigs, ownerKey := keygen3of5(t, ids, pools, "safe/3of5/owner-keygen")
_ = ownerConfigs
foreignConfigs, foreignKey := keygen3of5(t, ids, pools, "safe/3of5/foreign-keygen")
require.False(t, ownerKey.Equal(foreignKey), "the two DKGs must produce different keys")
ownerAddr := gethcommon.BytesToAddress(func() []byte { a := evmAddress(t, ownerKey); return a[:] }())
h := newEVMHarness(t)
singleton := h.deploy("SafeL2")
factory := h.deploy("SafeProxyFactory")
initializer := mustPack(t, h.abi, "setup",
[]gethcommon.Address{ownerAddr}, big.NewInt(1),
gethcommon.Address{}, []byte{}, gethcommon.Address{},
gethcommon.Address{}, big.NewInt(0), gethcommon.Address{})
createCall := mustPack(t, h.abi, "createProxyWithNonce", singleton, initializer, big.NewInt(0))
var safeAddr gethcommon.Address
require.NoError(t, h.abi.UnpackIntoInterface(&safeAddr, "createProxyWithNonce", h.call(factory, createCall)))
h.exec(factory, createCall)
h.fund(safeAddr, new(big.Int).Mul(big.NewInt(5), big.NewInt(1e18)))
recipient := gethcommon.HexToAddress("0x000000000000000000000000000000000000dead")
transfer := big.NewInt(1e18)
safeTxHashRaw := h.call(safeAddr, mustPack(t, h.abi, "getTransactionHash",
recipient, transfer, []byte{}, uint8(0),
big.NewInt(0), big.NewInt(0), big.NewInt(0),
gethcommon.Address{}, gethcommon.Address{}, big.NewInt(0)))
var safeTxHash [32]byte
require.NoError(t, h.abi.UnpackIntoInterface(&safeTxHash, "getTransactionHash", safeTxHashRaw))
// A perfectly valid signature — from the wrong key.
signers := ids[:policy.K]
sigResults, err := test.RunProtocolWithTimeout(t, signers, []byte("safe/3of5/foreign-sign"), cggmpTimeout,
func(id party.ID) protocol.StartFunc {
return cmp.Sign(foreignConfigs[id], signers, safeTxHash[:], pools[id])
})
require.NoError(t, err)
sig := sigResults[signers[0]].(*tecdsa.Signature)
require.True(t, sig.Verify(foreignKey, safeTxHash[:]), "the foreign signature is itself valid")
eth, err := sig.SigEthereum()
require.NoError(t, err)
safeSig := make([]byte, 65)
copy(safeSig, eth)
safeSig[64] = eth[64] + 27
execCall := mustPack(t, h.abi, "execTransaction",
recipient, transfer, []byte{}, uint8(0),
big.NewInt(0), big.NewInt(0), big.NewInt(0),
gethcommon.Address{}, gethcommon.Address{}, safeSig)
_, err = h.tryExec(safeAddr, execCall)
require.Error(t, err, "the Safe must reject a valid signature from a key that is not its owner")
t.Logf("Safe correctly rejected a foreign 3-of-5 signature: %v", err)
}
func mustPack(t *testing.T, abi gethabi.ABI, name string, args ...interface{}) []byte {
t.Helper()
packed, err := abi.Pack(name, args...)
require.NoError(t, err, "pack %s", name)
return packed
}
+1
View File
File diff suppressed because one or more lines are too long
+1
View File
@@ -0,0 +1 @@
608080604052346015576106c9908161001a8239f35b5f80fdfe60806040526004361015610011575f80fd5b5f3560e01c80631688f0b9146102735780631a334b0b146101ee5780631fdac6c5146101785780633408e4701461015e5780634662f9d6146100df57806353e5d935146100955763ec9e80bb14610066575f80fd5b3461009157602061007f610079366102c2565b91610403565b6040516001600160a01b039091168152f35b5f80fd5b34610091575f366003190112610091576100db61014e604051906100bc602082018361028c565b8082526105466020830139604051918291602083526020830190610353565b0390f35b346100915760207f6e78ae8c51534f0b801d7aa7cce5a2113f3c3368c61b06fa61415f361d400431610110366102c2565b9192906001600160a01b03610126848387610377565b1693849361014d60405193849360018060a01b031684526060898501526060840190610353565b9060408301520390a2604051908152f35b34610091575f366003190112610091576020604051468152f35b34610091576020366003190112610091576004356001600160a01b03811690819003610091576020906101d56101e361014e92604051936101bb8682018661028c565b8085526105468686013960405192839186830195866103ec565b03601f19810183528261028c565b519020604051908152f35b346100915760207f7a1c96f74273709828c0e67ab96189772005d3b4948bc6902693eb21e4e2cc6a61021f366102c2565b9192906001600160a01b03610235848387610403565b1693849361025c60405193849360018060a01b031684526080898501526080840190610353565b9060408301524660608301520390a2604051908152f35b3461009157602061007f610286366102c2565b91610377565b90601f8019910116810190811067ffffffffffffffff8211176102ae57604052565b634e487b7160e01b5f52604160045260245ffd5b906060600319830112610091576004356001600160a01b0381168103610091579160243567ffffffffffffffff811161009157816023820112156100915780600401359067ffffffffffffffff82116102ae576040519261032d601f8401601f19166020018561028c565b8284526024838301011161009157815f9260246020930183860137830101529060443590565b805180835260209291819084018484015e5f828201840152601f01601f1916010190565b9291906103af91815160208301209060405190602082019283526040820152604081526103a560608261028c565b5190209084610437565b6040516001600160a01b03938416815290928316907f4f51faf6c4561ff95f067657e43439f0f856d97c04d9ec9070a6199ad418e23590602090a2565b60209291908391805192839101825e019081520190565b9291906103af91815160208301209060405190602082019283526040820152466060820152606081526103a560808261028c565b92919092803b156105005761048461014e916040519261045a602082018561028c565b80845261054660208501396040519283916101d5916001600160a01b0390911690602084016103ec565b8051906020015ff5916001600160a01b038316156104c557805190816104a8575050565b5f91602083920182865af1156104ba57565b6040513d5f823e3d90fd5b60405162461bcd60e51b815260206004820152601360248201527210dc99585d194c8818d85b1b0819985a5b1959606a1b6044820152606490fd5b60405162461bcd60e51b815260206004820152601f60248201527f53696e676c65746f6e20636f6e7472616374206e6f74206465706c6f796564006044820152606490fdfe60803460c357601f61014e38819003918201601f19168301916001600160401b0383118484101760c75780849260209460405283398101031260c357516001600160a01b0381169081900360c35780156073575f80546001600160a01b031916919091179055604051607290816100dc8239f35b60405162461bcd60e51b815260206004820152602260248201527f496e76616c69642073696e676c65746f6e20616464726573732070726f766964604482015261195960f21b6064820152608490fd5b5f80fd5b634e487b7160e01b5f52604160045260245ffdfe60806040525f5463a619486e5f3560e01c146030575f8091368280378136915af43d5f803e15602c573d5ff35b3d5ffd5b60601b606c5260206060f3fea264697066735822122085570e10096bb010797663b22bd31c30ba30aceabb3dfe01a564f38c7b3dff6264736f6c634300081f0033a2646970667358221220a3012b8afbb1690f1e993fb464ab3a3991aa6da571f721d82fe3e705f0871b6464736f6c634300081f0033