diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..32fc958 --- /dev/null +++ b/Makefile @@ -0,0 +1,4 @@ +.PHONY: build + +build: + go build -o ./build/dump ./cmd/dump/... \ No newline at end of file diff --git a/Readme.md b/Readme.md index 3f88b54..0568cd0 100644 --- a/Readme.md +++ b/Readme.md @@ -1,7 +1,18 @@ ## BNB Beacon Chain Dump -### Dump Accounts +## Introduction +Originally conceived as a platform for issuing data assets, the Beacon Chain (BC) has evolved to host 7.6 million accounts on the BNB Beacon Chain, supporting 557 tokens compliant with BEP2 or BEP8 standards. The digital assets held by users on this chain are secure and will persist beyond the BC Fusion event. The responsibility for safeguarding these assets falls upon the BNB Chain, irrespective of their individual values. + +Our objective is to implement a solution that ensures the seamless execution of BC Fusion, followed by secure access to users' digital assets. + +Following the BC-Fusion plan, the BNB Beacon Chain has been officially decommissioned. + +This tool serves the purpose of dumping the state of the BNB Beacon Chain and generating Merkle tree proofs for user accounts. + +### Dump Accounts and Generate Merkle Tree Proofs +```bash ```bash -go build -o ./build/dump ./cmd/dump/... +make build +mkdir -p ./output ./build/dump export ./output/state.json --home ${DATA_HOME} ``` diff --git a/cmd/dump/main.go b/cmd/dump/main.go index 90e26d9..904851c 100644 --- a/cmd/dump/main.go +++ b/cmd/dump/main.go @@ -4,11 +4,14 @@ import ( "encoding/json" "fmt" "io" + "math/big" "os" "path" "path/filepath" + "time" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" "github.com/spf13/cobra" "github.com/spf13/viper" @@ -21,7 +24,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/bnb-chain/node/app" - "github.com/bnb-chain/node/wire" + nodetypes "github.com/bnb-chain/node/common/types" mt "github.com/txaty/go-merkletree" @@ -32,75 +35,212 @@ const ( flagTraceStore = "trace-store" ) -// ExportAccounts exports blockchain world state to json. -func ExportAccounts(app *app.BNBBeaconChain) (appState json.RawMessage, err error) { +func NewHashFunc(data []byte) ([]byte, error) { + return crypto.Keccak256(data), nil +} + +type leafNode struct { + Address sdk.AccAddress `json:"address"` + Coin sdk.Coin `json:"coin"` +} + +// Serialize implements merkle tree data Serialize method. +func (node *leafNode) Serialize() ([]byte, error) { + var symbol [32]byte + copy(symbol[:], node.Coin.Denom) + return crypto.Keccak256( + node.Address.Bytes(), + symbol[:], + big.NewInt(node.Coin.Amount).FillBytes(make([]byte, 32)), + ), nil +} + +func (node *leafNode) Print() string { + buf, _ := node.Serialize() + + return "0x" + common.Bytes2Hex(crypto.Keccak256(buf)) +} + +// ExportAccountsBalanceWithProof exports blockchain world state to json. +func ExportAccountsBalanceWithProof(app *app.BNBBeaconChain, outputPath string) (err error) { ctx := app.NewContext(sdk.RunTxModeCheck, abci.Header{}) // iterate to get the accounts accounts := []*types.ExportedAccount{} mtData := []mt.DataBlock{} - assets := types.ExportedAssets{} + appendAccount := func(acc sdk.Account) (stop bool) { - addr := acc.GetAddress() - coins := acc.GetCoins() - for _, coin := range coins { - assets[coin.Denom] += coins.AmountOf(coin.Denom) - } + namedAcc := acc.(nodetypes.NamedAccount) + addr := namedAcc.GetAddress() + coins := namedAcc.GetCoins() + frozenCoins := namedAcc.GetFrozenCoins() + lockedCoins := namedAcc.GetLockedCoins() + + allCoins := coins.Plus(frozenCoins) + allCoins = allCoins.Plus(lockedCoins) + account := types.ExportedAccount{ Address: addr, - AccountNumber: acc.GetAccountNumber(), - Coins: acc.GetCoins(), + AccountNumber: namedAcc.GetAccountNumber(), + Coins: allCoins.Sort(), } accounts = append(accounts, &account) - mtData = append(mtData, &account) - if err != nil { - fmt.Println(err) - return true + for index := range allCoins { + if allCoins[index].Amount > 0 { + mtData = append(mtData, &leafNode{ + Address: addr, + Coin: allCoins[index], + }) + } } + trace("address", acc.GetAddress(), "account:", account) + return false } + trace("iterate accounts...") app.AccountKeeper.IterateAccounts(ctx, appendAccount) + + trace("make merkle tree...") // create a Merkle Tree config and set parallel run parameters config := &mt.Config{ - RunInParallel: true, - NumRoutines: 4, - SortSiblingPairs: true, + HashFunc: NewHashFunc, + RunInParallel: true, + SortSiblingPairs: true, + DisableLeafHashing: true, } tree, err := mt.New(config, mtData) if err != nil { - return nil, err + return err } + + trace("make proofs...") proofs := tree.Proofs - exportedProof := make(types.ExportedProofs, len(proofs)) + maxProofLength := 0 + exportedProof := make([]*types.ExportedProof, 0, len(proofs)) + trace("proofs length", len(proofs)) for i := 0; i < len(mtData); i++ { proof := proofs[i] nProof := make([]string, 0, len(proof.Siblings)) - for i := 0; i < len(proof.Siblings); i++ { - nProof = append(nProof, "0x"+common.Bytes2Hex(proof.Siblings[i])) + for j := 0; j < len(proof.Siblings); j++ { + nProof = append(nProof, "0x"+common.Bytes2Hex(proof.Siblings[j])) } - exportedProof[accounts[i].Address.String()] = nProof + + leaf := mtData[i].(*leafNode) + exportedProof = append(exportedProof, &types.ExportedProof{ + Address: leaf.Address, + Coin: leaf.Coin, + Proof: nProof, + }) + if proofLength := len(proof.Siblings); proofLength > maxProofLength { + maxProofLength = proofLength + } + trace("address:", leaf.Address, "proof:", nProof, "leaf:", leaf.Print()) } + trace("max proof length:", maxProofLength) + genState := types.ExportedAccountState{ - Accounts: accounts, - Assets: assets, - StateRoot: "0x" + common.Bytes2Hex(tree.Root), - Proofs: exportedProof, + ChainID: app.CheckState.Ctx.ChainID(), + BlockHeight: app.LastBlockHeight(), + CommitID: app.LastCommitID(), + Accounts: accounts, + StateRoot: "0x" + common.Bytes2Hex(tree.Root), + Proofs: exportedProof, + } + + trace("write to file...") + + // write the state to the file + baseFile, err := os.OpenFile(path.Join(outputPath, "base.json"), os.O_CREATE|os.O_WRONLY, os.ModePerm) + if err != nil { + return err + } + defer baseFile.Close() + err = writeJSONFile(baseFile, genState) + if err != nil { + return err + } + + // write the accounts to the file + accountFile, err := os.OpenFile(path.Join(outputPath, "accounts.json"), os.O_CREATE|os.O_WRONLY, os.ModePerm) + if err != nil { + return err + } + defer accountFile.Close() + err = writeJSONFileInStream(accountFile, func(encoder *json.Encoder) error { + for i, account := range genState.Accounts { + err = encoder.Encode(account) + if err != nil { + return err + } + if i < len(accounts)-1 { + _, err = accountFile.WriteString(`,`) + if err != nil { + return err + } + } + } + return nil + }) + if err != nil { + return err } - appState, err = wire.MarshalJSONIndent(app.Codec, genState) + + // write the proofs to the file + proofFile, err := os.OpenFile(path.Join(outputPath, "proofs.json"), os.O_CREATE|os.O_WRONLY, os.ModePerm) + if err != nil { + return err + } + defer proofFile.Close() + err = writeJSONFileInStream(proofFile, func(encoder *json.Encoder) error { + for i, proof := range genState.Proofs { + err = encoder.Encode(proof) + if err != nil { + return err + } + if i < len(proofs)-1 { + _, err = proofFile.WriteString(`,`) + if err != nil { + return err + } + } + } + return nil + }) if err != nil { - return nil, err + return err } - return appState, nil + + return nil +} + +func writeJSONFile(file *os.File, data interface{}) error { + encoder := json.NewEncoder(file) + encoder.SetIndent("", "\t") + return encoder.Encode(data) +} +func writeJSONFileInStream(file *os.File, marshal func(*json.Encoder) error) error { + encoder := json.NewEncoder(file) + encoder.SetIndent("", "\t") + if _, err := file.WriteString(`[`); err != nil { + return err + } + if err := marshal(encoder); err != nil { + return err + } + if _, err := file.WriteString(`]`); err != nil { + return err + } + return nil } // ExportCmd dumps app state to JSON. func ExportCmd(ctx *server.Context, cdc *codec.Codec) *cobra.Command { return &cobra.Command{ - Use: "export ", + Use: "export ", Short: "Export state to JSON", RunE: func(cmd *cobra.Command, args []string) error { if len(args) != 1 { @@ -137,24 +277,11 @@ func ExportCmd(ctx *server.Context, cdc *codec.Codec) *cobra.Command { } dapp := app.NewBNBBeaconChain(ctx.Logger, db, traceWriter) - output, err := ExportAccounts(dapp) + err = ExportAccountsBalanceWithProof(dapp, args[0]) if err != nil { return err } - outputPath := args[0] - file, err := os.OpenFile(outputPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, os.ModePerm) - if err != nil { - return err - } - defer file.Close() - - encoder := json.NewEncoder(file) - encoder.SetIndent("", "\t") - err = encoder.Encode(output) - if err != nil { - return err - } return nil }, } @@ -188,6 +315,18 @@ func openTraceWriter(traceWriterFile string) (w io.Writer, err error) { return } +func trace(a ...any) { + if traceLog { + a = append([]any{"time:", time.Now()}, a...) + fmt.Println(a...) + } +} + +var ( + // TraceLog is a flag to print out full stack trace on errors + traceLog = false +) + func main() { cdc := app.Codec ctx := app.ServerContext @@ -199,7 +338,7 @@ func main() { } rootCmd.AddCommand(ExportCmd(ctx.ToCosmosServerCtx(), cdc)) - + rootCmd.PersistentFlags().BoolVar(&traceLog, "tracelog", false, "print out full stack trace on errors") // prepare and add flags executor := cli.PrepareBaseCmd(rootCmd, "BC", app.DefaultNodeHome) err := executor.Execute() diff --git a/go.mod b/go.mod index cd14e62..6d48a58 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/bnb-chain/node-dump -go 1.21.0 +go 1.21 replace ( github.com/Shopify/sarama v1.26.1 => github.com/Shopify/sarama v1.21.0 @@ -57,6 +57,7 @@ require ( github.com/herumi/bls-eth-go-binary v0.0.0-20210917013441-d37c07cfda4e // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jmhodges/levigo v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/cpuid/v2 v2.2.4 // indirect github.com/libp2p/go-buffer-pool v0.1.0 // indirect github.com/linkedin/goavro v0.0.0-20180427201934-fa8f6a30176c // indirect @@ -66,6 +67,8 @@ require ( github.com/minio/sha256-simd v1.0.0 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/mapstructure v1.4.1 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect github.com/natefinch/lumberjack v2.0.0+incompatible // indirect github.com/panjf2000/ants/v2 v2.5.0 // indirect diff --git a/go.sum b/go.sum index 8e26b51..fe21908 100644 --- a/go.sum +++ b/go.sum @@ -381,6 +381,7 @@ github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/u github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= @@ -441,9 +442,11 @@ github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag= github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= diff --git a/types/account.go b/types/account.go index 35231f1..61f397b 100644 --- a/types/account.go +++ b/types/account.go @@ -1,38 +1,29 @@ package types import ( - "math/big" - sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/ethereum/go-ethereum/crypto" ) // ExportedAccount is an exported account. type ExportedAccount struct { Address sdk.AccAddress `json:"address"` AccountNumber int64 `json:"account_number"` - Coins sdk.Coins `json:"coins"` + Coins sdk.Coins `json:"coins,omitempty"` } -// Serialize implements merkle tree data Serialize method. -func (acc *ExportedAccount) Serialize() ([]byte, error) { - return crypto.Keccak256Hash( - acc.Address.Bytes(), - big.NewInt(acc.AccountNumber).Bytes(), - []byte(acc.Coins.String()), - ).Bytes(), nil +// ExportedProof is an exported proof. +type ExportedProof struct { + Address sdk.AccAddress `json:"address"` + Coin sdk.Coin `json:"coin"` + Proof []string `json:"proof"` } -// ExportedAssets is a map of asset name to amount -type ExportedAssets map[string]int64 - -// ExportedProofs is a map of account address to merkle proof -type ExportedProofs map[string][]string - // ExportedAccountState is an exported account state. type ExportedAccountState struct { - Accounts []*ExportedAccount `json:"accounts"` - Assets ExportedAssets `json:"assets"` - StateRoot string `json:"state_root"` - Proofs ExportedProofs `json:"proofs"` + ChainID string `json:"chain_id"` + BlockHeight int64 `json:"block_height"` + CommitID sdk.CommitID `json:"commit_id"` + Accounts []*ExportedAccount `json:"-"` + StateRoot string `json:"state_root"` + Proofs []*ExportedProof `json:"-"` }