diff --git a/Makefile b/Makefile index 2b57054a6..ecc904395 100644 --- a/Makefile +++ b/Makefile @@ -226,6 +226,8 @@ proto: @protoc --proto_path=pkg/grpc/protobuf-schemas/proto/ --go_out=./ --go_opt=module=$(MODULE) --go-vtproto_out=./ --go-vtproto_opt=features=marshal_strict+unmarshal+size --go-vtproto_opt=module=$(MODULE) pkg/grpc/protobuf-schemas/proto/waves/lang/*.proto @protoc --proto_path=pkg/grpc/protobuf-schemas/proto/ --go_out=./ --go_opt=module=$(MODULE) --go-vtproto_out=./ --go-vtproto_opt=features=marshal_strict+unmarshal+size --go-vtproto_opt=module=$(MODULE) pkg/grpc/protobuf-schemas/proto/waves/events/*.proto @protoc --proto_path=pkg/grpc/protobuf-schemas/proto/ --go_out=./ --go_opt=module=$(MODULE) --go-grpc_out=./ --go-grpc_opt=require_unimplemented_servers=false --go-grpc_opt=module=$(MODULE) pkg/grpc/protobuf-schemas/proto/waves/events/grpc/*.proto +proto-l2: + @protoc --proto_path=pkg/grpc/protobuf-schemas/proto/ --proto_path=pkg/grpc/l2/blockchain_info/ --go_out=./ --go_opt=module=$(MODULE) --go-vtproto_out=./ --go-vtproto_opt=features=marshal_strict+unmarshal+size --go-vtproto_opt=module=$(MODULE) pkg/grpc/l2/blockchain_info/*.proto build-node-mainnet-amd64-deb-package: release-node @mkdir -p build/dist diff --git a/cmd/blockchaininfo/nats_subscriber.go b/cmd/blockchaininfo/nats_subscriber.go new file mode 100644 index 000000000..a5d13a70a --- /dev/null +++ b/cmd/blockchaininfo/nats_subscriber.go @@ -0,0 +1,183 @@ +package main + +import ( + "context" + "encoding/json" + "flag" + "log" + "os" + "os/signal" + "strconv" + "strings" + "syscall" + + "go.uber.org/zap" + + "github.com/nats-io/nats.go" + "github.com/pkg/errors" + "github.com/wavesplatform/gowaves/pkg/blockchaininfo" + g "github.com/wavesplatform/gowaves/pkg/grpc/l2/blockchain_info" + "github.com/wavesplatform/gowaves/pkg/proto" +) + +func printBlockInfo(blockInfoProto *g.BlockInfo) error { + blockInfo, err := blockchaininfo.BUpdatesInfoFromProto(blockInfoProto) + if err != nil { + return err + } + blockInfoJSON, err := json.Marshal(blockInfo) + if err != nil { + return err + } + log.Println(string(blockInfoJSON)) + return nil +} + +func printContractInfo(contractInfoProto *g.L2ContractDataEntries, scheme proto.Scheme, path string) error { + contractInfo, err := blockchaininfo.L2ContractDataEntriesFromProto(contractInfoProto, scheme) + if err != nil { + return err + } + // Delete data entries are not going to have "type" + prettyJSON, err := json.MarshalIndent(contractInfo, "", " ") + if err != nil { + log.Println("Error converting to pretty JSON:", err) + return err + } + heightStr := strconv.FormatUint(contractInfoProto.Height, 10) + // Write the pretty JSON to a file + err = os.WriteFile(path+heightStr+".json", prettyJSON, 0600) + if err != nil { + log.Println("Error writing to file:", err) + return err + } + + return nil +} + +func receiveBlockUpdates(msg *nats.Msg) { + blockUpdatesInfo := new(g.BlockInfo) + unmrshlErr := blockUpdatesInfo.UnmarshalVT(msg.Data) + if unmrshlErr != nil { + log.Printf("failed to unmarshal block updates, %v", unmrshlErr) + return + } + + err := printBlockInfo(blockUpdatesInfo) + if err != nil { + return + } + log.Printf("Received on %s:\n", msg.Subject) +} + +func receiveContractUpdates(msg *nats.Msg, contractMsg []byte, scheme proto.Scheme, path string) []byte { + zap.S().Infof("Received on %s:\n", msg.Subject) + + switch msg.Data[0] { + case blockchaininfo.NoPaging: + contractMsg = msg.Data[1:] + contractUpdatesInfo := new(g.L2ContractDataEntries) + if err := contractUpdatesInfo.UnmarshalVT(contractMsg); err != nil { + log.Printf("Failed to unmarshal contract updates: %v", err) + return contractMsg + } + if err := printContractInfo(contractUpdatesInfo, scheme, path); err != nil { + log.Printf("Failed to print contract info: %v", err) + return contractMsg + } + contractMsg = nil + + case blockchaininfo.StartPaging: + contractMsg = append(contractMsg, msg.Data[1:]...) + + case blockchaininfo.EndPaging: + if contractMsg != nil { + contractMsg = append(contractMsg, msg.Data[1:]...) + contractUpdatesInfo := new(g.L2ContractDataEntries) + if err := contractUpdatesInfo.UnmarshalVT(contractMsg); err != nil { + log.Printf("Failed to unmarshal contract updates: %v", err) + return contractMsg + } + + go func() { + if err := printContractInfo(contractUpdatesInfo, scheme, path); err != nil { + log.Printf("Failed to print contract info updates: %v", err) + } + }() + contractMsg = nil + } + } + + return contractMsg +} + +func main() { + var ( + blockchainType string + updatesPath string + natsURL string + ) + // Initialize the zap logger + l, err := zap.NewProduction() + if err != nil { + log.Fatalf("failed to initialize zap logger: %v", err) + } + defer func(l *zap.Logger) { + syncErr := l.Sync() + if syncErr != nil { + log.Fatalf("failed to sync zap logger %v", syncErr) + } + }(l) + + flag.StringVar(&blockchainType, "blockchain-type", "testnet", "Blockchain scheme (e.g., stagenet, testnet, mainnet)") + flag.StringVar(&updatesPath, "updates-path", "", "File path to store contract updates") + flag.StringVar(&natsURL, "nats-url", nats.DefaultURL, "URL for the NATS server") + + flag.Parse() + + scheme, err := schemeFromString(blockchainType) + if err != nil { + zap.S().Fatalf("Failed to parse the blockchain type: %v", err) + } + ctx, done := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) + defer done() + // Connect to a NATS server + nc, err := nats.Connect(natsURL) + if err != nil { + zap.S().Fatalf("Failed to connect to nats server: %v", err) + return + } + defer nc.Close() + + _, err = nc.Subscribe(blockchaininfo.BlockUpdates, func(msg *nats.Msg) { + receiveBlockUpdates(msg) + }) + if err != nil { + zap.S().Fatalf("Failed to subscribe to block updates: %v", err) + return + } + + var contractMsg []byte + _, err = nc.Subscribe(blockchaininfo.ContractUpdates, func(msg *nats.Msg) { + contractMsg = receiveContractUpdates(msg, contractMsg, scheme, updatesPath) + }) + if err != nil { + zap.S().Fatalf("Failed to subscribe to contract updates: %v", err) + return + } + <-ctx.Done() + zap.S().Info("NATS subscriber finished...") +} + +func schemeFromString(networkType string) (proto.Scheme, error) { + switch strings.ToLower(networkType) { + case "mainnet": + return proto.MainNetScheme, nil + case "testnet": + return proto.TestNetScheme, nil + case "stagenet": + return proto.StageNetScheme, nil + default: + return 0, errors.New("invalid blockchain type string") + } +} diff --git a/cmd/importer/importer.go b/cmd/importer/importer.go index b357af4ba..64145d3d4 100644 --- a/cmd/importer/importer.go +++ b/cmd/importer/importer.go @@ -192,7 +192,7 @@ func runImporter(c *cfg) error { return err } - st, err := state.NewState(c.dataDirPath, false, c.params(fds), ss, false) + st, err := state.NewState(c.dataDirPath, false, c.params(fds), ss, false, nil) if err != nil { return fmt.Errorf("failed to create state: %w", err) } diff --git a/cmd/node/node.go b/cmd/node/node.go index fdbeb8bf1..3282b302d 100644 --- a/cmd/node/node.go +++ b/cmd/node/node.go @@ -20,11 +20,13 @@ import ( "github.com/pkg/errors" "github.com/prometheus/client_golang/prometheus/promhttp" + "go.uber.org/zap" "go.uber.org/zap/zapcore" "golang.org/x/sync/errgroup" "github.com/wavesplatform/gowaves/pkg/api" + "github.com/wavesplatform/gowaves/pkg/blockchaininfo" "github.com/wavesplatform/gowaves/pkg/crypto" "github.com/wavesplatform/gowaves/pkg/grpc/server" "github.com/wavesplatform/gowaves/pkg/libs/microblock_cache" @@ -72,51 +74,52 @@ var defaultPeers = map[string]string{ type config struct { isParsed bool - logLevel zapcore.Level - logDevelopment bool - logNetwork bool - logNetworkData bool - logFSM bool - statePath string - blockchainType string - peerAddresses string - declAddr string - nodeName string - cfgPath string - apiAddr string - apiKey string - apiMaxConnections int - rateLimiterOptions string - grpcAddr string - grpcAPIMaxConnections int - enableMetaMaskAPI bool - enableMetaMaskAPILog bool - enableGrpcAPI bool - blackListResidenceTime time.Duration - buildExtendedAPI bool - serveExtendedAPI bool - buildStateHashes bool - bindAddress string - disableOutgoingConnections bool - minerVoteFeatures string - disableBloomFilter bool - reward int64 - obsolescencePeriod time.Duration - walletPath string - walletPassword string - limitAllConnections uint - minPeersMining int - disableMiner bool - profiler bool - prometheus string - metricsID int - metricsURL string - dropPeers bool - dbFileDescriptors uint - newConnectionsLimit int - disableNTP bool - microblockInterval time.Duration - enableLightMode bool + logLevel zapcore.Level + logDevelopment bool + logNetwork bool + logNetworkData bool + logFSM bool + statePath string + blockchainType string + peerAddresses string + declAddr string + nodeName string + cfgPath string + apiAddr string + apiKey string + apiMaxConnections int + rateLimiterOptions string + grpcAddr string + grpcAPIMaxConnections int + enableMetaMaskAPI bool + enableMetaMaskAPILog bool + enableGrpcAPI bool + blackListResidenceTime time.Duration + buildExtendedAPI bool + serveExtendedAPI bool + buildStateHashes bool + bindAddress string + disableOutgoingConnections bool + minerVoteFeatures string + disableBloomFilter bool + reward int64 + obsolescencePeriod time.Duration + walletPath string + walletPassword string + limitAllConnections uint + minPeersMining int + disableMiner bool + profiler bool + prometheus string + metricsID int + metricsURL string + dropPeers bool + dbFileDescriptors uint + newConnectionsLimit int + disableNTP bool + microblockInterval time.Duration + enableLightMode bool + enableBlockchainUpdatesPlugin bool } var errConfigNotParsed = stderrs.New("config is not parsed") @@ -169,6 +172,7 @@ func (c *config) logParameters() { zap.S().Debugf("disable-ntp: %t", c.disableNTP) zap.S().Debugf("microblock-interval: %s", c.microblockInterval) zap.S().Debugf("enable-light-mode: %t", c.enableLightMode) + zap.S().Debugf("enable-blockchain-updates-plugin: %t", c.enableBlockchainUpdatesPlugin) } func (c *config) parse() { @@ -266,6 +270,9 @@ func (c *config) parse() { "Interval between microblocks.") flag.BoolVar(&c.enableLightMode, "enable-light-mode", false, "Start node in light mode") + + flag.BoolVar(&c.enableBlockchainUpdatesPlugin, "enable-blockchain-info", false, + "Turn on blockchain updates plugin") flag.Parse() c.logLevel = *l } @@ -393,7 +400,17 @@ func runNode(ctx context.Context, nc *config) (_ io.Closer, retErr error) { return nil, errors.Wrap(err, "failed to create state parameters") } - st, err := state.NewState(path, true, params, cfg, nc.enableLightMode) + var bUpdatesExtension *blockchaininfo.BlockchainUpdatesExtension + if nc.enableBlockchainUpdatesPlugin { + var bUErr error + bUpdatesExtension, bUErr = runBlockchainUpdatesPlugin(ctx, cfg) + if bUErr != nil { + return nil, errors.Wrap(bUErr, "failed to run blockchain updates plugin") + } + } + + // Send updatesChannel into BlockchainSettings. Write updates into this channel + st, err := state.NewState(path, true, params, cfg, nc.enableLightMode, bUpdatesExtension) if err != nil { return nil, errors.Wrap(err, "failed to initialize node's state") } @@ -795,6 +812,33 @@ func runAPIs( return nil } +func runBlockchainUpdatesPlugin( + ctx context.Context, + cfg *settings.BlockchainSettings, +) (*blockchaininfo.BlockchainUpdatesExtension, error) { + const l2ContractAddr = "3Msx4Aq69zWUKy4d1wyKnQ4ofzEDAfv5Ngf" + + l2address, cnvrtErr := proto.NewAddressFromString(l2ContractAddr) + if cnvrtErr != nil { + return nil, errors.Wrapf(cnvrtErr, "failed to convert L2 contract address %q", l2ContractAddr) + } + + bUpdatesExtensionState := blockchaininfo.NewBUpdatesExtensionState( + blockchaininfo.StoreBlocksLimit, + cfg.AddressSchemeCharacter, + ) + + updatesChannel := make(chan blockchaininfo.BUpdatesInfo) + go func() { + err := bUpdatesExtensionState.RunBlockchainUpdatesPublisher(ctx, updatesChannel, cfg.AddressSchemeCharacter) + if err != nil { + zap.S().Fatalf("Failed to run blockchain updates publisher: %v", err) + } + }() + + return blockchaininfo.NewBlockchainUpdatesExtension(ctx, l2address, updatesChannel), nil +} + func FromArgs(scheme proto.Scheme, c *config) func(s *settings.NodeSettings) error { return func(s *settings.NodeSettings) error { s.DeclaredAddr = c.declAddr diff --git a/cmd/rollback/main.go b/cmd/rollback/main.go index dbb75ec53..3be581456 100644 --- a/cmd/rollback/main.go +++ b/cmd/rollback/main.go @@ -77,7 +77,7 @@ func main() { params.BuildStateHashes = *buildStateHashes params.StoreExtendedApiData = *buildExtendedAPI - s, err := state.NewState(*statePath, true, params, cfg, false) + s, err := state.NewState(*statePath, true, params, cfg, false, nil) if err != nil { zap.S().Error(err) return diff --git a/cmd/statehash/statehash.go b/cmd/statehash/statehash.go index 78c84bacd..45ec9a5bf 100644 --- a/cmd/statehash/statehash.go +++ b/cmd/statehash/statehash.go @@ -109,7 +109,7 @@ func run() error { params.BuildStateHashes = true params.ProvideExtendedApi = false - st, err := state.NewState(statePath, false, params, ss, false) + st, err := state.NewState(statePath, false, params, ss, false, nil) if err != nil { zap.S().Errorf("Failed to open state at '%s': %v", statePath, err) return err diff --git a/go.mod b/go.mod index 3ac001728..8ccd77b44 100644 --- a/go.mod +++ b/go.mod @@ -22,9 +22,12 @@ require ( github.com/influxdata/influxdb1-client v0.0.0-20200827194710-b269163b24ab github.com/jinzhu/copier v0.4.0 github.com/mr-tron/base58 v1.2.0 + github.com/nats-io/nats-server/v2 v2.10.17 + github.com/nats-io/nats.go v1.36.0 github.com/ory/dockertest/v3 v3.11.0 github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2 github.com/pkg/errors v0.9.1 + github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 github.com/prometheus/client_golang v1.20.5 github.com/qmuntal/stateless v1.7.1 github.com/semrush/zenrpc/v2 v2.1.1 @@ -83,10 +86,14 @@ require ( github.com/klauspost/compress v1.17.9 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect + github.com/minio/highwayhash v1.0.2 // indirect github.com/mmcloughlin/addchain v0.4.0 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect github.com/moby/term v0.5.0 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/nats-io/jwt/v2 v2.5.7 // indirect + github.com/nats-io/nkeys v0.4.7 // indirect + github.com/nats-io/nuid v1.0.1 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.0 // indirect github.com/opencontainers/runc v1.1.14 // indirect @@ -109,6 +116,7 @@ require ( golang.org/x/net v0.33.0 // indirect golang.org/x/term v0.27.0 // indirect golang.org/x/text v0.21.0 // indirect + golang.org/x/time v0.5.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index b7a40153b..9d165a3a1 100644 --- a/go.sum +++ b/go.sum @@ -183,6 +183,8 @@ github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/minio/highwayhash v1.0.2 h1:Aak5U0nElisjDCfPSG79Tgzkn2gl66NxOMspRrKnA/g= +github.com/minio/highwayhash v1.0.2/go.mod h1:BQskDq+xkJ12lmlUUi7U0M5Swg3EWR+dLTk+kldvVxY= github.com/mmcloughlin/addchain v0.4.0 h1:SobOdjm2xLj1KkXN5/n0xTIWyZA2+s99UCY1iPfkHRY= github.com/mmcloughlin/addchain v0.4.0/go.mod h1:A86O+tHqZLMNO4w6ZZ4FlVQEadcoqkyU72HC5wJ4RlU= github.com/mmcloughlin/profile v0.1.1/go.mod h1:IhHD7q1ooxgwTgjxQYkACGA77oFTDdFVejUS1/tS/qU= @@ -199,6 +201,16 @@ github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjW github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/nats-io/jwt/v2 v2.5.7 h1:j5lH1fUXCnJnY8SsQeB/a/z9Azgu2bYIDvtPVNdxe2c= +github.com/nats-io/jwt/v2 v2.5.7/go.mod h1:ZdWS1nZa6WMZfFwwgpEaqBV8EPGVgOTDHN/wTbz0Y5A= +github.com/nats-io/nats-server/v2 v2.10.17 h1:PTVObNBD3TZSNUDgzFb1qQsQX4mOgFmOuG9vhT+KBUY= +github.com/nats-io/nats-server/v2 v2.10.17/go.mod h1:5OUyc4zg42s/p2i92zbbqXvUNsbF0ivdTLKshVMn2YQ= +github.com/nats-io/nats.go v1.36.0 h1:suEUPuWzTSse/XhESwqLxXGuj8vGRuPRoG7MoRN/qyU= +github.com/nats-io/nats.go v1.36.0/go.mod h1:Ubdu4Nh9exXdSz0RVWRFBbRfrbSxOYd26oF0wkWclB8= +github.com/nats-io/nkeys v0.4.7 h1:RwNJbbIdYCoClSDNY7QVKZlyb/wfT6ugvFCiKy6vDvI= +github.com/nats-io/nkeys v0.4.7/go.mod h1:kqXRgRDPlGy7nGaEDMuYzmiJCIAAWDK0IMBtDmGD0nc= +github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= +github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= @@ -225,6 +237,8 @@ github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= @@ -399,6 +413,7 @@ golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190130150945-aca44879d564/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -435,6 +450,8 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= +golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= diff --git a/pkg/blockchaininfo/blockchaininfo.go b/pkg/blockchaininfo/blockchaininfo.go new file mode 100644 index 000000000..e73128d11 --- /dev/null +++ b/pkg/blockchaininfo/blockchaininfo.go @@ -0,0 +1,88 @@ +package blockchaininfo + +import ( + "bytes" + "encoding/binary" + "errors" + "fmt" + "strconv" + "strings" + + "github.com/wavesplatform/gowaves/pkg/proto" +) + +const EpochKeyPrefix = "epoch_" +const blockMeta0xKeyPrefix = "block_0x" + +// Helper function to read uint64 from bytes. +func readInt64(data *bytes.Reader) int64 { + var num int64 + err := binary.Read(data, binary.BigEndian, &num) + if err != nil { + panic(fmt.Sprintf("Failed to read uint64: %v", err)) + } + return num +} + +// Decode base64 and extract blockHeight and height. +func extractEpochFromBlockMeta(metaBlockValueBytes []byte) int64 { + // Create a bytes reader for easier parsing. + reader := bytes.NewReader(metaBlockValueBytes) + + // Extract blockHeight and epoch. + readInt64(reader) + epoch := readInt64(reader) + + return epoch +} + +func filterDataEntries(beforeHeight uint64, dataEntries []proto.DataEntry) ([]proto.DataEntry, error) { + var filteredDataEntries []proto.DataEntry + + for _, entry := range dataEntries { + key := entry.GetKey() + + switch { + // Filter "epoch_" prefixed keys. + case strings.HasPrefix(key, EpochKeyPrefix): + // Extract the numeric part after "epoch_" + epochStr := key[len(EpochKeyPrefix):] + + // Convert the epoch number to uint64. + epochNumber, err := strconv.ParseUint(epochStr, 10, 64) + if err != nil { + return nil, err + } + + // Compare epoch number with beforeHeight. + if epochNumber > beforeHeight { + // Add to filtered list if epochNumber is greater. + filteredDataEntries = append(filteredDataEntries, entry) + } + + // Filter block_0x binary entries. + case strings.HasPrefix(key, blockMeta0xKeyPrefix): + // Extract blockHeight and height from base64. + binaryEntry, ok := entry.(*proto.BinaryDataEntry) + if !ok { + return nil, errors.New("failed to convert block meta key to binary data entry") + } + epoch := extractEpochFromBlockMeta(binaryEntry.Value) + + if epoch < 0 { + return nil, errors.New("epoch is less than 0") + } + // Compare height with beforeHeight. + if uint64(epoch) > beforeHeight { + // Add to filtered list if height is less than beforeHeight. + filteredDataEntries = append(filteredDataEntries, entry) + } + + // Default case to handle non-epoch and non-base64 entries. + default: + filteredDataEntries = append(filteredDataEntries, entry) + } + } + + return filteredDataEntries, nil +} diff --git a/pkg/blockchaininfo/blockchaininfo_test.go b/pkg/blockchaininfo/blockchaininfo_test.go new file mode 100644 index 000000000..c88fd7e3a --- /dev/null +++ b/pkg/blockchaininfo/blockchaininfo_test.go @@ -0,0 +1,255 @@ +package blockchaininfo_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + "github.com/wavesplatform/gowaves/pkg/blockchaininfo" + "github.com/wavesplatform/gowaves/pkg/proto" +) + +// some random test data. +func testBlockUpdates() blockchaininfo.BlockUpdatesInfo { + var b blockchaininfo.BlockUpdatesInfo + + var ( + height uint64 = 100 + vrf = proto.B58Bytes{} + blockID = proto.BlockID{} + blockHeader = proto.BlockHeader{} + ) + + b.Height = height + b.VRF = vrf + b.BlockID = blockID + b.BlockHeader = blockHeader + + return b +} + +func containsDataEntry(changes []proto.DataEntry, key string, dataType string) bool { + for _, entry := range changes { + // Check if the key matches + if entry.GetKey() == key { + // Use a type switch to check the type + switch entry.(type) { + case *proto.BinaryDataEntry: + if dataType == "binary" { + return true + } + case *proto.DeleteDataEntry: + if dataType == "delete" { + return true + } + default: + } + } + } + return false +} + +// This tests check whether the changes generation will show the new records and will remove the old ones. +// Previous state contains 3 records, but the current state doesn't contain them and has 3 new records. +// The change result must be - 3 new records, and 3 old records for deletion. + +func TestChangesGenerationNewEntries(t *testing.T) { + previousFirstKey := "block_0x3a85dedc42db076c91cf61d72fa17c80777aeed70ba68dbc14d6829dd6e88614" + previousSecondKey := "block_0x3a9209ce524553a75fd0e9bde5c99ff254b1fb231916fc89755be957e51e5516" + previousThirdKey := "block_0x3b0181d3f66d9f0ddd8e1e8567b836a01f652b4cb873aa7b7c46fc8bd1e4eeee" + + previousDataEntries := []proto.DataEntry{ + &proto.BinaryDataEntry{Key: previousFirstKey, + Value: []byte("base64:AAAAAAAQYVwAAAAAADDSCpJJsd11jrOMW7AS/AHIMIDQ" + + "XjqmFyhDuGt2RPNvmcCXAVTy/URmfMOj7GNweXnZpzidmxHfPBfcP5A=")}, // height 3199498. + &proto.BinaryDataEntry{Key: previousSecondKey, + Value: []byte("base64:AAAAAAAQYywAAAAAADDSQBBubtiRmKwtaNFF1TrBhsfBu" + + "61fj3qiSrtyu1/kLLAlAVQp5GtuF7Hxji8CQ9SFOEZLLUv88nvIgg8=")}, // height 3199552. + &proto.BinaryDataEntry{Key: previousThirdKey, + Value: []byte("base64:AAAAAAAQZEUAAAAAADDSZeUUyashoWjUKurzA/wVU5prm" + + "68CambvjIo1ESLoLuAaAVRaS4vOsPl9cxvg7aeRj7RFZQzdpmvV/+A=")}, // height 3199589. + } + var previousHeight uint64 = 3199552 + + currentFirstKey := "block_0x3b5ad5c176473be02cc3d19207204af87af03f6fd75c76916765745658f7e842" + currentSecondKey := "block_0x3b72ee917fea7057fb88a357f619c22f6f8ddae03b701fab7c284953ecebbc8c" + currentThirdKey := "block_0x3b973acae11f248a524b463db7d198c7ddb47fd8aeda2f14699e639a0db19911" + + currentDataEntries := []proto.DataEntry{ + &proto.BinaryDataEntry{Key: currentFirstKey, + Value: []byte("base64:AAAAAAAQZKkAAAAAADDSb6xEaq4RsFQruG" + + "NeGdooPmtLBnlERR15qzc/mcKcQ461AVQp5GtuF7Hxji8CQ9SFOEZLLUv88nvIgg8=")}, // height 3199599. + &proto.BinaryDataEntry{Key: currentSecondKey, + Value: []byte("base64:AAAAAAAQZQkAAAAAADDSe+2CGv9zgiR7s" + + "65XEBkYzIbv6jbxcR7Zi3ByUqsX0bkwAVTEkyC5glOJH8Upe49iT3+BUV5zRaDT2dM=")}, // height 3199611. + &proto.BinaryDataEntry{Key: currentThirdKey, + Value: []byte("base64:AAAAAAAQZf8AAAAAADDSolzqc5gjHWP/s" + + "CzqK7+HkAjybjGxq8SxL9ID8yEIKxrlAVRN71D/MD4dykS8vqW7cXqCh5QOclg6DEU=")}, // height 3199650. + } + var currentHeight uint64 = 3199611 + + previousBlockInfo := blockchaininfo.BUpdatesInfo{ + BlockUpdatesInfo: testBlockUpdates(), + ContractUpdatesInfo: blockchaininfo.L2ContractDataEntries{ + AllDataEntries: previousDataEntries, + Height: previousHeight, + }, + } + + currentBlockInfo := blockchaininfo.BUpdatesInfo{ + BlockUpdatesInfo: testBlockUpdates(), + ContractUpdatesInfo: blockchaininfo.L2ContractDataEntries{ + AllDataEntries: currentDataEntries, + Height: currentHeight, + }, + } + + equal, changes, err := blockchaininfo.CompareBUpdatesInfo(currentBlockInfo, previousBlockInfo, + proto.TestNetScheme, 10) + if err != nil { + return + } + require.False(t, equal) + require.True(t, containsDataEntry(changes.ContractUpdatesInfo.AllDataEntries, currentFirstKey, "binary")) + require.True(t, containsDataEntry(changes.ContractUpdatesInfo.AllDataEntries, currentSecondKey, "binary")) + require.True(t, containsDataEntry(changes.ContractUpdatesInfo.AllDataEntries, currentThirdKey, "binary")) + + require.True(t, containsDataEntry(changes.ContractUpdatesInfo.AllDataEntries, previousFirstKey, "delete")) + require.True(t, containsDataEntry(changes.ContractUpdatesInfo.AllDataEntries, previousSecondKey, "delete")) + require.True(t, containsDataEntry(changes.ContractUpdatesInfo.AllDataEntries, previousThirdKey, "delete")) +} + +// This tests check whether the changes generation will only show the new records and will not remove the old ones. +// Previous state contains 3 records, the current state contains both the previous new records and 3 new ones. +// The change result must be - 3 new records. +func TestChangesGenerationContainsPrevious(t *testing.T) { + previousFirstKey := "block_0x3a85dedc42db076c91cf61d72fa17c80777aeed70ba68dbc14d6829dd6e88614" + previousSecondKey := "block_0x3a9209ce524553a75fd0e9bde5c99ff254b1fb231916fc89755be957e51e5516" + previousThirdKey := "block_0x3b0181d3f66d9f0ddd8e1e8567b836a01f652b4cb873aa7b7c46fc8bd1e4eeee" + + previousDataEntries := []proto.DataEntry{ + &proto.BinaryDataEntry{Key: previousFirstKey, + Value: []byte("base64:AAAAAAAQYVwAAAAAADDSCpJJsd11jrOMW7AS/AHIMIDQXj" + + "qmFyhDuGt2RPNvmcCXAVTy/URmfMOj7GNweXnZpzidmxHfPBfcP5A=")}, // height 3199498. + &proto.BinaryDataEntry{Key: previousSecondKey, + Value: []byte("base64:AAAAAAAQYywAAAAAADDSQBBubtiRmKwtaNFF1TrBhsfBu61" + + "fj3qiSrtyu1/kLLAlAVQp5GtuF7Hxji8CQ9SFOEZLLUv88nvIgg8=")}, // height 3199552. + &proto.BinaryDataEntry{Key: previousThirdKey, + Value: []byte("base64:AAAAAAAQZEUAAAAAADDSZeUUyashoWjUKurzA/wVU5prm68Ca" + + "mbvjIo1ESLoLuAaAVRaS4vOsPl9cxvg7aeRj7RFZQzdpmvV/+A=")}, // height 3199589. + } + var previousHeight uint64 = 3199552 + + currentFirstKey := "block_0x3b5ad5c176473be02cc3d19207204af87af03f6fd75c76916765745658f7e842" + currentSecondKey := "block_0x3b72ee917fea7057fb88a357f619c22f6f8ddae03b701fab7c284953ecebbc8c" + currentThirdKey := "block_0x3b973acae11f248a524b463db7d198c7ddb47fd8aeda2f14699e639a0db19911" + + currentDataEntries := []proto.DataEntry{ + &proto.BinaryDataEntry{Key: previousFirstKey, + Value: []byte("base64:AAAAAAAQYVwAAAAAADDSCpJJsd11jrOMW7AS/A" + + "HIMIDQXjqmFyhDuGt2RPNvmcCXAVTy/URmfMOj7GNweXnZpzidmxHfPBfcP5A=")}, // height 3199498. + &proto.BinaryDataEntry{Key: previousSecondKey, + Value: []byte("base64:AAAAAAAQYywAAAAAADDSQBBubtiRmKwtaNFF1T" + + "rBhsfBu61fj3qiSrtyu1/kLLAlAVQp5GtuF7Hxji8CQ9SFOEZLLUv88nvIgg8=")}, // height 3199552. + &proto.BinaryDataEntry{Key: previousThirdKey, + Value: []byte("base64:AAAAAAAQZEUAAAAAADDSZeUUyashoWjUKurzA/wV" + + "U5prm68CambvjIo1ESLoLuAaAVRaS4vOsPl9cxvg7aeRj7RFZQzdpmvV/+A=")}, // height 3199589. + + &proto.BinaryDataEntry{Key: currentFirstKey, + Value: []byte("base64:AAAAAAAQZKkAAAAAADDSb6xEaq4RsFQruGNeGdoo" + + "PmtLBnlERR15qzc/mcKcQ461AVQp5GtuF7Hxji8CQ9SFOEZLLUv88nvIgg8=")}, // height 3199599. + &proto.BinaryDataEntry{Key: currentSecondKey, + Value: []byte("base64:AAAAAAAQZQkAAAAAADDSe+2CGv9zgiR7s65XEBkYz" + + "Ibv6jbxcR7Zi3ByUqsX0bkwAVTEkyC5glOJH8Upe49iT3+BUV5zRaDT2dM=")}, // height 3199611. + &proto.BinaryDataEntry{Key: currentThirdKey, + Value: []byte("base64:AAAAAAAQZf8AAAAAADDSolzqc5gjHWP/sCzqK7+Hk" + + "AjybjGxq8SxL9ID8yEIKxrlAVRN71D/MD4dykS8vqW7cXqCh5QOclg6DEU=")}, // height 3199650. + } + var currentHeight uint64 = 3199611 + + previousBlockInfo := blockchaininfo.BUpdatesInfo{ + BlockUpdatesInfo: testBlockUpdates(), + ContractUpdatesInfo: blockchaininfo.L2ContractDataEntries{ + AllDataEntries: previousDataEntries, + Height: previousHeight, + }, + } + + currentBlockInfo := blockchaininfo.BUpdatesInfo{ + BlockUpdatesInfo: testBlockUpdates(), + ContractUpdatesInfo: blockchaininfo.L2ContractDataEntries{ + AllDataEntries: currentDataEntries, + Height: currentHeight, + }, + } + + equal, changes, err := blockchaininfo.CompareBUpdatesInfo(currentBlockInfo, previousBlockInfo, + proto.TestNetScheme, 10) + if err != nil { + return + } + require.False(t, equal) + + require.True(t, containsDataEntry(changes.ContractUpdatesInfo.AllDataEntries, currentFirstKey, "binary")) + require.True(t, containsDataEntry(changes.ContractUpdatesInfo.AllDataEntries, currentSecondKey, "binary")) + require.True(t, containsDataEntry(changes.ContractUpdatesInfo.AllDataEntries, currentThirdKey, "binary")) +} + +// This tests check whether the changes generation will not show anything, because there are no changes. +// Previous state contains 3 records, the current state contains the same records. +// The change result must be - 0 records. +func TestNoChangesGeneration(t *testing.T) { + previousFirstKey := "block_0x3a85dedc42db076c91cf61d72fa17c80777aeed70ba68dbc14d6829dd6e88614" + previousSecondKey := "block_0x3a9209ce524553a75fd0e9bde5c99ff254b1fb231916fc89755be957e51e5516" + previousThirdKey := "block_0x3b0181d3f66d9f0ddd8e1e8567b836a01f652b4cb873aa7b7c46fc8bd1e4eeee" + + previousDataEntries := []proto.DataEntry{ + &proto.BinaryDataEntry{Key: previousFirstKey, + Value: []byte("base64:AAAAAAAQYVwAAAAAADDSCpJJsd11jrO" + + "MW7AS/AHIMIDQXjqmFyhDuGt2RPNvmcCXAVTy/URmfMOj7GNweXnZpzidmxHfPBfcP5A=")}, // height 3199498. + &proto.BinaryDataEntry{Key: previousSecondKey, + Value: []byte("base64:AAAAAAAQYywAAAAAADDSQBBubtiRmKwt" + + "aNFF1TrBhsfBu61fj3qiSrtyu1/kLLAlAVQp5GtuF7Hxji8CQ9SFOEZLLUv88nvIgg8=")}, // height 3199552. + &proto.BinaryDataEntry{Key: previousThirdKey, + Value: []byte("base64:AAAAAAAQZEUAAAAAADDSZeUUyashoWjU" + + "KurzA/wVU5prm68CambvjIo1ESLoLuAaAVRaS4vOsPl9cxvg7aeRj7RFZQzdpmvV/+A=")}, // height 3199589. + } + var previousHeight uint64 = 3199552 + + currentDataEntries := []proto.DataEntry{ + &proto.BinaryDataEntry{Key: previousFirstKey, + Value: []byte("base64:AAAAAAAQYVwAAAAAADDSCpJJsd11jrO" + + "MW7AS/AHIMIDQXjqmFyhDuGt2RPNvmcCXAVTy/URmfMOj7GNweXnZpzidmxHfPBfcP5A=")}, // height 3199498. + &proto.BinaryDataEntry{Key: previousSecondKey, + Value: []byte("base64:AAAAAAAQYywAAAAAADDSQBBubtiRmKwta" + + "NFF1TrBhsfBu61fj3qiSrtyu1/kLLAlAVQp5GtuF7Hxji8CQ9SFOEZLLUv88nvIgg8=")}, // height 3199552. + &proto.BinaryDataEntry{Key: previousThirdKey, + Value: []byte("base64:AAAAAAAQZEUAAAAAADDSZeUUyashoWjU" + + "KurzA/wVU5prm68CambvjIo1ESLoLuAaAVRaS4vOsPl9cxvg7aeRj7RFZQzdpmvV/+A=")}, // height 3199589. + } + var currentHeight uint64 = 3199611 + + previousBlockInfo := blockchaininfo.BUpdatesInfo{ + BlockUpdatesInfo: testBlockUpdates(), + ContractUpdatesInfo: blockchaininfo.L2ContractDataEntries{ + AllDataEntries: previousDataEntries, + Height: previousHeight, + }, + } + + currentBlockInfo := blockchaininfo.BUpdatesInfo{ + BlockUpdatesInfo: testBlockUpdates(), + ContractUpdatesInfo: blockchaininfo.L2ContractDataEntries{ + AllDataEntries: currentDataEntries, + Height: currentHeight, + }, + } + + equal, changes, err := blockchaininfo.CompareBUpdatesInfo(currentBlockInfo, previousBlockInfo, + proto.TestNetScheme, 10) + if err != nil { + return + } + require.True(t, equal) + + require.True(t, len(changes.ContractUpdatesInfo.AllDataEntries) == 0) +} diff --git a/pkg/blockchaininfo/bupdates.go b/pkg/blockchaininfo/bupdates.go new file mode 100644 index 000000000..56bfcaeb2 --- /dev/null +++ b/pkg/blockchaininfo/bupdates.go @@ -0,0 +1,55 @@ +package blockchaininfo + +import ( + "context" + + "github.com/wavesplatform/gowaves/pkg/proto" +) + +type BlockchainUpdatesExtension struct { + ctx context.Context + enableBlockchainUpdatesPlugin bool + l2ContractAddress proto.WavesAddress + bUpdatesChannel chan<- BUpdatesInfo +} + +func NewBlockchainUpdatesExtension( + ctx context.Context, + l2ContractAddress proto.WavesAddress, + bUpdatesChannel chan<- BUpdatesInfo, +) *BlockchainUpdatesExtension { + return &BlockchainUpdatesExtension{ + ctx: ctx, + enableBlockchainUpdatesPlugin: true, + l2ContractAddress: l2ContractAddress, + bUpdatesChannel: bUpdatesChannel, + } +} + +func (e *BlockchainUpdatesExtension) EnableBlockchainUpdatesPlugin() bool { + return e != nil && e.enableBlockchainUpdatesPlugin +} + +func (e *BlockchainUpdatesExtension) L2ContractAddress() proto.WavesAddress { + return e.l2ContractAddress +} + +func (e *BlockchainUpdatesExtension) WriteBUpdates(bUpdates BUpdatesInfo) { + if e.bUpdatesChannel == nil { + return + } + select { + case e.bUpdatesChannel <- bUpdates: + case <-e.ctx.Done(): + e.close() + return + } +} + +func (e *BlockchainUpdatesExtension) close() { + if e.bUpdatesChannel == nil { + return + } + close(e.bUpdatesChannel) + e.bUpdatesChannel = nil +} diff --git a/pkg/blockchaininfo/nats_publisher.go b/pkg/blockchaininfo/nats_publisher.go new file mode 100644 index 000000000..701dc6ce8 --- /dev/null +++ b/pkg/blockchaininfo/nats_publisher.go @@ -0,0 +1,241 @@ +package blockchaininfo + +import ( + "context" + "log" + "time" + + "github.com/nats-io/nats-server/v2/server" + "github.com/nats-io/nats.go" + "github.com/pkg/errors" + + "github.com/wavesplatform/gowaves/pkg/proto" +) + +const StoreBlocksLimit = 200 +const PortDefault = 4222 +const HostDefault = "127.0.0.1" +const ConnectionsTimeoutDefault = 5 * server.AUTH_TIMEOUT + +const NatsMaxPayloadSize int32 = 1024 * 1024 // 1 MB + +const PublisherWaitingTime = 100 * time.Millisecond + +const ( + StartPaging = iota + EndPaging + NoPaging +) + +type BUpdatesExtensionState struct { + currentState *BUpdatesInfo + previousState *BUpdatesInfo // this information is what was just published + Limit uint64 + scheme proto.Scheme +} + +func NewBUpdatesExtensionState(limit uint64, scheme proto.Scheme) *BUpdatesExtensionState { + return &BUpdatesExtensionState{Limit: limit, scheme: scheme} +} + +func (bu *BUpdatesExtensionState) hasStateChanged() (bool, BUpdatesInfo, error) { + statesAreEqual, changes, err := statesEqual(*bu, bu.scheme) + if err != nil { + return false, BUpdatesInfo{}, err + } + if statesAreEqual { + return false, BUpdatesInfo{}, nil + } + return true, changes, nil +} + +func splitIntoChunks(array []byte, maxChunkSize int) [][]byte { + if maxChunkSize <= 0 { + return nil + } + var chunkedArray [][]byte + + for i := 0; i < len(array); i += maxChunkSize { + end := i + maxChunkSize + if end > len(array) { + end = len(array) + } + chunkedArray = append(chunkedArray, array[i:end]) + } + + return chunkedArray +} + +func (bu *BUpdatesExtensionState) publishContractUpdates(contractUpdates L2ContractDataEntries, nc *nats.Conn) error { + dataEntriesProtobuf, err := L2ContractDataEntriesToProto(contractUpdates).MarshalVTStrict() + if err != nil { + return err + } + + if len(dataEntriesProtobuf) <= int(NatsMaxPayloadSize-1) { + var msg []byte + msg = append(msg, NoPaging) + msg = append(msg, dataEntriesProtobuf...) + err = nc.Publish(ContractUpdates, msg) + if err != nil { + log.Printf("failed to publish message on topic %s", ContractUpdates) + return err + } + log.Printf("Published on topic: %s\n", ContractUpdates) + return nil + } + + chunkedPayload := splitIntoChunks(dataEntriesProtobuf, int(NatsMaxPayloadSize-1)/2) + + for i, chunk := range chunkedPayload { + var msg []byte + + if i == len(chunkedPayload)-1 { + msg = append(msg, EndPaging) + msg = append(msg, chunk...) + err = nc.Publish(ContractUpdates, msg) + if err != nil { + log.Printf("failed to publish message on topic %s", ContractUpdates) + return err + } + log.Printf("Published on topic: %s\n", ContractUpdates) + break + } + msg = append(msg, StartPaging) + msg = append(msg, chunk...) + err = nc.Publish(ContractUpdates, msg) + if err != nil { + log.Printf("failed to publish message on topic %s", ContractUpdates) + return err + } + log.Printf("Published on topic: %s\n", ContractUpdates) + time.Sleep(PublisherWaitingTime) + } + + return nil +} + +func (bu *BUpdatesExtensionState) publishBlockUpdates(updates BUpdatesInfo, nc *nats.Conn, scheme proto.Scheme) error { + blockInfo, err := BUpdatesInfoToProto(updates, scheme) + if err != nil { + return err + } + blockInfoProtobuf, err := blockInfo.MarshalVTStrict() + if err != nil { + return err + } + err = nc.Publish(BlockUpdates, blockInfoProtobuf) + if err != nil { + log.Printf("failed to publish message on topic %s", BlockUpdates) + return err + } + log.Printf("Published on topic: %s\n", BlockUpdates) + return nil +} + +func (bu *BUpdatesExtensionState) publishUpdates(updates BUpdatesInfo, nc *nats.Conn, scheme proto.Scheme) error { + /* first publish block data */ + err := bu.publishBlockUpdates(updates, nc, scheme) + if err != nil { + log.Printf("failed to publish message on topic %s", BlockUpdates) + return err + } + + /* second publish contract data entries */ + if updates.ContractUpdatesInfo.AllDataEntries != nil { + pblshErr := bu.publishContractUpdates(updates.ContractUpdatesInfo, nc) + if pblshErr != nil { + log.Printf("failed to publish message on topic %s", ContractUpdates) + return pblshErr + } + log.Printf("Published on topic: %s\n", ContractUpdates) + } + + return nil +} + +func handleBlockchainUpdate(updates BUpdatesInfo, bu *BUpdatesExtensionState, scheme proto.Scheme, nc *nats.Conn) { + // update current state + bu.currentState = &updates + if bu.previousState == nil { + // publish initial updates + + filteredDataEntries, err := filterDataEntries(updates.BlockUpdatesInfo.Height-bu.Limit, + updates.ContractUpdatesInfo.AllDataEntries) + if err != nil { + return + } + updates.ContractUpdatesInfo.AllDataEntries = filteredDataEntries + pblshErr := bu.publishUpdates(updates, nc, scheme) + if pblshErr != nil { + log.Printf("failed to publish updates, %v", pblshErr) + return + } + bu.previousState = &updates + return + } + // compare the current state to the previous state + stateChanged, changes, cmprErr := bu.hasStateChanged() + if cmprErr != nil { + log.Printf("failed to compare current and previous states, %v", cmprErr) + return + } + // if there is any diff, send the update + if stateChanged { + pblshErr := bu.publishUpdates(changes, nc, scheme) + log.Printf("published changes") + if pblshErr != nil { + log.Printf("failed to publish changes, %v", pblshErr) + } + bu.previousState = &updates + } +} + +func runPublisher(ctx context.Context, updatesChannel <-chan BUpdatesInfo, + bu *BUpdatesExtensionState, scheme proto.Scheme, nc *nats.Conn) { + for { + select { + case updates, ok := <-updatesChannel: + if !ok { + log.Printf("the updates channel for publisher was closed") + return + } + handleBlockchainUpdate(updates, bu, scheme, nc) + case <-ctx.Done(): + return + } + } +} + +func (bu *BUpdatesExtensionState) RunBlockchainUpdatesPublisher(ctx context.Context, + updatesChannel <-chan BUpdatesInfo, scheme proto.Scheme) error { + opts := &server.Options{ + MaxPayload: NatsMaxPayloadSize, + Host: HostDefault, + Port: PortDefault, + NoSigs: true, + } + s, err := server.NewServer(opts) + if err != nil { + return errors.Wrap(err, "failed to create NATS server") + } + go s.Start() + defer func() { + s.Shutdown() + s.WaitForShutdown() + }() + if !s.ReadyForConnections(ConnectionsTimeoutDefault) { + return errors.New("NATS server is not ready for connections") + } + + log.Printf("NATS Server is running on port %d", PortDefault) + + nc, err := nats.Connect(nats.DefaultURL) + if err != nil { + return errors.Wrap(err, "failed to connect to NATS server") + } + defer nc.Close() + + runPublisher(ctx, updatesChannel, bu, scheme, nc) + return nil +} diff --git a/pkg/blockchaininfo/nats_server.go b/pkg/blockchaininfo/nats_server.go new file mode 100644 index 000000000..75d0f6481 --- /dev/null +++ b/pkg/blockchaininfo/nats_server.go @@ -0,0 +1,29 @@ +package blockchaininfo + +import ( + "log" + + "github.com/nats-io/nats-server/v2/server" +) + +func RunBlockchainUpdatesServer() { + opts := &server.Options{ + Host: HostDefault, + Port: PortDefault, + } + s, err := server.NewServer(opts) + if err != nil { + log.Fatalf("failed to create NATS server: %v", err) + } + + go s.Start() + + if !s.ReadyForConnections(ConnectionsTimeoutDefault) { + log.Fatal("NATS Server not ready for connections") + } + + log.Println("NATS Server is running on port 4222") + + // Block main goroutine so the server keeps running + select {} +} diff --git a/pkg/blockchaininfo/proto_converters.go b/pkg/blockchaininfo/proto_converters.go new file mode 100644 index 000000000..728b84042 --- /dev/null +++ b/pkg/blockchaininfo/proto_converters.go @@ -0,0 +1,82 @@ +package blockchaininfo + +import ( + "github.com/pkg/errors" + + "github.com/wavesplatform/gowaves/pkg/grpc/generated/waves" + g "github.com/wavesplatform/gowaves/pkg/grpc/l2/blockchain_info" + "github.com/wavesplatform/gowaves/pkg/proto" +) + +func BUpdatesInfoToProto(blockInfo BUpdatesInfo, scheme proto.Scheme) (*g.BlockInfo, error) { + var ( + blockHeader *waves.Block_Header + err error + ) + + blockHeader, err = blockInfo.BlockUpdatesInfo.BlockHeader.HeaderToProtobufHeader(scheme) + if err != nil { + return nil, err + } + return &g.BlockInfo{ + Height: blockInfo.BlockUpdatesInfo.Height, + VRF: blockInfo.BlockUpdatesInfo.VRF, + BlockID: blockInfo.BlockUpdatesInfo.BlockID.Bytes(), + BlockHeader: blockHeader, + }, nil +} + +func BUpdatesInfoFromProto(blockInfoProto *g.BlockInfo) (BlockUpdatesInfo, error) { + if blockInfoProto == nil { + return BlockUpdatesInfo{}, errors.New("empty block info") + } + blockID, err := proto.NewBlockIDFromBytes(blockInfoProto.BlockID) + if err != nil { + return BlockUpdatesInfo{}, errors.Wrap(err, "failed to convert block ID") + } + var c proto.ProtobufConverter + blockHeader, err := c.PartialBlockHeader(blockInfoProto.BlockHeader) + if err != nil { + return BlockUpdatesInfo{}, errors.Wrap(err, "failed to convert block header") + } + blockHeader.ID = blockID // Set block ID to the one from the protobuf. + vrf := proto.B58Bytes(blockInfoProto.VRF) + return BlockUpdatesInfo{ + Height: blockInfoProto.Height, + VRF: vrf, + BlockID: blockID, + BlockHeader: blockHeader, + }, nil +} + +func L2ContractDataEntriesToProto(contractData L2ContractDataEntries) *g.L2ContractDataEntries { + var protobufDataEntries []*waves.DataEntry + for _, entry := range contractData.AllDataEntries { + entryProto := entry.ToProtobuf() + protobufDataEntries = append(protobufDataEntries, entryProto) + } + return &g.L2ContractDataEntries{ + DataEntries: protobufDataEntries, + Height: contractData.Height, + } +} + +func L2ContractDataEntriesFromProto( + protoDataEntries *g.L2ContractDataEntries, + scheme proto.Scheme, +) (L2ContractDataEntries, error) { + if protoDataEntries == nil { + return L2ContractDataEntries{}, errors.New("empty contract data") + } + converter := proto.ProtobufConverter{FallbackChainID: scheme} + dataEntries := make([]proto.DataEntry, 0, len(protoDataEntries.DataEntries)) + for _, protoEntry := range protoDataEntries.DataEntries { + entry, err := converter.Entry(protoEntry) + if err != nil { + return L2ContractDataEntries{}, errors.Wrap(err, "failed to convert data entry") + } + dataEntries = append(dataEntries, entry) + } + + return L2ContractDataEntries{AllDataEntries: dataEntries, Height: protoDataEntries.Height}, nil +} diff --git a/pkg/blockchaininfo/topics.go b/pkg/blockchaininfo/topics.go new file mode 100644 index 000000000..51bb99b01 --- /dev/null +++ b/pkg/blockchaininfo/topics.go @@ -0,0 +1,8 @@ +package blockchaininfo + +/* Topics. */ +const ( + BlockUpdates = "block_topic" + microblockUpdates = "microblock_topic" + ContractUpdates = "contract_topic" +) diff --git a/pkg/blockchaininfo/types.go b/pkg/blockchaininfo/types.go new file mode 100644 index 000000000..1568a6990 --- /dev/null +++ b/pkg/blockchaininfo/types.go @@ -0,0 +1,156 @@ +package blockchaininfo + +import ( + "bytes" + + "github.com/wavesplatform/gowaves/pkg/proto" +) + +// BlockUpdatesInfo Block updates. +type BlockUpdatesInfo struct { + Height uint64 `json:"height"` + VRF proto.B58Bytes `json:"vrf"` + BlockID proto.BlockID `json:"block_id"` + BlockHeader proto.BlockHeader `json:"block_header"` +} + +// L2ContractDataEntries L2 contract data entries. +type L2ContractDataEntries struct { + AllDataEntries []proto.DataEntry `json:"all_data_entries"` + Height uint64 `json:"height"` +} + +type BUpdatesInfo struct { + BlockUpdatesInfo BlockUpdatesInfo + ContractUpdatesInfo L2ContractDataEntries +} + +// TODO wrap errors. + +func CompareBUpdatesInfo(current, previous BUpdatesInfo, + scheme proto.Scheme, heightLimit uint64) (bool, BUpdatesInfo, error) { + changes := BUpdatesInfo{ + BlockUpdatesInfo: BlockUpdatesInfo{}, + ContractUpdatesInfo: L2ContractDataEntries{}, + } + + equal := true + // todo REMOVE POINTERS + if current.BlockUpdatesInfo.Height != previous.BlockUpdatesInfo.Height { + equal = false + changes.BlockUpdatesInfo.Height = current.BlockUpdatesInfo.Height + } + if !bytes.Equal(current.BlockUpdatesInfo.VRF, previous.BlockUpdatesInfo.VRF) { + equal = false + changes.BlockUpdatesInfo.VRF = current.BlockUpdatesInfo.VRF + } + if !bytes.Equal(current.BlockUpdatesInfo.BlockID.Bytes(), previous.BlockUpdatesInfo.BlockID.Bytes()) { + equal = false + changes.BlockUpdatesInfo.BlockID = current.BlockUpdatesInfo.BlockID + } + equalHeaders, err := compareBlockHeader(current.BlockUpdatesInfo.BlockHeader, + previous.BlockUpdatesInfo.BlockHeader, scheme) + if err != nil { + return false, BUpdatesInfo{}, err + } + if !equalHeaders { + equal = false + changes.BlockUpdatesInfo.BlockHeader = current.BlockUpdatesInfo.BlockHeader + } + + previousFilteredDataEntries, err := filterDataEntries(previous.BlockUpdatesInfo.Height-heightLimit, + previous.ContractUpdatesInfo.AllDataEntries) + if err != nil { + return false, BUpdatesInfo{}, err + } + currentFilteredDataEntries, err := filterDataEntries(current.BlockUpdatesInfo.Height-heightLimit, + current.ContractUpdatesInfo.AllDataEntries) + if err != nil { + return false, BUpdatesInfo{}, err + } + + equalEntries, dataEntryChanges, err := compareDataEntries(currentFilteredDataEntries, + previousFilteredDataEntries) + if err != nil { + return false, BUpdatesInfo{}, err + } + if !equalEntries { + equal = false + changes.ContractUpdatesInfo.AllDataEntries = dataEntryChanges + changes.ContractUpdatesInfo.Height = current.BlockUpdatesInfo.Height + } + + return equal, changes, nil +} + +func compareBlockHeader(a, b proto.BlockHeader, scheme proto.Scheme) (bool, error) { + blockAbytes, err := a.MarshalHeader(scheme) + if err != nil { + return false, err + } + + blockBbytes, err := b.MarshalHeader(scheme) + if err != nil { + return false, err + } + + return bytes.Equal(blockAbytes, blockBbytes), nil +} + +func compareDataEntries(current, previous proto.DataEntries) (bool, []proto.DataEntry, error) { + currentMap := make(map[string][]byte) // Data entries. + previousMap := make(map[string][]byte) // Data entries. + + for _, dataEntry := range current { + value, err := dataEntry.MarshalValue() + if err != nil { + return false, nil, err + } + currentMap[dataEntry.GetKey()] = value + } + + for _, dataEntry := range previous { + value, err := dataEntry.MarshalValue() + if err != nil { + return false, nil, err + } + previousMap[dataEntry.GetKey()] = value + } + var changes []proto.DataEntry + + for key, valueCur := range currentMap { + // Existing keys, not found in the previous state. This means that these keys were added. + if valuePrev, found := previousMap[key]; !found { + entryChange, err := proto.NewDataEntryFromValueBytes(valueCur) + if err != nil { + return false, nil, err + } + entryChange.SetKey(key) + changes = append(changes, entryChange) + // Existing keys, found in the previous state, different values. This means that data changed. + } else if !bytes.Equal(valuePrev, valueCur) { + entryChange, err := proto.NewDataEntryFromValueBytes(valueCur) + if err != nil { + return false, nil, err + } + entryChange.SetKey(key) + changes = append(changes, entryChange) + } + } + + // Keys existing in the previous state, not found in the current state. This means that these keys were deleted. + for key := range previousMap { + if _, found := currentMap[key]; !found { + deleteEntry := &proto.DeleteDataEntry{} + deleteEntry.SetKey(key) + changes = append(changes, deleteEntry) + } + } + + equal := changes == nil + return equal, changes, nil +} + +func statesEqual(state BUpdatesExtensionState, scheme proto.Scheme) (bool, BUpdatesInfo, error) { + return CompareBUpdatesInfo(*state.currentState, *state.previousState, scheme, state.Limit) +} diff --git a/pkg/grpc/l2/blockchain_info/blockchain_info.pb.go b/pkg/grpc/l2/blockchain_info/blockchain_info.pb.go new file mode 100644 index 000000000..a01810c98 --- /dev/null +++ b/pkg/grpc/l2/blockchain_info/blockchain_info.pb.go @@ -0,0 +1,260 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.27.1 +// protoc v3.12.4 +// source: blockchain_info.proto + +package blockchain_info + +import ( + waves "github.com/wavesplatform/gowaves/pkg/grpc/generated/waves" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type BlockInfo struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Height uint64 `protobuf:"varint,1,opt,name=Height,proto3" json:"Height,omitempty"` + VRF []byte `protobuf:"bytes,2,opt,name=VRF,proto3" json:"VRF,omitempty"` + BlockID []byte `protobuf:"bytes,3,opt,name=BlockID,proto3" json:"BlockID,omitempty"` + BlockHeader *waves.Block_Header `protobuf:"bytes,4,opt,name=BlockHeader,proto3" json:"BlockHeader,omitempty"` +} + +func (x *BlockInfo) Reset() { + *x = BlockInfo{} + if protoimpl.UnsafeEnabled { + mi := &file_blockchain_info_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *BlockInfo) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*BlockInfo) ProtoMessage() {} + +func (x *BlockInfo) ProtoReflect() protoreflect.Message { + mi := &file_blockchain_info_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use BlockInfo.ProtoReflect.Descriptor instead. +func (*BlockInfo) Descriptor() ([]byte, []int) { + return file_blockchain_info_proto_rawDescGZIP(), []int{0} +} + +func (x *BlockInfo) GetHeight() uint64 { + if x != nil { + return x.Height + } + return 0 +} + +func (x *BlockInfo) GetVRF() []byte { + if x != nil { + return x.VRF + } + return nil +} + +func (x *BlockInfo) GetBlockID() []byte { + if x != nil { + return x.BlockID + } + return nil +} + +func (x *BlockInfo) GetBlockHeader() *waves.Block_Header { + if x != nil { + return x.BlockHeader + } + return nil +} + +type L2ContractDataEntries struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + DataEntries []*waves.DataEntry `protobuf:"bytes,5,rep,name=DataEntries,proto3" json:"DataEntries,omitempty"` + Height uint64 `protobuf:"varint,1,opt,name=Height,proto3" json:"Height,omitempty"` +} + +func (x *L2ContractDataEntries) Reset() { + *x = L2ContractDataEntries{} + if protoimpl.UnsafeEnabled { + mi := &file_blockchain_info_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *L2ContractDataEntries) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*L2ContractDataEntries) ProtoMessage() {} + +func (x *L2ContractDataEntries) ProtoReflect() protoreflect.Message { + mi := &file_blockchain_info_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use L2ContractDataEntries.ProtoReflect.Descriptor instead. +func (*L2ContractDataEntries) Descriptor() ([]byte, []int) { + return file_blockchain_info_proto_rawDescGZIP(), []int{1} +} + +func (x *L2ContractDataEntries) GetDataEntries() []*waves.DataEntry { + if x != nil { + return x.DataEntries + } + return nil +} + +func (x *L2ContractDataEntries) GetHeight() uint64 { + if x != nil { + return x.Height + } + return 0 +} + +var File_blockchain_info_proto protoreflect.FileDescriptor + +var file_blockchain_info_proto_rawDesc = []byte{ + 0x0a, 0x15, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x5f, 0x69, 0x6e, 0x66, + 0x6f, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0f, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x63, 0x68, + 0x61, 0x69, 0x6e, 0x5f, 0x69, 0x6e, 0x66, 0x6f, 0x1a, 0x17, 0x77, 0x61, 0x76, 0x65, 0x73, 0x2f, + 0x74, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x1a, 0x11, 0x77, 0x61, 0x76, 0x65, 0x73, 0x2f, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x86, 0x01, 0x0a, 0x09, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x49, 0x6e, + 0x66, 0x6f, 0x12, 0x16, 0x0a, 0x06, 0x48, 0x65, 0x69, 0x67, 0x68, 0x74, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x04, 0x52, 0x06, 0x48, 0x65, 0x69, 0x67, 0x68, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x56, 0x52, + 0x46, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x03, 0x56, 0x52, 0x46, 0x12, 0x18, 0x0a, 0x07, + 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x49, 0x44, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x42, + 0x6c, 0x6f, 0x63, 0x6b, 0x49, 0x44, 0x12, 0x35, 0x0a, 0x0b, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x48, + 0x65, 0x61, 0x64, 0x65, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x77, 0x61, + 0x76, 0x65, 0x73, 0x2e, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x2e, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, + 0x52, 0x0b, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x22, 0x63, 0x0a, + 0x15, 0x4c, 0x32, 0x43, 0x6f, 0x6e, 0x74, 0x72, 0x61, 0x63, 0x74, 0x44, 0x61, 0x74, 0x61, 0x45, + 0x6e, 0x74, 0x72, 0x69, 0x65, 0x73, 0x12, 0x32, 0x0a, 0x0b, 0x44, 0x61, 0x74, 0x61, 0x45, 0x6e, + 0x74, 0x72, 0x69, 0x65, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x77, 0x61, + 0x76, 0x65, 0x73, 0x2e, 0x44, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0b, 0x44, + 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x69, 0x65, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x48, 0x65, + 0x69, 0x67, 0x68, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x48, 0x65, 0x69, 0x67, + 0x68, 0x74, 0x42, 0x62, 0x0a, 0x1a, 0x63, 0x6f, 0x6d, 0x2e, 0x77, 0x61, 0x76, 0x65, 0x73, 0x70, + 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, + 0x5a, 0x3c, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x77, 0x61, 0x76, + 0x65, 0x73, 0x70, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x2f, 0x67, 0x6f, 0x77, 0x61, 0x76, + 0x65, 0x73, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x67, 0x72, 0x70, 0x63, 0x2f, 0x6c, 0x32, 0x2f, 0x62, + 0x6c, 0x6f, 0x63, 0x6b, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x5f, 0x69, 0x6e, 0x66, 0x6f, 0xaa, 0x02, + 0x05, 0x57, 0x61, 0x76, 0x65, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_blockchain_info_proto_rawDescOnce sync.Once + file_blockchain_info_proto_rawDescData = file_blockchain_info_proto_rawDesc +) + +func file_blockchain_info_proto_rawDescGZIP() []byte { + file_blockchain_info_proto_rawDescOnce.Do(func() { + file_blockchain_info_proto_rawDescData = protoimpl.X.CompressGZIP(file_blockchain_info_proto_rawDescData) + }) + return file_blockchain_info_proto_rawDescData +} + +var file_blockchain_info_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_blockchain_info_proto_goTypes = []interface{}{ + (*BlockInfo)(nil), // 0: blockchain_info.BlockInfo + (*L2ContractDataEntries)(nil), // 1: blockchain_info.L2ContractDataEntries + (*waves.Block_Header)(nil), // 2: waves.Block.Header + (*waves.DataEntry)(nil), // 3: waves.DataEntry +} +var file_blockchain_info_proto_depIdxs = []int32{ + 2, // 0: blockchain_info.BlockInfo.BlockHeader:type_name -> waves.Block.Header + 3, // 1: blockchain_info.L2ContractDataEntries.DataEntries:type_name -> waves.DataEntry + 2, // [2:2] is the sub-list for method output_type + 2, // [2:2] is the sub-list for method input_type + 2, // [2:2] is the sub-list for extension type_name + 2, // [2:2] is the sub-list for extension extendee + 0, // [0:2] is the sub-list for field type_name +} + +func init() { file_blockchain_info_proto_init() } +func file_blockchain_info_proto_init() { + if File_blockchain_info_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_blockchain_info_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*BlockInfo); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_blockchain_info_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*L2ContractDataEntries); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_blockchain_info_proto_rawDesc, + NumEnums: 0, + NumMessages: 2, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_blockchain_info_proto_goTypes, + DependencyIndexes: file_blockchain_info_proto_depIdxs, + MessageInfos: file_blockchain_info_proto_msgTypes, + }.Build() + File_blockchain_info_proto = out.File + file_blockchain_info_proto_rawDesc = nil + file_blockchain_info_proto_goTypes = nil + file_blockchain_info_proto_depIdxs = nil +} diff --git a/pkg/grpc/l2/blockchain_info/blockchain_info.proto b/pkg/grpc/l2/blockchain_info/blockchain_info.proto new file mode 100644 index 000000000..ce2876402 --- /dev/null +++ b/pkg/grpc/l2/blockchain_info/blockchain_info.proto @@ -0,0 +1,25 @@ +syntax = "proto3"; + +package blockchain_info; +option java_package = "com.wavesplatform.protobuf"; +option csharp_namespace = "Waves"; +option go_package = "github.com/wavesplatform/gowaves/pkg/grpc/l2/blockchain_info"; + +import "waves/transaction.proto"; +import "waves/block.proto"; + + +message BlockInfo { + uint64 Height = 1; + bytes VRF = 2; + bytes BlockID = 3; + waves.Block.Header BlockHeader = 4; +} + +message L2ContractDataEntries { + repeated waves.DataEntry DataEntries = 5; + uint64 Height = 1; +} + + + diff --git a/pkg/grpc/l2/blockchain_info/blockchain_info_vtproto.pb.go b/pkg/grpc/l2/blockchain_info/blockchain_info_vtproto.pb.go new file mode 100644 index 000000000..544628391 --- /dev/null +++ b/pkg/grpc/l2/blockchain_info/blockchain_info_vtproto.pb.go @@ -0,0 +1,508 @@ +// Code generated by protoc-gen-go-vtproto. DO NOT EDIT. +// protoc-gen-go-vtproto version: v0.6.0 +// source: blockchain_info.proto + +package blockchain_info + +import ( + fmt "fmt" + protohelpers "github.com/planetscale/vtprotobuf/protohelpers" + waves "github.com/wavesplatform/gowaves/pkg/grpc/generated/waves" + proto "google.golang.org/protobuf/proto" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + io "io" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +func (m *BlockInfo) MarshalVTStrict() (dAtA []byte, err error) { + if m == nil { + return nil, nil + } + size := m.SizeVT() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBufferVTStrict(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *BlockInfo) MarshalToVTStrict(dAtA []byte) (int, error) { + size := m.SizeVT() + return m.MarshalToSizedBufferVTStrict(dAtA[:size]) +} + +func (m *BlockInfo) MarshalToSizedBufferVTStrict(dAtA []byte) (int, error) { + if m == nil { + return 0, nil + } + i := len(dAtA) + _ = i + var l int + _ = l + if m.unknownFields != nil { + i -= len(m.unknownFields) + copy(dAtA[i:], m.unknownFields) + } + if m.BlockHeader != nil { + if vtmsg, ok := interface{}(m.BlockHeader).(interface { + MarshalToSizedBufferVTStrict([]byte) (int, error) + }); ok { + size, err := vtmsg.MarshalToSizedBufferVTStrict(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) + } else { + encoded, err := proto.Marshal(m.BlockHeader) + if err != nil { + return 0, err + } + i -= len(encoded) + copy(dAtA[i:], encoded) + i = protohelpers.EncodeVarint(dAtA, i, uint64(len(encoded))) + } + i-- + dAtA[i] = 0x22 + } + if len(m.BlockID) > 0 { + i -= len(m.BlockID) + copy(dAtA[i:], m.BlockID) + i = protohelpers.EncodeVarint(dAtA, i, uint64(len(m.BlockID))) + i-- + dAtA[i] = 0x1a + } + if len(m.VRF) > 0 { + i -= len(m.VRF) + copy(dAtA[i:], m.VRF) + i = protohelpers.EncodeVarint(dAtA, i, uint64(len(m.VRF))) + i-- + dAtA[i] = 0x12 + } + if m.Height != 0 { + i = protohelpers.EncodeVarint(dAtA, i, uint64(m.Height)) + i-- + dAtA[i] = 0x8 + } + return len(dAtA) - i, nil +} + +func (m *L2ContractDataEntries) MarshalVTStrict() (dAtA []byte, err error) { + if m == nil { + return nil, nil + } + size := m.SizeVT() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBufferVTStrict(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *L2ContractDataEntries) MarshalToVTStrict(dAtA []byte) (int, error) { + size := m.SizeVT() + return m.MarshalToSizedBufferVTStrict(dAtA[:size]) +} + +func (m *L2ContractDataEntries) MarshalToSizedBufferVTStrict(dAtA []byte) (int, error) { + if m == nil { + return 0, nil + } + i := len(dAtA) + _ = i + var l int + _ = l + if m.unknownFields != nil { + i -= len(m.unknownFields) + copy(dAtA[i:], m.unknownFields) + } + if len(m.DataEntries) > 0 { + for iNdEx := len(m.DataEntries) - 1; iNdEx >= 0; iNdEx-- { + if vtmsg, ok := interface{}(m.DataEntries[iNdEx]).(interface { + MarshalToSizedBufferVTStrict([]byte) (int, error) + }); ok { + size, err := vtmsg.MarshalToSizedBufferVTStrict(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) + } else { + encoded, err := proto.Marshal(m.DataEntries[iNdEx]) + if err != nil { + return 0, err + } + i -= len(encoded) + copy(dAtA[i:], encoded) + i = protohelpers.EncodeVarint(dAtA, i, uint64(len(encoded))) + } + i-- + dAtA[i] = 0x2a + } + } + if m.Height != 0 { + i = protohelpers.EncodeVarint(dAtA, i, uint64(m.Height)) + i-- + dAtA[i] = 0x8 + } + return len(dAtA) - i, nil +} + +func (m *BlockInfo) SizeVT() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + if m.Height != 0 { + n += 1 + protohelpers.SizeOfVarint(uint64(m.Height)) + } + l = len(m.VRF) + if l > 0 { + n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } + l = len(m.BlockID) + if l > 0 { + n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } + if m.BlockHeader != nil { + if size, ok := interface{}(m.BlockHeader).(interface { + SizeVT() int + }); ok { + l = size.SizeVT() + } else { + l = proto.Size(m.BlockHeader) + } + n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } + n += len(m.unknownFields) + return n +} + +func (m *L2ContractDataEntries) SizeVT() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + if m.Height != 0 { + n += 1 + protohelpers.SizeOfVarint(uint64(m.Height)) + } + if len(m.DataEntries) > 0 { + for _, e := range m.DataEntries { + if size, ok := interface{}(e).(interface { + SizeVT() int + }); ok { + l = size.SizeVT() + } else { + l = proto.Size(e) + } + n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } + } + n += len(m.unknownFields) + return n +} + +func (m *BlockInfo) UnmarshalVT(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: BlockInfo: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: BlockInfo: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field Height", wireType) + } + m.Height = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.Height |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field VRF", wireType) + } + var byteLen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + byteLen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if byteLen < 0 { + return protohelpers.ErrInvalidLength + } + postIndex := iNdEx + byteLen + if postIndex < 0 { + return protohelpers.ErrInvalidLength + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.VRF = append(m.VRF[:0], dAtA[iNdEx:postIndex]...) + if m.VRF == nil { + m.VRF = []byte{} + } + iNdEx = postIndex + case 3: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field BlockID", wireType) + } + var byteLen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + byteLen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if byteLen < 0 { + return protohelpers.ErrInvalidLength + } + postIndex := iNdEx + byteLen + if postIndex < 0 { + return protohelpers.ErrInvalidLength + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.BlockID = append(m.BlockID[:0], dAtA[iNdEx:postIndex]...) + if m.BlockID == nil { + m.BlockID = []byte{} + } + iNdEx = postIndex + case 4: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field BlockHeader", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return protohelpers.ErrInvalidLength + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return protohelpers.ErrInvalidLength + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if m.BlockHeader == nil { + m.BlockHeader = &waves.Block_Header{} + } + if unmarshal, ok := interface{}(m.BlockHeader).(interface { + UnmarshalVT([]byte) error + }); ok { + if err := unmarshal.UnmarshalVT(dAtA[iNdEx:postIndex]); err != nil { + return err + } + } else { + if err := proto.Unmarshal(dAtA[iNdEx:postIndex], m.BlockHeader); err != nil { + return err + } + } + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := protohelpers.Skip(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return protohelpers.ErrInvalidLength + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + m.unknownFields = append(m.unknownFields, dAtA[iNdEx:iNdEx+skippy]...) + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *L2ContractDataEntries) UnmarshalVT(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: L2ContractDataEntries: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: L2ContractDataEntries: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field Height", wireType) + } + m.Height = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.Height |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 5: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field DataEntries", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return protohelpers.ErrInvalidLength + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return protohelpers.ErrInvalidLength + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.DataEntries = append(m.DataEntries, &waves.DataEntry{}) + if unmarshal, ok := interface{}(m.DataEntries[len(m.DataEntries)-1]).(interface { + UnmarshalVT([]byte) error + }); ok { + if err := unmarshal.UnmarshalVT(dAtA[iNdEx:postIndex]); err != nil { + return err + } + } else { + if err := proto.Unmarshal(dAtA[iNdEx:postIndex], m.DataEntries[len(m.DataEntries)-1]); err != nil { + return err + } + } + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := protohelpers.Skip(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return protohelpers.ErrInvalidLength + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + m.unknownFields = append(m.unknownFields, dAtA[iNdEx:iNdEx+skippy]...) + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} diff --git a/pkg/grpc/server/common_test.go b/pkg/grpc/server/common_test.go index 5952ee307..a01f861cb 100644 --- a/pkg/grpc/server/common_test.go +++ b/pkg/grpc/server/common_test.go @@ -86,7 +86,7 @@ func stateWithCustomGenesis(t *testing.T, genesisPath string) state.State { // Activate data transactions. sets.PreactivatedFeatures = []int16{5} params := defaultStateParams() - st, err := state.NewState(dataDir, true, params, sets, false) + st, err := state.NewState(dataDir, true, params, sets, false, nil) require.NoError(t, err) t.Cleanup(func() { require.NoError(t, st.Close()) @@ -120,7 +120,7 @@ func withAutoCancel(t *testing.T, ctx context.Context) context.Context { func newTestState(t *testing.T, amend bool, params state.StateParams, settings *settings.BlockchainSettings) state.State { dataDir := t.TempDir() - st, err := state.NewState(dataDir, amend, params, settings, false) + st, err := state.NewState(dataDir, amend, params, settings, false, nil) require.NoError(t, err) t.Cleanup(func() { require.NoError(t, st.Close()) diff --git a/pkg/proto/protobuf_converters.go b/pkg/proto/protobuf_converters.go index d00f7ae8a..385abf746 100644 --- a/pkg/proto/protobuf_converters.go +++ b/pkg/proto/protobuf_converters.go @@ -1752,43 +1752,66 @@ func (c *ProtobufConverter) stateHash(stateHashBytes []byte) *crypto.Digest { return &sh } -func (c *ProtobufConverter) BlockHeader(block *g.Block) (BlockHeader, error) { - if block == nil { - return BlockHeader{}, errors.New("empty block") - } - if block.Header == nil { +// PartialBlockHeader converts protobuf block header to domain block header. +// Some fields can't be set in this conversion because the information is not available in the protobuf message. +// The following fields are not set: +// +// - BlockHeader.BlockSignature - the field is not set because it requires the whole block message; +// +// - BlockHeader.TransactionCount - the field is not set because it requires the whole block message; +// +// - BlockHeader.ID - the field is not set because it requires the scheme and filled block signature field. +func (c *ProtobufConverter) PartialBlockHeader(pbHeader *g.Block_Header) (BlockHeader, error) { + if pbHeader == nil { return BlockHeader{}, errors.New("empty block header") } - features := c.features(block.Header.FeatureVotes) - consensus := c.consensus(block.Header) - v := BlockVersion(c.byte(block.Header.Version)) + features := c.features(pbHeader.FeatureVotes) + consensus := c.consensus(pbHeader) + v := BlockVersion(c.byte(pbHeader.Version)) header := BlockHeader{ Version: v, - Timestamp: c.uint64(block.Header.Timestamp), - Parent: c.blockID(block.Header.Reference), + Timestamp: c.uint64(pbHeader.Timestamp), + Parent: c.blockID(pbHeader.Reference), FeaturesCount: len(features), Features: features, - RewardVote: block.Header.RewardVote, + RewardVote: pbHeader.RewardVote, ConsensusBlockLength: uint32(consensus.BinarySize()), NxtConsensus: consensus, - TransactionCount: len(block.Transactions), - GeneratorPublicKey: c.publicKey(block.Header.Generator), - BlockSignature: c.signature(block.Signature), - TransactionsRoot: block.Header.TransactionsRoot, - StateHash: c.stateHash(block.Header.StateHash), - ChallengedHeader: c.challengedHeader(block.Header.ChallengedHeader), + TransactionCount: 0, // not set, can't be set without g.Block structure + GeneratorPublicKey: c.publicKey(pbHeader.Generator), + BlockSignature: crypto.Signature{}, // not set, can't be set without g.Block structure + TransactionsRoot: pbHeader.TransactionsRoot, + StateHash: c.stateHash(pbHeader.StateHash), + ChallengedHeader: c.challengedHeader(pbHeader.ChallengedHeader), + ID: BlockID{}, // not set, can't be calculated without the scheme and the block signature } if c.err != nil { err := c.err c.reset() return BlockHeader{}, err } + return header, nil +} + +func (c *ProtobufConverter) BlockHeader(block *g.Block) (BlockHeader, error) { + if block == nil { + return BlockHeader{}, errors.New("empty block") + } + if block.Header == nil { + return BlockHeader{}, errors.New("empty block header") + } + header, err := c.PartialBlockHeader(block.Header) + if err != nil { + return BlockHeader{}, err + } + header.TransactionCount = len(block.Transactions) // set tx count from whole block message + header.BlockSignature = c.signature(block.Signature) // set block signature from whole block message scheme := c.byte(block.Header.ChainId) if scheme == 0 { scheme = c.FallbackChainID } - if err := header.GenerateBlockID(scheme); err != nil { - return BlockHeader{}, err + if gbErr := header.GenerateBlockID(scheme); gbErr != nil { // generate block ID after setting all fields + return BlockHeader{}, gbErr } return header, nil } diff --git a/pkg/proto/types.go b/pkg/proto/types.go index dd8ac63fb..b82ff90d8 100644 --- a/pkg/proto/types.go +++ b/pkg/proto/types.go @@ -3168,6 +3168,18 @@ func NewDataEntryFromJSON(data []byte) (DataEntry, error) { // DataEntries the slice of various entries of DataTransaction type DataEntries []DataEntry +func (e DataEntries) Len() int { + return len(e) +} + +func (e DataEntries) Less(i int, j int) bool { + return e[i].GetKey() < e[j].GetKey() +} + +func (e DataEntries) Swap(i int, j int) { + e[i], e[j] = e[j], e[i] +} + // PayloadSize returns summary payload size of all entries. func (e DataEntries) PayloadSize() int { pl := 0 @@ -3208,9 +3220,9 @@ func (e *DataEntries) UnmarshalJSON(data []byte) error { entries := make([]DataEntry, len(ets)) for i, row := range ets { - et, err := guessDataEntryType(row) - if err != nil { - return wrapError(err) + et, guessErr := guessDataEntryType(row) + if guessErr != nil { + return wrapError(guessErr) } entries[i] = et } diff --git a/pkg/ride/environment.go b/pkg/ride/environment.go index 0d9eaf991..0a1ebe11b 100644 --- a/pkg/ride/environment.go +++ b/pkg/ride/environment.go @@ -84,6 +84,10 @@ func (ws *WrappedState) NewestTransactionByID(id []byte) (proto.Transaction, err return ws.diff.state.NewestTransactionByID(id) } +func (ws *WrappedState) RetrieveEntries(account proto.Recipient) ([]proto.DataEntry, error) { + return ws.diff.state.RetrieveEntries(account) +} + func (ws *WrappedState) NewestTransactionHeightByID(id []byte) (uint64, error) { return ws.diff.state.NewestTransactionHeightByID(id) } diff --git a/pkg/ride/smart_state_moq_test.go b/pkg/ride/smart_state_moq_test.go index 4fd0eacc2..21755cd21 100644 --- a/pkg/ride/smart_state_moq_test.go +++ b/pkg/ride/smart_state_moq_test.go @@ -87,6 +87,9 @@ var _ types.SmartState = &MockSmartState{} // NewestWavesBalanceFunc: func(account proto.Recipient) (uint64, error) { // panic("mock out the NewestWavesBalance method") // }, +// RetrieveEntriesFunc: func(account proto.Recipient) ([]proto.DataEntry, error) { +// panic("mock out the RetrieveEntries method") +// }, // RetrieveNewestBinaryEntryFunc: func(account proto.Recipient, key string) (*proto.BinaryDataEntry, error) { // panic("mock out the RetrieveNewestBinaryEntry method") // }, @@ -175,6 +178,9 @@ type MockSmartState struct { // NewestWavesBalanceFunc mocks the NewestWavesBalance method. NewestWavesBalanceFunc func(account proto.Recipient) (uint64, error) + // RetrieveEntriesFunc mocks the RetrieveEntries method. + RetrieveEntriesFunc func(account proto.Recipient) ([]proto.DataEntry, error) + // RetrieveNewestBinaryEntryFunc mocks the RetrieveNewestBinaryEntry method. RetrieveNewestBinaryEntryFunc func(account proto.Recipient, key string) (*proto.BinaryDataEntry, error) @@ -302,6 +308,11 @@ type MockSmartState struct { // Account is the account argument value. Account proto.Recipient } + // RetrieveEntries holds details about calls to the RetrieveEntries method. + RetrieveEntries []struct { + // Account is the account argument value. + Account proto.Recipient + } // RetrieveNewestBinaryEntry holds details about calls to the RetrieveNewestBinaryEntry method. RetrieveNewestBinaryEntry []struct { // Account is the account argument value. @@ -358,6 +369,7 @@ type MockSmartState struct { lockNewestTransactionByID sync.RWMutex lockNewestTransactionHeightByID sync.RWMutex lockNewestWavesBalance sync.RWMutex + lockRetrieveEntries sync.RWMutex lockRetrieveNewestBinaryEntry sync.RWMutex lockRetrieveNewestBooleanEntry sync.RWMutex lockRetrieveNewestIntegerEntry sync.RWMutex @@ -1067,6 +1079,38 @@ func (mock *MockSmartState) NewestWavesBalanceCalls() []struct { return calls } +// RetrieveEntries calls RetrieveEntriesFunc. +func (mock *MockSmartState) RetrieveEntries(account proto.Recipient) ([]proto.DataEntry, error) { + if mock.RetrieveEntriesFunc == nil { + panic("MockSmartState.RetrieveEntriesFunc: method is nil but SmartState.RetrieveEntries was just called") + } + callInfo := struct { + Account proto.Recipient + }{ + Account: account, + } + mock.lockRetrieveEntries.Lock() + mock.calls.RetrieveEntries = append(mock.calls.RetrieveEntries, callInfo) + mock.lockRetrieveEntries.Unlock() + return mock.RetrieveEntriesFunc(account) +} + +// RetrieveEntriesCalls gets all the calls that were made to RetrieveEntries. +// Check the length with: +// +// len(mockedSmartState.RetrieveEntriesCalls()) +func (mock *MockSmartState) RetrieveEntriesCalls() []struct { + Account proto.Recipient +} { + var calls []struct { + Account proto.Recipient + } + mock.lockRetrieveEntries.RLock() + calls = mock.calls.RetrieveEntries + mock.lockRetrieveEntries.RUnlock() + return calls +} + // RetrieveNewestBinaryEntry calls RetrieveNewestBinaryEntryFunc. func (mock *MockSmartState) RetrieveNewestBinaryEntry(account proto.Recipient, key string) (*proto.BinaryDataEntry, error) { if mock.RetrieveNewestBinaryEntryFunc == nil { diff --git a/pkg/state/accounts_data_storage.go b/pkg/state/accounts_data_storage.go index c60e2539d..b56b14ede 100644 --- a/pkg/state/accounts_data_storage.go +++ b/pkg/state/accounts_data_storage.go @@ -242,7 +242,7 @@ func (s *accountsDataStorage) entryBytes(addr proto.Address, entryKey string) ([ func (s *accountsDataStorage) retrieveEntries(addr proto.Address) ([]proto.DataEntry, error) { addrNum, err := s.addrToNum(addr) if err != nil { - return nil, err + return nil, wrapErr(NotFoundError, err) } key := accountsDataStorKey{addrNum: addrNum} iter, err := s.hs.newTopEntryIteratorByPrefix(key.accountPrefix()) @@ -282,6 +282,79 @@ func (s *accountsDataStorage) retrieveEntries(addr proto.Address) ([]proto.DataE return entries, nil } +func (s *accountsDataStorage) retrieveEntriesAtHeight(addr proto.Address, height uint64) ([]proto.DataEntry, error) { + addrNum, err := s.addrToNum(addr) + if err != nil { + return nil, proto.ErrNotFound + } + key := accountsDataStorKey{addrNum: addrNum} + + recordBytes, err := s.hs.entryDataAtHeight(key.bytes(), height) + if errors.Is(err, keyvalue.ErrNotFound) || errors.Is(err, errEmptyHist) || recordBytes == nil { + // 0 votes for unknown feature. + return nil, keyvalue.ErrNotFound + } + if err != nil { + return nil, errors.Wrap(err, "failed to retrieve data at height") + } + + var dataEntries proto.DataEntries + pos := 0 + + for { + // Check if there's enough data for a key size + if pos+2 > len(recordBytes) { + break + } + + keySize := binary.BigEndian.Uint16(recordBytes[pos : pos+2]) + pos += 2 + + // Check if there's enough data for the key + if pos+int(keySize) > len(recordBytes) { + break + } + + key := recordBytes[pos : pos+int(keySize)] + pos += int(keySize) + + // Check if there's enough data for a value size + if pos+2 > len(recordBytes) { + break + } + + valueSize := binary.BigEndian.Uint16(recordBytes[pos : pos+2]) + pos += 2 + + // Check if there's enough data for the value + if pos+int(valueSize) > len(recordBytes) { + break + } + + value := recordBytes[pos : pos+int(valueSize)] + pos += int(valueSize) + + var record dataEntryRecord + if unmrshlErr := record.unmarshalBinary(value); unmrshlErr != nil { + return nil, unmrshlErr + } + + var entryKey accountsDataStorKey + if unmrshlErr := entryKey.unmarshal(key); unmrshlErr != nil { + return nil, unmrshlErr + } + + entry, cnvrtErr := proto.NewDataEntryFromValueBytes(record.value) + if cnvrtErr != nil { + return nil, cnvrtErr + } + + entry.SetKey(entryKey.entryKey) + dataEntries = append(dataEntries, entry) + } + return dataEntries, nil +} + func (s *accountsDataStorage) newestEntryExists(addr proto.Address) (bool, error) { addrNum, newest, err := s.newestAddrToNum(addr) if err != nil { diff --git a/pkg/state/address_transactions_test.go b/pkg/state/address_transactions_test.go index c2f201b76..e24b6d5da 100644 --- a/pkg/state/address_transactions_test.go +++ b/pkg/state/address_transactions_test.go @@ -13,7 +13,7 @@ import ( func testIterImpl(t *testing.T, params StateParams) { dataDir := t.TempDir() - st, err := NewState(dataDir, true, params, settings.MustMainNetSettings(), false) + st, err := NewState(dataDir, true, params, settings.MustMainNetSettings(), false, nil) require.NoError(t, err) t.Cleanup(func() { diff --git a/pkg/state/api.go b/pkg/state/api.go index 57c35769f..bd7f92086 100644 --- a/pkg/state/api.go +++ b/pkg/state/api.go @@ -6,6 +6,7 @@ import ( "github.com/pkg/errors" + "github.com/wavesplatform/gowaves/pkg/blockchaininfo" "github.com/wavesplatform/gowaves/pkg/crypto" "github.com/wavesplatform/gowaves/pkg/keyvalue" "github.com/wavesplatform/gowaves/pkg/libs/ntptime" @@ -232,8 +233,9 @@ func NewState( params StateParams, settings *settings.BlockchainSettings, enableLightNode bool, + bUpdatesExtension *blockchaininfo.BlockchainUpdatesExtension, ) (State, error) { - s, err := newStateManager(dataDir, amend, params, settings, enableLightNode) + s, err := newStateManager(dataDir, amend, params, settings, enableLightNode, bUpdatesExtension) if err != nil { return nil, errors.Wrap(err, "failed to create new state instance") } diff --git a/pkg/state/appender.go b/pkg/state/appender.go index 56ff0e6f0..2feff4751 100644 --- a/pkg/state/appender.go +++ b/pkg/state/appender.go @@ -7,6 +7,8 @@ import ( "github.com/pkg/errors" "go.uber.org/zap" + "github.com/wavesplatform/gowaves/pkg/blockchaininfo" + "github.com/wavesplatform/gowaves/pkg/crypto" "github.com/wavesplatform/gowaves/pkg/errs" "github.com/wavesplatform/gowaves/pkg/proto" @@ -54,6 +56,8 @@ type txAppender struct { // buildApiData flag indicates that additional data for API is built when // appending transactions. buildApiData bool + + bUpdatesExtension *blockchaininfo.BlockchainUpdatesExtension } func newTxAppender( @@ -64,6 +68,7 @@ func newTxAppender( stateDB *stateDB, atx *addressTransactions, snapshotApplier *blockSnapshotsApplier, + bUpdatesExtension *blockchaininfo.BlockchainUpdatesExtension, ) (*txAppender, error) { buildAPIData, err := stateDB.stateStoresApiData() if err != nil { @@ -112,6 +117,7 @@ func newTxAppender( diffApplier: diffApplier, buildApiData: buildAPIData, ethTxKindResolver: ethKindResolver, + bUpdatesExtension: bUpdatesExtension, }, nil } @@ -834,6 +840,20 @@ func (a *txAppender) appendBlock(params *appendBlockParams) error { if err != nil { return err } + + // write updates into the updatesChannel here + // TODO possibly run it in a goroutine? make sure goroutines run in order? + if a.bUpdatesExtension != nil && a.bUpdatesExtension.EnableBlockchainUpdatesPlugin() { + // TODO get info from block snapshot? + + // blockSnapshot.TxSnapshots. + + updtErr := a.updateBlockchainUpdateInfo(blockInfo, params.block) + if updtErr != nil { + return updtErr + } + } + // check whether the calculated snapshot state hash equals with the provided one if blockStateHash, present := params.block.GetStateHash(); present && blockStateHash != stateHash { return errors.Wrapf(errBlockSnapshotStateHashMismatch, @@ -855,6 +875,29 @@ func (a *txAppender) appendBlock(params *appendBlockParams) error { return a.blockDiffer.saveCurFeeDistr(params.block) } +func (a *txAppender) updateBlockchainUpdateInfo(blockInfo *proto.BlockInfo, blockHeader *proto.BlockHeader) error { + // TODO improve this by using diffs instead grabbing all the records every time + dataEntries, err := a.ia.state.RetrieveEntries(proto.NewRecipientFromAddress(a.bUpdatesExtension.L2ContractAddress())) + if err != nil && !errors.Is(err, proto.ErrNotFound) { + return err + } + blockID := blockHeader.BlockID() + bUpdatesInfo := blockchaininfo.BUpdatesInfo{ + BlockUpdatesInfo: blockchaininfo.BlockUpdatesInfo{ + Height: blockInfo.Height, + VRF: blockInfo.VRF, + BlockID: blockID, + BlockHeader: *blockHeader, + }, + ContractUpdatesInfo: blockchaininfo.L2ContractDataEntries{ + AllDataEntries: dataEntries, + Height: blockInfo.Height, + }, + } + a.bUpdatesExtension.WriteBUpdates(bUpdatesInfo) + return nil +} + func (a *txAppender) createCheckerInfo(params *appendBlockParams) (*checkerInfo, error) { rideV5Activated, err := a.stor.features.newestIsActivated(int16(settings.RideV5)) if err != nil { diff --git a/pkg/state/invoke_applier_test.go b/pkg/state/invoke_applier_test.go index f1a514c6d..7882eb856 100644 --- a/pkg/state/invoke_applier_test.go +++ b/pkg/state/invoke_applier_test.go @@ -35,7 +35,8 @@ type invokeApplierTestObjects struct { } func createInvokeApplierTestObjects(t *testing.T) *invokeApplierTestObjects { - state, err := newStateManager(t.TempDir(), true, DefaultTestingStateParams(), settings.MustMainNetSettings(), false) + state, err := newStateManager(t.TempDir(), true, DefaultTestingStateParams(), + settings.MustMainNetSettings(), false, nil) assert.NoError(t, err, "newStateManager() failed") to := &invokeApplierTestObjects{state} randGenesisBlockID := genRandBlockId(t) diff --git a/pkg/state/smart_state_moq_test.go b/pkg/state/smart_state_moq_test.go index d3a2e656d..00d678954 100644 --- a/pkg/state/smart_state_moq_test.go +++ b/pkg/state/smart_state_moq_test.go @@ -87,6 +87,9 @@ var _ types.EnrichedSmartState = &AnotherMockSmartState{} // NewestWavesBalanceFunc: func(account proto.Recipient) (uint64, error) { // panic("mock out the NewestWavesBalance method") // }, +// RetrieveEntriesFunc: func(account proto.Recipient) ([]proto.DataEntry, error) { +// panic("mock out the RetrieveEntries method") +// }, // RetrieveNewestBinaryEntryFunc: func(account proto.Recipient, key string) (*proto.BinaryDataEntry, error) { // panic("mock out the RetrieveNewestBinaryEntry method") // }, @@ -175,6 +178,9 @@ type AnotherMockSmartState struct { // NewestWavesBalanceFunc mocks the NewestWavesBalance method. NewestWavesBalanceFunc func(account proto.Recipient) (uint64, error) + // RetrieveEntriesFunc mocks the RetrieveEntries method. + RetrieveEntriesFunc func(account proto.Recipient) ([]proto.DataEntry, error) + // RetrieveNewestBinaryEntryFunc mocks the RetrieveNewestBinaryEntry method. RetrieveNewestBinaryEntryFunc func(account proto.Recipient, key string) (*proto.BinaryDataEntry, error) @@ -302,6 +308,11 @@ type AnotherMockSmartState struct { // Account is the account argument value. Account proto.Recipient } + // RetrieveEntries holds details about calls to the RetrieveEntries method. + RetrieveEntries []struct { + // Account is the account argument value. + Account proto.Recipient + } // RetrieveNewestBinaryEntry holds details about calls to the RetrieveNewestBinaryEntry method. RetrieveNewestBinaryEntry []struct { // Account is the account argument value. @@ -358,6 +369,7 @@ type AnotherMockSmartState struct { lockNewestTransactionByID sync.RWMutex lockNewestTransactionHeightByID sync.RWMutex lockNewestWavesBalance sync.RWMutex + lockRetrieveEntries sync.RWMutex lockRetrieveNewestBinaryEntry sync.RWMutex lockRetrieveNewestBooleanEntry sync.RWMutex lockRetrieveNewestIntegerEntry sync.RWMutex @@ -1067,6 +1079,38 @@ func (mock *AnotherMockSmartState) NewestWavesBalanceCalls() []struct { return calls } +// RetrieveEntries calls RetrieveEntriesFunc. +func (mock *AnotherMockSmartState) RetrieveEntries(account proto.Recipient) ([]proto.DataEntry, error) { + if mock.RetrieveEntriesFunc == nil { + panic("AnotherMockSmartState.RetrieveEntriesFunc: method is nil but SmartState.RetrieveEntries was just called") + } + callInfo := struct { + Account proto.Recipient + }{ + Account: account, + } + mock.lockRetrieveEntries.Lock() + mock.calls.RetrieveEntries = append(mock.calls.RetrieveEntries, callInfo) + mock.lockRetrieveEntries.Unlock() + return mock.RetrieveEntriesFunc(account) +} + +// RetrieveEntriesCalls gets all the calls that were made to RetrieveEntries. +// Check the length with: +// +// len(mockedSmartState.RetrieveEntriesCalls()) +func (mock *AnotherMockSmartState) RetrieveEntriesCalls() []struct { + Account proto.Recipient +} { + var calls []struct { + Account proto.Recipient + } + mock.lockRetrieveEntries.RLock() + calls = mock.calls.RetrieveEntries + mock.lockRetrieveEntries.RUnlock() + return calls +} + // RetrieveNewestBinaryEntry calls RetrieveNewestBinaryEntryFunc. func (mock *AnotherMockSmartState) RetrieveNewestBinaryEntry(account proto.Recipient, key string) (*proto.BinaryDataEntry, error) { if mock.RetrieveNewestBinaryEntryFunc == nil { diff --git a/pkg/state/state.go b/pkg/state/state.go index e6fcf5a19..a65273f7f 100644 --- a/pkg/state/state.go +++ b/pkg/state/state.go @@ -16,6 +16,7 @@ import ( "go.uber.org/atomic" "go.uber.org/zap" + "github.com/wavesplatform/gowaves/pkg/blockchaininfo" "github.com/wavesplatform/gowaves/pkg/consensus" "github.com/wavesplatform/gowaves/pkg/crypto" "github.com/wavesplatform/gowaves/pkg/errs" @@ -519,6 +520,7 @@ func newStateManager( params StateParams, settings *settings.BlockchainSettings, enableLightNode bool, + bUpdatesExtension *blockchaininfo.BlockchainUpdatesExtension, ) (_ *stateManager, retErr error) { if err := validateSettings(settings); err != nil { return nil, err @@ -602,7 +604,7 @@ func newStateManager( // Set fields which depend on state. // Consensus validator is needed to check block headers. snapshotApplier := newBlockSnapshotsApplier(nil, newSnapshotApplierStorages(stor, rw)) - appender, err := newTxAppender(state, rw, stor, settings, sdb, atx, &snapshotApplier) + appender, err := newTxAppender(state, rw, stor, settings, sdb, atx, &snapshotApplier, bUpdatesExtension) if err != nil { return nil, wrapErr(Other, err) } @@ -2362,6 +2364,24 @@ func (s *stateManager) RetrieveEntries(account proto.Recipient) ([]proto.DataEnt } entries, err := s.stor.accountsDataStor.retrieveEntries(addr) if err != nil { + if IsNotFound(err) { + return nil, err + } + return nil, wrapErr(RetrievalError, err) + } + return entries, nil +} + +func (s *stateManager) RetrieveEntriesAtHeight(account proto.Recipient, height uint64) ([]proto.DataEntry, error) { + addr, err := s.recipientToAddress(account) + if err != nil { + return nil, wrapErr(RetrievalError, err) + } + entries, err := s.stor.accountsDataStor.retrieveEntriesAtHeight(addr, height) + if err != nil { + if errors.Is(err, proto.ErrNotFound) { + return nil, err + } return nil, wrapErr(RetrievalError, err) } return entries, nil diff --git a/pkg/state/state_test.go b/pkg/state/state_test.go index abc54b066..11affcb4e 100644 --- a/pkg/state/state_test.go +++ b/pkg/state/state_test.go @@ -47,7 +47,7 @@ func bigFromStr(s string) *big.Int { func newTestState(t *testing.T, amend bool, params StateParams, settings *settings.BlockchainSettings) State { dataDir := t.TempDir() - m, err := NewState(dataDir, amend, params, settings, false) + m, err := NewState(dataDir, amend, params, settings, false, nil) require.NoError(t, err) t.Cleanup(func() { require.NoError(t, m.Close(), "manager.Close() failed") @@ -57,7 +57,7 @@ func newTestState(t *testing.T, amend bool, params StateParams, settings *settin func newTestStateManager(t *testing.T, amend bool, params StateParams, settings *settings.BlockchainSettings) *stateManager { dataDir := t.TempDir() - m, err := newStateManager(dataDir, amend, params, settings, false) + m, err := newStateManager(dataDir, amend, params, settings, false, nil) require.NoError(t, err) t.Cleanup(func() { require.NoError(t, m.Close(), "manager.Close() failed") @@ -69,7 +69,7 @@ func TestHandleAmendFlag(t *testing.T) { dataDir := t.TempDir() bs := settings.MustMainNetSettings() // first open with false amend - manager, err := newStateManager(dataDir, false, DefaultTestingStateParams(), bs, false) + manager, err := newStateManager(dataDir, false, DefaultTestingStateParams(), bs, false, nil) assert.NoError(t, err, "newStateManager() failed") t.Cleanup(func() { assert.NoError(t, manager.Close(), "manager.Close() failed") @@ -78,18 +78,20 @@ func TestHandleAmendFlag(t *testing.T) { // open with true amend assert.NoError(t, manager.Close(), "manager.Close() failed") - manager, err = newStateManager(dataDir, true, DefaultTestingStateParams(), bs, false) + manager, err = newStateManager(dataDir, true, DefaultTestingStateParams(), bs, false, nil) assert.NoError(t, err, "newStateManager() failed") assert.True(t, manager.stor.hs.amend) // open with false amend again. Result amend should be true assert.NoError(t, manager.Close(), "manager.Close() failed") - manager, err = newStateManager(dataDir, false, DefaultTestingStateParams(), bs, false) + manager, err = newStateManager(dataDir, false, DefaultTestingStateParams(), bs, + false, nil) assert.NoError(t, err, "newStateManager() failed") assert.True(t, manager.stor.hs.amend) // first open with true amend - newManager, err := newStateManager(t.TempDir(), true, DefaultTestingStateParams(), bs, false) + newManager, err := newStateManager(t.TempDir(), true, DefaultTestingStateParams(), bs, + false, nil) assert.NoError(t, err, "newStateManager() failed") t.Cleanup(func() { assert.NoError(t, newManager.Close(), "newManager.Close() failed") @@ -394,7 +396,7 @@ func TestStateManager_TopBlock(t *testing.T) { bs := settings.MustMainNetSettings() assert.NoError(t, err) dataDir := t.TempDir() - manager, err := newStateManager(dataDir, true, DefaultTestingStateParams(), bs, false) + manager, err := newStateManager(dataDir, true, DefaultTestingStateParams(), bs, false, nil) assert.NoError(t, err, "newStateManager() failed") t.Cleanup(func() { @@ -428,7 +430,7 @@ func TestStateManager_TopBlock(t *testing.T) { // Test after closure. err = manager.Close() assert.NoError(t, err, "manager.Close() failed") - manager, err = newStateManager(dataDir, true, DefaultTestingStateParams(), settings.MustMainNetSettings(), false) + manager, err = newStateManager(dataDir, true, DefaultTestingStateParams(), settings.MustMainNetSettings(), false, nil) assert.NoError(t, err, "newStateManager() failed") assert.Equal(t, correct, manager.TopBlock()) } @@ -523,6 +525,7 @@ func createMockStateManager(t *testing.T, bs *settings.BlockchainSettings) (*sta state.stateDB, state.atx, &snapshotApplier, + nil, ) require.NoError(t, err, "newTxAppender() failed") state.appender = appender diff --git a/pkg/state/transaction_handler.go b/pkg/state/transaction_handler.go index a3964ba47..103af3f89 100644 --- a/pkg/state/transaction_handler.go +++ b/pkg/state/transaction_handler.go @@ -264,6 +264,7 @@ func (h *transactionHandler) performTx( if err := snapshot.Apply(h.sa, tx, validatingUTX); err != nil { return txSnapshot{}, errors.Wrap(err, "failed to apply transaction snapshot") } + return snapshot, nil } diff --git a/pkg/types/types.go b/pkg/types/types.go index 222e1268f..ec7397f28 100644 --- a/pkg/types/types.go +++ b/pkg/types/types.go @@ -125,6 +125,7 @@ type SmartState interface { RetrieveNewestBooleanEntry(account proto.Recipient, key string) (*proto.BooleanDataEntry, error) RetrieveNewestStringEntry(account proto.Recipient, key string) (*proto.StringDataEntry, error) RetrieveNewestBinaryEntry(account proto.Recipient, key string) (*proto.BinaryDataEntry, error) + RetrieveEntries(account proto.Recipient) ([]proto.DataEntry, error) NewestAssetIsSponsored(assetID crypto.Digest) (bool, error) NewestAssetConstInfo(assetID proto.AssetID) (*proto.AssetConstInfo, error) NewestAssetInfo(assetID crypto.Digest) (*proto.AssetInfo, error)