diff --git a/go/common/host/services.go b/go/common/host/services.go index cd96a8b929..db73a52cdf 100644 --- a/go/common/host/services.go +++ b/go/common/host/services.go @@ -101,10 +101,8 @@ type L1Publisher interface { InitializeSecret(attestation *common.AttestationReport, encSecret common.EncryptedSharedEnclaveSecret) error // RequestSecret will send a management contract transaction to request a secret from the enclave, returning the L1 head at time of sending RequestSecret(report *common.AttestationReport) (gethcommon.Hash, error) - // ExtractSecretResponses will return all secret response tx from an L1 block - ExtractSecretResponses(block *types.Block) []*ethadapter.L1RespondSecretTx - // ExtractRollupTxs will return all rollup txs from an L1 block - ExtractRollupTxs(block *types.Block) []*ethadapter.L1RollupTx + // ExtractObscuroRelevantTransactions will return all Obscuro relevant tx from an L1 block + ExtractObscuroRelevantTransactions(block *types.Block) ([]*ethadapter.L1RespondSecretTx, []*ethadapter.L1RollupTx, []*ethadapter.L1SetImportantContractsTx) // PublishRollup will create and publish a rollup tx to the management contract - fire and forget we don't wait for receipt // todo (#1624) - With a single sequencer, it is problematic if rollup publication fails; handle this case better PublishRollup(producedRollup *common.ExtRollup) @@ -114,6 +112,11 @@ type L1Publisher interface { FetchLatestPeersList() ([]string, error) FetchLatestSeqNo() (*big.Int, error) + + // GetImportantContracts returns a (cached) record of addresses of the important network contracts + GetImportantContracts() map[string]gethcommon.Address + // ResyncImportantContracts will fetch the latest important contracts from the management contract, update the cache + ResyncImportantContracts() error } // L2BatchRepository provides an interface for the host to request L2 batch data (live-streaming and historical) diff --git a/go/common/query_types.go b/go/common/query_types.go index 596a4e4341..9648e4b597 100644 --- a/go/common/query_types.go +++ b/go/common/query_types.go @@ -89,4 +89,5 @@ type ObscuroNetworkInfo struct { L1StartHash common.Hash SequencerID common.Address MessageBusAddress common.Address + ImportantContracts map[string]common.Address // map of contract name to address } diff --git a/go/ethadapter/l1_transaction.go b/go/ethadapter/l1_transaction.go index 8bba33108e..b13d2bf004 100644 --- a/go/ethadapter/l1_transaction.go +++ b/go/ethadapter/l1_transaction.go @@ -33,6 +33,11 @@ type L1RespondSecretTx struct { HostAddress string } +type L1SetImportantContractsTx struct { + Key string + NewAddress gethcommon.Address +} + // Sign signs the payload with a given private key func (l *L1RespondSecretTx) Sign(privateKey *ecdsa.PrivateKey) *L1RespondSecretTx { var data []byte diff --git a/go/ethadapter/mgmtcontractlib/mgmt_contract_ABI.go b/go/ethadapter/mgmtcontractlib/mgmt_contract_ABI.go index 83b66bfd20..78776d51f5 100644 --- a/go/ethadapter/mgmtcontractlib/mgmt_contract_ABI.go +++ b/go/ethadapter/mgmtcontractlib/mgmt_contract_ABI.go @@ -3,11 +3,14 @@ package mgmtcontractlib import "github.com/ten-protocol/go-ten/contracts/generated/ManagementContract" const ( - AddRollupMethod = "AddRollup" - RespondSecretMethod = "RespondNetworkSecret" - RequestSecretMethod = "RequestNetworkSecret" - InitializeSecretMethod = "InitializeNetworkSecret" //#nosec - GetHostAddressesMethod = "GetHostAddresses" + AddRollupMethod = "AddRollup" + RespondSecretMethod = "RespondNetworkSecret" + RequestSecretMethod = "RequestNetworkSecret" + InitializeSecretMethod = "InitializeNetworkSecret" //#nosec + GetHostAddressesMethod = "GetHostAddresses" + GetImportantContractKeysMethod = "GetImportantContractKeys" + SetImportantContractsMethod = "SetImportantContractAddress" + GetImportantAddressMethod = "importantContractAddresses" ) var MgmtContractABI = ManagementContract.ManagementContractMetaData.ABI diff --git a/go/ethadapter/mgmtcontractlib/mgmt_contract_lib.go b/go/ethadapter/mgmtcontractlib/mgmt_contract_lib.go index bad61696a0..b9f683dcf5 100644 --- a/go/ethadapter/mgmtcontractlib/mgmt_contract_lib.go +++ b/go/ethadapter/mgmtcontractlib/mgmt_contract_lib.go @@ -28,13 +28,23 @@ type MgmtContractLib interface { CreateRequestSecret(tx *ethadapter.L1RequestSecretTx) types.TxData CreateRespondSecret(tx *ethadapter.L1RespondSecretTx, verifyAttester bool) types.TxData CreateInitializeSecret(tx *ethadapter.L1InitializeSecretTx) types.TxData - GetHostAddresses() (ethereum.CallMsg, error) // DecodeTx receives a *types.Transaction and converts it to an common.L1Transaction DecodeTx(tx *types.Transaction) ethadapter.L1Transaction - // DecodeCallResponse unpacks a call response into a slice of strings. - DecodeCallResponse(callResponse []byte) ([][]string, error) GetContractAddr() *gethcommon.Address + + // The methods below are used to create call messages for mgmt contract data and unpack the responses + + GetHostAddressesMsg() (ethereum.CallMsg, error) + DecodeHostAddressesResponse(callResponse []byte) ([]string, error) + + SetImportantContractMsg(key string, address gethcommon.Address) (ethereum.CallMsg, error) + + GetImportantContractKeysMsg() (ethereum.CallMsg, error) + DecodeImportantContractKeysResponse(callResponse []byte) ([]string, error) + + GetImportantAddressCallMsg(key string) (ethereum.CallMsg, error) + DecodeImportantAddressResponse(callResponse []byte) (gethcommon.Address, error) } type contractLibImpl struct { @@ -93,6 +103,14 @@ func (c *contractLibImpl) DecodeTx(tx *types.Transaction) ethadapter.L1Transacti case InitializeSecretMethod: return c.unpackInitSecretTx(tx, method, contractCallData) + + case SetImportantContractsMethod: + tx, err := c.unpackSetImportantContractsTx(tx, method, contractCallData) + if err != nil { + c.logger.Warn("could not unpack set important contracts tx", log.ErrKey, err) + return nil + } + return tx } return nil @@ -180,7 +198,7 @@ func (c *contractLibImpl) CreateInitializeSecret(tx *ethadapter.L1InitializeSecr } } -func (c *contractLibImpl) GetHostAddresses() (ethereum.CallMsg, error) { +func (c *contractLibImpl) GetHostAddressesMsg() (ethereum.CallMsg, error) { data, err := c.contractABI.Pack(GetHostAddressesMethod) if err != nil { return ethereum.CallMsg{}, fmt.Errorf("could not pack the call data. Cause: %w", err) @@ -188,23 +206,108 @@ func (c *contractLibImpl) GetHostAddresses() (ethereum.CallMsg, error) { return ethereum.CallMsg{To: c.addr, Data: data}, nil } -func (c *contractLibImpl) DecodeCallResponse(callResponse []byte) ([][]string, error) { +func (c *contractLibImpl) DecodeHostAddressesResponse(callResponse []byte) ([]string, error) { unpackedResponse, err := c.contractABI.Unpack(GetHostAddressesMethod, callResponse) if err != nil { return nil, fmt.Errorf("could not unpack call response. Cause: %w", err) } - // We convert the returned interfaces to strings. - unpackedResponseStrings := make([][]string, 0, len(unpackedResponse)) - for _, obj := range unpackedResponse { - str, ok := obj.([]string) - if !ok { - return nil, fmt.Errorf("could not convert interface in call response to string") - } - unpackedResponseStrings = append(unpackedResponseStrings, str) + // We expect the response to be a list containing one element, that element is a list of address strings + if len(unpackedResponse) != 1 { + return nil, fmt.Errorf("unexpected number of results (%d) returned from call, response: %s", len(unpackedResponse), unpackedResponse) + } + addresses, ok := unpackedResponse[0].([]string) + if !ok { + return nil, fmt.Errorf("could not convert element in call response to list of strings") + } + + return addresses, nil +} + +func (c *contractLibImpl) GetContractNamesMsg() (ethereum.CallMsg, error) { + data, err := c.contractABI.Pack(GetImportantContractKeysMethod) + if err != nil { + return ethereum.CallMsg{}, fmt.Errorf("could not pack the call data. Cause: %w", err) + } + return ethereum.CallMsg{To: c.addr, Data: data}, nil +} + +func (c *contractLibImpl) DecodeContractNamesResponse(callResponse []byte) ([]string, error) { + unpackedResponse, err := c.contractABI.Unpack(GetImportantContractKeysMethod, callResponse) + if err != nil { + return nil, fmt.Errorf("could not unpack call response. Cause: %w", err) + } + + // We expect the response to be a list containing one element, that element is a list of address strings + if len(unpackedResponse) != 1 { + return nil, fmt.Errorf("unexpected number of results (%d) returned from call, response: %s", len(unpackedResponse), unpackedResponse) + } + contractNames, ok := unpackedResponse[0].([]string) + if !ok { + return nil, fmt.Errorf("could not convert element in call response to list of strings") + } + + return contractNames, nil +} + +func (c *contractLibImpl) SetImportantContractMsg(key string, address gethcommon.Address) (ethereum.CallMsg, error) { + data, err := c.contractABI.Pack(SetImportantContractsMethod, key, address) + if err != nil { + return ethereum.CallMsg{}, fmt.Errorf("could not pack the call data. Cause: %w", err) } + return ethereum.CallMsg{To: c.addr, Data: data}, nil +} - return unpackedResponseStrings, nil +func (c *contractLibImpl) GetImportantContractKeysMsg() (ethereum.CallMsg, error) { + data, err := c.contractABI.Pack(GetImportantContractKeysMethod) + if err != nil { + return ethereum.CallMsg{}, fmt.Errorf("could not pack the call data. Cause: %w", err) + } + return ethereum.CallMsg{To: c.addr, Data: data}, nil +} + +func (c *contractLibImpl) DecodeImportantContractKeysResponse(callResponse []byte) ([]string, error) { + unpackedResponse, err := c.contractABI.Unpack(GetImportantContractKeysMethod, callResponse) + if err != nil { + return nil, fmt.Errorf("could not unpack call response. Cause: %w", err) + } + + // We expect the response to be a list containing one element, that element is a list of address strings + if len(unpackedResponse) != 1 { + return nil, fmt.Errorf("unexpected number of results (%d) returned from call, response: %s", len(unpackedResponse), unpackedResponse) + } + contractNames, ok := unpackedResponse[0].([]string) + if !ok { + return nil, fmt.Errorf("could not convert element in call response to list of strings") + } + + return contractNames, nil +} + +func (c *contractLibImpl) GetImportantAddressCallMsg(key string) (ethereum.CallMsg, error) { + data, err := c.contractABI.Pack(GetImportantAddressMethod, key) + if err != nil { + return ethereum.CallMsg{}, fmt.Errorf("could not pack the call data. Cause: %w", err) + } + return ethereum.CallMsg{To: c.addr, Data: data}, nil +} + +func (c *contractLibImpl) DecodeImportantAddressResponse(callResponse []byte) (gethcommon.Address, error) { + unpackedResponse, err := c.contractABI.Unpack(GetImportantAddressMethod, callResponse) + if err != nil { + return gethcommon.Address{}, fmt.Errorf("could not unpack call response. Cause: %w", err) + } + + // We expect the response to be a list containing one element, that element is a list of address strings + if len(unpackedResponse) != 1 { + return gethcommon.Address{}, fmt.Errorf("unexpected number of results (%d) returned from call, response: %s", len(unpackedResponse), unpackedResponse) + } + address, ok := unpackedResponse[0].(gethcommon.Address) + if !ok { + return gethcommon.Address{}, fmt.Errorf("could not convert element in call response to list of strings") + } + + return address, nil } func (c *contractLibImpl) unpackInitSecretTx(tx *types.Transaction, method *abi.Method, contractCallData map[string]interface{}) *ethadapter.L1InitializeSecretTx { @@ -297,6 +400,36 @@ func (c *contractLibImpl) unpackRespondSecretTx(tx *types.Transaction, method *a } } +func (c *contractLibImpl) unpackSetImportantContractsTx(tx *types.Transaction, method *abi.Method, contractCallData map[string]interface{}) (*ethadapter.L1SetImportantContractsTx, error) { + err := method.Inputs.UnpackIntoMap(contractCallData, tx.Data()[methodBytesLen:]) + if err != nil { + return nil, fmt.Errorf("could not unpack transaction. Cause: %w", err) + } + + keyData, found := contractCallData["key"] + if !found { + return nil, fmt.Errorf("call data not found for key") + } + keyString, ok := keyData.(string) + if !ok { + return nil, fmt.Errorf("could not decode key data") + } + + contractAddressData, found := contractCallData["newAddress"] + if !found { + return nil, fmt.Errorf("call data not found for newAddress") + } + contractAddress, ok := contractAddressData.(gethcommon.Address) + if !ok { + return nil, fmt.Errorf("could not decode newAddress data") + } + + return ðadapter.L1SetImportantContractsTx{ + Key: keyString, + NewAddress: contractAddress, + }, nil +} + // base64EncodeToString encodes a byte array to a string func base64EncodeToString(bytes []byte) string { return base64.StdEncoding.EncodeToString(bytes) diff --git a/go/host/enclave/guardian.go b/go/host/enclave/guardian.go index 7ce08d815c..2069fcaa06 100644 --- a/go/host/enclave/guardian.go +++ b/go/host/enclave/guardian.go @@ -276,10 +276,7 @@ func (g *Guardian) provideSecret() error { if err != nil { return fmt.Errorf("next block after block=%s not found - %w", awaitFromBlock, err) } - secretRespTxs := g.sl.L1Publisher().ExtractSecretResponses(nextBlock) - if err != nil { - return fmt.Errorf("could not extract secret responses from block=%s - %w", nextBlock.Hash(), err) - } + secretRespTxs, _, _ := g.sl.L1Publisher().ExtractObscuroRelevantTransactions(nextBlock) for _, scrt := range secretRespTxs { if scrt.RequesterID.Hex() == g.hostData.ID.Hex() { err = g.enclaveClient.InitEnclave(scrt.Secret) @@ -435,13 +432,12 @@ func (g *Guardian) submitL1Block(block *common.L1Block, isLatest bool) (bool, er func (g *Guardian) processL1BlockTransactions(block *common.L1Block) { // if there are any secret responses in the block we should refresh our P2P list to re-sync with the network - respTxs := g.sl.L1Publisher().ExtractSecretResponses(block) - if len(respTxs) > 0 { + secretRespTxs, rollupTxs, contractAddressTxs := g.sl.L1Publisher().ExtractObscuroRelevantTransactions(block) + if len(secretRespTxs) > 0 { // new peers may have been granted access to the network, notify p2p service to refresh its peer list go g.sl.P2P().RefreshPeerList() } - rollupTxs := g.sl.L1Publisher().ExtractRollupTxs(block) for _, rollup := range rollupTxs { r, err := common.DecodeRollup(rollup.Rollup) if err != nil { @@ -456,6 +452,15 @@ func (g *Guardian) processL1BlockTransactions(block *common.L1Block) { } } } + + if len(contractAddressTxs) > 0 { + go func() { + err := g.sl.L1Publisher().ResyncImportantContracts() + if err != nil { + g.logger.Error("Could not resync important contracts", log.ErrKey, err) + } + }() + } } func (g *Guardian) publishSharedSecretResponses(scrtResponses []*common.ProducedSecretResponse) error { diff --git a/go/host/host.go b/go/host/host.go index 854179c7df..62ae258025 100644 --- a/go/host/host.go +++ b/go/host/host.go @@ -204,8 +204,9 @@ func (h *host) ObscuroConfig() (*common.ObscuroNetworkInfo, error) { ManagementContractAddress: h.config.ManagementContractAddress, L1StartHash: h.config.L1StartHash, - SequencerID: h.config.SequencerID, - MessageBusAddress: h.config.MessageBusAddress, + SequencerID: h.config.SequencerID, + MessageBusAddress: h.config.MessageBusAddress, + ImportantContracts: h.services.L1Publisher().GetImportantContracts(), }, nil } diff --git a/go/host/l1/publisher.go b/go/host/l1/publisher.go index fc7cdbb90a..1406e1bdf7 100644 --- a/go/host/l1/publisher.go +++ b/go/host/l1/publisher.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "math/big" + "sync" "time" "github.com/ten-protocol/go-ten/go/common/stopcontrol" @@ -27,6 +28,11 @@ type Publisher struct { ethClient ethadapter.EthClient mgmtContractLib mgmtcontractlib.MgmtContractLib // Library to handle Management Contract lib operations + // cached map of important contract addresses (updated when we see a SetImportantContractsTx) + importantContractAddresses map[string]gethcommon.Address + // lock for the important contract addresses map + importantAddressesMutex sync.RWMutex + repository host.L1BlockRepository logger gethlog.Logger @@ -57,10 +63,20 @@ func NewL1Publisher( logger: logger, maxWaitForL1Receipt: maxWaitForL1Receipt, retryIntervalForL1Receipt: retryIntervalForL1Receipt, + + importantContractAddresses: map[string]gethcommon.Address{}, + importantAddressesMutex: sync.RWMutex{}, } } func (p *Publisher) Start() error { + go func() { + // Do an initial read of important contract addresses when service starts up + err := p.ResyncImportantContracts() + if err != nil { + p.logger.Error("Could not load important contract addresses", log.ErrKey, err) + } + }() return nil } @@ -145,8 +161,12 @@ func (p *Publisher) PublishSecretResponse(secretResponse *common.ProducedSecretR return nil } -func (p *Publisher) ExtractSecretResponses(block *types.Block) []*ethadapter.L1RespondSecretTx { +// ExtractObscuroRelevantTransactions will extract any transactions from the block that are relevant to obscuro +// todo (#2495) we should monitor for relevant L1 events instead of scanning every transaction in the block +func (p *Publisher) ExtractObscuroRelevantTransactions(block *types.Block) ([]*ethadapter.L1RespondSecretTx, []*ethadapter.L1RollupTx, []*ethadapter.L1SetImportantContractsTx) { var secretRespTxs []*ethadapter.L1RespondSecretTx + var rollupTxs []*ethadapter.L1RollupTx + var contractAddressTxs []*ethadapter.L1SetImportantContractsTx for _, tx := range block.Transactions() { t := p.mgmtContractLib.DecodeTx(tx) if t == nil { @@ -154,23 +174,18 @@ func (p *Publisher) ExtractSecretResponses(block *types.Block) []*ethadapter.L1R } if scrtTx, ok := t.(*ethadapter.L1RespondSecretTx); ok { secretRespTxs = append(secretRespTxs, scrtTx) - } - } - return secretRespTxs -} - -func (p *Publisher) ExtractRollupTxs(block *types.Block) []*ethadapter.L1RollupTx { - var rollupTxs []*ethadapter.L1RollupTx - for _, tx := range block.Transactions() { - t := p.mgmtContractLib.DecodeTx(tx) - if t == nil { continue } if rollupTx, ok := t.(*ethadapter.L1RollupTx); ok { rollupTxs = append(rollupTxs, rollupTx) + continue + } + if contractAddressTx, ok := t.(*ethadapter.L1SetImportantContractsTx); ok { + contractAddressTxs = append(contractAddressTxs, contractAddressTx) + continue } } - return rollupTxs + return secretRespTxs, rollupTxs, contractAddressTxs } func (p *Publisher) FetchLatestSeqNo() (*big.Int, error) { @@ -208,7 +223,7 @@ func (p *Publisher) PublishRollup(producedRollup *common.ExtRollup) { } func (p *Publisher) FetchLatestPeersList() ([]string, error) { - msg, err := p.mgmtContractLib.GetHostAddresses() + msg, err := p.mgmtContractLib.GetHostAddressesMsg() if err != nil { return nil, err } @@ -216,11 +231,10 @@ func (p *Publisher) FetchLatestPeersList() ([]string, error) { if err != nil { return nil, err } - decodedResponse, err := p.mgmtContractLib.DecodeCallResponse(response) + hostAddresses, err := p.mgmtContractLib.DecodeHostAddressesResponse(response) if err != nil { return nil, err } - hostAddresses := decodedResponse[0] // We remove any duplicate addresses and our own address from the retrieved peer list var filteredHostAddresses []string @@ -239,6 +253,55 @@ func (p *Publisher) FetchLatestPeersList() ([]string, error) { return filteredHostAddresses, nil } +func (p *Publisher) GetImportantContracts() map[string]gethcommon.Address { + p.importantAddressesMutex.RLock() + defer p.importantAddressesMutex.RUnlock() + return p.importantContractAddresses +} + +// ResyncImportantContracts will fetch the latest important contracts from the management contract and update the cached map +// Note: this should be run in a goroutine as it makes L1 transactions in series and will block. +// Cache is not overwritten until it completes. +func (p *Publisher) ResyncImportantContracts() error { + getKeysCallMsg, err := p.mgmtContractLib.GetImportantContractKeysMsg() + if err != nil { + return fmt.Errorf("could not build callMsg for important contracts: %w", err) + } + keysResp, err := p.ethClient.CallContract(getKeysCallMsg) + if err != nil { + return fmt.Errorf("could not fetch important contracts: %w", err) + } + + importantContracts, err := p.mgmtContractLib.DecodeImportantContractKeysResponse(keysResp) + if err != nil { + return fmt.Errorf("could not decode important contracts resp: %w", err) + } + + contractsMap := make(map[string]gethcommon.Address) + + for _, contract := range importantContracts { + getAddressCallMsg, err := p.mgmtContractLib.GetImportantAddressCallMsg(contract) + if err != nil { + return fmt.Errorf("could not build callMsg for important contract=%s: %w", contract, err) + } + addrResp, err := p.ethClient.CallContract(getAddressCallMsg) + if err != nil { + return fmt.Errorf("could not fetch important contract=%s: %w", contract, err) + } + contractAddress, err := p.mgmtContractLib.DecodeImportantAddressResponse(addrResp) + if err != nil { + return fmt.Errorf("could not decode important contract=%s resp: %w", contract, err) + } + contractsMap[contract] = contractAddress + } + + p.importantAddressesMutex.Lock() + defer p.importantAddressesMutex.Unlock() + p.importantContractAddresses = contractsMap + + return nil +} + // publishTransaction will keep trying unless the L1 seems to be unavailable or the tx is otherwise rejected // It is responsible for keeping the nonce accurate, according to the following rules: // - Caller should not increment the wallet nonce before this method is called diff --git a/integration/ethereummock/mgmt_contract_lib.go b/integration/ethereummock/mgmt_contract_lib.go index 232beb66bf..7201272cc1 100644 --- a/integration/ethereummock/mgmt_contract_lib.go +++ b/integration/ethereummock/mgmt_contract_lib.go @@ -71,12 +71,32 @@ func (m *mockContractLib) CreateInitializeSecret(tx *ethadapter.L1InitializeSecr return encodeTx(tx, initializeSecretTxAddr) } -func (m *mockContractLib) GetHostAddresses() (ethereum.CallMsg, error) { +func (m *mockContractLib) GetHostAddressesMsg() (ethereum.CallMsg, error) { return ethereum.CallMsg{}, nil } -func (m *mockContractLib) DecodeCallResponse([]byte) ([][]string, error) { - return [][]string{{""}}, nil +func (m *mockContractLib) DecodeHostAddressesResponse([]byte) ([]string, error) { + return []string{""}, nil +} + +func (m *mockContractLib) GetImportantContractKeysMsg() (ethereum.CallMsg, error) { + return ethereum.CallMsg{}, nil +} + +func (m *mockContractLib) DecodeImportantContractKeysResponse([]byte) ([]string, error) { + return []string{""}, nil +} + +func (m *mockContractLib) SetImportantContractMsg(string, gethcommon.Address) (ethereum.CallMsg, error) { + return ethereum.CallMsg{}, nil +} + +func (m *mockContractLib) GetImportantAddressCallMsg(string) (ethereum.CallMsg, error) { + return ethereum.CallMsg{}, nil +} + +func (m *mockContractLib) DecodeImportantAddressResponse([]byte) (gethcommon.Address, error) { + return gethcommon.Address{}, nil } func decodeTx(tx *types.Transaction) ethadapter.L1Transaction { diff --git a/integration/networktest/actions/l1/important_contracts.go b/integration/networktest/actions/l1/important_contracts.go new file mode 100644 index 0000000000..cc8fa4279c --- /dev/null +++ b/integration/networktest/actions/l1/important_contracts.go @@ -0,0 +1,106 @@ +package l1 + +import ( + "context" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/pkg/errors" + "github.com/ten-protocol/go-ten/go/common/retry" + "github.com/ten-protocol/go-ten/go/ethadapter/mgmtcontractlib" + "github.com/ten-protocol/go-ten/go/obsclient" + "github.com/ten-protocol/go-ten/integration/common/testlog" + "github.com/ten-protocol/go-ten/integration/networktest" +) + +type setImportantContract struct { + contractKey string + contractAddress common.Address +} + +func SetImportantContract(contractKey string, contractAddress common.Address) networktest.Action { + return &setImportantContract{ + contractKey: contractKey, + contractAddress: contractAddress, + } +} + +func (s *setImportantContract) Run(ctx context.Context, network networktest.NetworkConnector) (context.Context, error) { + obsClient, err := obsclient.Dial(network.ValidatorRPCAddress(0)) + if err != nil { + return ctx, errors.Wrap(err, "failed to dial obsClient") + } + + networkCfg, err := obsClient.GetConfig() + if err != nil { + return ctx, errors.Wrap(err, "failed to get network config") + } + + l1Client, err := network.GetL1Client() + if err != nil { + return ctx, errors.Wrap(err, "failed to get L1 client") + } + + mgmtContract := mgmtcontractlib.NewMgmtContractLib(&networkCfg.ManagementContractAddress, testlog.Logger()) + + msg, err := mgmtContract.SetImportantContractMsg(s.contractKey, s.contractAddress) + if err != nil { + return ctx, errors.Wrap(err, "failed to create SetImportantContractMsg") + } + + txData := &types.LegacyTx{ + To: &networkCfg.ManagementContractAddress, + Data: msg.Data, + } + mcOwner, err := network.GetMCOwnerWallet() + if err != nil { + return ctx, errors.Wrap(err, "failed to get MC owner wallet") + } + // !! Important note !! + // The ownerOnly check in the contract doesn't like the gas estimate in here, to test you may need to hardcode a + // the gas value when the estimate errors + tx, err := l1Client.PrepareTransactionToSend(txData, networkCfg.ManagementContractAddress, mcOwner.GetNonceAndIncrement()) + if err != nil { + return ctx, errors.Wrap(err, "failed to prepare tx") + } + signedTx, err := mcOwner.SignTransaction(tx) + if err != nil { + return ctx, errors.Wrap(err, "failed to sign tx") + } + err = l1Client.SendTransaction(signedTx) + if err != nil { + return nil, errors.Wrap(err, "failed to send tx") + } + + // wait for tx to be mined + return ctx, retry.Do(func() error { + receipt, err := l1Client.TransactionReceipt(signedTx.Hash()) + if err != nil { + return err + } + if receipt.Status != types.ReceiptStatusSuccessful { + return retry.FailFast(errors.New("tx failed")) + } + return nil + }, retry.NewTimeoutStrategy(15*time.Second, 1*time.Second)) +} + +func (s *setImportantContract) Verify(_ context.Context, network networktest.NetworkConnector) error { + cli, err := obsclient.Dial(network.ValidatorRPCAddress(0)) + if err != nil { + return errors.Wrap(err, "failed to dial obsClient") + } + networkCfg, err := cli.GetConfig() + if err != nil { + return errors.Wrap(err, "failed to get network config") + } + + if networkCfg.ImportantContracts == nil || len(networkCfg.ImportantContracts) == 0 { + return errors.New("no important contracts set") + } + if addr, ok := networkCfg.ImportantContracts[s.contractKey]; !ok || addr != s.contractAddress { + return errors.New("important contract not set") + } + return nil +} diff --git a/integration/networktest/env/testnet.go b/integration/networktest/env/testnet.go index a3548adbc9..3f7001909a 100644 --- a/integration/networktest/env/testnet.go +++ b/integration/networktest/env/testnet.go @@ -4,11 +4,14 @@ import ( "bytes" "context" "encoding/json" + "errors" "fmt" "math/big" "net/http" "time" + "github.com/ten-protocol/go-ten/go/wallet" + "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" "github.com/ten-protocol/go-ten/integration/networktest/userwallet" @@ -124,3 +127,7 @@ func (t *testnetConnector) AllocateFaucetFundsWithWallet(ctx context.Context, ac } return nil } + +func (t *testnetConnector) GetMCOwnerWallet() (wallet.Wallet, error) { + return nil, errors.New("testnet connector environments cannot access the MC owner wallet") +} diff --git a/integration/networktest/interfaces.go b/integration/networktest/interfaces.go index 87e2d6703b..acc27fb7f7 100644 --- a/integration/networktest/interfaces.go +++ b/integration/networktest/interfaces.go @@ -4,13 +4,16 @@ import ( "context" "github.com/ten-protocol/go-ten/go/ethadapter" + "github.com/ten-protocol/go-ten/go/wallet" "github.com/ethereum/go-ethereum/common" ) // NetworkConnector represents the network being tested against, e.g. testnet, dev-testnet, dev-sim // -// It provides network details (standard contract addresses) and easy client setup for sim users +// # It provides network details (standard contract addresses) and easy client setup for sim users +// +// Note: some of these methods may not be available for some networks (e.g. MC Owner wallet for live testnets) type NetworkConnector interface { ChainID() int64 // AllocateFaucetFunds uses the networks default faucet mechanism for allocating funds to a test account @@ -21,6 +24,7 @@ type NetworkConnector interface { GetSequencerNode() NodeOperator GetValidatorNode(idx int) NodeOperator GetL1Client() (ethadapter.EthClient, error) + GetMCOwnerWallet() (wallet.Wallet, error) // wallet that owns the management contract (network admin) } // Action is any step in a test, they will typically be either minimally small steps in the test or they will be containers diff --git a/integration/networktest/tests/bridge/important_contracts_test.go b/integration/networktest/tests/bridge/important_contracts_test.go new file mode 100644 index 0000000000..e608569a98 --- /dev/null +++ b/integration/networktest/tests/bridge/important_contracts_test.go @@ -0,0 +1,23 @@ +package bridge + +import ( + "testing" + + gethcommon "github.com/ethereum/go-ethereum/common" + "github.com/ten-protocol/go-ten/integration/networktest" + "github.com/ten-protocol/go-ten/integration/networktest/actions" + "github.com/ten-protocol/go-ten/integration/networktest/actions/l1" + "github.com/ten-protocol/go-ten/integration/networktest/env" +) + +func TestImportantContractsLookup(t *testing.T) { + networktest.TestOnlyRunsInIDE(t) + networktest.Run( + "important-contracts-lookup", + t, + env.LocalDevNetwork(), + actions.Series( + l1.SetImportantContract("L1TestContract", gethcommon.HexToAddress("0x64")), + ), + ) +} diff --git a/integration/simulation/devnetwork/dev_network.go b/integration/simulation/devnetwork/dev_network.go index 160d905fc7..462bc26feb 100644 --- a/integration/simulation/devnetwork/dev_network.go +++ b/integration/simulation/devnetwork/dev_network.go @@ -54,6 +54,10 @@ type InMemDevNetwork struct { faucetLock sync.Mutex } +func (s *InMemDevNetwork) GetMCOwnerWallet() (wallet.Wallet, error) { + return s.networkWallets.MCOwnerWallet, nil +} + func (s *InMemDevNetwork) ChainID() int64 { return integration.ObscuroChainID }