diff --git a/Makefile b/Makefile index 62abe9738..eb23a3582 100644 --- a/Makefile +++ b/Makefile @@ -170,6 +170,10 @@ applications/echo-dapp: ## Create echo-dapp test application @mkdir -p applications @cartesi-machine --ram-length=128Mi --store=applications/echo-dapp --final-hash -- ioctl-echo-loop --vouchers=1 --notices=1 --reports=1 --verbose=1 +deploy-echo-dapp: ## Deploy echo-dapp test application + @echo "Deploying echo-dapp test application" + @./cartesi-rollups-cli app deploy -t applications/echo-dapp/ -v + # ============================================================================= # Static Analysis # ============================================================================= diff --git a/cmd/cartesi-rollups-cli/root/app/app.go b/cmd/cartesi-rollups-cli/root/app/app.go index cf424950c..f9d4d0fc8 100644 --- a/cmd/cartesi-rollups-cli/root/app/app.go +++ b/cmd/cartesi-rollups-cli/root/app/app.go @@ -5,6 +5,7 @@ package app import ( "github.com/cartesi/rollups-node/cmd/cartesi-rollups-cli/root/app/add" + "github.com/cartesi/rollups-node/cmd/cartesi-rollups-cli/root/app/deploy" "github.com/cartesi/rollups-node/cmd/cartesi-rollups-cli/root/app/list" "github.com/cartesi/rollups-node/cmd/cartesi-rollups-cli/root/common" "github.com/spf13/cobra" @@ -22,10 +23,11 @@ func init() { &common.PostgresEndpoint, "postgres-endpoint", "p", - "postgres://postgres:password@localhost:5432/postgres", + "postgres://postgres:password@localhost:5432/rollupsdb?sslmode=disable", "Postgres endpoint", ) Cmd.AddCommand(add.Cmd) + Cmd.AddCommand(deploy.Cmd) Cmd.AddCommand(list.Cmd) } diff --git a/cmd/cartesi-rollups-cli/root/app/deploy/deploy.go b/cmd/cartesi-rollups-cli/root/app/deploy/deploy.go new file mode 100644 index 000000000..94ac00d19 --- /dev/null +++ b/cmd/cartesi-rollups-cli/root/app/deploy/deploy.go @@ -0,0 +1,342 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +package deploy + +import ( + "context" + "encoding/hex" + "fmt" + "log" + "log/slog" + "math/big" + "os" + "strings" + + cmdcommom "github.com/cartesi/rollups-node/cmd/cartesi-rollups-cli/root/common" + "github.com/cartesi/rollups-node/internal/node/model" + "github.com/cartesi/rollups-node/pkg/contracts/iapplicationfactory" + "github.com/cartesi/rollups-node/pkg/contracts/iauthorityfactory" + "github.com/cartesi/rollups-node/pkg/ethutil" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/ethclient" + "github.com/spf13/cobra" +) + +var Cmd = &cobra.Command{ + Use: "deploy", + Short: "Deploy an application and add it to the node", + Example: examples, + Run: run, +} + +const examples = `# Adds an application to Rollups Node: +cartesi-rollups-cli app deploy -a 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF -i 0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA` //nolint:lll + +const ( + statusRunning = "running" + statusNotRunning = "not-running" +) + +var ( + owner string + templatePath string + status string + iConsensusAddr string + appFactoryAddr string + authorityFactoryAddr string + rpcURL string + privateKey string + mnemonic string + salt string +) + +func init() { + Cmd.Flags().StringVarP( + &owner, + "owner", + "o", + "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "Application owner", + ) + + Cmd.Flags().StringVarP( + &templatePath, + "template-path", + "t", + "", + "Application template URI", + ) + cobra.CheckErr(Cmd.MarkFlagRequired("template-path")) + + Cmd.Flags().StringVarP( + &status, + "status", + "s", + statusRunning, + "Sets the application status", + ) + + Cmd.Flags().StringVarP( + &appFactoryAddr, + "app-factory", + "a", + "0xA1DA32BF664109D62208a1cb0d69aACc6a484873", + "Application Factory Address", + ) + + Cmd.Flags().StringVarP( + &authorityFactoryAddr, + "authority-factory", + "c", + "0xbDC5D42771A4Ae55eC7670AAdD2458D1d9C7C8A8", + "Authority Factory Address", + ) + + Cmd.Flags().StringVarP( + &iConsensusAddr, + "iconsensus", + "i", + "", + "Application IConsensus Address", + ) + + Cmd.Flags().StringVar(&rpcURL, "rpc-url", "http://localhost:8545", "Ethereum RPC URL") + Cmd.Flags().StringVar(&privateKey, "private-key", "", "Private key for signing transactions") + Cmd.Flags().StringVar(&mnemonic, "mnemonic", ethutil.FoundryMnemonic, "Mnemonic for signing transactions") + Cmd.Flags().StringVar(&salt, "salt", "0000000000000000000000000000000000000000000000000000000000000000", "salt") +} + +func run(cmd *cobra.Command, args []string) { + ctx := cmd.Context() + + if cmdcommom.Database == nil { + panic("Database was not initialized") + } + + var applicationStatus model.ApplicationStatus + switch status { + case statusRunning: + applicationStatus = model.ApplicationStatusRunning + case statusNotRunning: + applicationStatus = model.ApplicationStatusNotRunning + default: + slog.Error("Invalid application status", "status", status) + os.Exit(1) + } + + authorityFactoryAddress := common.HexToAddress(authorityFactoryAddr) + authorityAddr, err := deployAuthority(ctx, owner, authorityFactoryAddress, 10, salt) + if err != nil { + panic(err) + } + + templateHash := "26833f01a44f15efeaf83ef9a7ef6cbeda856d6de1b3b2d62b6c7343dbab48ed" + + applicationFactoryAddress := common.HexToAddress(appFactoryAddr) + appAddr, err := deployApplication(ctx, owner, applicationFactoryAddress, authorityAddr, templateHash, salt) + + if err != nil { + slog.Error("Invalid application status", "status", status) + os.Exit(1) + } + + application := model.Application{ + ContractAddress: appAddr, + TemplateHash: common.HexToHash(templateHash), + LastProcessedBlock: 0, + Status: applicationStatus, + IConsensusAddress: authorityAddr, + } + + _, err = cmdcommom.Database.InsertApplication(ctx, &application) + cobra.CheckErr(err) + fmt.Printf("Application %v successfully added\n", appAddr) +} + +// FIXME remove this +func deployApplication(ctx context.Context, owner string, applicationFactoryAddr, authorityAddr common.Address, templateHash string, salt string) (common.Address, error) { + client, err := ethclient.Dial(rpcURL) + if err != nil { + log.Fatalf("Failed to connect to the Ethereum client: %v", err) + } + + ownerAddr := common.HexToAddress(owner) + templateHashBytes, err := hex.DecodeString(templateHash) + if err != nil { + log.Fatalf("Failed to decode template hash: %v", err) + } + saltBytes, err := hex.DecodeString(salt) + if err != nil { + log.Fatalf("Failed to decode salt: %v", err) + } + + auth, err := getAuth(ctx, client) + if err != nil { + log.Fatalf("Failed to get transaction signer: %v", err) + } + + factory, err := iapplicationfactory.NewIApplicationFactory(applicationFactoryAddr, client) + if err != nil { + log.Fatalf("Failed to instantiate contract: %v", err) + } + + tx, err := factory.NewApplication(auth, authorityAddr, ownerAddr, toBytes32(templateHashBytes), toBytes32(saltBytes)) + if err != nil { + log.Fatalf("Transaction failed: %v", err) + } + + fmt.Printf("Transaction submitted: %s\n", tx.Hash().Hex()) + + // Wait for the transaction to be mined + receipt, err := bind.WaitMined(context.Background(), client, tx) + if err != nil { + log.Fatalf("Failed to wait for transaction mining: %v", err) + } + + if receipt.Status == 1 { + fmt.Println("Transaction successful!") + } else { + log.Fatalf("Transaction failed!") + } + + // Parse logs to get the address of the new application contract + contractABI, err := abi.JSON(strings.NewReader(iapplicationfactory.IApplicationFactoryABI)) + if err != nil { + log.Fatalf("Failed to parse ABI: %v", err) + } + + // Look for the specific event in the receipt logs + for _, vLog := range receipt.Logs { + event := struct { + Consensus common.Address + AppOwner common.Address + TemplateHash [32]byte + AppContract common.Address + }{} + + // Parse log for ApplicationCreated event + err := contractABI.UnpackIntoInterface(&event, "ApplicationCreated", vLog.Data) + if err != nil { + continue // Skip logs that don't match + } + + fmt.Printf("New Application contract deployed at address: %s\n", event.AppContract.Hex()) + return event.AppContract, nil + } + + return common.Address{}, fmt.Errorf("failed to find ApplicationCreated event in receipt logs") +} + +// FIXME remove this +func deployAuthority(ctx context.Context, owner string, authorityFactoryAddr common.Address, epochLength uint64, salt string) (common.Address, error) { + client, err := ethclient.Dial(rpcURL) + if err != nil { + log.Fatalf("Failed to connect to the Ethereum client: %v", err) + } + + ownerAddr := common.HexToAddress(owner) + saltBytes, err := hex.DecodeString(salt) + if err != nil { + log.Fatalf("Failed to decode salt: %v", err) + } + + auth, err := getAuth(ctx, client) + if err != nil { + log.Fatalf("Failed to get transaction signer: %v", err) + } + + contract, err := iauthorityfactory.NewIAuthorityFactory(authorityFactoryAddr, client) + if err != nil { + log.Fatalf("Failed to instantiate contract: %v", err) + } + + tx, err := contract.NewAuthority0(auth, ownerAddr, big.NewInt(int64(epochLength)), toBytes32(saltBytes)) + if err != nil { + log.Fatalf("Transaction failed: %v", err) + } + + fmt.Printf("Transaction submitted: %s\n", tx.Hash().Hex()) + + // Wait for the transaction to be mined + receipt, err := bind.WaitMined(context.Background(), client, tx) + if err != nil { + log.Fatalf("Failed to wait for transaction mining: %v", err) + } + + if receipt.Status == 1 { + fmt.Println("Transaction successful!") + } else { + log.Fatalf("Transaction failed!") + } + + // Parse logs to get the address of the new application contract + contractABI, err := abi.JSON(strings.NewReader(iauthorityfactory.IAuthorityFactoryABI)) + if err != nil { + log.Fatalf("Failed to parse ABI: %v", err) + } + + // Look for the specific event in the receipt logs + for _, vLog := range receipt.Logs { + event := struct { + Authority common.Address + }{} + + // Parse log for ApplicationCreated event + err := contractABI.UnpackIntoInterface(&event, "AuthorityCreated", vLog.Data) + if err != nil { + continue // Skip logs that don't match + } + + fmt.Printf("New Authority contract deployed at address: %s\n", event.Authority.Hex()) + return event.Authority, nil + } + + return common.Address{}, fmt.Errorf("failed to find AuthorityCreated event in receipt logs") +} + +func getAuth(ctx context.Context, client *ethclient.Client) (*bind.TransactOpts, error) { + var auth *bind.TransactOpts + if privateKey != "" { + key, err := crypto.HexToECDSA(privateKey) + if err != nil { + return nil, err + } + auth, err = bind.NewKeyedTransactorWithChainID(key, big.NewInt(1)) + if err != nil { + return nil, err + } + } else if mnemonic != "" { + signer, err := ethutil.NewMnemonicSigner(ctx, client, mnemonic, 0) + if err != nil { + return nil, err + } + auth, err = signer.MakeTransactor() + if err != nil { + return nil, err + } + } else { + // Default private key (unsafe for production!) + key, err := crypto.HexToECDSA("YOUR_DEFAULT_PRIVATE_KEY") + if err != nil { + return nil, err + } + auth, err = bind.NewKeyedTransactorWithChainID(key, big.NewInt(1)) + if err != nil { + return nil, err + } + } + return auth, nil +} + +func toBytes32(data []byte) [32]byte { + var arr [32]byte + if len(data) != 32 { + log.Fatalf("Invalid length: expected 32 bytes, got %d bytes", len(data)) + } + copy(arr[:], data) + return arr +}