Commit dfcd8feb authored by mergify[bot]'s avatar mergify[bot] Committed by GitHub

Merge branch 'develop' into feat/migration-fixes

parents 452bb4bd ad4d235d
---
'@eth-optimism/atst': minor
---
Move react api to @eth-optimism/atst/react so react isn't required to run the core sdk
---
'@eth-optimism/atst': patch
---
Fixed bug with atst not defaulting to currently connected chain
......@@ -1132,6 +1132,13 @@ workflows:
docker_tags: <<pipeline.git.revision>>,<<pipeline.git.branch>>
context:
- oplabs-gcr
- docker-publish:
name: chain-mon-docker-publish
docker_file: ./ops/docker/Dockerfile.packages
docker_name: chain-mon
docker_tags: <<pipeline.git.revision>>,<<pipeline.git.branch>>
context:
- oplabs-gcr
- hive-test:
name: hive-test-rpc
version: <<pipeline.git.revision>>
......@@ -1227,4 +1234,4 @@ workflows:
context:
- oplabs-gcr-release
requires:
- hold
\ No newline at end of file
- hold
......@@ -184,42 +184,68 @@ func (s *L1Replica) L1Client(t Testing, cfg *rollup.Config) *sources.L1Client {
return l1F
}
// ActL1FinalizeNext finalizes the next block, which must be marked as safe before doing so (see ActL1SafeNext).
func (s *L1Replica) ActL1FinalizeNext(t Testing) {
func (s *L1Replica) UnsafeNum() uint64 {
head := s.l1Chain.CurrentBlock()
headNum := uint64(0)
if head != nil {
headNum = head.NumberU64()
}
return headNum
}
func (s *L1Replica) SafeNum() uint64 {
safe := s.l1Chain.CurrentSafeBlock()
safeNum := uint64(0)
if safe != nil {
safeNum = safe.NumberU64()
}
return safeNum
}
func (s *L1Replica) FinalizedNum() uint64 {
finalized := s.l1Chain.CurrentFinalizedBlock()
finalizedNum := uint64(0)
if finalized != nil {
finalizedNum = finalized.NumberU64()
}
if safeNum <= finalizedNum {
return finalizedNum
}
// ActL1Finalize finalizes a later block, which must be marked as safe before doing so (see ActL1SafeNext).
func (s *L1Replica) ActL1Finalize(t Testing, num uint64) {
safeNum := s.SafeNum()
finalizedNum := s.FinalizedNum()
if safeNum < num {
t.InvalidAction("need to move forward safe block before moving finalized block")
return
}
next := s.l1Chain.GetBlockByNumber(finalizedNum + 1)
if next == nil {
t.Fatalf("expected next block after finalized L1 block %d, safe head is ahead", finalizedNum)
newFinalized := s.l1Chain.GetBlockByNumber(num)
if newFinalized == nil {
t.Fatalf("expected block at %d after finalized L1 block %d, safe head is ahead", num, finalizedNum)
}
s.l1Chain.SetFinalized(next)
s.l1Chain.SetFinalized(newFinalized)
}
// ActL1SafeNext marks the next unsafe block as safe.
func (s *L1Replica) ActL1SafeNext(t Testing) {
safe := s.l1Chain.CurrentSafeBlock()
safeNum := uint64(0)
if safe != nil {
safeNum = safe.NumberU64()
}
next := s.l1Chain.GetBlockByNumber(safeNum + 1)
if next == nil {
t.InvalidAction("if head of chain is marked as safe then there's no next block")
// ActL1FinalizeNext finalizes the next block, which must be marked as safe before doing so (see ActL1SafeNext).
func (s *L1Replica) ActL1FinalizeNext(t Testing) {
n := s.FinalizedNum() + 1
s.ActL1Finalize(t, n)
}
// ActL1Safe marks the given unsafe block as safe.
func (s *L1Replica) ActL1Safe(t Testing, num uint64) {
newSafe := s.l1Chain.GetBlockByNumber(num)
if newSafe == nil {
t.InvalidAction("could not find L1 block %d, cannot label it as safe", num)
return
}
s.l1Chain.SetSafe(next)
s.l1Chain.SetSafe(newSafe)
}
// ActL1SafeNext marks the next unsafe block as safe.
func (s *L1Replica) ActL1SafeNext(t Testing) {
n := s.SafeNum() + 1
s.ActL1Safe(t, n)
}
func (s *L1Replica) Close() error {
......
......@@ -196,6 +196,62 @@ func TestL2Finalization(gt *testing.T) {
require.Equal(t, heightToSubmit, sequencer.SyncStatus().FinalizedL2.Number, "unknown/bad finalized L1 blocks are ignored")
}
// TestL2FinalizationWithSparseL1 tests that safe L2 blocks can be finalized even if we do not regularly get a L1 finalization signal
func TestL2FinalizationWithSparseL1(gt *testing.T) {
t := NewDefaultTesting(gt)
dp := e2eutils.MakeDeployParams(t, defaultRollupTestParams)
sd := e2eutils.Setup(t, dp, defaultAlloc)
log := testlog.Logger(t, log.LvlDebug)
miner, engine, sequencer := setupSequencerTest(t, sd, log)
sequencer.ActL2PipelineFull(t)
miner.ActEmptyBlock(t)
sequencer.ActL1HeadSignal(t)
sequencer.ActBuildToL1Head(t)
startStatus := sequencer.SyncStatus()
require.Less(t, startStatus.SafeL2.Number, startStatus.UnsafeL2.Number, "sequencer has unsafe L2 block")
batcher := NewL2Batcher(log, sd.RollupCfg, &BatcherCfg{
MinL1TxSize: 0,
MaxL1TxSize: 128_000,
BatcherKey: dp.Secrets.Batcher,
}, sequencer.RollupClient(), miner.EthClient(), engine.EthClient())
batcher.ActSubmitAll(t)
// include in L1
miner.ActL1StartBlock(12)(t)
miner.ActL1IncludeTx(dp.Addresses.Batcher)(t)
miner.ActL1EndBlock(t)
// Make 2 L1 blocks without batches
miner.ActEmptyBlock(t)
miner.ActEmptyBlock(t)
// See the L1 head, and traverse the pipeline to it
sequencer.ActL1HeadSignal(t)
sequencer.ActL2PipelineFull(t)
updatedStatus := sequencer.SyncStatus()
require.Equal(t, updatedStatus.SafeL2.Number, updatedStatus.UnsafeL2.Number, "unsafe L2 block is now safe")
require.Less(t, updatedStatus.FinalizedL2.Number, updatedStatus.UnsafeL2.Number, "submitted block is not yet finalized")
// Now skip straight to the head with L1 signals (sequencer has traversed the L1 blocks, but they did not have L2 contents)
headL1Num := miner.UnsafeNum()
miner.ActL1Safe(t, headL1Num)
miner.ActL1Finalize(t, headL1Num)
sequencer.ActL1SafeSignal(t)
sequencer.ActL1FinalizedSignal(t)
// Now see if the signals can be processed
sequencer.ActL2PipelineFull(t)
finalStatus := sequencer.SyncStatus()
// Verify the signal was processed, even though we signalled a later L1 block than the one with the batch.
require.Equal(t, finalStatus.FinalizedL2.Number, finalStatus.UnsafeL2.Number, "sequencer submitted its L2 block and it finalized")
}
// TestGarbageBatch tests the behavior of an invalid/malformed output channel frame containing
// valid batches being submitted to the batch inbox. These batches should always be rejected
// and the safe L2 head should remain unaltered.
......
......@@ -4,8 +4,6 @@ import (
"math/big"
"testing"
"github.com/ethereum-optimism/optimism/op-node/rollup/derive"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/params"
......@@ -144,24 +142,19 @@ func TestL2Sequencer_SequencerOnlyReorg(gt *testing.T) {
// so it'll keep the L2 block with the old L1 origin, since no conflict is detected.
sequencer.ActL1HeadSignal(t)
sequencer.ActL2PipelineFull(t)
// TODO: CLI-3405 we can detect the inconsistency of the L1 origin of the unsafe L2 head:
// as verifier, there is no need to wait for sequencer to recognize it.
// Verifier should detect the inconsistency of the L1 origin and reset the pipeline to follow the reorg
newStatus := sequencer.SyncStatus()
require.Equal(t, status.HeadL1.Hash, newStatus.UnsafeL2.L1Origin.Hash, "still have old bad L1 origin")
require.Zero(t, newStatus.UnsafeL2.L1Origin.Number, "back to genesis block with good L1 origin, drop old unsafe L2 chain with bad L1 origins")
require.NotEqual(t, status.HeadL1.Hash, newStatus.HeadL1.Hash, "did see the new L1 head change")
require.Equal(t, newStatus.HeadL1.Hash, newStatus.CurrentL1.Hash, "did sync the new L1 head as verifier")
// the block N+1 cannot build on the old N which still refers to the now orphaned L1 origin
require.Equal(t, status.UnsafeL2.L1Origin.Number, newStatus.HeadL1.Number-1, "seeing N+1 to attempt to build on N")
require.NotEqual(t, status.UnsafeL2.L1Origin.Hash, newStatus.HeadL1.ParentHash, "but N+1 cannot fit on N")
sequencer.ActL1HeadSignal(t)
// sequence more L2 blocks, until we actually need the next L1 origin
sequencer.ActBuildToL1HeadExclUnsafe(t)
// We expect block building to fail when the next L1 block is not consistent with the existing L1 origin
sequencer.ActL2StartBlockCheckErr(t, derive.ErrReset)
// After hitting a reset error, it reset derivation, and drops the old L1 chain
// After hitting a reset error, it resets derivation, and drops the old L1 chain
sequencer.ActL2PipelineFull(t)
require.Zero(t, sequencer.SyncStatus().UnsafeL2.L1Origin.Number, "back to genesis block with good L1 origin, drop old unsafe L2 chain with bad L1 origins")
// Can build new L2 blocks with good L1 origin
sequencer.ActBuildToL1HeadUnsafe(t)
require.Equal(t, newStatus.HeadL1.Hash, sequencer.SyncStatus().UnsafeL2.L1Origin.Hash, "build L2 chain with new correct L1 origins")
......
......@@ -464,7 +464,7 @@ func (cfg SystemConfig) Start() (*System, error) {
c.P2P = p
if c.Driver.SequencerEnabled {
c.P2PSigner = &p2p.PreparedSigner{Signer: p2p.NewLegacyLocalSigner(cfg.Secrets.SequencerP2P)}
c.P2PSigner = &p2p.PreparedSigner{Signer: p2p.NewLocalSigner(cfg.Secrets.SequencerP2P)}
}
}
......
......@@ -19,6 +19,7 @@ import (
"time"
"github.com/ethereum-optimism/optimism/op-node/eth"
ophttp "github.com/ethereum-optimism/optimism/op-node/http"
"github.com/ethereum/go-ethereum/log"
)
......@@ -161,7 +162,8 @@ func runServer() {
mux.HandleFunc("/logs", makeGzipHandler(logsHandler))
log.Info("running webserver...")
if err := http.Serve(l, mux); err != nil && !errors.Is(err, http.ErrServerClosed) {
httpServer := ophttp.NewHttpServer(mux)
if err := httpServer.Serve(l); err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Crit("http server failed", "message", err)
}
}
......
package http
import (
"net/http"
"github.com/ethereum/go-ethereum/rpc"
)
// Use default timeouts from Geth as battle tested default values
var timeouts = rpc.DefaultHTTPTimeouts
func NewHttpServer(handler http.Handler) *http.Server {
return &http.Server{
Handler: handler,
ReadTimeout: timeouts.ReadTimeout,
ReadHeaderTimeout: timeouts.ReadHeaderTimeout,
WriteTimeout: timeouts.WriteTimeout,
IdleTimeout: timeouts.IdleTimeout,
}
}
......@@ -7,10 +7,10 @@ import (
"errors"
"fmt"
"net"
"net/http"
"strconv"
"time"
ophttp "github.com/ethereum-optimism/optimism/op-node/http"
"github.com/ethereum-optimism/optimism/op-service/metrics"
pb "github.com/libp2p/go-libp2p-pubsub/pb"
......@@ -528,12 +528,10 @@ func (m *Metrics) RecordSequencerSealingTime(duration time.Duration) {
// The server will be closed when the passed-in context is cancelled.
func (m *Metrics) Serve(ctx context.Context, hostname string, port int) error {
addr := net.JoinHostPort(hostname, strconv.Itoa(port))
server := &http.Server{
Addr: addr,
Handler: promhttp.InstrumentMetricHandler(
m.registry, promhttp.HandlerFor(m.registry, promhttp.HandlerOpts{}),
),
}
server := ophttp.NewHttpServer(promhttp.InstrumentMetricHandler(
m.registry, promhttp.HandlerFor(m.registry, promhttp.HandlerOpts{}),
))
server.Addr = addr
go func() {
<-ctx.Done()
server.Close()
......
......@@ -7,6 +7,7 @@ import (
"net/http"
"strconv"
ophttp "github.com/ethereum-optimism/optimism/op-node/http"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/node"
"github.com/ethereum/go-ethereum/rpc"
......@@ -87,7 +88,7 @@ func (s *rpcServer) Start() error {
}
s.listenAddr = listener.Addr()
s.httpServer = &http.Server{Handler: mux}
s.httpServer = ophttp.NewHttpServer(mux)
go func() {
if err := s.httpServer.Serve(listener); err != nil && !errors.Is(err, http.ErrServerClosed) { // todo improve error handling
s.log.Error("http server failed", "err", err)
......
......@@ -24,7 +24,7 @@ func LoadSignerSetup(ctx *cli.Context) (p2p.SignerSetup, error) {
return nil, fmt.Errorf("failed to read batch submitter key: %w", err)
}
return &p2p.PreparedSigner{Signer: p2p.NewLegacyLocalSigner(priv)}, nil
return &p2p.PreparedSigner{Signer: p2p.NewLocalSigner(priv)}, nil
}
// TODO: create remote signer
......
......@@ -49,7 +49,7 @@ func TestVerifyBlockSignature(t *testing.T) {
}{
{
name: "Legacy",
newSigner: NewLegacyLocalSigner,
newSigner: newLegacyLocalSigner,
},
{
name: "Updated",
......@@ -102,3 +102,7 @@ func TestVerifyBlockSignature(t *testing.T) {
})
}
}
func newLegacyLocalSigner(priv *ecdsa.PrivateKey) *LocalSigner {
return &LocalSigner{priv: priv, hasher: LegacySigningHash}
}
......@@ -315,7 +315,7 @@ func TestDiscovery(t *testing.T) {
// B and C don't know each other yet, but both have A as a bootnode.
// It should only be a matter of time for them to connect, if they discover each other via A.
timeout := time.After(time.Second * 10)
timeout := time.After(time.Second * 60)
var peersOfB []peer.ID
// B should be connected to the bootnode (A) it used (it's a valid optimism node to connect to here)
// C should also be connected, although this one might take more time to discover
......
......@@ -64,10 +64,6 @@ type LocalSigner struct {
hasher func(domain [32]byte, chainID *big.Int, payloadBytes []byte) (common.Hash, error)
}
func NewLegacyLocalSigner(priv *ecdsa.PrivateKey) *LocalSigner {
return &LocalSigner{priv: priv, hasher: LegacySigningHash}
}
func NewLocalSigner(priv *ecdsa.PrivateKey) *LocalSigner {
return &LocalSigner{priv: priv, hasher: SigningHash}
}
......
......@@ -224,7 +224,13 @@ func (eq *EngineQueue) Step(ctx context.Context) error {
}
outOfData := false
if len(eq.safeAttributes) == 0 {
eq.origin = eq.prev.Origin()
newOrigin := eq.prev.Origin()
// Check if the L2 unsafe head origin is consistent with the new origin
if err := eq.verifyNewL1Origin(ctx, newOrigin); err != nil {
return err
}
eq.origin = newOrigin
eq.postProcessSafeL2() // make sure we track the last L2 safe head for every new L1 block
if next, err := eq.prev.NextAttributes(ctx, eq.safeHead); err == io.EOF {
outOfData = true
} else if err != nil {
......@@ -245,6 +251,38 @@ func (eq *EngineQueue) Step(ctx context.Context) error {
}
}
// verifyNewL1Origin checks that the L2 unsafe head still has a L1 origin that is on the canonical chain.
// If the unsafe head origin is after the new L1 origin it is assumed to still be canonical.
// The check is only required when moving to a new L1 origin.
func (eq *EngineQueue) verifyNewL1Origin(ctx context.Context, newOrigin eth.L1BlockRef) error {
if newOrigin == eq.origin {
return nil
}
unsafeOrigin := eq.unsafeHead.L1Origin
if newOrigin.Number == unsafeOrigin.Number && newOrigin.ID() != unsafeOrigin {
return NewResetError(fmt.Errorf("l1 origin was inconsistent with l2 unsafe head origin, need reset to resolve: l1 origin: %v; unsafe origin: %v",
newOrigin.ID(), unsafeOrigin))
}
// Avoid requesting an older block by checking against the parent hash
if newOrigin.Number == unsafeOrigin.Number+1 && newOrigin.ParentHash != unsafeOrigin.Hash {
return NewResetError(fmt.Errorf("l2 unsafe head origin is no longer canonical, need reset to resolve: canonical hash: %v; unsafe origin hash: %v",
newOrigin.ParentHash, unsafeOrigin.Hash))
}
if newOrigin.Number > unsafeOrigin.Number+1 {
// If unsafe origin is further behind new origin, check it's still on the canonical chain.
canonical, err := eq.l1Fetcher.L1BlockRefByNumber(ctx, unsafeOrigin.Number)
if err != nil {
return NewTemporaryError(fmt.Errorf("failed to fetch canonical L1 block at slot: %v; err: %w", unsafeOrigin.Number, err))
}
if canonical.ID() != unsafeOrigin {
eq.log.Error("Resetting due to origin mismatch")
return NewResetError(fmt.Errorf("l2 unsafe head origin is no longer canonical, need reset to resolve: canonical: %v; unsafe origin: %v",
canonical, unsafeOrigin))
}
}
return nil
}
// tryFinalizeL2 traverses the past L1 blocks, checks if any has been finalized,
// and then marks the latest fully derived L2 block from this as finalized,
// or defaults to the current finalized L2 block.
......@@ -279,9 +317,15 @@ func (eq *EngineQueue) postProcessSafeL2() {
L2Block: eq.safeHead,
L1Block: eq.origin.ID(),
})
last := &eq.finalityData[len(eq.finalityData)-1]
eq.log.Debug("extended finality-data", "last_l1", last.L1Block, "last_l2", last.L2Block)
} else {
// if it's a now L2 block that was derived from the same latest L1 block, then just update the entry
eq.finalityData[len(eq.finalityData)-1].L2Block = eq.safeHead
// if it's a new L2 block that was derived from the same latest L1 block, then just update the entry
last := &eq.finalityData[len(eq.finalityData)-1]
if last.L2Block != eq.safeHead { // avoid logging if there are no changes
last.L2Block = eq.safeHead
eq.log.Debug("updated finality-data", "last_l1", last.L1Block, "last_l2", last.L2Block)
}
}
}
......
This diff is collapsed.
......@@ -6,6 +6,20 @@
"types": "src/index.ts",
"module": "dist/index.cjs",
"license": "MIT",
"exports": {
".": {
"types": "./src/index.ts",
"default": "./dist/index.js",
"import": "./dist/index.js",
"require": "./dist/index.cjs"
},
"./react": {
"types": "./src/react.ts",
"default": "./dist/react.js",
"import": "./dist/react.js",
"require": "./dist/react.cjs"
}
},
"bin": {
"atst": "./dist/cli.js"
},
......
......@@ -28,5 +28,3 @@ export type { AttestationCreatedEvent } from './types/AttestationCreatedEvent'
export type { AttestationReadParams } from './types/AttestationReadParams'
export type { DataTypeOption } from './types/DataTypeOption'
export type { WagmiBytes } from './types/WagmiBytes'
// react
export * from './react'
import { ethers } from 'ethers'
import { Address } from 'wagmi'
import type { Address } from '@wagmi/core'
import { ATTESTATION_STATION_ADDRESS } from '../constants/attestationStationAddress'
import { abi } from '../lib/abi'
......
......@@ -10,7 +10,7 @@ export const prepareWriteAttestation = async (
about: Address,
key: string,
value: string | WagmiBytes | number | boolean,
chainId = 10,
chainId: number | undefined = undefined,
contractAddress: Address = ATTESTATION_STATION_ADDRESS
) => {
let formattedKey: WagmiBytes
......
......@@ -14,7 +14,7 @@ type Attestation = {
export const prepareWriteAttestations = async (
attestations: Attestation[],
chainId = 10,
chainId: number | undefined = undefined,
contractAddress: Address = ATTESTATION_STATION_ADDRESS
) => {
const formattedAttestations = attestations.map((attestation) => {
......@@ -27,9 +27,7 @@ export const prepareWriteAttestations = async (
`key is longer than 32 bytes: ${attestation.key}. Try using a shorter key or using 'encodeRawKey' to encode the key into 32 bytes first`
)
}
const formattedValue = createValue(
attestation.value
) as WagmiBytes
const formattedValue = createValue(attestation.value) as WagmiBytes
return {
about: attestation.about,
key: formattedKey,
......
......@@ -11,4 +11,5 @@ export interface AttestationReadParams {
key: string
dataType?: DataTypeOption
contractAddress?: Address
chainId?: number
}
import { BigNumber } from 'ethers'
import { Address } from 'wagmi'
import type { Address } from '@wagmi/core'
import { DataTypeOption } from './DataTypeOption'
import { WagmiBytes } from './WagmiBytes'
......
......@@ -10,7 +10,7 @@ export default defineConfig({
*
* @see https://tsup.egoist.dev/#building-cli-app
*/
entry: ['src/index.ts', 'src/cli.ts'],
entry: ['src/index.ts', 'src/cli.ts', 'src/react.ts'],
outDir: 'dist',
target: 'es2021',
// will create a .js file for commonjs and a .cjs file for esm
......
Bytes_slice_Test:test_slice_acrossMultipleWords_works() (gas: 9423)
Bytes_slice_Test:test_slice_acrossWords_works() (gas: 1418)
Bytes_slice_Test:test_slice_fromNonZeroIdx_works() (gas: 17154)
Bytes_slice_Test:test_slice_fromZeroIdx_works() (gas: 20694)
Bytes_slice_Test:test_slice_acrossMultipleWords_works() (gas: 9413)
Bytes_slice_Test:test_slice_acrossWords_works() (gas: 1430)
Bytes_slice_Test:test_slice_fromNonZeroIdx_works() (gas: 17240)
Bytes_slice_Test:test_slice_fromZeroIdx_works() (gas: 20826)
Bytes_toNibbles_Test:test_toNibbles_expectedResult128Bytes_works() (gas: 129874)
Bytes_toNibbles_Test:test_toNibbles_expectedResult5Bytes_works() (gas: 6132)
Bytes_toNibbles_Test:test_toNibbles_zeroLengthInput_works() (gas: 944)
......
......@@ -122,6 +122,58 @@ contract Bytes_slice_Test is Test {
vm.expectRevert("slice_overflow");
Bytes.slice(_input, _start, _length);
}
/**
* @notice Tests that the `slice` function correctly updates the free memory pointer depending
* on the length of the slice.
*/
function testFuzz_slice_memorySafety_succeeds(
bytes memory _input,
uint256 _start,
uint256 _length
) public {
// The start should never be more than the length of the input bytes array - 1
vm.assume(_start < _input.length);
// The length should never be more than the length of the input bytes array - the starting
// slice index.
vm.assume(_length <= _input.length - _start);
// Grab the free memory pointer before the slice operation
uint256 initPtr;
assembly {
initPtr := mload(0x40)
}
// Slice the input bytes array from `_start` to `_start + _length`
bytes memory slice = Bytes.slice(_input, _start, _length);
// Grab the free memory pointer after the slice operation
uint256 finalPtr;
assembly {
finalPtr := mload(0x40)
}
// The free memory pointer should have been updated properly
if (_length == 0) {
// If the slice length is zero, only 32 bytes of memory should have been allocated.
assertEq(finalPtr, initPtr + 0x20);
} else {
// If the slice length is greater than zero, the memory allocated should be the
// length of the slice rounded up to the next 32 byte word + 32 bytes for the
// length of the byte array.
//
// Note that we use a slightly less efficient, but equivalent method of rounding
// up `_length` to the next multiple of 32 than is used in the `slice` function.
// This is to diff test the method used in `slice`.
assertEq(finalPtr, initPtr + 0x20 + (((_length + 0x1F) >> 5) << 5));
// Sanity check for equivalence of the rounding methods.
assertEq(((_length + 0x1F) >> 5) << 5, (_length + 0x1F) & ~uint256(0x1F));
}
// The slice length should be equal to `_length`
assertEq(slice.length, _length);
}
}
contract Bytes_toNibbles_Test is Test {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment