mirror of
https://github.com/OffchainLabs/prysm.git
synced 2026-01-09 21:38:05 -05:00
Implement SubmitVoluntaryExit and SubmitProposerSlashing in the beacon API (#8532)
* SubmitProposerSlashing * SubmitVoluntaryExit * rename variables
This commit is contained in:
@@ -30,8 +30,9 @@ func (m *PoolMock) InsertAttesterSlashing(_ context.Context, _ *state.BeaconStat
|
||||
}
|
||||
|
||||
// InsertProposerSlashing --
|
||||
func (m *PoolMock) InsertProposerSlashing(_ context.Context, _ *state.BeaconState, _ *ethpb.ProposerSlashing) error {
|
||||
panic("implement me")
|
||||
func (m *PoolMock) InsertProposerSlashing(_ context.Context, _ *state.BeaconState, slashing *ethpb.ProposerSlashing) error {
|
||||
m.PendingPropSlashings = append(m.PendingPropSlashings, slashing)
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarkIncludedAttesterSlashing --
|
||||
|
||||
@@ -19,8 +19,8 @@ func (m *PoolMock) PendingExits(_ *beaconstate.BeaconState, _ types.Slot, _ bool
|
||||
}
|
||||
|
||||
// InsertVoluntaryExit --
|
||||
func (*PoolMock) InsertVoluntaryExit(_ context.Context, _ *beaconstate.BeaconState, _ *eth.SignedVoluntaryExit) {
|
||||
panic("implement me")
|
||||
func (m *PoolMock) InsertVoluntaryExit(_ context.Context, _ *beaconstate.BeaconState, exit *eth.SignedVoluntaryExit) {
|
||||
m.Exits = append(m.Exits, exit)
|
||||
}
|
||||
|
||||
// MarkIncluded --
|
||||
|
||||
@@ -59,13 +59,13 @@ func (bs *Server) SubmitAttesterSlashing(ctx context.Context, req *ethpb.Atteste
|
||||
return nil, status.Errorf(codes.Internal, "Could not get head state: %v", err)
|
||||
}
|
||||
|
||||
v1Slashing := migration.V1AttSlashingToV1Alpha1(req)
|
||||
err = blocks.VerifyAttesterSlashing(ctx, headState, v1Slashing)
|
||||
alphaSlashing := migration.V1AttSlashingToV1Alpha1(req)
|
||||
err = blocks.VerifyAttesterSlashing(ctx, headState, alphaSlashing)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "Invalid attester slashing: %v", err)
|
||||
}
|
||||
|
||||
err = bs.SlashingsPool.InsertAttesterSlashing(ctx, headState, v1Slashing)
|
||||
err = bs.SlashingsPool.InsertAttesterSlashing(ctx, headState, alphaSlashing)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "Could not insert attester slashing into pool: %v", err)
|
||||
}
|
||||
@@ -103,7 +103,31 @@ func (bs *Server) ListPoolProposerSlashings(ctx context.Context, req *ptypes.Emp
|
||||
// SubmitProposerSlashing submits AttesterSlashing object to node's pool and if
|
||||
// passes validation node MUST broadcast it to network.
|
||||
func (bs *Server) SubmitProposerSlashing(ctx context.Context, req *ethpb.ProposerSlashing) (*ptypes.Empty, error) {
|
||||
return nil, errors.New("unimplemented")
|
||||
ctx, span := trace.StartSpan(ctx, "beaconv1.SubmitProposerSlashing")
|
||||
defer span.End()
|
||||
|
||||
headState, err := bs.ChainInfoFetcher.HeadState(ctx)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "Could not get head state: %v", err)
|
||||
}
|
||||
|
||||
alphaSlashing := migration.V1ProposerSlashingToV1Alpha1(req)
|
||||
err = blocks.VerifyProposerSlashing(headState, alphaSlashing)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "Invalid proposer slashing: %v", err)
|
||||
}
|
||||
|
||||
err = bs.SlashingsPool.InsertProposerSlashing(ctx, headState, alphaSlashing)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "Could not insert proposer slashing into pool: %v", err)
|
||||
}
|
||||
if !featureconfig.Get().DisableBroadcastSlashings {
|
||||
if err := bs.Broadcaster.Broadcast(ctx, req); err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "Could not broadcast slashing object: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
return &ptypes.Empty{}, nil
|
||||
}
|
||||
|
||||
// ListPoolVoluntaryExits retrieves voluntary exits known by the node but
|
||||
@@ -132,5 +156,28 @@ func (bs *Server) ListPoolVoluntaryExits(ctx context.Context, req *ptypes.Empty)
|
||||
// SubmitVoluntaryExit submits SignedVoluntaryExit object to node's pool
|
||||
// and if passes validation node MUST broadcast it to network.
|
||||
func (bs *Server) SubmitVoluntaryExit(ctx context.Context, req *ethpb.SignedVoluntaryExit) (*ptypes.Empty, error) {
|
||||
return nil, errors.New("unimplemented")
|
||||
ctx, span := trace.StartSpan(ctx, "beaconv1.SubmitVoluntaryExit")
|
||||
defer span.End()
|
||||
|
||||
headState, err := bs.ChainInfoFetcher.HeadState(ctx)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "Could not get head state: %v", err)
|
||||
}
|
||||
|
||||
validator, err := headState.ValidatorAtIndexReadOnly(req.Exit.ValidatorIndex)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "Could not get exiting validator: %v", err)
|
||||
}
|
||||
alphaExit := migration.V1ExitToV1Alpha1(req)
|
||||
err = blocks.VerifyExitAndSignature(validator, headState.Slot(), headState.Fork(), alphaExit, headState.GenesisValidatorRoot())
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "Invalid voluntary exit: %v", err)
|
||||
}
|
||||
|
||||
bs.VoluntaryExitsPool.InsertVoluntaryExit(ctx, headState, alphaExit)
|
||||
if err := bs.Broadcaster.Broadcast(ctx, req); err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "Could not broadcast voluntary exit object: %v", err)
|
||||
}
|
||||
|
||||
return &ptypes.Empty{}, nil
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/gogo/protobuf/types"
|
||||
eth2types "github.com/prysmaticlabs/eth2-types"
|
||||
ethpb "github.com/prysmaticlabs/ethereumapis/eth/v1"
|
||||
eth "github.com/prysmaticlabs/ethereumapis/eth/v1alpha1"
|
||||
chainMock "github.com/prysmaticlabs/prysm/beacon-chain/blockchain/testing"
|
||||
@@ -314,3 +315,220 @@ func TestSubmitAttesterSlashing_InvalidSlashing(t *testing.T) {
|
||||
require.ErrorContains(t, "Invalid attester slashing", err)
|
||||
assert.Equal(t, false, broadcaster.BroadcastCalled)
|
||||
}
|
||||
|
||||
func TestSubmitProposerSlashing_Ok(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
_, keys, err := testutil.DeterministicDepositsAndKeys(1)
|
||||
require.NoError(t, err)
|
||||
validator := ð.Validator{
|
||||
ExitEpoch: params.BeaconConfig().FarFutureEpoch,
|
||||
PublicKey: keys[0].PublicKey().Marshal(),
|
||||
WithdrawalCredentials: make([]byte, 32),
|
||||
WithdrawableEpoch: eth2types.Epoch(1),
|
||||
}
|
||||
state, err := testutil.NewBeaconState(func(state *pb.BeaconState) {
|
||||
state.Validators = []*eth.Validator{validator}
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
slashing := ðpb.ProposerSlashing{
|
||||
Header_1: ðpb.SignedBeaconBlockHeader{
|
||||
Header: ðpb.BeaconBlockHeader{
|
||||
Slot: 1,
|
||||
ProposerIndex: 0,
|
||||
ParentRoot: bytesutil.PadTo([]byte("parentroot1"), 32),
|
||||
StateRoot: bytesutil.PadTo([]byte("stateroot1"), 32),
|
||||
BodyRoot: bytesutil.PadTo([]byte("bodyroot1"), 32),
|
||||
},
|
||||
Signature: make([]byte, 96),
|
||||
},
|
||||
Header_2: ðpb.SignedBeaconBlockHeader{
|
||||
Header: ðpb.BeaconBlockHeader{
|
||||
Slot: 1,
|
||||
ProposerIndex: 0,
|
||||
ParentRoot: bytesutil.PadTo([]byte("parentroot2"), 32),
|
||||
StateRoot: bytesutil.PadTo([]byte("stateroot2"), 32),
|
||||
BodyRoot: bytesutil.PadTo([]byte("bodyroot2"), 32),
|
||||
},
|
||||
Signature: make([]byte, 96),
|
||||
},
|
||||
}
|
||||
|
||||
for _, h := range []*ethpb.SignedBeaconBlockHeader{slashing.Header_1, slashing.Header_2} {
|
||||
sb, err := helpers.ComputeDomainAndSign(
|
||||
state,
|
||||
helpers.SlotToEpoch(h.Header.Slot),
|
||||
h.Header,
|
||||
params.BeaconConfig().DomainBeaconProposer,
|
||||
keys[0],
|
||||
)
|
||||
require.NoError(t, err)
|
||||
sig, err := bls.SignatureFromBytes(sb)
|
||||
require.NoError(t, err)
|
||||
h.Signature = sig.Marshal()
|
||||
}
|
||||
|
||||
broadcaster := &p2pMock.MockBroadcaster{}
|
||||
s := &Server{
|
||||
ChainInfoFetcher: &chainMock.ChainService{State: state},
|
||||
SlashingsPool: &slashings.PoolMock{},
|
||||
Broadcaster: broadcaster,
|
||||
}
|
||||
|
||||
_, err = s.SubmitProposerSlashing(ctx, slashing)
|
||||
require.NoError(t, err)
|
||||
pendingSlashings := s.SlashingsPool.PendingProposerSlashings(ctx, state, true)
|
||||
require.Equal(t, 1, len(pendingSlashings))
|
||||
assert.DeepEqual(t, migration.V1ProposerSlashingToV1Alpha1(slashing), pendingSlashings[0])
|
||||
assert.Equal(t, true, broadcaster.BroadcastCalled)
|
||||
}
|
||||
|
||||
func TestSubmitProposerSlashing_InvalidSlashing(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
state, err := testutil.NewBeaconState()
|
||||
require.NoError(t, err)
|
||||
|
||||
header := ðpb.SignedBeaconBlockHeader{
|
||||
Header: ðpb.BeaconBlockHeader{
|
||||
Slot: 1,
|
||||
ProposerIndex: 0,
|
||||
ParentRoot: bytesutil.PadTo([]byte("parentroot1"), 32),
|
||||
StateRoot: bytesutil.PadTo([]byte("stateroot1"), 32),
|
||||
BodyRoot: bytesutil.PadTo([]byte("bodyroot1"), 32),
|
||||
},
|
||||
Signature: make([]byte, 96),
|
||||
}
|
||||
|
||||
slashing := ðpb.ProposerSlashing{
|
||||
Header_1: header,
|
||||
Header_2: header,
|
||||
}
|
||||
|
||||
broadcaster := &p2pMock.MockBroadcaster{}
|
||||
s := &Server{
|
||||
ChainInfoFetcher: &chainMock.ChainService{State: state},
|
||||
SlashingsPool: &slashings.PoolMock{},
|
||||
Broadcaster: broadcaster,
|
||||
}
|
||||
|
||||
_, err = s.SubmitProposerSlashing(ctx, slashing)
|
||||
require.ErrorContains(t, "Invalid proposer slashing", err)
|
||||
assert.Equal(t, false, broadcaster.BroadcastCalled)
|
||||
}
|
||||
|
||||
func TestSubmitVoluntaryExit_Ok(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
_, keys, err := testutil.DeterministicDepositsAndKeys(1)
|
||||
require.NoError(t, err)
|
||||
validator := ð.Validator{
|
||||
ExitEpoch: params.BeaconConfig().FarFutureEpoch,
|
||||
PublicKey: keys[0].PublicKey().Marshal(),
|
||||
WithdrawalCredentials: make([]byte, 32),
|
||||
}
|
||||
state, err := testutil.NewBeaconState(func(state *pb.BeaconState) {
|
||||
state.Validators = []*eth.Validator{validator}
|
||||
// Satisfy activity time required before exiting.
|
||||
state.Slot = params.BeaconConfig().SlotsPerEpoch.Mul(uint64(params.BeaconConfig().ShardCommitteePeriod))
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
exit := ðpb.SignedVoluntaryExit{
|
||||
Exit: ðpb.VoluntaryExit{
|
||||
Epoch: 0,
|
||||
ValidatorIndex: 0,
|
||||
},
|
||||
Signature: make([]byte, 96),
|
||||
}
|
||||
|
||||
sb, err := helpers.ComputeDomainAndSign(state, exit.Exit.Epoch, exit.Exit, params.BeaconConfig().DomainVoluntaryExit, keys[0])
|
||||
require.NoError(t, err)
|
||||
sig, err := bls.SignatureFromBytes(sb)
|
||||
require.NoError(t, err)
|
||||
exit.Signature = sig.Marshal()
|
||||
|
||||
broadcaster := &p2pMock.MockBroadcaster{}
|
||||
s := &Server{
|
||||
ChainInfoFetcher: &chainMock.ChainService{State: state},
|
||||
VoluntaryExitsPool: &voluntaryexits.PoolMock{},
|
||||
Broadcaster: broadcaster,
|
||||
}
|
||||
|
||||
_, err = s.SubmitVoluntaryExit(ctx, exit)
|
||||
require.NoError(t, err)
|
||||
pendingExits := s.VoluntaryExitsPool.PendingExits(state, state.Slot(), true)
|
||||
require.Equal(t, 1, len(pendingExits))
|
||||
assert.DeepEqual(t, migration.V1ExitToV1Alpha1(exit), pendingExits[0])
|
||||
assert.Equal(t, true, broadcaster.BroadcastCalled)
|
||||
}
|
||||
|
||||
func TestSubmitVoluntaryExit_InvalidValidatorIndex(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
_, keys, err := testutil.DeterministicDepositsAndKeys(1)
|
||||
require.NoError(t, err)
|
||||
validator := ð.Validator{
|
||||
ExitEpoch: params.BeaconConfig().FarFutureEpoch,
|
||||
PublicKey: keys[0].PublicKey().Marshal(),
|
||||
WithdrawalCredentials: make([]byte, 32),
|
||||
}
|
||||
state, err := testutil.NewBeaconState(func(state *pb.BeaconState) {
|
||||
state.Validators = []*eth.Validator{validator}
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
exit := ðpb.SignedVoluntaryExit{
|
||||
Exit: ðpb.VoluntaryExit{
|
||||
Epoch: 0,
|
||||
ValidatorIndex: 99,
|
||||
},
|
||||
Signature: make([]byte, 96),
|
||||
}
|
||||
|
||||
broadcaster := &p2pMock.MockBroadcaster{}
|
||||
s := &Server{
|
||||
ChainInfoFetcher: &chainMock.ChainService{State: state},
|
||||
VoluntaryExitsPool: &voluntaryexits.PoolMock{},
|
||||
Broadcaster: broadcaster,
|
||||
}
|
||||
|
||||
_, err = s.SubmitVoluntaryExit(ctx, exit)
|
||||
require.ErrorContains(t, "Could not get exiting validator", err)
|
||||
assert.Equal(t, false, broadcaster.BroadcastCalled)
|
||||
}
|
||||
|
||||
func TestSubmitVoluntaryExit_InvalidExit(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
_, keys, err := testutil.DeterministicDepositsAndKeys(1)
|
||||
require.NoError(t, err)
|
||||
validator := ð.Validator{
|
||||
ExitEpoch: params.BeaconConfig().FarFutureEpoch,
|
||||
PublicKey: keys[0].PublicKey().Marshal(),
|
||||
WithdrawalCredentials: make([]byte, 32),
|
||||
}
|
||||
state, err := testutil.NewBeaconState(func(state *pb.BeaconState) {
|
||||
state.Validators = []*eth.Validator{validator}
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
exit := ðpb.SignedVoluntaryExit{
|
||||
Exit: ðpb.VoluntaryExit{
|
||||
Epoch: 0,
|
||||
ValidatorIndex: 0,
|
||||
},
|
||||
Signature: make([]byte, 96),
|
||||
}
|
||||
|
||||
broadcaster := &p2pMock.MockBroadcaster{}
|
||||
s := &Server{
|
||||
ChainInfoFetcher: &chainMock.ChainService{State: state},
|
||||
VoluntaryExitsPool: &voluntaryexits.PoolMock{},
|
||||
Broadcaster: broadcaster,
|
||||
}
|
||||
|
||||
_, err = s.SubmitVoluntaryExit(ctx, exit)
|
||||
require.ErrorContains(t, "Invalid voluntary exit", err)
|
||||
assert.Equal(t, false, broadcaster.BroadcastCalled)
|
||||
}
|
||||
|
||||
@@ -111,6 +111,23 @@ func V1Alpha1SignedHeaderToV1(v1alpha1Hdr *ethpb_alpha.SignedBeaconBlockHeader)
|
||||
}
|
||||
}
|
||||
|
||||
// V1SignedHeaderToV1Alpha1 converts a v1 signed beacon block header to v1alpha1.
|
||||
func V1SignedHeaderToV1Alpha1(v1Header *ethpb.SignedBeaconBlockHeader) *ethpb_alpha.SignedBeaconBlockHeader {
|
||||
if v1Header == nil || v1Header.Header == nil {
|
||||
return ðpb_alpha.SignedBeaconBlockHeader{}
|
||||
}
|
||||
return ðpb_alpha.SignedBeaconBlockHeader{
|
||||
Header: ðpb_alpha.BeaconBlockHeader{
|
||||
Slot: v1Header.Header.Slot,
|
||||
ProposerIndex: v1Header.Header.ProposerIndex,
|
||||
ParentRoot: v1Header.Header.ParentRoot,
|
||||
StateRoot: v1Header.Header.StateRoot,
|
||||
BodyRoot: v1Header.Header.BodyRoot,
|
||||
},
|
||||
Signature: v1Header.Signature,
|
||||
}
|
||||
}
|
||||
|
||||
// V1Alpha1ProposerSlashingToV1 converts a v1alpha1 proposer slashing to v1.
|
||||
func V1Alpha1ProposerSlashingToV1(v1alpha1Slashing *ethpb_alpha.ProposerSlashing) *ethpb.ProposerSlashing {
|
||||
if v1alpha1Slashing == nil {
|
||||
@@ -136,6 +153,20 @@ func V1Alpha1ExitToV1(v1alpha1Exit *ethpb_alpha.SignedVoluntaryExit) *ethpb.Sign
|
||||
}
|
||||
}
|
||||
|
||||
// V1ExitToV1Alpha1 converts a v1 SignedVoluntaryExit to v1alpha1.
|
||||
func V1ExitToV1Alpha1(v1Exit *ethpb.SignedVoluntaryExit) *ethpb_alpha.SignedVoluntaryExit {
|
||||
if v1Exit == nil || v1Exit.Exit == nil {
|
||||
return ðpb_alpha.SignedVoluntaryExit{}
|
||||
}
|
||||
return ðpb_alpha.SignedVoluntaryExit{
|
||||
Exit: ðpb_alpha.VoluntaryExit{
|
||||
Epoch: v1Exit.Exit.Epoch,
|
||||
ValidatorIndex: v1Exit.Exit.ValidatorIndex,
|
||||
},
|
||||
Signature: v1Exit.Signature,
|
||||
}
|
||||
}
|
||||
|
||||
// V1IndexedAttToV1Alpha1 converts a v1 indexed attestation to v1alpha1.
|
||||
func V1IndexedAttToV1Alpha1(v1Att *ethpb.IndexedAttestation) *ethpb_alpha.IndexedAttestation {
|
||||
if v1Att == nil {
|
||||
@@ -178,3 +209,14 @@ func V1AttSlashingToV1Alpha1(v1Slashing *ethpb.AttesterSlashing) *ethpb_alpha.At
|
||||
Attestation_2: V1IndexedAttToV1Alpha1(v1Slashing.Attestation_2),
|
||||
}
|
||||
}
|
||||
|
||||
// V1ProposerSlashingToV1Alpha1 converts a v1 proposer slashing to v1alpha1.
|
||||
func V1ProposerSlashingToV1Alpha1(v1Slashing *ethpb.ProposerSlashing) *ethpb_alpha.ProposerSlashing {
|
||||
if v1Slashing == nil {
|
||||
return ðpb_alpha.ProposerSlashing{}
|
||||
}
|
||||
return ðpb_alpha.ProposerSlashing{
|
||||
Header_1: V1SignedHeaderToV1Alpha1(v1Slashing.Header_1),
|
||||
Header_2: V1SignedHeaderToV1Alpha1(v1Slashing.Header_2),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -166,6 +166,23 @@ func Test_V1Alpha1ExitToV1(t *testing.T) {
|
||||
assert.DeepEqual(t, alphaRoot, v1Root)
|
||||
}
|
||||
|
||||
func Test_V1ExitToV1Alpha1(t *testing.T) {
|
||||
v1Exit := ðpb.SignedVoluntaryExit{
|
||||
Exit: ðpb.VoluntaryExit{
|
||||
Epoch: epoch,
|
||||
ValidatorIndex: validatorIndex,
|
||||
},
|
||||
Signature: signature,
|
||||
}
|
||||
|
||||
alphaExit := V1ExitToV1Alpha1(v1Exit)
|
||||
alphaRoot, err := alphaExit.HashTreeRoot()
|
||||
require.NoError(t, err)
|
||||
v1Root, err := v1Exit.HashTreeRoot()
|
||||
require.NoError(t, err)
|
||||
assert.DeepEqual(t, alphaRoot, v1Root)
|
||||
}
|
||||
|
||||
func Test_V1AttSlashingToV1Alpha1(t *testing.T) {
|
||||
v1Attestation := ðpb.IndexedAttestation{
|
||||
AttestingIndices: attestingIndices,
|
||||
@@ -196,3 +213,27 @@ func Test_V1AttSlashingToV1Alpha1(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
assert.DeepEqual(t, v1Root, alphaRoot)
|
||||
}
|
||||
|
||||
func Test_V1ProposerSlashingToV1Alpha1(t *testing.T) {
|
||||
v1Header := ðpb.SignedBeaconBlockHeader{
|
||||
Header: ðpb.BeaconBlockHeader{
|
||||
Slot: slot,
|
||||
ProposerIndex: validatorIndex,
|
||||
ParentRoot: parentRoot,
|
||||
StateRoot: stateRoot,
|
||||
BodyRoot: bodyRoot,
|
||||
},
|
||||
Signature: signature,
|
||||
}
|
||||
v1Slashing := ðpb.ProposerSlashing{
|
||||
Header_1: v1Header,
|
||||
Header_2: v1Header,
|
||||
}
|
||||
|
||||
alphaSlashing := V1ProposerSlashingToV1Alpha1(v1Slashing)
|
||||
alphaRoot, err := alphaSlashing.HashTreeRoot()
|
||||
require.NoError(t, err)
|
||||
v1Root, err := v1Slashing.HashTreeRoot()
|
||||
require.NoError(t, err)
|
||||
assert.DeepEqual(t, alphaRoot, v1Root)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user