Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion plugin/evm/vm.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import (
"github.com/ava-labs/subnet-evm/plugin/evm/message"
"github.com/ava-labs/subnet-evm/trie/triedb/hashdb"

warpcontract "github.com/ava-labs/subnet-evm/precompile/contracts/warp"
"github.com/ava-labs/subnet-evm/rpc"
statesyncclient "github.com/ava-labs/subnet-evm/sync/client"
"github.com/ava-labs/subnet-evm/sync/client/stats"
Expand Down Expand Up @@ -1019,7 +1020,18 @@ func (vm *VM) CreateHandlers(context.Context) (map[string]http.Handler, error) {
}

if vm.config.WarpAPIEnabled {
validatorsState := warpValidators.NewState(vm.ctx)
requirePrimaryNetworkSigners := func() bool {
warpCfgIntf, ok := vm.currentRules().ActivePrecompiles[warpcontract.ContractAddress]
if !ok {
return false
}
warpCfg, ok := warpCfgIntf.(*warpcontract.Config)
if !ok {
return false
}
return warpCfg.RequirePrimaryNetworkSigners
}
validatorsState := warpValidators.NewState(vm.ctx, requirePrimaryNetworkSigners)
if err := handler.RegisterName("warp", warp.NewAPI(vm.ctx.NetworkID, vm.ctx.SubnetID, vm.ctx.ChainID, validatorsState, vm.warpBackend, vm.client)); err != nil {
return nil, err
}
Expand Down Expand Up @@ -1067,6 +1079,12 @@ func (vm *VM) GetCurrentNonce(address common.Address) (uint64, error) {
return state.GetNonce(address), nil
}

// currentRules returns the chain rules for the current block.
func (vm *VM) currentRules() params.Rules {
header := vm.eth.APIBackend.CurrentHeader()
return vm.chainConfig.Rules(header.Number, header.Time)
}

func (vm *VM) startContinuousProfiler() {
// If the profiler directory is empty, return immediately
// without creating or starting a continuous profiler.
Expand Down
152 changes: 117 additions & 35 deletions plugin/evm/vm_warp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,13 @@ import (
_ "embed"

"github.com/ava-labs/avalanchego/ids"
commonEng "github.com/ava-labs/avalanchego/snow/engine/common"
"github.com/ava-labs/avalanchego/snow/engine/snowman/block"
"github.com/ava-labs/avalanchego/snow/validators"
"github.com/ava-labs/avalanchego/snow/validators/validatorstest"
"github.com/ava-labs/avalanchego/upgrade"
avagoUtils "github.com/ava-labs/avalanchego/utils"
"github.com/ava-labs/avalanchego/utils/constants"
"github.com/ava-labs/avalanchego/utils/crypto/bls"
"github.com/ava-labs/avalanchego/utils/set"
"github.com/ava-labs/avalanchego/vms/components/chain"
Expand Down Expand Up @@ -408,74 +410,142 @@ func TestReceiveWarpMessage(t *testing.T) {
}
genesisJSON, err := genesis.MarshalJSON()
require.NoError(err)
issuer, vm, _, _ := GenesisVM(t, true, string(genesisJSON), "", "")

// disable warp so we can re-enable it with RequirePrimaryNetworkSigners
disableTime := upgrade.InitiallyActiveTime.Add(10 * time.Second)
disableConfig := warp.NewDisableConfig(utils.TimeToNewUint64(disableTime))

// re-enable warp with RequirePrimaryNetworkSigners
reEnableTime := disableTime.Add(10 * time.Second)
reEnableConfig := warp.NewDefaultConfig(utils.TimeToNewUint64(reEnableTime)).WithRequirePrimaryNetworkSigners(true)

upgradeConfig := params.UpgradeConfig{
PrecompileUpgrades: []params.PrecompileUpgrade{
{Config: disableConfig},
{Config: reEnableConfig},
},
}
upgradeBytes, err := json.Marshal(upgradeConfig)
require.NoError(err)

issuer, vm, _, _ := GenesisVM(t, true, string(genesisJSON), "", string(upgradeBytes))

defer func() {
require.NoError(vm.Shutdown(context.Background()))
}()

acceptedLogsChan := make(chan []*types.Log, 10)
logsSub := vm.eth.APIBackend.SubscribeAcceptedLogsEvent(acceptedLogsChan)
defer logsSub.Unsubscribe()
// Subnet messages should use subnet signers
testReceiveWarpMessage(t, issuer, vm, false, false, upgrade.InitiallyActiveTime)

payloadData := avagoUtils.RandomBytes(100)
/// At genesis, primary network messages should use subnet signers
blockTime := upgrade.InitiallyActiveTime.Add(2 * time.Second) // for fees
require.True(blockTime.Before(disableTime))
testReceiveWarpMessage(t, issuer, vm, true, false, blockTime)

// After re-enabled, primary network messages should use primary signers
testReceiveWarpMessage(t, issuer, vm, true, true, reEnableTime)
}

func testReceiveWarpMessage(
t *testing.T, issuer chan commonEng.Message, vm *VM,
fromPrimary, usePrimarySigners bool,
blockTime time.Time,
) {
require := require.New(t)
payloadData := avagoUtils.RandomBytes(100)
addressedPayload, err := payload.NewAddressedCall(
testEthAddrs[0].Bytes(),
payloadData,
)
require.NoError(err)

vm.ctx.SubnetID = ids.GenerateTestID()
vm.ctx.NetworkID = testNetworkID
unsignedMessage, err := avalancheWarp.NewUnsignedMessage(
vm.ctx.NetworkID,
vm.ctx.ChainID,
addressedPayload.Bytes(),
)
require.NoError(err)

nodeID1 := ids.GenerateTestNodeID()
blsSecretKey1, err := bls.NewSecretKey()
require.NoError(err)
blsPublicKey1 := bls.PublicFromSecretKey(blsSecretKey1)
blsSignature1 := bls.Sign(blsSecretKey1, unsignedMessage.Bytes())
type signer struct {
networkID ids.ID
nodeID ids.NodeID
secret *bls.SecretKey
signature *bls.Signature
weight uint64
}
newSigner := func(networkID ids.ID, weight uint64) signer {
secret, err := bls.NewSecretKey()
require.NoError(err)
return signer{
networkID: networkID,
nodeID: ids.GenerateTestNodeID(),
secret: secret,
signature: bls.Sign(secret, unsignedMessage.Bytes()),
weight: weight,
}
}

nodeID2 := ids.GenerateTestNodeID()
blsSecretKey2, err := bls.NewSecretKey()
require.NoError(err)
blsPublicKey2 := bls.PublicFromSecretKey(blsSecretKey2)
blsSignature2 := bls.Sign(blsSecretKey2, unsignedMessage.Bytes())
var (
primarySigners = []signer{
newSigner(constants.PrimaryNetworkID, 50),
newSigner(constants.PrimaryNetworkID, 50),
}
subnetSigners = []signer{
newSigner(vm.ctx.SubnetID, 50),
newSigner(vm.ctx.SubnetID, 50),
}
)
signers := subnetSigners
if usePrimarySigners {
signers = primarySigners
}

blsAggregatedSignature, err := bls.AggregateSignatures([]*bls.Signature{blsSignature1, blsSignature2})
blsSignatures := make([]*bls.Signature, len(signers))
for i := range signers {
blsSignatures[i] = signers[i].signature
}
blsAggregatedSignature, err := bls.AggregateSignatures(blsSignatures)
require.NoError(err)

minimumValidPChainHeight := uint64(10)
getValidatorSetTestErr := errors.New("can't get validator set test error")

vm.ctx.ValidatorState = &validatorstest.State{
GetSubnetIDF: func(ctx context.Context, chainID ids.ID) (ids.ID, error) {
return ids.Empty, nil
if fromPrimary {
return constants.PrimaryNetworkID, nil
}
return vm.ctx.SubnetID, nil
},
GetValidatorSetF: func(ctx context.Context, height uint64, subnetID ids.ID) (map[ids.NodeID]*validators.GetValidatorOutput, error) {
if height < minimumValidPChainHeight {
return nil, getValidatorSetTestErr
}
return map[ids.NodeID]*validators.GetValidatorOutput{
nodeID1: {
NodeID: nodeID1,
PublicKey: blsPublicKey1,
Weight: 50,
},
nodeID2: {
NodeID: nodeID2,
PublicKey: blsPublicKey2,
Weight: 50,
},
}, nil
toValidatorOutput := func(signers []signer) map[ids.NodeID]*validators.GetValidatorOutput {
vdrOutput := make(map[ids.NodeID]*validators.GetValidatorOutput)
for _, s := range signers {
vdrOutput[s.nodeID] = &validators.GetValidatorOutput{
NodeID: s.nodeID,
PublicKey: bls.PublicFromSecretKey(s.secret),
Weight: s.weight,
}
}
return vdrOutput
}
validators := subnetSigners
if subnetID == constants.PrimaryNetworkID {
validators = primarySigners
}
return toValidatorOutput(validators), nil
},
}

signersBitSet := set.NewBits()
signersBitSet.Add(0)
signersBitSet.Add(1)
for i := range signers {
signersBitSet.Add(i)
}

warpSignature := &avalancheWarp.BitSetSignature{
Signers: signersBitSet.Bytes(),
Expand All @@ -495,7 +565,7 @@ func TestReceiveWarpMessage(t *testing.T) {
getVerifiedWarpMessageTx, err := types.SignTx(
predicate.NewPredicateTx(
vm.chainConfig.ChainID,
0,
vm.txPool.Nonce(testEthAddrs[0]),
&warp.Module.Address,
1_000_000,
big.NewInt(225*params.GWei),
Expand All @@ -519,19 +589,32 @@ func TestReceiveWarpMessage(t *testing.T) {
validProposerCtx := &block.Context{
PChainHeight: minimumValidPChainHeight,
}
vm.clock.Set(vm.clock.Time().Add(2 * time.Second))
vm.clock.Set(blockTime)
<-issuer

block2, err := vm.BuildBlockWithContext(context.Background(), validProposerCtx)
require.NoError(err)
require.NoError(vm.SetPreference(context.Background(), block2.ID()))

// Require the block was built with a successful predicate result
ethBlock := block2.(*chain.BlockWrapper).Block.(*Block).ethBlock
headerPredicateResultsBytes, ok := predicate.GetPredicateResultBytes(ethBlock.Extra())
require.True(ok)
results, err := predicate.ParseResults(headerPredicateResultsBytes)
require.NoError(err)
txResultsBytes := results.GetResults(
getVerifiedWarpMessageTx.Hash(),
warp.ContractAddress,
)
bitset := set.BitsFromBytes(txResultsBytes)
require.Zero(bitset.Len()) // Empty bitset indicates success

block2VerifyWithCtx, ok := block2.(block.WithVerifyContext)
require.True(ok)
shouldVerifyWithCtx, err := block2VerifyWithCtx.ShouldVerifyWithContext(context.Background())
require.NoError(err)
require.True(shouldVerifyWithCtx)
require.NoError(block2VerifyWithCtx.VerifyWithContext(context.Background(), validProposerCtx))
require.NoError(vm.SetPreference(context.Background(), block2.ID()))

// Verify the block with another valid context with identical predicate results
require.NoError(block2VerifyWithCtx.VerifyWithContext(context.Background(), &block.Context{
Expand All @@ -548,7 +631,6 @@ func TestReceiveWarpMessage(t *testing.T) {
require.NoError(block2.Accept(context.Background()))
vm.blockChain.DrainAcceptorQueue()

ethBlock := block2.(*chain.BlockWrapper).Block.(*Block).ethBlock
verifiedMessageReceipts := vm.blockChain.GetReceiptsByHash(ethBlock.Hash())
require.Len(verifiedMessageReceipts, 1)
verifiedMessageTxReceipt := verifiedMessageReceipts[0]
Expand Down
10 changes: 8 additions & 2 deletions precompile/contracts/warp/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@ var (
// adds specific configuration for Warp.
type Config struct {
precompileconfig.Upgrade
QuorumNumerator uint64 `json:"quorumNumerator"`
QuorumNumerator uint64 `json:"quorumNumerator"`
RequirePrimaryNetworkSigners bool `json:"requirePrimaryNetworkSigners"`
}

// NewConfig returns a config for a network upgrade at [blockTimestamp] that enables
Expand All @@ -59,6 +60,11 @@ func NewConfig(blockTimestamp *uint64, quorumNumerator uint64) *Config {
}
}

func (c *Config) WithRequirePrimaryNetworkSigners(requirePrimaryNetworkSigners bool) *Config {
c.RequirePrimaryNetworkSigners = requirePrimaryNetworkSigners
return c
}

// NewDefaultConfig returns a config for a network upgrade at [blockTimestamp] that enables
// Warp with the default quorum numerator (0 denotes using the default).
func NewDefaultConfig(blockTimestamp *uint64) *Config {
Expand Down Expand Up @@ -202,7 +208,7 @@ func (c *Config) VerifyPredicate(predicateContext *precompileconfig.PredicateCon
context.Background(),
&warpMsg.UnsignedMessage,
predicateContext.SnowCtx.NetworkID,
warpValidators.NewState(predicateContext.SnowCtx), // Wrap validators.State on the chain snow context to special case the Primary Network
warpValidators.NewState(predicateContext.SnowCtx, func() bool { return c.RequirePrimaryNetworkSigners }), // Wrap validators.State on the chain snow context to special case the Primary Network
predicateContext.ProposerVMBlockCtx.PChainHeight,
quorumNumerator,
WarpQuorumDenominator,
Expand Down
14 changes: 12 additions & 2 deletions precompile/contracts/warp/predicate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,12 @@ func createValidPredicateTest(snowCtx *snow.Context, numKeys uint64, predicateBy
}

func TestWarpMessageFromPrimaryNetwork(t *testing.T) {
for _, requirePrimaryNetworkSigners := range []bool{true, false} {
testWarpMessageFromPrimaryNetwork(t, requirePrimaryNetworkSigners)
}
}

func testWarpMessageFromPrimaryNetwork(t *testing.T, requirePrimaryNetworkSigners bool) {
require := require.New(t)
numKeys := 10
cChainID := ids.GenerateTestID()
Expand Down Expand Up @@ -273,13 +279,17 @@ func TestWarpMessageFromPrimaryNetwork(t *testing.T) {
return constants.PrimaryNetworkID, nil // Return Primary Network SubnetID
},
GetValidatorSetF: func(ctx context.Context, height uint64, subnetID ids.ID) (map[ids.NodeID]*validators.GetValidatorOutput, error) {
require.Equal(snowCtx.SubnetID, subnetID)
expectedSubnetID := snowCtx.SubnetID
if requirePrimaryNetworkSigners {
expectedSubnetID = constants.PrimaryNetworkID
}
require.Equal(expectedSubnetID, subnetID)
return getValidatorsOutput, nil
},
}

test := testutils.PredicateTest{
Config: NewDefaultConfig(utils.NewUint64(0)),
Config: NewDefaultConfig(utils.NewUint64(0)).WithRequirePrimaryNetworkSigners(requirePrimaryNetworkSigners),
PredicateContext: &precompileconfig.PredicateContext{
SnowCtx: snowCtx,
ProposerVMBlockCtx: &block.Context{
Expand Down
16 changes: 9 additions & 7 deletions warp/validators/state.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,20 @@ var _ validators.State = (*State)(nil)
// signatures from a threshold of the RECEIVING subnet validator set rather than the full Primary Network
// since the receiving subnet already relies on a majority of its validators being correct.
type State struct {
chainContext *snow.Context
validators.State
chainContext *snow.Context
requirePrimaryNetworkSigners func() bool
}

// NewState returns a wrapper of [validators.State] which special cases the handling of the Primary Network.
//
// The wrapped state will return the chainContext's Subnet validator set instead of the Primary Network when
// the Primary Network SubnetID is passed in.
func NewState(chainContext *snow.Context) *State {
func NewState(chainContext *snow.Context, requirePrimaryNetworkSigners func() bool) *State {
return &State{
chainContext: chainContext,
State: chainContext.ValidatorState,
State: chainContext.ValidatorState,
chainContext: chainContext,
requirePrimaryNetworkSigners: requirePrimaryNetworkSigners,
}
}

Expand All @@ -39,9 +41,9 @@ func (s *State) GetValidatorSet(
height uint64,
subnetID ids.ID,
) (map[ids.NodeID]*validators.GetValidatorOutput, error) {
// If the subnetID is anything other than the Primary Network, this is a direct
// passthrough
if subnetID != constants.PrimaryNetworkID {
// If the subnetID is anything other than the Primary Network, or Primary
// Network signers are required, this is a direct passthrough.
if s.requirePrimaryNetworkSigners() || subnetID != constants.PrimaryNetworkID {
return s.State.GetValidatorSet(ctx, height, subnetID)
}

Expand Down
2 changes: 1 addition & 1 deletion warp/validators/state_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ func TestGetValidatorSetPrimaryNetwork(t *testing.T) {
snowCtx := utils.TestSnowContext()
snowCtx.SubnetID = mySubnetID
snowCtx.ValidatorState = mockState
state := NewState(snowCtx)
state := NewState(snowCtx, func() bool { return false })
// Expect that requesting my validator set returns my validator set
mockState.EXPECT().GetValidatorSet(gomock.Any(), gomock.Any(), mySubnetID).Return(make(map[ids.NodeID]*validators.GetValidatorOutput), nil)
output, err := state.GetValidatorSet(context.Background(), 10, mySubnetID)
Expand Down