Unintended chain split affecting less than 25% of the network (Network partition)
Description
Brief/Intro
A potential chain fork has been discovered in the Ethereum client Erigon ( https://github.com/erigontech/erigon ) due to the shallow copy of the byte slice in precompile contract dataCopy.
Vulnerability Details
The issue outlined in this report pertains to the precompile contract dataCopy, detailed as follows:
This discrepancy in implementation could lead to chain fork as observed in go-ethereum clients in the post mortem: https://gist.github.com/karalabe/e1891c8a99fdc16c4e60d9713c35401f
Impact Details
This discrepancy in implementation of shallow copy and deep copy could lead to chain fork.
This attack scenario has been observed in two opcodes RETURNDATASIZE and RETURNDATACOPY in go-ethereum as described in the post mortem: https://gist.github.com/karalabe/e1891c8a99fdc16c4e60d9713c35401f
Here we provide a unit test to show the difference between the implementation of data copy precompile contract with shallow copy and deep copy:
Test result:
The result shows that the shallow copy modifies the original input while deep copy does not.
Since Erigon uses the shallow copy in the data copy precompile contract, once the input is modified, it would lead to inconsistent data with other Ethereum clients, potentially lead to chain fork/split.
// data copy implemented as a native contract.
type dataCopy struct{}
// RequiredGas returns the gas required to execute the pre-compiled contract.
//
// This method does not require any overflow checking as the input size gas costs
// required for anything significant is so high it's impossible to pay for.
func (c *dataCopy) RequiredGas(input []byte) uint64 {
return uint64(len(input)+31)/32*params.IdentityPerWordGas + params.IdentityBaseGas
}
func (c *dataCopy) Run(in []byte) ([]byte, error) {
return in, nil
}
package vm
import (
"bytes"
"encoding/json"
"fmt"
"os"
"testing"
"time"
libcommon "github.com/ledgerwatch/erigon-lib/common"
"github.com/ledgerwatch/erigon/common"
)
func CopyBytes(b []byte) (copiedBytes []byte) {
if b == nil {
return nil
}
copiedBytes = make([]byte, len(b))
copy(copiedBytes, b)
return
}
func TestDatacopyPrecompile(t *testing.T) {
dataCopyContract := PrecompiledContractsCancun[libcommon.BytesToAddress([]byte{0x04})]
input1 := []byte{0x01, 0x02, 0x03, 0x04}
maxGas := uint64(10000)
//////////////shallow copy/////////
output1, suppliedGas, err := RunPrecompiledContract(dataCopyContract, input1, maxGas)
if err == nil {
fmt.Printf("Output is: %x, supplied gas is: %d\n", output1, suppliedGas)
}
output1[0] = 0xff
fmt.Println("//////////////shallow copy/////////")
fmt.Printf("Input is changed from %x, to: %x\n", input1, output1)
fmt.Printf("Output is: %x\n", output1)
//////////////deep copy/////////
input2 := []byte{0x01, 0x02, 0x03, 0x04}
output2 := CopyBytes(input2)
output2[0] = 0xff
fmt.Println("//////////////deep copy/////////")
fmt.Printf("Input is changed from %x, to: %x\n", input2, output2)
fmt.Printf("Output is: %x\n", output2)
}
=== RUN TestDatacopyPrecompile
Output is: 01020304, supplied gas is: 9982
//////////////shallow copy/////////
Input is changed from ff020304, to: ff020304
Output is: ff020304
//////////////deep copy/////////
Input is changed from 01020304, to: ff020304
Output is: ff020304
--- PASS: TestDatacopyPrecompile (0.00s)
PASS