From 4cb43a1a07e87923173a4e9fccc79c34db1f055a Mon Sep 17 00:00:00 2001 From: Jasmin Bakalovic Date: Thu, 25 Jan 2024 09:23:43 -0800 Subject: [PATCH 1/5] Feature: Add isWeb3Checksummed function --- eval/eval.go | 3 +- examples.yaml | 12 +++ functions/is_web3_checksum.go | 115 ++++++++++++++++++++++++++++ functions/is_web3_checksum_test.go | 118 +++++++++++++++++++++++++++++ go.mod | 6 ++ go.sum | 13 ++++ 6 files changed, 266 insertions(+), 1 deletion(-) create mode 100644 functions/is_web3_checksum.go create mode 100644 functions/is_web3_checksum_test.go diff --git a/eval/eval.go b/eval/eval.go index e5ede0e..a142efc 100644 --- a/eval/eval.go +++ b/eval/eval.go @@ -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"), diff --git a/examples.yaml b/examples.yaml index b9c9e15..c495add 100644 --- a/examples.yaml +++ b/examples.yaml @@ -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: "" diff --git a/functions/is_web3_checksum.go b/functions/is_web3_checksum.go new file mode 100644 index 0000000..4182e4f --- /dev/null +++ b/functions/is_web3_checksum.go @@ -0,0 +1,115 @@ +// Copyright 2024 Peter Olds +// +// 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() +} diff --git a/functions/is_web3_checksum_test.go b/functions/is_web3_checksum_test.go new file mode 100644 index 0000000..60e2575 --- /dev/null +++ b/functions/is_web3_checksum_test.go @@ -0,0 +1,118 @@ +// Copyright 2024 Peter Olds +// +// 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) + }) + } +} diff --git a/go.mod b/go.mod index 1df8a82..37b3143 100644 --- a/go.mod +++ b/go.mod @@ -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 ) diff --git a/go.sum b/go.sum index f93dbd6..0ff215f 100644 --- a/go.sum +++ b/go.sum @@ -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= @@ -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= From 9646eddc9f1812d7993fc9a546c08290930a98f8 Mon Sep 17 00:00:00 2001 From: Peter Olds Date: Mon, 4 Mar 2024 07:34:11 -0800 Subject: [PATCH 2/5] Update functions/is_web3_checksum.go --- functions/is_web3_checksum.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/functions/is_web3_checksum.go b/functions/is_web3_checksum.go index 4182e4f..3dc8eb9 100644 --- a/functions/is_web3_checksum.go +++ b/functions/is_web3_checksum.go @@ -74,7 +74,7 @@ func arrayChecksummed(v []any) (bool, error) { func checksummed(address string) (bool, error) { if len(address) != 42 { - return false, fmt.Errorf("Address needs to be 42 characters long") + return false, fmt.Errorf("address needs to be 42 characters long") } if !strings.HasPrefix(address, "0x") { From fb9e9e39b430e7074387ece761abbcb8b9b71a0f Mon Sep 17 00:00:00 2001 From: Peter Olds Date: Mon, 4 Mar 2024 07:34:20 -0800 Subject: [PATCH 3/5] Update functions/is_web3_checksum.go --- functions/is_web3_checksum.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/functions/is_web3_checksum.go b/functions/is_web3_checksum.go index 3dc8eb9..ba642af 100644 --- a/functions/is_web3_checksum.go +++ b/functions/is_web3_checksum.go @@ -78,7 +78,7 @@ func checksummed(address string) (bool, error) { } if !strings.HasPrefix(address, "0x") { - return false, fmt.Errorf("Address needs to start with 0x") + return false, fmt.Errorf("address needs to start with 0x") } return common.IsHexAddress(address) && checksumAddress(address) == address, nil From e0668b5a3bdda215470edd2a4998dc81259edfea Mon Sep 17 00:00:00 2001 From: Peter Olds Date: Mon, 4 Mar 2024 07:34:25 -0800 Subject: [PATCH 4/5] Update functions/is_web3_checksum.go --- functions/is_web3_checksum.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/functions/is_web3_checksum.go b/functions/is_web3_checksum.go index ba642af..6e4667a 100644 --- a/functions/is_web3_checksum.go +++ b/functions/is_web3_checksum.go @@ -86,7 +86,7 @@ func checksummed(address string) (bool, error) { // Algorithm for checksumming a web3 address: // - Convert the address to lowercase -// - Hash the address usinga keccak256 +// - Hash the address using 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 From d49db6e2f74d623cba400170b9c686763f63233d Mon Sep 17 00:00:00 2001 From: Peter Olds Date: Mon, 4 Mar 2024 07:34:30 -0800 Subject: [PATCH 5/5] Update functions/is_web3_checksum.go --- functions/is_web3_checksum.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/functions/is_web3_checksum.go b/functions/is_web3_checksum.go index 6e4667a..8fd90da 100644 --- a/functions/is_web3_checksum.go +++ b/functions/is_web3_checksum.go @@ -53,7 +53,7 @@ func isWeb3Checksummed(v any) (any, error) { case string: return checksummed(t) default: - return false, fmt.Errorf("type %s is not supported", reflect.TypeOf(v)) + return false, fmt.Errorf("type %T is not supported", t) } }