diff --git a/core/predicate_check.go b/core/predicate_check.go index 325fa14796..e5823f7cb2 100644 --- a/core/predicate_check.go +++ b/core/predicate_check.go @@ -9,7 +9,7 @@ import ( "github.com/ava-labs/subnet-evm/core/types" "github.com/ava-labs/subnet-evm/params" "github.com/ava-labs/subnet-evm/precompile/precompileconfig" - "github.com/ava-labs/subnet-evm/utils" + predicateutils "github.com/ava-labs/subnet-evm/utils/predicate" "github.com/ethereum/go-ethereum/common" ) @@ -49,7 +49,7 @@ func checkPrecompilePredicates(rules params.Rules, predicateContext *precompilec return fmt.Errorf("predicate %s failed verification for tx %s: specified %s in access list multiple times", address, tx.Hash(), address) } precompileAddressChecks[address] = struct{}{} - predicateBytes := utils.HashSliceToBytes(accessTuple.StorageKeys) + predicateBytes := predicateutils.HashSliceToBytes(accessTuple.StorageKeys) if err := predicater.VerifyPredicate(predicateContext, predicateBytes); err != nil { return fmt.Errorf("predicate %s failed verification for tx %s: %w", address, tx.Hash(), err) } @@ -77,7 +77,7 @@ func checkProposerPrecompilePredicates(rules params.Rules, predicateContext *pre return fmt.Errorf("predicate %s failed verification for tx %s: specified %s in access list multiple times", address, tx.Hash(), address) } precompileAddressChecks[address] = struct{}{} - predicateBytes := utils.HashSliceToBytes(accessTuple.StorageKeys) + predicateBytes := predicateutils.HashSliceToBytes(accessTuple.StorageKeys) if err := predicater.VerifyPredicate(predicateContext, predicateBytes); err != nil { return fmt.Errorf("predicate %s failed verification for tx %s: %w", address, tx.Hash(), err) } diff --git a/core/state/statedb.go b/core/state/statedb.go index b1a8bd3ad0..ae880208d5 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -40,7 +40,7 @@ import ( "github.com/ava-labs/subnet-evm/metrics" "github.com/ava-labs/subnet-evm/params" "github.com/ava-labs/subnet-evm/trie" - "github.com/ava-labs/subnet-evm/utils" + predicateutils "github.com/ava-labs/subnet-evm/utils/predicate" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/log" @@ -1109,7 +1109,7 @@ func (s *StateDB) preparePredicateStorageSlots(rules params.Rules, list types.Ac if !rules.PredicateExists(el.Address) { continue } - s.predicateStorageSlots[el.Address] = utils.HashSliceToBytes(el.StorageKeys) + s.predicateStorageSlots[el.Address] = predicateutils.HashSliceToBytes(el.StorageKeys) } } diff --git a/core/state_transition.go b/core/state_transition.go index 7c949ce6ad..b1b3bd2eca 100644 --- a/core/state_transition.go +++ b/core/state_transition.go @@ -37,7 +37,7 @@ import ( "github.com/ava-labs/subnet-evm/core/vm" "github.com/ava-labs/subnet-evm/params" "github.com/ava-labs/subnet-evm/precompile/contracts/txallowlist" - "github.com/ava-labs/subnet-evm/utils" + predicateutils "github.com/ava-labs/subnet-evm/utils/predicate" "github.com/ava-labs/subnet-evm/vmerrs" "github.com/ethereum/go-ethereum/common" commonMath "github.com/ethereum/go-ethereum/common/math" @@ -219,13 +219,13 @@ func accessListGas(rules params.Rules, accessList types.AccessList) (uint64, err func applyPredicateGas(rules params.Rules, accessTuple types.AccessTuple) (uint64, error) { predicate, ok := rules.PredicatePrecompiles[accessTuple.Address] if ok { - return predicate.PredicateGas(utils.HashSliceToBytes(accessTuple.StorageKeys)) + return predicate.PredicateGas(predicateutils.HashSliceToBytes(accessTuple.StorageKeys)) } proposerPredicate, ok := rules.ProposerPredicates[accessTuple.Address] if !ok { return 0, nil } - return proposerPredicate.PredicateGas(utils.HashSliceToBytes(accessTuple.StorageKeys)) + return proposerPredicate.PredicateGas(predicateutils.HashSliceToBytes(accessTuple.StorageKeys)) } // NewStateTransition initialises and returns a new state transition object. diff --git a/utils/bytes.go b/utils/bytes.go index 54258b20f4..186e3c41ef 100644 --- a/utils/bytes.go +++ b/utils/bytes.go @@ -3,8 +3,6 @@ package utils -import "github.com/ethereum/go-ethereum/common" - // IncrOne increments bytes value by one func IncrOne(bytes []byte) { index := len(bytes) - 1 @@ -18,27 +16,3 @@ func IncrOne(bytes []byte) { } } } - -// HashSliceToBytes serializes a []common.Hash into a tightly packed byte array. -func HashSliceToBytes(hashes []common.Hash) []byte { - bytes := make([]byte, common.HashLength*len(hashes)) - for i, hash := range hashes { - copy(bytes[i*common.HashLength:], hash[:]) - } - return bytes -} - -// BytesToHashSlice packs [b] into a slice of hash values with zero padding -// to the right if the length of b is not a multiple of 32. -func BytesToHashSlice(b []byte) []common.Hash { - var ( - numHashes = (len(b) + 31) / 32 - hashes = make([]common.Hash, numHashes) - ) - - for i := range hashes { - start := i * common.HashLength - copy(hashes[i][:], b[start:]) - } - return hashes -} diff --git a/utils/bytes_test.go b/utils/bytes_test.go index 7f3619c16a..121410c85a 100644 --- a/utils/bytes_test.go +++ b/utils/bytes_test.go @@ -4,13 +4,10 @@ package utils import ( - "bytes" "testing" - "github.com/ava-labs/avalanchego/utils" "github.com/ethereum/go-ethereum/common" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) func TestIncrOne(t *testing.T) { @@ -39,28 +36,3 @@ func TestIncrOne(t *testing.T) { }) } } - -func testBytesToHashSlice(t testing.TB, b []byte) { - hashSlice := BytesToHashSlice(b) - - copiedBytes := HashSliceToBytes(hashSlice) - - if len(b)%32 == 0 { - require.Equal(t, b, copiedBytes) - } else { - require.Equal(t, b, copiedBytes[:len(b)]) - // Require that any additional padding is all zeroes - padding := copiedBytes[len(b):] - require.Equal(t, bytes.Repeat([]byte{0x00}, len(padding)), padding) - } -} - -func FuzzHashSliceToBytes(f *testing.F) { - for i := 0; i < 100; i++ { - f.Add(utils.RandomBytes(i)) - } - - f.Fuzz(func(t *testing.T, a []byte) { - testBytesToHashSlice(t, a) - }) -} diff --git a/utils/predicate/README.md b/utils/predicate/README.md new file mode 100644 index 0000000000..34d8f02aae --- /dev/null +++ b/utils/predicate/README.md @@ -0,0 +1,11 @@ +# Predicate Utils + +This package provides simple helpers to pack/unpack byte slices for a predicate transaction, where a byte slice of size N is encoded in the access list of a transaction. + +## Encoding + +A byte slice of size N is encoded as: + +1. Slice of N bytes +2. Delimiter byte `0xff` +3. Appended 0s to the nearest multiple of 32 bytes diff --git a/utils/predicate/predicate_bytes.go b/utils/predicate/predicate_bytes.go new file mode 100644 index 0000000000..db94b14d61 --- /dev/null +++ b/utils/predicate/predicate_bytes.go @@ -0,0 +1,68 @@ +// (c) 2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package predicateutils + +import ( + "fmt" + + "github.com/ethereum/go-ethereum/common" +) + +// PredicateEndByte is used as a delimiter for the bytes packed into a precompile predicate. +// Precompile predicates are encoded in the Access List of transactions in the access tuples +// which means that its length must be a multiple of 32 (common.HashLength). +// For messages with a length that does not comply to that, this delimiter is used to +// append/remove padding. +var PredicateEndByte = byte(0xff) + +// PackPredicate packs [predicate] by delimiting the actual message with [PredicateEndByte] +// and zero padding to reach a length that is a multiple of 32. +func PackPredicate(predicate []byte) []byte { + predicate = append(predicate, PredicateEndByte) + return common.RightPadBytes(predicate, (len(predicate)+31)/32*32) +} + +// UnpackPredicate unpacks a predicate by stripping right padded zeroes, checking for the delimter, +// ensuring there is not excess padding, and returning the original message. +// Returns an error if it finds an incorrect encoding. +func UnpackPredicate(paddedPredicate []byte) ([]byte, error) { + trimmedPredicateBytes := common.TrimRightZeroes(paddedPredicate) + if len(trimmedPredicateBytes) == 0 { + return nil, fmt.Errorf("predicate specified invalid all zero bytes: 0x%x", paddedPredicate) + } + + if expectedPaddedLength := (len(trimmedPredicateBytes) + 31) / 32 * 32; expectedPaddedLength != len(paddedPredicate) { + return nil, fmt.Errorf("predicate specified invalid padding with length (%d), expected length (%d)", len(paddedPredicate), expectedPaddedLength) + } + + if trimmedPredicateBytes[len(trimmedPredicateBytes)-1] != PredicateEndByte { + return nil, fmt.Errorf("invalid end delimiter") + } + + return trimmedPredicateBytes[:len(trimmedPredicateBytes)-1], nil +} + +// HashSliceToBytes serializes a []common.Hash into a tightly packed byte array. +func HashSliceToBytes(hashes []common.Hash) []byte { + bytes := make([]byte, common.HashLength*len(hashes)) + for i, hash := range hashes { + copy(bytes[i*common.HashLength:], hash[:]) + } + return bytes +} + +// BytesToHashSlice packs [b] into a slice of hash values with zero padding +// to the right if the length of b is not a multiple of 32. +func BytesToHashSlice(b []byte) []common.Hash { + var ( + numHashes = (len(b) + 31) / 32 + hashes = make([]common.Hash, numHashes) + ) + + for i := range hashes { + start := i * common.HashLength + copy(hashes[i][:], b[start:]) + } + return hashes +} diff --git a/utils/predicate/predicate_bytes_test.go b/utils/predicate/predicate_bytes_test.go new file mode 100644 index 0000000000..c39758edea --- /dev/null +++ b/utils/predicate/predicate_bytes_test.go @@ -0,0 +1,73 @@ +// (c) 2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package predicateutils + +import ( + "bytes" + "testing" + + "github.com/ava-labs/avalanchego/utils" + "github.com/stretchr/testify/require" +) + +func testBytesToHashSlice(t testing.TB, b []byte) { + hashSlice := BytesToHashSlice(b) + + copiedBytes := HashSliceToBytes(hashSlice) + + if len(b)%32 == 0 { + require.Equal(t, b, copiedBytes) + } else { + require.Equal(t, b, copiedBytes[:len(b)]) + // Require that any additional padding is all zeroes + padding := copiedBytes[len(b):] + require.Equal(t, bytes.Repeat([]byte{0x00}, len(padding)), padding) + } +} + +func FuzzHashSliceToBytes(f *testing.F) { + for i := 0; i < 100; i++ { + f.Add(utils.RandomBytes(i)) + } + + f.Fuzz(func(t *testing.T, b []byte) { + testBytesToHashSlice(t, b) + }) +} + +func testPackPredicate(t testing.TB, b []byte) { + packedPredicate := PackPredicate(b) + unpackedPredicated, err := UnpackPredicate(packedPredicate) + require.NoError(t, err) + require.Equal(t, b, unpackedPredicated) +} + +func FuzzPackPredicate(f *testing.F) { + for i := 0; i < 100; i++ { + f.Add(utils.RandomBytes(i)) + } + + f.Fuzz(func(t *testing.T, b []byte) { + testPackPredicate(t, b) + }) +} + +func FuzzUnpackInvalidPredicate(f *testing.F) { + // Seed the fuzzer with non-zero length padding of zeroes or non-zeroes. + for i := 1; i < 100; i++ { + f.Add(utils.RandomBytes(i)) + f.Add(make([]byte, i)) + } + + f.Fuzz(func(t *testing.T, b []byte) { + // Ensure that adding the invalid padding to any length correctly packed predicate + // results in failing to unpack it. + for _, l := range []int{0, 1, 31, 32, 33, 63, 64, 65} { + validPredicate := PackPredicate(utils.RandomBytes(l)) + invalidPredicate := append(validPredicate, b...) + _, err := UnpackPredicate(invalidPredicate) + require.Error(t, err) + } + }) +}