Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion eval/eval.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,9 @@ type RunResponse struct {

var exprEnvOptions = []expr.Option{
expr.AsAny(),
// Inject a custom isSorted function into the environment.
// Inject a custom functions into the environment.
functions.IsSorted(),
functions.IsWeb3Checksummed(),

// Provide a constant timestamp to the expression environment.
expr.DisableBuiltin("now"),
Expand Down
12 changes: 12 additions & 0 deletions examples.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -488,6 +488,18 @@ examples:
upstream_host_metadata: "NULL"
category: "Istio"

- name: "Address checksummed"
expr: |
isWeb3Checksummed(network_1.address) && isWeb3Checksummed(network_2.addresses)
data: |
network_1:
address: 0xb0F001C7F6C665b7b8e12F29EDC1107613fe980D
network_2:
addresses:
- 0xb0F001C7F6C665b7b8e12F29EDC1107613fe980D
- 0x3106E2e148525b3DB36795b04691D444c24972fB
category: "Web3"

- name: "Blank"
expr: ""
data: ""
Expand Down
115 changes: 115 additions & 0 deletions functions/is_web3_checksum.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
// Copyright 2024 Peter Olds <me@polds.dev>
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package functions

import (
"encoding/hex"
"fmt"
"reflect"
"strings"

"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
"github.com/expr-lang/expr"
)

// IsWeb3Checksummed is a function that checks whether the given address (or list of addresses) is checksummed. It is provided as an Expr function.
// It supports the following types:
// - string
// - []any (which should contain only string elements)

// Examples:
// - isWeb3Checksummed("0xb0F001C7F6C665b7b8e12F29EDC1107613fe980D")
// - isWeb3Checksummed(["0xb0F001C7F6C665b7b8e12F29EDC1107613fe980D", "0x3106E2e148525b3DB36795b04691D444c24972fB"])
func IsWeb3Checksummed() expr.Option {
return expr.Function("isWeb3Checksummed", func(params ...any) (any, error) {
return isWeb3Checksummed(params[0])
},
new(func([]any) (bool, error)),
new(func(string) (bool, error)),
)
}

func isWeb3Checksummed(v any) (any, error) {
if v == nil {
return false, nil
}

switch t := v.(type) {
case []any:
return arrayChecksummed(t)
case string:
return checksummed(t)
default:
return false, fmt.Errorf("type %s is not supported", reflect.TypeOf(v))
}
}

func arrayChecksummed(v []any) (bool, error) {
switch t := v[0].(type) {
case string:
for _, address := range v {
res, err := checksummed(address.(string))
if err != nil || !res {
return res, err
}
}
return true, nil
default:
return false, fmt.Errorf("unsupported type %T", t)
}
}

func checksummed(address string) (bool, error) {
if len(address) != 42 {
return false, fmt.Errorf("Address needs to be 42 characters long")
}

if !strings.HasPrefix(address, "0x") {
return false, fmt.Errorf("Address needs to start with 0x")
}

return common.IsHexAddress(address) && checksumAddress(address) == address, nil
}

// Algorithm for checksumming a web3 address:
// - Convert the address to lowercase
// - Hash the address usinga keccak256
// - Take 40 characters of the hash, drop the rest (40 because of the address length)
// - Iterate through each character in the original address
// - If the checksum character >= 8 and character in the original address at the same idx is [a, f] then capitalize
// - Otherwise, add character
//
// For visualization, you can watch the following video: https://www.youtube.com/watch?v=2vH_CQ_rvbc
func checksumAddress(address string) string {
if strings.HasPrefix(address, "0x") {
address = address[2:]
}

lowercaseAddress := strings.ToLower(address)
hashedAddress := crypto.Keccak256([]byte(lowercaseAddress))
checksum := hex.EncodeToString(hashedAddress)[:40]

var checksumAddress strings.Builder
for idx, char := range lowercaseAddress {
if checksum[idx] >= '8' && (char >= 'a' && char <= 'f') {
checksumAddress.WriteRune(char - 32)
} else {
checksumAddress.WriteRune(char)
}
}

return "0x" + checksumAddress.String()
}
118 changes: 118 additions & 0 deletions functions/is_web3_checksum_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
// Copyright 2024 Peter Olds <me@polds.dev>
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package functions

import (
"testing"

"github.com/expr-lang/expr"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestIsWeb3Checksummed(t *testing.T) {
tests := []struct {
name string
expr string
want bool
wantCompileErr bool
wantRuntimeErr bool
}{
{
name: "nil",
expr: `isWeb3Checksummed(nil)`,
want: false,
},
{
name: "string - not checksummed",
expr: `isWeb3Checksummed('0x30F4283a3d6302f968909Ff7c02ceCB2ac6C27Ac')`,
want: false,
},
{
name: "string - checksummed",
expr: `isWeb3Checksummed('0x30D873664Ba766C983984C7AF9A921ccE36D34e1')`,
want: true,
},
{
name: "string slice - checksummed",
expr: `isWeb3Checksummed(['0x55028780918330FD00a34a61D9a7Efd3f43ca845', '0xAA95A3e367b427477bAdAB3d104f7D04ba158895'])`,
want: true,
},
{
name: "string slice - checksummed",
expr: `isWeb3Checksummed(['0x869C8ADA0fb9AfC753159b7D6D72Cc8bf58e6987', '0x2a92BCecd6e702702864E134821FD2DE73C3e180'])`,
want: false,
},
{
name: "address needs to start with 0x",
expr: `isWeb3Checksummed('0034B03Cb9086d7D758AC55af71584F81A598759FE')`,
wantRuntimeErr: true,
},
{
name: "address needs to be 42 characters long",
expr: `isWeb3Checksummed('34B03Cb9086d7D758AC55af71584F81A598759FE')`,
wantRuntimeErr: true,
},
{
name: "unsupported type int",
expr: `isWeb3Checksummed(0)`,
wantCompileErr: true,
},
{
name: "unsupported type int",
expr: `isWeb3Checksummed([0])`,
wantRuntimeErr: true,
},
{
name: "not enough arguments",
expr: `isWeb3Checksummed()`,
wantCompileErr: true,
},
}

opts := []expr.Option{
expr.AsBool(),
expr.DisableAllBuiltins(),
IsWeb3Checksummed(),
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
program, err := expr.Compile(tc.expr, opts...)
if tc.wantCompileErr && err == nil {
require.Error(t, err)
}
if !tc.wantCompileErr && err != nil {
require.NoError(t, err)
}
if tc.wantCompileErr {
return
}

got, err := expr.Run(program, nil)
if tc.wantRuntimeErr && err == nil {
require.Error(t, err)
}
if !tc.wantRuntimeErr && err != nil {
require.NoError(t, err)
}
if tc.wantRuntimeErr {
return
}
assert.IsType(t, tc.want, got)
assert.Equal(t, tc.want, got)
})
}
}
6 changes: 6 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,15 @@ require (
)

require (
github.com/btcsuite/btcd/btcec/v2 v2.2.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect
github.com/ethereum/go-ethereum v1.13.10 // indirect
github.com/holiman/uint256 v1.2.4 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rogpeppe/go-internal v1.11.0 // indirect
golang.org/x/crypto v0.17.0 // indirect
golang.org/x/sys v0.15.0 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
)
13 changes: 13 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,10 +1,19 @@
github.com/btcsuite/btcd/btcec/v2 v2.2.0 h1:fzn1qaOt32TuLjFlkzYSsBC35Q3KUjT1SwPxiMSCF5k=
github.com/btcsuite/btcd/btcec/v2 v2.2.0/go.mod h1:U7MHm051Al6XmscBQ0BoNydpOTsFAn707034b5nY8zU=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 h1:YLtO71vCjJRCBcrPMtQ9nqBsqpA1m5sE92cU+pd5Mcc=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs=
github.com/ethereum/go-ethereum v1.13.10 h1:Ppdil79nN+Vc+mXfge0AuUgmKWuVv4eMqzoIVSdqZek=
github.com/ethereum/go-ethereum v1.13.10/go.mod h1:sc48XYQxCzH3fG9BcrXCOOgQk2JfZzNAmIKnceogzsA=
github.com/expr-lang/expr v1.15.8 h1:FL8+d3rSSP4tmK9o+vKfSMqqpGL8n15pEPiHcnBpxoI=
github.com/expr-lang/expr v1.15.8/go.mod h1:uCkhfG+x7fcZ5A5sXHKuQ07jGZRl6J0FCAaf2k4PtVQ=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/holiman/uint256 v1.2.4 h1:jUc4Nk8fm9jZabQuqr2JzednajVmBpC+oiTiXZJEApU=
github.com/holiman/uint256 v1.2.4/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
Expand All @@ -20,6 +29,10 @@ github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDN
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
Expand Down