Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add etna-time configs #463

Merged
merged 16 commits into from
Aug 30, 2024
Merged
Show file tree
Hide file tree
Changes from 3 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
3 changes: 3 additions & 0 deletions config/api_config.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
// Copyright (C) 2024, Ava Labs, Inc. All rights reserved.
// See the file LICENSE for licensing terms.

package config

import (
Expand Down
4 changes: 4 additions & 0 deletions relayer/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"errors"
"fmt"
"net/url"
"time"

basecfg "github.com/ava-labs/awm-relayer/config"
"github.com/ava-labs/awm-relayer/peers"
Expand Down Expand Up @@ -67,6 +68,9 @@ type Config struct {
DeciderURL string `mapstructure:"decider-url" json:"decider-url"`
SignatureCacheSize uint64 `mapstructure:"signature-cache-size" json:"signature-cache-size"`

// mapstructure doesn't handle time.Time out of the box so handle it manually
EtnaTime time.Time `json:"etna-time"`
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would have liked to do omitempty so that tests don't write the field out when marshaling JSON here but zero time value still gets written out unless the field type is *time.Time which seemed messier.

The value that does get written out is correctly parsed by viper to be zero time value. As a result tests run the pre-Etna case unless otherwise specified.


// convenience field to fetch a blockchain's subnet ID
blockchainIDToSubnetID map[ids.ID]ids.ID
overwrittenOptions []string
Expand Down
1 change: 1 addition & 0 deletions relayer/config/keys.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,5 @@ const (
ManualWarpMessagesKey = "manual-warp-messages"
DBWriteIntervalSecondsKey = "db-write-interval-seconds"
SignatureCacheSizeKey = "signature-cache-size"
EtnaTimeKey = "etna-time"
)
3 changes: 3 additions & 0 deletions relayer/config/viper.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,9 @@ func BuildConfig(v *viper.Viper) (Config, error) {
return cfg, fmt.Errorf("failed to unmarshal viper config: %w", err)
}

// Manually set EtnaTime field since it's not automatically parseable using mapstructure
cfg.EtnaTime = v.GetTime(EtnaTimeKey)

// Explicitly overwrite the configured account private key
// If account-private-key is set as a flag or environment variable,
// overwrite all destination subnet configurations to use that key
Expand Down
1 change: 1 addition & 0 deletions relayer/main/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,7 @@ func main() {
prometheus.DefaultRegisterer,
),
messageCreator,
cfg.EtnaTime,
)
if err != nil {
logger.Fatal("Failed to create signature aggregator", zap.Error(err))
Expand Down
4 changes: 2 additions & 2 deletions signature-aggregator/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,9 @@ curl --location 'https://api.avax-test.network/ext/bc/C/rpc' \
The topic of the message will be `0x56600c567728a800c0aa927500f831cb451df66a7af570eb4df4dfbf4674887d` which is the output of`cast keccak "SendWarpMessage(address,bytes32,bytes)"`
4. Use the data field of the log message found in step 2 and send it to the locally running service via curl.
```bash
curl --location 'http://localhost:8080/aggregate-signatures/by-raw-message' \
curl --location 'http://localhost:8080/aggregate-signatures' \
--header 'Content-Type: application/json' \
--data '{
"data": "<hex encoded unsigned message bytes retrieved from the logs>",
"message": "<hex encoded unsigned message bytes retrieved from the logs>"
}'
```
82 changes: 68 additions & 14 deletions signature-aggregator/aggregator/aggregator.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"github.com/ava-labs/avalanchego/proto/pb/p2p"
"github.com/ava-labs/avalanchego/proto/pb/sdk"
"github.com/ava-labs/avalanchego/subnets"
"github.com/ava-labs/avalanchego/utils/constants"
"github.com/ava-labs/avalanchego/utils/crypto/bls"
"github.com/ava-labs/avalanchego/utils/logging"
"github.com/ava-labs/avalanchego/utils/set"
Expand All @@ -27,6 +28,8 @@ import (
"github.com/ava-labs/awm-relayer/signature-aggregator/aggregator/cache"
"github.com/ava-labs/awm-relayer/signature-aggregator/metrics"
"github.com/ava-labs/awm-relayer/utils"
corethMsg "github.com/ava-labs/coreth/plugin/evm/message"
msg "github.com/ava-labs/subnet-evm/plugin/evm/message"
"go.uber.org/zap"
"google.golang.org/protobuf/proto"
)
Expand All @@ -42,6 +45,9 @@ const (
)

var (
codec = msg.Codec
corethCodec = corethMsg.Codec

// Errors
errNotEnoughSignatures = errors.New("failed to collect a threshold of signatures")
errNotEnoughConnectedStake = errors.New("failed to connect to a threshold of stake")
Expand All @@ -57,6 +63,7 @@ type SignatureAggregator struct {
subnetsMapLock sync.RWMutex
metrics *metrics.SignatureAggregatorMetrics
cache *cache.Cache
etnaTime time.Time
}

func NewSignatureAggregator(
Expand All @@ -65,6 +72,7 @@ func NewSignatureAggregator(
signatureCacheSize uint64,
metrics *metrics.SignatureAggregatorMetrics,
messageCreator message.Creator,
etnaTime time.Time,
) (*SignatureAggregator, error) {
cache, err := cache.NewCache(signatureCacheSize, logger)
if err != nil {
Expand All @@ -81,6 +89,7 @@ func NewSignatureAggregator(
messageCreator: messageCreator,
currentRequestID: atomic.Uint32{},
cache: cache,
etnaTime: etnaTime,
}
sa.currentRequestID.Store(rand.Uint32())
return &sa, nil
Expand Down Expand Up @@ -174,14 +183,7 @@ func (s *SignatureAggregator) CreateSignedMessage(
))
}

reqBytes := networkP2P.ProtocolPrefix(networkP2P.SignatureRequestHandlerID)
messageBytes, err := proto.Marshal(
&sdk.SignatureRequest{
Message: unsignedMessage.Bytes(),
Justification: justification,
},
)
reqBytes = append(reqBytes, messageBytes...)
reqBytes, err := s.marshalRequest(unsignedMessage, justification, sourceSubnet)
if err != nil {
msg := "Failed to marshal request bytes"
s.logger.Error(
Expand Down Expand Up @@ -517,15 +519,13 @@ func (s *SignatureAggregator) isValidSignatureResponse(
return blsSignatureBuf{}, false
}

sigResponse := sdk.SignatureResponse{}
err := proto.Unmarshal(appResponse.AppBytes, &sigResponse)
signature, err := s.unmarshalResponse(appResponse.AppBytes)
if err != nil {
s.logger.Error(
"Error unmarshaling signature response",
zap.Error(err),
)
}
signature := sigResponse.Signature

// If the node returned an empty signature, then it has not yet seen the warp message. Retry later.
emptySignature := blsSignatureBuf{}
Expand Down Expand Up @@ -560,9 +560,8 @@ func (s *SignatureAggregator) isValidSignatureResponse(
)
return blsSignatureBuf{}, false
}
blsSig := blsSignatureBuf{}
copy(blsSig[:], signature[:])
return blsSig, true

return signature, true
}

// aggregateSignatures constructs a BLS aggregate signature from the collected validator signatures. Also
Expand Down Expand Up @@ -594,3 +593,58 @@ func (s *SignatureAggregator) aggregateSignatures(
}
return aggSig, vdrBitSet, nil
}

func (s *SignatureAggregator) marshalRequest(
unsignedMessage *avalancheWarp.UnsignedMessage,
justification []byte,
sourceSubnet ids.ID,
) ([]byte, error) {
if !s.etnaTime.IsZero() && s.etnaTime.Before(time.Now()) {
// Post-Etna case
reqBytes := networkP2P.ProtocolPrefix(networkP2P.SignatureRequestHandlerID)
messageBytes, err := proto.Marshal(
&sdk.SignatureRequest{
Message: unsignedMessage.Bytes(),
Justification: justification,
},
)
if err != nil {
return []byte{}, err
}
reqBytes = append(reqBytes, messageBytes...)
return reqBytes, nil
} else {
// Pre-Etna case
if sourceSubnet == constants.PrimaryNetworkID {
req := corethMsg.MessageSignatureRequest{
MessageID: unsignedMessage.ID(),
}
return corethMsg.RequestToBytes(corethCodec, req)
} else {
req := msg.MessageSignatureRequest{
MessageID: unsignedMessage.ID(),
}
return msg.RequestToBytes(codec, req)
}
}
}

func (s *SignatureAggregator) unmarshalResponse(responseBytes []byte) (blsSignatureBuf, error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we leave TODOs and create a ticket for when this logic is okay to clean up?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added TODO and entered: #466

if !s.etnaTime.IsZero() && s.etnaTime.Before(time.Now()) {
// Post-Etna case
var sigResponse sdk.SignatureResponse
err := proto.Unmarshal(responseBytes, &sigResponse)
if err != nil {
return blsSignatureBuf{}, err
}
return blsSignatureBuf(sigResponse.Signature), nil
} else {
// Pre-Etna case
var sigResponse msg.SignatureResponse
_, err := msg.Codec.Unmarshal(responseBytes, &sigResponse)
if err != nil {
return blsSignatureBuf{}, err
}
return sigResponse.Signature, nil
}
}
4 changes: 4 additions & 0 deletions signature-aggregator/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package config

import (
"fmt"
"time"

"github.com/ava-labs/avalanchego/utils/logging"
basecfg "github.com/ava-labs/awm-relayer/config"
Expand Down Expand Up @@ -34,6 +35,9 @@ type Config struct {
APIPort uint16 `mapstructure:"api-port" json:"api-port"`
MetricsPort uint16 `mapstructure:"metrics-port" json:"metrics-port"`
SignatureCacheSize uint64 `mapstructure:"signature-cache-size" json:"signature-cache-size"`

// mapstructure doesn't support time.Time out of the box so handle it manually
EtnaTime time.Time `json:"etna-time"`
}

func DisplayUsageText() {
Expand Down
1 change: 1 addition & 0 deletions signature-aggregator/config/keys.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,5 @@ const (
APIPortKey = "api-port"
MetricsPortKey = "metrics-port"
SignatureCacheSizeKey = "signature-cache-size"
EtnaTimeKey = "etna-time"
)
3 changes: 3 additions & 0 deletions signature-aggregator/config/viper.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,5 +76,8 @@ func BuildConfig(v *viper.Viper) (Config, error) {
return cfg, fmt.Errorf("failed to unmarshal viper config: %w", err)
}

// mapstructure doesn't support time.Time out of the box so handle it manually
cfg.EtnaTime = v.GetTime(EtnaTimeKey)

return cfg, nil
}
1 change: 1 addition & 0 deletions signature-aggregator/main/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ func main() {
cfg.SignatureCacheSize,
metricsInstance,
messageCreator,
cfg.EtnaTime,
)
if err != nil {
logger.Fatal("Failed to create signature aggregator", zap.Error(err))
Expand Down
3 changes: 3 additions & 0 deletions tests/e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -174,4 +174,7 @@ var _ = ginkgo.Describe("[AWM Relayer Integration Tests", func() {
ginkgo.It("Signature Aggregator", func() {
SignatureAggregatorAPI(localNetworkInstance)
})
ginkgo.It("Etna Upgrade", func() {
EtnaUpgrade(localNetworkInstance)
})
})
109 changes: 109 additions & 0 deletions tests/etna_upgrade.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
// Copyright (C) 2024, Ava Labs, Inc. All rights reserved.
// See the file LICENSE for licensing terms.

package tests

import (
"context"
"time"

testUtils "github.com/ava-labs/awm-relayer/tests/utils"
"github.com/ava-labs/teleporter/tests/interfaces"
"github.com/ava-labs/teleporter/tests/utils"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/log"
. "github.com/onsi/gomega"
)

// This tests basic functionality of the relayer in the context
// of Etna network upgrade using the following cases:
// - Relaying from Subnet A to Subnet B using Pre-Etna config
// - Relaying from Subnet B to Subnet A using Post-Etna config
func EtnaUpgrade(network interfaces.LocalNetwork) {
subnetAInfo := network.GetPrimaryNetworkInfo()
subnetBInfo, _ := utils.GetTwoSubnets(network)
fundedAddress, fundedKey := network.GetFundedAccountInfo()
teleporterContractAddress := network.GetTeleporterContractAddress()
err := testUtils.ClearRelayerStorage()
Expect(err).Should(BeNil())
//
// Fund the relayer address on all subnets
//
ctx := context.Background()

log.Info("Funding relayer address on all subnets")
relayerKey, err := crypto.GenerateKey()
Expect(err).Should(BeNil())
testUtils.FundRelayers(ctx, []interfaces.SubnetTestInfo{subnetAInfo, subnetBInfo}, fundedKey, relayerKey)

//
// Set up relayer config
//
relayerConfig := testUtils.CreateDefaultRelayerConfig(
[]interfaces.SubnetTestInfo{subnetAInfo, subnetBInfo},
[]interfaces.SubnetTestInfo{subnetAInfo, subnetBInfo},
teleporterContractAddress,
fundedAddress,
relayerKey,
)
relayerConfig.EtnaTime = time.Now().AddDate(0, 0, 1)
// The config needs to be validated in order to be passed to database.GetConfigRelayerIDs
relayerConfig.Validate()

relayerConfigPath := testUtils.WriteRelayerConfig(relayerConfig, testUtils.DefaultRelayerCfgFname)

//
// Test Relaying from Subnet A to Subnet B
//
log.Info("Test Relaying from Subnet A to Subnet B")

log.Info("Starting the relayer")
relayerCleanup, readyChan := testUtils.RunRelayerExecutable(
ctx,
relayerConfigPath,
relayerConfig,
)
defer relayerCleanup()

// Wait for relayer to start up
startupCtx, startupCancel := context.WithTimeout(ctx, 15*time.Second)
defer startupCancel()
testUtils.WaitForChannelClose(startupCtx, readyChan)

log.Info("Sending transaction from Subnet A to Subnet B, Pre-Etna")
testUtils.RelayBasicMessage(
ctx,
subnetAInfo,
subnetBInfo,
teleporterContractAddress,
fundedKey,
fundedAddress,
)
// Shutdown the relayer and write a new config with EtnaTime set to yesterday
relayerCleanup()

relayerConfig.EtnaTime = time.Now().AddDate(0, 0, -1)
relayerConfigPath = testUtils.WriteRelayerConfig(relayerConfig, testUtils.DefaultRelayerCfgFname)

relayerCleanup, readyChan = testUtils.RunRelayerExecutable(
ctx,
relayerConfigPath,
relayerConfig,
)
defer relayerCleanup()

// Wait for relayer to start up
startupCtx, startupCancel = context.WithTimeout(ctx, 15*time.Second)
defer startupCancel()
testUtils.WaitForChannelClose(startupCtx, readyChan)

log.Info("Test Relaying from Subnet B to Subnet A - Post-Etna")
testUtils.RelayBasicMessage(
ctx,
subnetBInfo,
subnetAInfo,
teleporterContractAddress,
fundedKey,
fundedAddress,
)
}