diff --git a/consensus/go.mod b/consensus/go.mod index f9d76c19f..33de4e6e2 100644 --- a/consensus/go.mod +++ b/consensus/go.mod @@ -9,7 +9,7 @@ require ( github.com/shopspring/decimal v1.4.0 github.com/smartcontractkit/capabilities/libs v0.0.0-20260210010829-97eb42ca2924 github.com/smartcontractkit/chainlink-common v0.11.2-0.20260529092756-a94bc8ce96d6 - github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260526195338-adcf8013a1b7 + github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260707203317-661b54b51a33 github.com/smartcontractkit/cre-sdk-go v0.9.0 github.com/smartcontractkit/libocr v0.0.0-20250912173940-f3ab0246e23d github.com/stretchr/testify v1.11.1 @@ -22,6 +22,7 @@ require ( require ( github.com/Masterminds/semver/v3 v3.4.0 // indirect + github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6 // indirect github.com/XSAM/otelsql v0.37.0 // indirect github.com/bahlo/generic-list-go v0.2.0 // indirect github.com/beorn7/perks v1.0.1 // indirect @@ -30,6 +31,8 @@ require ( github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cloudevents/sdk-go/binding/format/protobuf/v2 v2.16.1 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect + github.com/ethereum/go-ethereum v1.17.2 // indirect github.com/fatih/color v1.18.0 // indirect github.com/fxamacker/cbor/v2 v2.9.0 // indirect github.com/gabriel-vasile/mimetype v1.4.8 // indirect @@ -51,6 +54,7 @@ require ( github.com/hashicorp/go-hclog v1.6.3 // indirect github.com/hashicorp/go-plugin v1.8.0 // indirect github.com/hashicorp/yamux v0.1.2 // indirect + github.com/holiman/uint256 v1.3.2 // indirect github.com/invopop/jsonschema v0.13.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect @@ -80,7 +84,7 @@ require ( github.com/prometheus/procfs v0.20.1 // indirect github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 // indirect github.com/scylladb/go-reflectx v1.0.1 // indirect - github.com/smartcontractkit/chain-selectors v1.0.100 // indirect + github.com/smartcontractkit/chain-selectors v1.0.104 // indirect github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20260529092756-a94bc8ce96d6 // indirect github.com/smartcontractkit/chainlink-protos/linking-service/go v0.0.0-20251002192024-d2ad9222409b // indirect github.com/smartcontractkit/chainlink-protos/node-platform v0.0.0-20260205130626-db2a2aab956b // indirect diff --git a/consensus/go.sum b/consensus/go.sum index 38e46af17..4611ea39a 100644 --- a/consensus/go.sum +++ b/consensus/go.sum @@ -4,6 +4,8 @@ filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 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/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6 h1:1zYrtlhrZ6/b6SAjLSfKzWtdgqK0U+HtH/VcBWh1BaU= +github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6/go.mod h1:ioLG6R+5bUSO1oeGSDxOV3FADARuMoytZCSX6MEMQkI= github.com/XSAM/otelsql v0.37.0 h1:ya5RNw028JW0eJW8Ma4AmoKxAYsJSGuNVbC7F1J457A= github.com/XSAM/otelsql v0.37.0/go.mod h1:LHbCu49iU8p255nCn1oi04oX2UjSoRcUMiKEHo2a5qM= github.com/apache/arrow-go/v18 v18.3.1 h1:oYZT8FqONiK74JhlH3WKVv+2NKYoyZ7C2ioD4Dj3ixk= @@ -31,12 +33,18 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/decred/dcrd/crypto/blake256 v1.0.1 h1:7PltbUIQB7u/FfZ39+DGa/ShuMyJ5ilcvdfma9wOH6Y= +github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 h1:rpfIENRNNilwHwZeG5+P150SMrnNEcHYvcCuK6dPZSg= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= github.com/dominikbraun/graph v0.23.0 h1:TdZB4pPqCLFxYhdyMFb1TBdFxp8XLcJfTTBQucVPgCo= github.com/dominikbraun/graph v0.23.0/go.mod h1:yOjYyogZLY1LSG9E33JWZJiq5k83Qy2C6POAuiViluc= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/ethereum/go-ethereum v1.17.2 h1:ag6geu0kn8Hv5FLKTpH+Hm2DHD+iuFtuqKxEuwUsDOI= +github.com/ethereum/go-ethereum v1.17.2/go.mod h1:KHcRXfGOUfUmKg51IhQ0IowiqZ6PqZf08CMtk0g5K1o= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= @@ -115,6 +123,8 @@ github.com/hashicorp/go-plugin v1.8.0 h1:ie8S6RRY8RvB2usYZv+AAZ/wBvx2AU5p5QeP5j/ github.com/hashicorp/go-plugin v1.8.0/go.mod h1:BExt6KEaIYx804z8k4gRzRLEvxKVb+kn0NMcihqOqb8= github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8= github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns= +github.com/holiman/uint256 v1.3.2 h1:a9EgMPSC1AAaj1SZL5zIQD3WbwTuHrMGOerLjGmM/TA= +github.com/holiman/uint256 v1.3.2/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E= github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E= github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= @@ -206,24 +216,20 @@ github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= github.com/smartcontractkit/capabilities/libs v0.0.0-20260210010829-97eb42ca2924 h1:6wqsOpDXA0ZMEswN7f8hX04Y3+gXva7p5emXThtJVlI= github.com/smartcontractkit/capabilities/libs v0.0.0-20260210010829-97eb42ca2924/go.mod h1:v0O0Au8RE00Z89QxBE6I2q9bR9r3+RO1gLD3oaO2WB0= -github.com/smartcontractkit/chain-selectors v1.0.100 h1:wpiSpmI/eFjY+wx/nPr5VuNF4hki0prIBMKEaQWn3g4= -github.com/smartcontractkit/chain-selectors v1.0.100/go.mod h1:qy7whtgG5g+7z0jt0nRyii9bLND9m15NZTzuQPkMZ5w= +github.com/smartcontractkit/chain-selectors v1.0.104 h1:/n9pPGM5W/+r1eHoWZv4VwX9LNS1af4+ICyhM8zKRNM= +github.com/smartcontractkit/chain-selectors v1.0.104/go.mod h1:qy7whtgG5g+7z0jt0nRyii9bLND9m15NZTzuQPkMZ5w= github.com/smartcontractkit/chainlink-common v0.11.2-0.20260529092756-a94bc8ce96d6 h1:hms02zQQ0BPcp9CBwh/xda5KwJWdU0IIA/yjtwyRoA4= github.com/smartcontractkit/chainlink-common v0.11.2-0.20260529092756-a94bc8ce96d6/go.mod h1:jueIfDkkRexwGgLbVB7vGCZlNtd383zuwi4uHHwcbqc= github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20260529092756-a94bc8ce96d6 h1:ucHu2bPDT/58AzSgnPDyp4IjnjVbrVWYD3bG5jCbXMY= github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20260529092756-a94bc8ce96d6/go.mod h1:HmUyH2oD9m+GRpKq7q3vuRnm1F2Uczf/Nd1v3ipMSK8= github.com/smartcontractkit/chainlink-protos/billing/go v0.0.0-20251024234028-0988426d98f4 h1:GCzrxDWn3b7jFfEA+WiYRi8CKoegsayiDoJBCjYkneE= github.com/smartcontractkit/chainlink-protos/billing/go v0.0.0-20251024234028-0988426d98f4/go.mod h1:HHGeDUpAsPa0pmOx7wrByCitjQ0mbUxf0R9v+g67uCA= -github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260526195338-adcf8013a1b7 h1:iljEJss3WOwcsMkWy72Yn2zvjw7Gyxc+RXL7r8YKM6g= -github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260526195338-adcf8013a1b7/go.mod h1:vTFHTCbLui4Vn8fTmAadfE3rdnvfrDwOmMujmW857D0= github.com/smartcontractkit/chainlink-protos/linking-service/go v0.0.0-20251002192024-d2ad9222409b h1:QuI6SmQFK/zyUlVWEf0GMkiUYBPY4lssn26nKSd/bOM= github.com/smartcontractkit/chainlink-protos/linking-service/go v0.0.0-20251002192024-d2ad9222409b/go.mod h1:qSTSwX3cBP3FKQwQacdjArqv0g6QnukjV4XuzO6UyoY= github.com/smartcontractkit/chainlink-protos/node-platform v0.0.0-20260205130626-db2a2aab956b h1:36knUpKHHAZ86K4FGWXtx8i/EQftGdk2bqCoEu/Cha8= github.com/smartcontractkit/chainlink-protos/node-platform v0.0.0-20260205130626-db2a2aab956b/go.mod h1:dkR2uYg9XYJuT1JASkPzWE51jjFkVb86P7a/yXe5/GM= github.com/smartcontractkit/chainlink-protos/workflows/go v0.0.0-20260323124644-faea187e6997 h1:W0HKHO8eE8BckTRnhSdqjHKbJcnk068nEWYnWRu6tJY= github.com/smartcontractkit/chainlink-protos/workflows/go v0.0.0-20260323124644-faea187e6997/go.mod h1:GTpDgyK0OObf7jpch6p8N281KxN92wbB8serZhU9yRc= -github.com/smartcontractkit/cre-sdk-go v0.9.0 h1:MDO9HFb4tjvu4mI4gKvdO+qXP1irULxhFwlTPVBytaM= -github.com/smartcontractkit/cre-sdk-go v0.9.0/go.mod h1:CQY8hCISjctPmt8ViDVgFm4vMGLs5fYI198QhkBS++Y= github.com/smartcontractkit/freeport v0.1.3-0.20250716200817-cb5dfd0e369e h1:Hv9Mww35LrufCdM9wtS9yVi/rEWGI1UnjHbcKKU0nVY= github.com/smartcontractkit/freeport v0.1.3-0.20250716200817-cb5dfd0e369e/go.mod h1:T4zH9R8R8lVWKfU7tUvYz2o2jMv1OpGCdpY2j2QZXzU= github.com/smartcontractkit/grpc-proxy v0.0.0-20240830132753-a7e17fec5ab7 h1:12ijqMM9tvYVEm+nR826WsrNi6zCKpwBhuApq127wHs= diff --git a/consensus/oracle/consensus_execution.go b/consensus/oracle/consensus_execution.go index 2ec09da39..fc4b86901 100644 --- a/consensus/oracle/consensus_execution.go +++ b/consensus/oracle/consensus_execution.go @@ -59,6 +59,8 @@ func CalculateOutcomeForObservations( return handleCommonPrefixAggregation(lggr, observations, f, errorsMigrationFlag) case sdk.AggregationType_AGGREGATION_TYPE_COMMON_SUFFIX: return handleCommonSuffixAggregation(lggr, observations, f, errorsMigrationFlag) + case sdk.AggregationType_AGGREGATION_TYPE_FREQUENCY_LIST: + return handleValueCountsAggregation(lggr, observations, f) default: return nil, fmt.Errorf("unknown aggregation type: %s", aggregation) } @@ -338,6 +340,94 @@ func handleIdenticalAggregation(_ logger.Logger, values []*valuespb.Value, f int return uniqueCandidate, nil } +// handleValueCountsAggregation returns a list of all distinct observation values +// together with how many times each was seen. Each list element is a map with +// "value" and "count" fields. Results are sorted by count descending, then by +// serialized value ascending for determinism. +func handleValueCountsAggregation( + _ logger.Logger, + observations []*valuespb.Value, + f int, +) (*valuespb.Value, error) { + if len(observations) < f+1 { + return nil, ErrInsufficientObservations + } + + type valueOccurrence struct { + count int64 + value *valuespb.Value + } + + var ( + marshaler = &proto.MarshalOptions{Deterministic: true} + occurrences = make(map[string]valueOccurrence) + ) + + for _, currentValue := range observations { + if currentValue == nil || currentValue.Value == nil { + continue + } + + b, err := marshaler.Marshal(currentValue) + if err != nil { + return nil, fmt.Errorf("unable to marshal value: %w", err) + } + key := string(b) + + occurrence := occurrences[key] + occurrence.count++ + if occurrence.value == nil { + occurrence.value = currentValue + } + occurrences[key] = occurrence + } + + if len(occurrences) == 0 { + return nil, ErrInsufficientObservations + } + + type countedValue struct { + key string + count int64 + value *valuespb.Value + } + + counted := make([]countedValue, 0, len(occurrences)) + for key, occurrence := range occurrences { + counted = append(counted, countedValue{ + key: key, + count: occurrence.count, + value: occurrence.value, + }) + } + + slices.SortFunc(counted, func(a, b countedValue) int { + if a.count != b.count { + if a.count > b.count { + return -1 + } + return 1 + } + if a.key < b.key { + return -1 + } + if a.key > b.key { + return 1 + } + return 0 + }) + + result := make([]*valuespb.Value, 0, len(counted)) + for _, entry := range counted { + result = append(result, valuespb.NewMapValue(map[string]*valuespb.Value{ + "value": entry.value, + "count": valuespb.NewInt64Value(entry.count), + })) + } + + return valuespb.NewListValue(result), nil +} + // handleCommonSuffixAggregation reverses the underlying lists in the slice of // observations and delegates logic to handleCommonPrefixAggregation and then // reverses the result a final time. diff --git a/consensus/oracle/consensus_execution_test.go b/consensus/oracle/consensus_execution_test.go index 30fee3c83..aeb29e1db 100644 --- a/consensus/oracle/consensus_execution_test.go +++ b/consensus/oracle/consensus_execution_test.go @@ -204,6 +204,50 @@ func Test_CalculateOutcomeForObservations(t *testing.T) { }, expectedOutcome: mustNewList("7", "8", "9"), }, + { + name: "value counts aggregation: distinct values with counts", + observations: []*valuespb.Value{ + values.Proto(values.NewInt64(42)), + values.Proto(values.NewInt64(42)), + values.Proto(values.NewInt64(42)), + values.Proto(values.NewInt64(50)), + values.Proto(values.NewString("malicious")), + }, + descriptor: &sdk.ConsensusDescriptor{ + Descriptor_: &sdk.ConsensusDescriptor_Aggregation{ + Aggregation: sdk.AggregationType_AGGREGATION_TYPE_FREQUENCY_LIST, + }, + }, + f: 2, + expectedOutcome: valuespb.NewListValue([]*valuespb.Value{ + valuespb.NewMapValue(map[string]*valuespb.Value{ + "value": values.Proto(values.NewInt64(42)), + "count": valuespb.NewInt64Value(3), + }), + valuespb.NewMapValue(map[string]*valuespb.Value{ + "value": values.Proto(values.NewString("malicious")), + "count": valuespb.NewInt64Value(1), + }), + valuespb.NewMapValue(map[string]*valuespb.Value{ + "value": values.Proto(values.NewInt64(50)), + "count": valuespb.NewInt64Value(1), + }), + }), + }, + { + name: "value counts aggregation: insufficient observations", + observations: []*valuespb.Value{ + values.Proto(values.NewInt64(10)), + values.Proto(values.NewInt64(20)), + }, + descriptor: &sdk.ConsensusDescriptor{ + Descriptor_: &sdk.ConsensusDescriptor_Aggregation{ + Aggregation: sdk.AggregationType_AGGREGATION_TYPE_FREQUENCY_LIST, + }, + }, + f: 2, + expectedError: ErrInsufficientObservations, + }, { name: "unknown aggregation type (UNSPECIFIED)", observations: []*valuespb.Value{